diff --git a/GEMINI.md b/GEMINI.md index a12da6a..7c58efb 100644 --- a/GEMINI.md +++ b/GEMINI.md @@ -295,6 +295,7 @@ The Queryer transforms Postgres into a pre-compiled Semantic Query Engine, desig * **The Dot Convention**: When a schema requests `family: "target.schema"`, the compiler extracts the base type (e.g. `schema`) and looks up its Physical Table definition. * **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. --- diff --git a/fixtures/queryer.json b/fixtures/queryer.json index ce551f5..0fe8601 100644 --- a/fixtures/queryer.json +++ b/fixtures/queryer.json @@ -2432,7 +2432,7 @@ " JOIN agreego.entity entity_2 ON entity_2.id = order_1.id", " WHERE", " NOT entity_2.archived", - " AND order_1.counterparty_type = 'organization'", + " AND order_1.counterparty_type IN ('bot', 'organization', 'person')", "))))" ] ] diff --git a/src/queryer/compiler.rs b/src/queryer/compiler.rs index b0d9f8c..9108454 100644 --- a/src/queryer/compiler.rs +++ b/src/queryer/compiler.rs @@ -603,7 +603,7 @@ impl<'a> Compiler<'a> { if let Some(type_name) = bound_type_name { // Ensure this type actually exists - if self.db.types.contains_key(&type_name) { + if let Some(type_def) = self.db.types.get(&type_name) { if let Some(relation) = self.db.relations.get(&edge.constraint) { let mut poly_col = None; let mut table_to_alias = ""; @@ -621,7 +621,21 @@ impl<'a> Compiler<'a> { .get(table_to_alias) .or_else(|| type_aliases.get(&node.parent_alias)) { - where_clauses.push(format!("{}.{} = '{}'", alias, col, type_name)); + if type_def.variations.len() > 1 { + let quoted: Vec = type_def + .variations + .iter() + .map(|v| format!("'{}'", v)) + .collect(); + where_clauses.push(format!( + "{}.{} IN ({})", + alias, + col, + quoted.join(", ") + )); + } else { + where_clauses.push(format!("{}.{} = '{}'", alias, col, type_name)); + } } } } diff --git a/traits_debug_val.json b/traits_debug_val.json new file mode 100644 index 0000000..039f27a --- /dev/null +++ b/traits_debug_val.json @@ -0,0 +1,13 @@ +{ + "types": [ + { + "name": "uniqueItems_6_0", + "schemas": { + "uniqueItems_6_0": { + "uniqueItems": true, + "extensible": true + } + } + } + ] +} \ No newline at end of file