diff --git a/GEMINI.md b/GEMINI.md index 2077e37..d51b457 100644 --- a/GEMINI.md +++ b/GEMINI.md @@ -374,6 +374,7 @@ The Queryer transforms Postgres into a pre-compiled Semantic Query Engine, desig * **Multi-Table Branching**: If the Physical Table is a parent to other tables (e.g. `organization` has variations `["organization", "bot", "person"]`), the compiler generates a dynamic `CASE WHEN type = '...' THEN ...` query, expanding into sub-queries for each variation. To ensure safe resolution, the compiler dynamically evaluates correlation boundaries: it attempts standard Relational Edge discovery first. If no explicit relational edge exists (indicating pure Table Inheritance rather than a standard foreign-key graph relationship), it safely invokes a **Table Parity Fallback**. This generates an explicit ID correlation constraint (`AND inner.id = outer.id`), perfectly binding the structural variations back to the parent row to eliminate Cartesian products. * **Single-Table Bypass**: If the Physical Table is a leaf node with only one variation (e.g. `person` has variations `["person"]`), the compiler cleanly bypasses `CASE` generation and compiles a simple `SELECT` across the base table, as all schema extensions (e.g. `light.person`, `full.person`) are guaranteed to reside in the exact same physical row. * **Polymorphic Relation Type Filtering**: When a relationship maps to a polymorphic target with variations, the Queryer compiles an `IN` clause containing all allowed table variations (e.g., `counterparty_type IN ('bot', 'organization', 'person')`) rather than matching the base type literal, ensuring all polymorphic types are loaded correctly. +* **Tenant Scoping & ReBAC Filtering**: When `jspg_query` compiles root-level or relationship-level entity queries, the generated SQL queries evaluate within the calling PostgreSQL transaction's session configuration (e.g. `auth.organization_ids`, `auth.roles`). The compiled queries seamlessly interact with PostgreSQL Row Level Security (RLS) policies on base tables (such as `agreego.entity`) and pre-materialized Zanzibar graph edges (`agreego.relationship`), ensuring multi-tenant isolation and $O(1)$ query evaluation without requiring manual WHERE scoping logic across every application punc. * **Static Relation Constraints (Kind Constraints)**: When a relationship (such as a nested object or array) is defined with a schema that constrains a field value statically using a `const` or `enum` keyword (for example, `kind` constrained to `"cover"` in a `cover_attachment`), the Queryer automatically extracts these static assertions during AST compilation. It injects them directly as static filters into the SQL subquery's `WHERE` clause (e.g. `AND attachment.kind = 'cover'`), allowing developers to query pre-filtered subsets of related tables natively through the schema. * **Proxy Schema Dereferencing / Resolution**: To support punc endpoints that return non-polymorphic table-backed shapes (using `type: "full.X"` proxy schemas at the root response level), the Queryer compiler automatically dereferences non-table schema pointers to their target schemas prior to checking the types. This allows the Queryer to correctly resolve the table relationship edges pre-compiled on the full schema, while avoiding polluting the database registry with relations on ad-hoc punc response schemas during setup. diff --git a/fixtures/merger.json b/fixtures/merger.json index eafb4db..45b57ea 100644 --- a/fixtures/merger.json +++ b/fixtures/merger.json @@ -1357,10 +1357,10 @@ "LEFT JOIN agreego.\"user\" t3 ON t3.id = t1.id", "LEFT JOIN agreego.\"person\" t4 ON t4.id = t1.id", "WHERE", - " (\"first_name\" = 'LookupFirst'", - " AND \"last_name\" = 'LookupLast'", - " AND \"date_of_birth\" = '{{timestamp}}'", - " AND \"pronouns\" = 'they/them'))" + " (t4.\"first_name\" = 'LookupFirst'", + " AND t4.\"last_name\" = 'LookupLast'", + " AND t4.\"date_of_birth\" = '{{timestamp}}'", + " AND t4.\"pronouns\" = 'they/them'))" ], [ "INSERT INTO agreego.\"entity\" (", @@ -1521,10 +1521,10 @@ "LEFT JOIN agreego.\"user\" t3 ON t3.id = t1.id", "LEFT JOIN agreego.\"person\" t4 ON t4.id = t1.id", "WHERE", - " (\"first_name\" = 'LookupFirst'", - " AND \"last_name\" = 'LookupLast'", - " AND \"date_of_birth\" = '{{timestamp}}'", - " AND \"pronouns\" = 'they/them'))" + " (t4.\"first_name\" = 'LookupFirst'", + " AND t4.\"last_name\" = 'LookupLast'", + " AND t4.\"date_of_birth\" = '{{timestamp}}'", + " AND t4.\"pronouns\" = 'they/them'))" ], [ "INSERT INTO agreego.\"entity\" (", @@ -1687,17 +1687,17 @@ "LEFT JOIN agreego.\"user\" t3 ON t3.id = t1.id", "LEFT JOIN agreego.\"person\" t4 ON t4.id = t1.id", "WHERE", - " (\"first_name\" = 'LookupFirst'", - " AND \"last_name\" = 'LookupLast'", - " AND \"date_of_birth\" = '{{timestamp}}'", - " AND \"pronouns\" = 'they/them')", + " (t4.\"first_name\" = 'LookupFirst'", + " AND t4.\"last_name\" = 'LookupLast'", + " AND t4.\"date_of_birth\" = '{{timestamp}}'", + " AND t4.\"pronouns\" = 'they/them')", "UNION SELECT COALESCE(to_jsonb(t1.*), '{}') || COALESCE(to_jsonb(t2.*), '{}') || COALESCE(to_jsonb(t3.*), '{}') || COALESCE(to_jsonb(t4.*), '{}')", "FROM agreego.\"entity\" t1", "LEFT JOIN agreego.\"organization\" t2 ON t2.id = t1.id", "LEFT JOIN agreego.\"user\" t3 ON t3.id = t1.id", "LEFT JOIN agreego.\"person\" t4 ON t4.id = t1.id", "WHERE", - " (\"name\" = 'LookupName'))" + " (t3.\"name\" = 'LookupName'))" ], [ "INSERT INTO agreego.\"entity\" (", @@ -1862,10 +1862,10 @@ "LEFT JOIN agreego.\"user\" t3 ON t3.id = t1.id", "LEFT JOIN agreego.\"person\" t4 ON t4.id = t1.id", "WHERE", - " (\"first_name\" = 'LookupFirst'", - " AND \"last_name\" = 'LookupLast'", - " AND \"date_of_birth\" = '{{timestamp}}'", - " AND \"pronouns\" = 'they/them'))" + " (t4.\"first_name\" = 'LookupFirst'", + " AND t4.\"last_name\" = 'LookupLast'", + " AND t4.\"date_of_birth\" = '{{timestamp}}'", + " AND t4.\"pronouns\" = 'they/them'))" ], [ "INSERT INTO agreego.\"entity\" (", @@ -2796,10 +2796,10 @@ "LEFT JOIN agreego.\"relationship\" t2 ON t2.id = t1.id", "LEFT JOIN agreego.\"contact\" t3 ON t3.id = t1.id", "WHERE", - " (t1.\"type\" = 'contact'", - " AND \"source_id\" = '{{uuid:generated_0}}'", - " AND \"target_id\" = '{{uuid:generated_1}}'", - " AND \"start_date\" = '{{timestamp}}'))" + " (t2.\"type\" = 'contact'", + " AND t2.\"source_id\" = '{{uuid:generated_0}}'", + " AND t2.\"target_id\" = '{{uuid:generated_1}}'", + " AND t2.\"start_date\" = '{{timestamp}}'))" ], [ "INSERT INTO agreego.\"entity\" (", @@ -2928,10 +2928,10 @@ "LEFT JOIN agreego.\"relationship\" t2 ON t2.id = t1.id", "LEFT JOIN agreego.\"contact\" t3 ON t3.id = t1.id", "WHERE", - " (t1.\"type\" = 'contact'", - " AND \"source_id\" = '{{uuid:generated_0}}'", - " AND \"target_id\" = '{{uuid:generated_5}}'", - " AND \"start_date\" = '{{timestamp}}'))" + " (t2.\"type\" = 'contact'", + " AND t2.\"source_id\" = '{{uuid:generated_0}}'", + " AND t2.\"target_id\" = '{{uuid:generated_5}}'", + " AND t2.\"start_date\" = '{{timestamp}}'))" ], [ "INSERT INTO agreego.\"entity\" (", @@ -3060,10 +3060,10 @@ "LEFT JOIN agreego.\"relationship\" t2 ON t2.id = t1.id", "LEFT JOIN agreego.\"contact\" t3 ON t3.id = t1.id", "WHERE", - " (t1.\"type\" = 'contact'", - " AND \"source_id\" = '{{uuid:generated_0}}'", - " AND \"target_id\" = '{{uuid:generated_9}}'", - " AND \"start_date\" = '{{timestamp}}'))" + " (t2.\"type\" = 'contact'", + " AND t2.\"source_id\" = '{{uuid:generated_0}}'", + " AND t2.\"target_id\" = '{{uuid:generated_9}}'", + " AND t2.\"start_date\" = '{{timestamp}}'))" ], [ "INSERT INTO agreego.\"entity\" (", diff --git a/src/merger/mod.rs b/src/merger/mod.rs index 22f22b9..84a9bba 100644 --- a/src/merger/mod.rs +++ b/src/merger/mod.rs @@ -818,17 +818,20 @@ impl Merger { } for (lookup_fields, parent_type) in lookup_satisfied_keys { + let t_alias = entity_type + .hierarchy + .iter() + .position(|name| name == &parent_type.name) + .map(|idx| format!("t{}", idx + 1)) + .unwrap_or_else(|| "t1".to_string()); + let mut lookup_predicates = Vec::new(); for column in lookup_fields { let val = entity_fields .get(column) .or_else(|| parent_type.field_defaults.get(column)) .unwrap_or(&Value::Null); - if column == "type" { - lookup_predicates.push(format!("t1.\"{}\" = {}", column, Self::quote_literal(val))); - } else { - lookup_predicates.push(format!("\"{}\" = {}", column, Self::quote_literal(val))); - } + lookup_predicates.push(format!("{}.\"{}\" = {}", t_alias, column, Self::quote_literal(val))); } where_parts.push(format!("({})", lookup_predicates.join(" AND "))); }