Compare commits

...

3 Commits

Author SHA1 Message Date
5b8596f7bd version: 1.0.179 2026-07-09 18:47:25 -04:00
6220a0964d fixed proxy references without null option not being treated as proxies 2026-07-09 18:47:12 -04:00
6cd5bfea7e doc update 2026-07-09 15:26:17 -04:00
8 changed files with 255 additions and 339 deletions

View File

@ -229,6 +229,42 @@ Traits are reusable, non-generating schema fragments used to share properties an
* **Scalars / Arrays / Items**: Host definitions completely override included traits. * **Scalars / Arrays / Items**: Host definitions completely override included traits.
* The `"include"` keyword is stripped, and `"traits"` maps are omitted from serialization. * The `"include"` keyword is stripped, and `"traits"` maps are omitted from serialization.
### Static Relation Constraints (Kind Constraints)
When modeling relational properties on a schema, a developer can define a specialized subset of a related table by applying static property constraints via the `const` or `enum` validation keywords.
For example, given a general `attachment` table containing a `kind` column (e.g. `'cover'`, `'thumbnail'`, `'document'`), you can define a `cover.attachment` schema that narrows the type using a static `const` assertion:
```json
"cover.attachment": {
"type": "attachment",
"properties": {
"kind": {
"const": "cover"
}
}
}
```
A parent entity can then define a relationship using this constrained schema under a local property name (e.g. `cover_attachment`):
```json
"cover_attachment": {
"properties": {
"cover_attachment": {
"type": [
"cover.attachment",
"null"
]
}
}
}
```
**What it does:**
1. **Validation (L1)**: During payload validation (`jspg_validate`), any incoming object mapped to the constrained property is validated against the static rules (e.g. throwing `CONST_VIOLATED` if `kind` is not `"cover"`).
2. **Query Generation (L0)**: When fetching data via `jspg_query`, the Queryer automatically detects the static constraint and compiles it into the SQL subquery's `WHERE` clause (e.g. adding `AND attachment_X.kind = 'cover'`). This produces a pre-filtered view of the related entities natively at the database level.
--- ---
## 3. Database ## 3. Database
@ -314,6 +350,8 @@ 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. * **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. * **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. * **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.
--- ---

View File

@ -211,6 +211,13 @@
"gender": {}, "gender": {},
"gender.condition": { "gender.condition": {
"type": "condition", "type": "condition",
"compiledPropertyNames": [
"kind",
"$eq",
"$ne",
"$of",
"$nof"
],
"properties": { "properties": {
"$eq": { "$eq": {
"type": [ "type": [
@ -224,15 +231,6 @@
"null" "null"
] ]
}, },
"$of": {
"type": [
"array",
"null"
],
"items": {
"type": "gender"
}
},
"$nof": { "$nof": {
"type": [ "type": [
"array", "array",
@ -241,158 +239,20 @@
"items": { "items": {
"type": "gender" "type": "gender"
} }
},
"$of": {
"type": [
"array",
"null"
],
"items": {
"type": "gender"
}
} }
}, }
"compiledPropertyNames": [
"kind",
"$eq",
"$ne",
"$of",
"$nof"
]
}, },
"person": {}, "person": {},
"person.filter": { "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": [ "compiledPropertyNames": [
"first_name", "first_name",
"age", "age",
@ -404,34 +264,133 @@
"ad_hoc", "ad_hoc",
"$and", "$and",
"$or" "$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": {},
"address.filter": { "address.filter": {
"type": "filter", "type": "filter",
"compiledPropertyNames": [
"city",
"$and",
"$or"
],
"properties": { "properties": {
"city": {
"type": [
"string.condition",
"null"
],
"compiledPropertyNames": [
"kind",
"$eq"
]
},
"$and": { "$and": {
"type": [ "type": [
"array", "array",
"null" "null"
], ],
"items": { "items": {
"type": "address.filter",
"compiledPropertyNames": [ "compiledPropertyNames": [
"city", "city",
"$and", "$and",
"$or" "$or"
] ],
"type": "address.filter"
} }
}, },
"$or": { "$or": {
@ -440,20 +399,21 @@
"null" "null"
], ],
"items": { "items": {
"type": "address.filter",
"compiledPropertyNames": [ "compiledPropertyNames": [
"city", "city",
"$and", "$and",
"$or" "$or"
] ],
"type": "address.filter"
} }
},
"city": {
"type": [
"string.condition",
"null"
]
} }
}, }
"compiledPropertyNames": [
"city",
"$and",
"$or"
]
}, },
"condition": {}, "condition": {},
"filter": {}, "filter": {},
@ -464,47 +424,28 @@
"search": {}, "search": {},
"search.filter": { "search.filter": {
"type": "filter", "type": "filter",
"compiledPropertyNames": [
"kind",
"name",
"filter",
"$and",
"$or"
],
"properties": { "properties": {
"kind": {
"type": [
"string.condition",
"null"
],
"compiledPropertyNames": [
"kind",
"$eq"
]
},
"name": {
"type": [
"string.condition",
"null"
],
"compiledPropertyNames": [
"kind",
"$eq"
]
},
"filter": {
"type": [
"$kind.filter",
"null"
]
},
"$and": { "$and": {
"type": [ "type": [
"array", "array",
"null" "null"
], ],
"items": { "items": {
"type": "search.filter",
"compiledPropertyNames": [ "compiledPropertyNames": [
"kind", "kind",
"name", "name",
"filter", "filter",
"$and", "$and",
"$or" "$or"
] ],
"type": "search.filter"
} }
}, },
"$or": { "$or": {
@ -513,24 +454,35 @@
"null" "null"
], ],
"items": { "items": {
"type": "search.filter",
"compiledPropertyNames": [ "compiledPropertyNames": [
"kind", "kind",
"name", "name",
"filter", "filter",
"$and", "$and",
"$or" "$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"
]
} }
} }
} }

View File

@ -2547,10 +2547,7 @@
"type": "pet", "type": "pet",
"properties": { "properties": {
"cover_attachment": { "cover_attachment": {
"type": [ "type": "attachment",
"attachment",
"null"
],
"properties": { "properties": {
"kind": { "kind": {
"const": "cover" "const": "cover"

View File

@ -29,21 +29,10 @@ impl Schema {
if db.types.contains_key(&base_type_name) { if db.types.contains_key(&base_type_name) {
parent_type_name = Some(base_type_name); parent_type_name = Some(base_type_name);
} }
} else if let Some(t_or_arr) = &self.obj.type_ { } else if let Some(crate::database::object::SchemaTypeOrArray::Single(t)) = &self.obj.type_ {
match t_or_arr { // 3. Nested graphs trust their explicit struct pointer reference
crate::database::object::SchemaTypeOrArray::Single(t) => { if !crate::database::object::is_primitive_type(t) {
if !crate::database::object::is_primitive_type(t) { parent_type_name = Some(t.split('.').next_back().unwrap_or(t).to_string());
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;
}
}
}
} }
} }
@ -57,23 +46,13 @@ impl Schema {
let mut is_array = false; let mut is_array = false;
// Structurally unpack the inner target entity if the object maps to an array list // Structurally unpack the inner target entity if the object maps to an array list
if let Some(t_or_arr) = &prop_schema.obj.type_ { if let Some(crate::database::object::SchemaTypeOrArray::Single(t)) =
match t_or_arr { &prop_schema.obj.type_
crate::database::object::SchemaTypeOrArray::Single(t) => { {
if t == "array" { if t == "array" {
is_array = true; is_array = true;
if let Some(items) = &prop_schema.obj.items { if let Some(items) = &prop_schema.obj.items {
target_schema = items.clone(); 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();
}
}
} }
} }
} }
@ -81,21 +60,11 @@ impl Schema {
// Determine the physical Postgres table backing the nested child schema recursively // Determine the physical Postgres table backing the nested child schema recursively
if let Some(family) = &target_schema.obj.family { if let Some(family) = &target_schema.obj.family {
child_type_name = Some(family.split('.').next_back().unwrap_or(family).to_string()); child_type_name = Some(family.split('.').next_back().unwrap_or(family).to_string());
} else if let Some(t_or_arr) = &target_schema.obj.type_ { } else if let Some(crate::database::object::SchemaTypeOrArray::Single(t)) =
match t_or_arr { &target_schema.obj.type_
crate::database::object::SchemaTypeOrArray::Single(t) => { {
if !crate::database::object::is_primitive_type(t) { if !crate::database::object::is_primitive_type(t) {
child_type_name = Some(t.split('.').next_back().unwrap_or(t).to_string()); 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(arr) = &target_schema.obj.one_of { } else if let Some(arr) = &target_schema.obj.one_of {
if let Some(first) = arr.first() { if let Some(first) = arr.first() {

View File

@ -40,7 +40,6 @@ impl Schema {
} }
let mut props = IndexMap::new(); let mut props = IndexMap::new();
let mut inherited_edges = IndexMap::new();
// 1. Resolve INHERITANCE dependencies first // 1. Resolve INHERITANCE dependencies first
if let Some(crate::database::object::SchemaTypeOrArray::Single(t)) = &self.obj.type_ { 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() { if let Some(p_props) = parent.obj.compiled_properties.get() {
props.extend(p_props.clone()); 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 !crate::database::object::is_primitive_type(t) && !t.starts_with('$') {
if let Some(parent) = db.schemas.get(t).cloned() { if let Some(parent) = db.schemas.get(t).cloned() {
parent.as_ref().compile(db, t, t.clone(), errors); 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); let _ = self.obj.compiled_property_names.set(names);
// 5. Compute Edges natively // 5. Compute Edges natively
let mut schema_edges = self.compile_edges(db, root_id, &path, &props, errors); let 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 _ = self.obj.compiled_edges.set(schema_edges); let _ = self.obj.compiled_edges.set(schema_edges);
// 5. Build our inline children properties recursively NOW! (Depth-first search) // 5. Build our inline children properties recursively NOW! (Depth-first search)

View File

@ -87,29 +87,13 @@ impl<'a> Compiler<'a> {
.next_back() .next_back()
.unwrap_or(family_target); .unwrap_or(family_target);
resolved_type = self.db.types.get(base_type_name); resolved_type = self.db.types.get(base_type_name);
} else if let Some(t_or_arr) = &items.obj.type_ { } else if let Some(crate::database::object::SchemaTypeOrArray::Single(t)) = &items.obj.type_
match t_or_arr { {
crate::database::object::SchemaTypeOrArray::Single(t) => { if !crate::database::object::is_primitive_type(t) {
if !crate::database::object::is_primitive_type(t) { resolved_type = self
resolved_type = self .db
.db .types
.types .get(&t.split('.').next_back().unwrap_or(t).to_string());
.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;
}
}
}
}
} }
} }
} }
@ -137,6 +121,21 @@ impl<'a> Compiler<'a> {
} }
fn compile_reference(&mut self, node: Node<'a>) -> Result<(String, String), String> { 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 // Determine if this schema represents a Database Entity
let mut resolved_type = None; let mut resolved_type = None;
@ -152,25 +151,12 @@ impl<'a> Compiler<'a> {
.next_back() .next_back()
.unwrap_or(family_target); .unwrap_or(family_target);
resolved_type = self.db.types.get(base_type_name); resolved_type = self.db.types.get(base_type_name);
} else if let Some(t_or_arr) = &node.schema.obj.type_ { } else if let Some(crate::database::object::SchemaTypeOrArray::Single(t)) =
match t_or_arr { &node.schema.obj.type_
crate::database::object::SchemaTypeOrArray::Single(t) => { {
if !crate::database::object::is_primitive_type(t) { if !crate::database::object::is_primitive_type(t) {
let base_type_name = t.split('.').next_back().unwrap_or(t).to_string(); let base_type_name = t.split('.').next_back().unwrap_or(t).to_string();
resolved_type = self.db.types.get(&base_type_name); 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;
}
}
}
}
} }
} }
} }
@ -179,16 +165,9 @@ impl<'a> Compiler<'a> {
return self.compile_entity(type_def, node.clone(), false); 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 let Some(crate::database::object::SchemaTypeOrArray::Single(t)) = &node.schema.obj.type_ {
if !crate::database::object::is_primitive_type(t) { 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)); return Err(format!("Unresolved schema type pointer: {}", t));
} }
} }
@ -469,9 +448,6 @@ impl<'a> Compiler<'a> {
Some(crate::database::object::SchemaTypeOrArray::Single(s)) => { Some(crate::database::object::SchemaTypeOrArray::Single(s)) => {
!crate::database::object::is_primitive_type(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, _ => false,
}; };

View File

@ -157,10 +157,7 @@ fn test_library_api() {
} }
}, },
"name": { "type": ["string.condition", "null"] }, "name": { "type": ["string.condition", "null"] },
"target": { "target": { "type": ["target_schema.filter", "null"] },
"type": ["target_schema.filter", "null"],
"compiledPropertyNames": ["value", "$and", "$or"]
},
"type": { "type": ["string.condition", "null"] } "type": { "type": ["string.condition", "null"] }
}, },
"type": "filter" "type": "filter"

View File

@ -1 +1 @@
1.0.178 1.0.179