diff --git a/GEMINI.md b/GEMINI.md index fb2f018..61f8342 100644 --- a/GEMINI.md +++ b/GEMINI.md @@ -351,6 +351,7 @@ The Queryer transforms Postgres into a pre-compiled Semantic Query Engine, desig * **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. * **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/filter.json b/fixtures/filter.json index b07bf14..5ce1415 100644 --- a/fixtures/filter.json +++ b/fixtures/filter.json @@ -211,6 +211,13 @@ "gender": {}, "gender.condition": { "type": "condition", + "compiledPropertyNames": [ + "kind", + "$eq", + "$ne", + "$of", + "$nof" + ], "properties": { "$eq": { "type": [ @@ -224,15 +231,6 @@ "null" ] }, - "$of": { - "type": [ - "array", - "null" - ], - "items": { - "type": "gender" - } - }, "$nof": { "type": [ "array", @@ -241,158 +239,20 @@ "items": { "type": "gender" } + }, + "$of": { + "type": [ + "array", + "null" + ], + "items": { + "type": "gender" + } } - }, - "compiledPropertyNames": [ - "kind", - "$eq", - "$ne", - "$of", - "$nof" - ] + } }, "person": {}, "person.filter": { - "type": "filter", - "properties": { - "first_name": { - "type": [ - "string.condition", - "null" - ], - "compiledPropertyNames": [ - "kind", - "$eq" - ] - }, - "age": { - "type": [ - "integer.condition", - "null" - ], - "compiledPropertyNames": [ - "kind", - "$eq" - ] - }, - "billing_address": { - "type": [ - "address.filter", - "null" - ], - "compiledPropertyNames": [ - "city", - "$and", - "$or" - ] - }, - "gender": { - "type": [ - "gender.condition", - "null" - ], - "compiledPropertyNames": [ - "kind", - "$eq", - "$ne", - "$of", - "$nof" - ] - }, - "birth_date": { - "type": [ - "date.condition", - "null" - ], - "compiledPropertyNames": [ - "kind", - "$eq" - ] - }, - "uuid_field": { - "type": [ - "uuid.condition", - "null" - ], - "compiledPropertyNames": [ - "kind", - "$eq" - ] - }, - "tags": { - "type": [ - "string.condition", - "null" - ], - "compiledPropertyNames": [ - "kind", - "$eq" - ] - }, - "ad_hoc": { - "type": [ - "filter", - "null" - ], - "properties": { - "foo": { - "type": [ - "string.condition", - "null" - ], - "compiledPropertyNames": [ - "kind", - "$eq" - ] - } - }, - "compiledPropertyNames": [ - "foo" - ] - }, - "$and": { - "type": [ - "array", - "null" - ], - "items": { - "type": "person.filter", - "compiledPropertyNames": [ - "first_name", - "age", - "billing_address", - "gender", - "birth_date", - "uuid_field", - "tags", - "ad_hoc", - "$and", - "$or" - ] - } - }, - "$or": { - "type": [ - "array", - "null" - ], - "items": { - "type": "person.filter", - "compiledPropertyNames": [ - "first_name", - "age", - "billing_address", - "gender", - "birth_date", - "uuid_field", - "tags", - "ad_hoc", - "$and", - "$or" - ] - } - } - }, "compiledPropertyNames": [ "first_name", "age", @@ -404,34 +264,133 @@ "ad_hoc", "$and", "$or" - ] + ], + "properties": { + "$and": { + "items": { + "compiledPropertyNames": [ + "first_name", + "age", + "billing_address", + "gender", + "birth_date", + "uuid_field", + "tags", + "ad_hoc", + "$and", + "$or" + ], + "type": "person.filter" + }, + "type": [ + "array", + "null" + ] + }, + "$or": { + "items": { + "compiledPropertyNames": [ + "first_name", + "age", + "billing_address", + "gender", + "birth_date", + "uuid_field", + "tags", + "ad_hoc", + "$and", + "$or" + ], + "type": "person.filter" + }, + "type": [ + "array", + "null" + ] + }, + "ad_hoc": { + "compiledPropertyNames": [ + "foo" + ], + "properties": { + "foo": { + "type": [ + "string.condition", + "null" + ] + } + }, + "type": [ + "filter", + "null" + ] + }, + "age": { + "type": [ + "integer.condition", + "null" + ] + }, + "billing_address": { + "type": [ + "address.filter", + "null" + ] + }, + "birth_date": { + "type": [ + "date.condition", + "null" + ] + }, + "uuid_field": { + "type": [ + "uuid.condition", + "null" + ] + }, + "first_name": { + "type": [ + "string.condition", + "null" + ] + }, + "gender": { + "type": [ + "gender.condition", + "null" + ] + }, + "tags": { + "type": [ + "string.condition", + "null" + ] + } + }, + "type": "filter" }, "address": {}, "address.filter": { "type": "filter", + "compiledPropertyNames": [ + "city", + "$and", + "$or" + ], "properties": { - "city": { - "type": [ - "string.condition", - "null" - ], - "compiledPropertyNames": [ - "kind", - "$eq" - ] - }, "$and": { "type": [ "array", "null" ], "items": { - "type": "address.filter", "compiledPropertyNames": [ "city", "$and", "$or" - ] + ], + "type": "address.filter" } }, "$or": { @@ -440,20 +399,21 @@ "null" ], "items": { - "type": "address.filter", "compiledPropertyNames": [ "city", "$and", "$or" - ] + ], + "type": "address.filter" } + }, + "city": { + "type": [ + "string.condition", + "null" + ] } - }, - "compiledPropertyNames": [ - "city", - "$and", - "$or" - ] + } }, "condition": {}, "filter": {}, @@ -464,47 +424,28 @@ "search": {}, "search.filter": { "type": "filter", + "compiledPropertyNames": [ + "kind", + "name", + "filter", + "$and", + "$or" + ], "properties": { - "kind": { - "type": [ - "string.condition", - "null" - ], - "compiledPropertyNames": [ - "kind", - "$eq" - ] - }, - "name": { - "type": [ - "string.condition", - "null" - ], - "compiledPropertyNames": [ - "kind", - "$eq" - ] - }, - "filter": { - "type": [ - "$kind.filter", - "null" - ] - }, "$and": { "type": [ "array", "null" ], "items": { - "type": "search.filter", "compiledPropertyNames": [ "kind", "name", "filter", "$and", "$or" - ] + ], + "type": "search.filter" } }, "$or": { @@ -513,24 +454,35 @@ "null" ], "items": { - "type": "search.filter", "compiledPropertyNames": [ "kind", "name", "filter", "$and", "$or" - ] + ], + "type": "search.filter" } + }, + "filter": { + "type": [ + "$kind.filter", + "null" + ] + }, + "kind": { + "type": [ + "string.condition", + "null" + ] + }, + "name": { + "type": [ + "string.condition", + "null" + ] } - }, - "compiledPropertyNames": [ - "kind", - "name", - "filter", - "$and", - "$or" - ] + } } } } diff --git a/fixtures/queryer.json b/fixtures/queryer.json index 9c5b074..ddb1ac0 100644 --- a/fixtures/queryer.json +++ b/fixtures/queryer.json @@ -2547,10 +2547,7 @@ "type": "pet", "properties": { "cover_attachment": { - "type": [ - "attachment", - "null" - ], + "type": "attachment", "properties": { "kind": { "const": "cover" diff --git a/src/database/compile/edges.rs b/src/database/compile/edges.rs index 4c1f4e7..11073ac 100644 --- a/src/database/compile/edges.rs +++ b/src/database/compile/edges.rs @@ -29,21 +29,10 @@ impl Schema { if db.types.contains_key(&base_type_name) { parent_type_name = Some(base_type_name); } - } else if let Some(t_or_arr) = &self.obj.type_ { - match t_or_arr { - crate::database::object::SchemaTypeOrArray::Single(t) => { - if !crate::database::object::is_primitive_type(t) { - parent_type_name = Some(t.split('.').next_back().unwrap_or(t).to_string()); - } - } - crate::database::object::SchemaTypeOrArray::Multiple(types) => { - for t in types { - if !crate::database::object::is_primitive_type(t) { - parent_type_name = Some(t.split('.').next_back().unwrap_or(t).to_string()); - break; - } - } - } + } else if let Some(crate::database::object::SchemaTypeOrArray::Single(t)) = &self.obj.type_ { + // 3. Nested graphs trust their explicit struct pointer reference + if !crate::database::object::is_primitive_type(t) { + parent_type_name = Some(t.split('.').next_back().unwrap_or(t).to_string()); } } @@ -57,23 +46,13 @@ impl Schema { let mut is_array = false; // Structurally unpack the inner target entity if the object maps to an array list - if let Some(t_or_arr) = &prop_schema.obj.type_ { - match t_or_arr { - crate::database::object::SchemaTypeOrArray::Single(t) => { - if t == "array" { - is_array = true; - if let Some(items) = &prop_schema.obj.items { - target_schema = items.clone(); - } - } - } - crate::database::object::SchemaTypeOrArray::Multiple(types) => { - if types.contains(&"array".to_string()) { - is_array = true; - if let Some(items) = &prop_schema.obj.items { - target_schema = items.clone(); - } - } + if let Some(crate::database::object::SchemaTypeOrArray::Single(t)) = + &prop_schema.obj.type_ + { + if t == "array" { + is_array = true; + if let Some(items) = &prop_schema.obj.items { + target_schema = items.clone(); } } } @@ -81,21 +60,11 @@ impl Schema { // Determine the physical Postgres table backing the nested child schema recursively if let Some(family) = &target_schema.obj.family { child_type_name = Some(family.split('.').next_back().unwrap_or(family).to_string()); - } else if let Some(t_or_arr) = &target_schema.obj.type_ { - match t_or_arr { - crate::database::object::SchemaTypeOrArray::Single(t) => { - if !crate::database::object::is_primitive_type(t) { - child_type_name = Some(t.split('.').next_back().unwrap_or(t).to_string()); - } - } - crate::database::object::SchemaTypeOrArray::Multiple(types) => { - for t in types { - if !crate::database::object::is_primitive_type(t) { - child_type_name = Some(t.split('.').next_back().unwrap_or(t).to_string()); - break; - } - } - } + } else if let Some(crate::database::object::SchemaTypeOrArray::Single(t)) = + &target_schema.obj.type_ + { + if !crate::database::object::is_primitive_type(t) { + child_type_name = Some(t.split('.').next_back().unwrap_or(t).to_string()); } } else if let Some(arr) = &target_schema.obj.one_of { if let Some(first) = arr.first() { diff --git a/src/database/compile/mod.rs b/src/database/compile/mod.rs index a8ef6d5..26a3218 100644 --- a/src/database/compile/mod.rs +++ b/src/database/compile/mod.rs @@ -40,7 +40,6 @@ impl Schema { } let mut props = IndexMap::new(); - let mut inherited_edges = IndexMap::new(); // 1. Resolve INHERITANCE dependencies first if let Some(crate::database::object::SchemaTypeOrArray::Single(t)) = &self.obj.type_ { @@ -50,9 +49,6 @@ impl Schema { if let Some(p_props) = parent.obj.compiled_properties.get() { props.extend(p_props.clone()); } - if let Some(p_edges) = parent.obj.compiled_edges.get() { - inherited_edges.extend(p_edges.clone()); - } } } } @@ -81,12 +77,6 @@ impl Schema { if !crate::database::object::is_primitive_type(t) && !t.starts_with('$') { if let Some(parent) = db.schemas.get(t).cloned() { parent.as_ref().compile(db, t, t.clone(), errors); - if let Some(p_props) = parent.obj.compiled_properties.get() { - props.extend(p_props.clone()); - } - if let Some(p_edges) = parent.obj.compiled_edges.get() { - inherited_edges.extend(p_edges.clone()); - } } } } @@ -126,10 +116,7 @@ impl Schema { let _ = self.obj.compiled_property_names.set(names); // 5. Compute Edges natively - let mut schema_edges = self.compile_edges(db, root_id, &path, &props, errors); - for (k, v) in inherited_edges { - schema_edges.entry(k).or_insert(v); - } + let schema_edges = self.compile_edges(db, root_id, &path, &props, errors); let _ = self.obj.compiled_edges.set(schema_edges); // 5. Build our inline children properties recursively NOW! (Depth-first search) diff --git a/src/queryer/compiler.rs b/src/queryer/compiler.rs index eef1d87..8d9f8db 100644 --- a/src/queryer/compiler.rs +++ b/src/queryer/compiler.rs @@ -87,29 +87,13 @@ impl<'a> Compiler<'a> { .next_back() .unwrap_or(family_target); resolved_type = self.db.types.get(base_type_name); - } else if let Some(t_or_arr) = &items.obj.type_ { - match t_or_arr { - crate::database::object::SchemaTypeOrArray::Single(t) => { - if !crate::database::object::is_primitive_type(t) { - resolved_type = self - .db - .types - .get(&t.split('.').next_back().unwrap_or(t).to_string()); - } - } - crate::database::object::SchemaTypeOrArray::Multiple(types) => { - for t in types { - if !crate::database::object::is_primitive_type(t) { - resolved_type = self - .db - .types - .get(&t.split('.').next_back().unwrap_or(t).to_string()); - if resolved_type.is_some() { - break; - } - } - } - } + } else if let Some(crate::database::object::SchemaTypeOrArray::Single(t)) = &items.obj.type_ + { + if !crate::database::object::is_primitive_type(t) { + resolved_type = self + .db + .types + .get(&t.split('.').next_back().unwrap_or(t).to_string()); } } } @@ -137,6 +121,21 @@ impl<'a> Compiler<'a> { } fn compile_reference(&mut self, node: Node<'a>) -> Result<(String, String), String> { + // Handle Direct Refs via type pointer first + if let Some(crate::database::object::SchemaTypeOrArray::Single(t)) = &node.schema.obj.type_ { + if !crate::database::object::is_primitive_type(t) { + if !self.db.types.contains_key(t) { + // If it's just an ad-hoc struct ref, we should resolve it + if let Some(target_schema) = self.db.schemas.get(t).cloned() { + let mut ref_node = node.clone(); + ref_node.schema = target_schema.clone(); + ref_node.schema_id = Some(t.clone()); + return self.compile_node(ref_node); + } + } + } + } + // Determine if this schema represents a Database Entity let mut resolved_type = None; @@ -152,25 +151,12 @@ impl<'a> Compiler<'a> { .next_back() .unwrap_or(family_target); resolved_type = self.db.types.get(base_type_name); - } else if let Some(t_or_arr) = &node.schema.obj.type_ { - match t_or_arr { - crate::database::object::SchemaTypeOrArray::Single(t) => { - if !crate::database::object::is_primitive_type(t) { - let base_type_name = t.split('.').next_back().unwrap_or(t).to_string(); - resolved_type = self.db.types.get(&base_type_name); - } - } - crate::database::object::SchemaTypeOrArray::Multiple(types) => { - for t in types { - if !crate::database::object::is_primitive_type(t) { - let base_type_name = t.split('.').next_back().unwrap_or(t).to_string(); - resolved_type = self.db.types.get(&base_type_name); - if resolved_type.is_some() { - break; - } - } - } - } + } else if let Some(crate::database::object::SchemaTypeOrArray::Single(t)) = + &node.schema.obj.type_ + { + if !crate::database::object::is_primitive_type(t) { + let base_type_name = t.split('.').next_back().unwrap_or(t).to_string(); + resolved_type = self.db.types.get(&base_type_name); } } } @@ -179,16 +165,9 @@ impl<'a> Compiler<'a> { return self.compile_entity(type_def, node.clone(), false); } - // Handle Direct Refs via type pointer + // Fallback error if the schema pointer was unresolved if let Some(crate::database::object::SchemaTypeOrArray::Single(t)) = &node.schema.obj.type_ { if !crate::database::object::is_primitive_type(t) { - // If it's just an ad-hoc struct ref, we should resolve it - if let Some(target_schema) = self.db.schemas.get(t).cloned() { - let mut ref_node = node.clone(); - ref_node.schema = target_schema.clone(); - ref_node.schema_id = Some(t.clone()); - return self.compile_node(ref_node); - } return Err(format!("Unresolved schema type pointer: {}", t)); } } @@ -469,9 +448,6 @@ impl<'a> Compiler<'a> { Some(crate::database::object::SchemaTypeOrArray::Single(s)) => { !crate::database::object::is_primitive_type(s) } - Some(crate::database::object::SchemaTypeOrArray::Multiple(v)) => { - v.iter().any(|s| !crate::database::object::is_primitive_type(s)) - } _ => false, }; diff --git a/src/tests/mod.rs b/src/tests/mod.rs index 1c43021..fcee8b2 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -157,10 +157,7 @@ fn test_library_api() { } }, "name": { "type": ["string.condition", "null"] }, - "target": { - "type": ["target_schema.filter", "null"], - "compiledPropertyNames": ["value", "$and", "$or"] - }, + "target": { "type": ["target_schema.filter", "null"] }, "type": { "type": ["string.condition", "null"] } }, "type": "filter"