Compare commits

..

9 Commits
1.0.98 ... main

Author SHA1 Message Date
29d8dfb608 flow update 2026-03-28 16:51:26 -04:00
5b36ecf06c doc update and more code comments 2026-03-27 19:25:15 -04:00
76467a6fed log cleanup 2026-03-27 19:19:27 -04:00
930d0513cd version: 1.0.101 2026-03-27 19:14:26 -04:00
cad651dbd8 version: 1.0.100 2026-03-27 19:14:08 -04:00
ea9ac8469c maybe working 2026-03-27 19:14:02 -04:00
ebcdb661fa maybe working 2026-03-27 19:13:44 -04:00
c893e29c59 version: 1.0.99 2026-03-27 18:02:29 -04:00
7523431007 test pgrx no fixes 2026-03-27 18:02:24 -04:00
10 changed files with 459 additions and 356 deletions

View File

@ -24,10 +24,10 @@ To support high-throughput operations while allowing for runtime updates (e.g.,
4. **Lock-Free Reads**: Incoming operations acquire a read lock just long enough to clone the `Arc` inside an `RwLock<Option<Arc<Validator>>>`, ensuring zero blocking during schema updates. 4. **Lock-Free Reads**: Incoming operations acquire a read lock just long enough to clone the `Arc` inside an `RwLock<Option<Arc<Validator>>>`, ensuring zero blocking during schema updates.
### Relational Edge Resolution ### Relational Edge Resolution
When compiling nested object graphs or arrays, the JSPG engine must dynamically infer which Postgres Foreign Key constraint correctly bridges the parent to the nested schema. It utilizes a strict 3-step hierarchical resolution: When compiling nested object graphs or arrays, the JSPG engine must dynamically infer which Postgres Foreign Key constraint correctly bridges the parent to the nested schema. It utilizes a strict 3-step hierarchical resolution applied during the `OnceLock` Compilation phase:
1. **Direct Prefix Match**: If an explicitly prefixed Foreign Key (e.g. `fk_invoice_counterparty_entity` -> `prefix: "counterparty"`) matches the exact name of the requested schema property (e.g. `{"counterparty": {...}}`), it is instantly selected. 1. **Exact Prefix Match**: If an explicitly prefixed Foreign Key (e.g. `fk_invoice_counterparty_entity` -> `prefix: "counterparty"`) directly matches the name of the requested schema property (e.g. `{"counterparty": {...}}`), it is instantly selected.
2. **Base Edge Fallback (1:M)**: If no explicit prefix directly matches the property name, the compiler filters for explicitly one remaining relation with a `null` prefix (e.g. `fk_invoice_line_invoice` -> `prefix: null`). A `null` prefix mathematically denotes the standard structural parent-child ownership edge (bypassing any M:M ambiguity) and is safely picked over explicit (but unmatched) property edges. 2. **Ambiguity Elimination (M:M Twin Deduction)**: If multiple explicitly prefixed relations remain (which happens by design in Many-to-Many junction tables like `contact` or `role`), the compiler uses a process of elimination. It inspects the compiled child JSON schema AST to see which of the relational prefixes the child *natively consumes* as an explicit outbound property (e.g. `contact` natively defines `{ "target": ... }`). It considers that prefix arrow "used up" by the child, and mathematically deduces that its exact twin providing reverse ownership (`"source"`) MUST be the inbound link mapping from the parent. This logic relies on `OnceLock` recursive compilation to accurately peek at child structures.
3. **Ambiguity Elimination (M:M)**: If multiple explicitly prefixed relations remain (which happens by design in Many-to-Many junction tables like `contact` utilizing `fk_relationship_source` and `fk_relationship_target`), the compiler uses a process of elimination. It checks which of the prefix names the child schema *natively consumes* as an outbound property (e.g. `contact` defines `{ "target": ... }`). It considers that prefix "used up" and mathematically deduces the *remaining* explicitly prefixed relation (`"source"`) must be the inbound link from the parent. 3. **Implicit Base Fallback (1:M)**: If no explicit prefix matches, and M:M deduction fails, the compiler filters for exactly one remaining relation with a `null` prefix (e.g. `fk_invoice_line_invoice` -> `prefix: null`). A `null` prefix mathematically denotes the core structural parent-child ownership edge and is safely used as a fallback.
### Global API Reference ### Global API Reference
These functions operate on the global `GLOBAL_JSPG` engine instance and provide administrative boundaries: These functions operate on the global `GLOBAL_JSPG` engine instance and provide administrative boundaries:

View File

@ -19,7 +19,7 @@
{ {
"id": "22222222-2222-2222-2222-222222222222", "id": "22222222-2222-2222-2222-222222222222",
"type": "relation", "type": "relation",
"constraint": "fk_order_customer", "constraint": "fk_order_customer_person",
"source_type": "order", "source_type": "order",
"source_columns": [ "source_columns": [
"customer_id" "customer_id"
@ -41,8 +41,7 @@
"destination_type": "order", "destination_type": "order",
"destination_columns": [ "destination_columns": [
"id" "id"
], ]
"prefix": "lines"
}, },
{ {
"id": "44444444-4444-4444-4444-444444444444", "id": "44444444-4444-4444-4444-444444444444",
@ -75,6 +74,20 @@
"type" "type"
], ],
"prefix": "target" "prefix": "target"
},
{
"id": "66666666-6666-6666-6666-666666666666",
"type": "relation",
"constraint": "fk_entity_organization",
"source_type": "entity",
"source_columns": [
"organization_id"
],
"destination_type": "organization",
"destination_columns": [
"id"
],
"prefix": null
} }
], ],
"types": [ "types": [
@ -283,6 +296,17 @@
} }
} }
} }
},
"email_addresses": {
"type": "array",
"items": {
"$ref": "contact",
"properties": {
"target": {
"$ref": "email_address"
}
}
}
} }
} }
} }
@ -1834,16 +1858,18 @@
"type": "contact", "type": "contact",
"is_primary": false, "is_primary": false,
"target": { "target": {
"type": "phone_number", "type": "email_address",
"number": "555-0002" "address": "test@example.com"
} }
}, }
],
"email_addresses": [
{ {
"type": "contact", "type": "contact",
"is_primary": false, "is_primary": false,
"target": { "target": {
"type": "email_address", "type": "email_address",
"address": "test@example.com" "address": "test2@example.com"
} }
} }
] ]
@ -1935,7 +1961,10 @@
" modified_by", " modified_by",
") VALUES (", ") VALUES (",
" NULL,", " NULL,",
" '{\"number\":\"555-0001\",\"type\":\"phone_number\"}',", " '{",
" \"number\":\"555-0001\",",
" \"type\":\"phone_number\"",
" }',",
" '{{uuid:phone1_id}}',", " '{{uuid:phone1_id}}',",
" '{{uuid}}',", " '{{uuid}}',",
" 'create',", " 'create',",
@ -2006,115 +2035,6 @@
" '00000000-0000-0000-0000-000000000000'", " '00000000-0000-0000-0000-000000000000'",
")" ")"
], ],
[
"INSERT INTO agreego.\"entity\" (",
" \"created_at\",",
" \"created_by\",",
" \"id\",",
" \"modified_at\",",
" \"modified_by\",",
" \"type\"",
") VALUES (",
" '{{timestamp}}',",
" '00000000-0000-0000-0000-000000000000',",
" '{{uuid:phone2_id}}',",
" '{{timestamp}}',",
" '00000000-0000-0000-0000-000000000000',",
" 'phone_number'",
")"
],
[
"INSERT INTO agreego.\"phone_number\" (",
" \"number\"",
") VALUES (",
" '555-0002'",
")"
],
[
"INSERT INTO agreego.change (",
" \"old\",",
" \"new\",",
" entity_id,",
" id,",
" kind,",
" modified_at,",
" modified_by",
") VALUES (",
" NULL,",
" '{",
" \"number\":\"555-0002\",",
" \"type\":\"phone_number\"",
" }',",
" '{{uuid:phone2_id}}',",
" '{{uuid}}',",
" 'create',",
" '{{timestamp}}',",
" '00000000-0000-0000-0000-000000000000'",
")"
],
[
"INSERT INTO agreego.\"entity\" (",
" \"created_at\",",
" \"created_by\",",
" \"id\",",
" \"modified_at\",",
" \"modified_by\",",
" \"type\"",
") VALUES (",
" '{{timestamp}}',",
" '00000000-0000-0000-0000-000000000000',",
" '{{uuid:contact2_id}}',",
" '{{timestamp}}',",
" '00000000-0000-0000-0000-000000000000',",
" 'contact'",
")"
],
[
"INSERT INTO agreego.\"relationship\" (",
" \"source_id\",",
" \"source_type\",",
" \"target_id\",",
" \"target_type\"",
") VALUES (",
" '{{uuid:person_id}}',",
" 'person',",
" '{{uuid:phone2_id}}',",
" 'phone_number'",
")"
],
[
"INSERT INTO agreego.\"contact\" (",
" \"is_primary\"",
") VALUES (",
" false",
")"
],
[
"INSERT INTO agreego.change (",
" \"old\",",
" \"new\",",
" entity_id,",
" id,",
" kind,",
" modified_at,",
" modified_by",
") VALUES (",
" NULL,",
" '{",
" \"is_primary\":false,",
" \"source_id\":\"{{uuid:person_id}}\",",
" \"source_type\":\"person\",",
" \"target_id\":\"{{uuid:phone2_id}}\",",
" \"target_type\":\"phone_number\",",
" \"type\":\"contact\"",
" }',",
" '{{uuid:contact2_id}}',",
" '{{uuid}}',",
" 'create',",
" '{{timestamp}}',",
" '00000000-0000-0000-0000-000000000000'",
")"
],
[ [
"INSERT INTO agreego.\"entity\" (", "INSERT INTO agreego.\"entity\" (",
" \"created_at\",", " \"created_at\",",
@ -2172,7 +2092,7 @@
") VALUES (", ") VALUES (",
" '{{timestamp}}',", " '{{timestamp}}',",
" '00000000-0000-0000-0000-000000000000',", " '00000000-0000-0000-0000-000000000000',",
" '{{uuid:contact3_id}}',", " '{{uuid:contact2_id}}',",
" '{{timestamp}}',", " '{{timestamp}}',",
" '00000000-0000-0000-0000-000000000000',", " '00000000-0000-0000-0000-000000000000',",
" 'contact'", " 'contact'",
@ -2217,6 +2137,115 @@
" \"target_type\":\"email_address\",", " \"target_type\":\"email_address\",",
" \"type\":\"contact\"", " \"type\":\"contact\"",
" }',", " }',",
" '{{uuid:contact2_id}}',",
" '{{uuid}}',",
" 'create',",
" '{{timestamp}}',",
" '00000000-0000-0000-0000-000000000000'",
")"
],
[
"INSERT INTO agreego.\"entity\" (",
" \"created_at\",",
" \"created_by\",",
" \"id\",",
" \"modified_at\",",
" \"modified_by\",",
" \"type\"",
") VALUES (",
" '{{timestamp}}',",
" '00000000-0000-0000-0000-000000000000',",
" '{{uuid:email2_id}}',",
" '{{timestamp}}',",
" '00000000-0000-0000-0000-000000000000',",
" 'email_address'",
")"
],
[
"INSERT INTO agreego.\"email_address\" (",
" \"address\"",
") VALUES (",
" 'test2@example.com'",
")"
],
[
"INSERT INTO agreego.change (",
" \"old\",",
" \"new\",",
" entity_id,",
" id,",
" kind,",
" modified_at,",
" modified_by",
") VALUES (",
" NULL,",
" '{",
" \"address\":\"test2@example.com\",",
" \"type\":\"email_address\"",
" }',",
" '{{uuid:email2_id}}',",
" '{{uuid}}',",
" 'create',",
" '{{timestamp}}',",
" '00000000-0000-0000-0000-000000000000'",
")"
],
[
"INSERT INTO agreego.\"entity\" (",
" \"created_at\",",
" \"created_by\",",
" \"id\",",
" \"modified_at\",",
" \"modified_by\",",
" \"type\"",
") VALUES (",
" '{{timestamp}}',",
" '00000000-0000-0000-0000-000000000000',",
" '{{uuid:contact3_id}}',",
" '{{timestamp}}',",
" '00000000-0000-0000-0000-000000000000',",
" 'contact'",
")"
],
[
"INSERT INTO agreego.\"relationship\" (",
" \"source_id\",",
" \"source_type\",",
" \"target_id\",",
" \"target_type\"",
") VALUES (",
" '{{uuid:person_id}}',",
" 'person',",
" '{{uuid:email2_id}}',",
" 'email_address'",
")"
],
[
"INSERT INTO agreego.\"contact\" (",
" \"is_primary\"",
") VALUES (",
" false",
")"
],
[
"INSERT INTO agreego.change (",
" \"old\",",
" \"new\",",
" entity_id,",
" id,",
" kind,",
" modified_at,",
" modified_by",
") VALUES (",
" NULL,",
" '{",
" \"is_primary\":false,",
" \"source_id\":\"{{uuid:person_id}}\",",
" \"source_type\":\"person\",",
" \"target_id\":\"{{uuid:email2_id}}\",",
" \"target_type\":\"email_address\",",
" \"type\":\"contact\"",
" }',",
" '{{uuid:contact3_id}}',", " '{{uuid:contact3_id}}',",
" '{{uuid}}',", " '{{uuid}}',",
" 'create',", " 'create',",
@ -2319,48 +2348,6 @@
" \"modified_by\":\"00000000-0000-0000-0000-000000000000\",", " \"modified_by\":\"00000000-0000-0000-0000-000000000000\",",
" \"source_id\":\"{{uuid:person_id}}\",", " \"source_id\":\"{{uuid:person_id}}\",",
" \"source_type\":\"person\",", " \"source_type\":\"person\",",
" \"target_id\":\"{{uuid:phone2_id}}\",",
" \"target_type\":\"phone_number\",",
" \"type\":\"contact\"",
" },",
" \"new\":{",
" \"is_primary\":false,",
" \"source_id\":\"{{uuid:person_id}}\",",
" \"source_type\":\"person\",",
" \"target_id\":\"{{uuid:phone2_id}}\",",
" \"target_type\":\"phone_number\",",
" \"type\":\"contact\"",
" }",
" }')"
],
[
"SELECT pg_notify('entity', '{",
" \"complete\":{",
" \"created_at\":\"{{timestamp}}\",",
" \"created_by\":\"00000000-0000-0000-0000-000000000000\",",
" \"id\":\"{{uuid:phone2_id}}\",",
" \"modified_at\":\"{{timestamp}}\",",
" \"modified_by\":\"00000000-0000-0000-0000-000000000000\",",
" \"number\":\"555-0002\",",
" \"type\":\"phone_number\"",
" },",
" \"new\":{",
" \"number\":\"555-0002\",",
" \"type\":\"phone_number\"",
" }",
" }')"
],
[
"SELECT pg_notify('entity', '{",
" \"complete\":{",
" \"created_at\":\"{{timestamp}}\",",
" \"created_by\":\"00000000-0000-0000-0000-000000000000\",",
" \"id\":\"{{uuid:contact3_id}}\",",
" \"is_primary\":false,",
" \"modified_at\":\"{{timestamp}}\",",
" \"modified_by\":\"00000000-0000-0000-0000-000000000000\",",
" \"source_id\":\"{{uuid:person_id}}\",",
" \"source_type\":\"person\",",
" \"target_id\":\"{{uuid:email1_id}}\",", " \"target_id\":\"{{uuid:email1_id}}\",",
" \"target_type\":\"email_address\",", " \"target_type\":\"email_address\",",
" \"type\":\"contact\"", " \"type\":\"contact\"",
@ -2391,6 +2378,48 @@
" \"type\":\"email_address\"", " \"type\":\"email_address\"",
" }", " }",
"}')" "}')"
],
[
"SELECT pg_notify('entity', '{",
" \"complete\":{",
" \"created_at\":\"{{timestamp}}\",",
" \"created_by\":\"00000000-0000-0000-0000-000000000000\",",
" \"id\":\"{{uuid:contact3_id}}\",",
" \"is_primary\":false,",
" \"modified_at\":\"{{timestamp}}\",",
" \"modified_by\":\"00000000-0000-0000-0000-000000000000\",",
" \"source_id\":\"{{uuid:person_id}}\",",
" \"source_type\":\"person\",",
" \"target_id\":\"{{uuid:email2_id}}\",",
" \"target_type\":\"email_address\",",
" \"type\":\"contact\"",
" },",
" \"new\":{",
" \"is_primary\":false,",
" \"source_id\":\"{{uuid:person_id}}\",",
" \"source_type\":\"person\",",
" \"target_id\":\"{{uuid:email2_id}}\",",
" \"target_type\":\"email_address\",",
" \"type\":\"contact\"",
" }",
"}')"
],
[
"SELECT pg_notify('entity', '{",
" \"complete\":{",
" \"address\":\"test2@example.com\",",
" \"created_at\":\"{{timestamp}}\",",
" \"created_by\":\"00000000-0000-0000-0000-000000000000\",",
" \"id\":\"{{uuid:email2_id}}\",",
" \"modified_at\":\"{{timestamp}}\",",
" \"modified_by\":\"00000000-0000-0000-0000-000000000000\",",
" \"type\":\"email_address\"",
" },",
" \"new\":{",
" \"address\":\"test2@example.com\",",
" \"type\":\"email_address\"",
" }",
"}')"
] ]
] ]
} }

View File

@ -71,7 +71,7 @@
{ {
"id": "22222222-2222-2222-2222-222222222222", "id": "22222222-2222-2222-2222-222222222222",
"type": "relation", "type": "relation",
"constraint": "fk_order_customer", "constraint": "fk_order_customer_person",
"source_type": "order", "source_type": "order",
"source_columns": [ "source_columns": [
"customer_id" "customer_id"
@ -79,7 +79,8 @@
"destination_type": "person", "destination_type": "person",
"destination_columns": [ "destination_columns": [
"id" "id"
] ],
"prefix": "customer"
}, },
{ {
"id": "22222222-2222-2222-2222-222222222227", "id": "22222222-2222-2222-2222-222222222227",

2
flows

Submodule flows updated: a7b0f5dc4d...4d61e13e00

View File

@ -45,7 +45,7 @@ impl MockExecutor {
#[cfg(test)] #[cfg(test)]
impl DatabaseExecutor for MockExecutor { impl DatabaseExecutor for MockExecutor {
fn query(&self, sql: &str, _args: Option<&[Value]>) -> Result<Value, String> { fn query(&self, sql: &str, _args: Option<&[Value]>) -> Result<Value, String> {
println!("DEBUG SQL QUERY: {}", sql); println!("JSPG_SQL: {}", sql);
MOCK_STATE.with(|state| { MOCK_STATE.with(|state| {
let mut s = state.borrow_mut(); let mut s = state.borrow_mut();
s.captured_queries.push(sql.to_string()); s.captured_queries.push(sql.to_string());
@ -66,7 +66,7 @@ impl DatabaseExecutor for MockExecutor {
} }
fn execute(&self, sql: &str, _args: Option<&[Value]>) -> Result<(), String> { fn execute(&self, sql: &str, _args: Option<&[Value]>) -> Result<(), String> {
println!("DEBUG SQL EXECUTE: {}", sql); println!("JSPG_SQL: {}", sql);
MOCK_STATE.with(|state| { MOCK_STATE.with(|state| {
let mut s = state.borrow_mut(); let mut s = state.borrow_mut();
s.captured_queries.push(sql.to_string()); s.captured_queries.push(sql.to_string());

View File

@ -79,9 +79,9 @@ impl DatabaseExecutor for SpiExecutor {
} }
} }
pgrx::debug1!("JSPG_SQL: {}", sql);
self.transact(|| { self.transact(|| {
Spi::connect(|client| { Spi::connect(|client| {
pgrx::notice!("JSPG_SQL: {}", sql);
match client.select(sql, Some(args_with_oid.len() as i64), &args_with_oid) { match client.select(sql, Some(args_with_oid.len() as i64), &args_with_oid) {
Ok(tup_table) => { Ok(tup_table) => {
let mut results = Vec::new(); let mut results = Vec::new();
@ -110,9 +110,9 @@ impl DatabaseExecutor for SpiExecutor {
} }
} }
pgrx::debug1!("JSPG_SQL: {}", sql);
self.transact(|| { self.transact(|| {
Spi::connect_mut(|client| { Spi::connect_mut(|client| {
pgrx::notice!("JSPG_SQL: {}", sql);
match client.update(sql, Some(args_with_oid.len() as i64), &args_with_oid) { match client.update(sql, Some(args_with_oid.len() as i64), &args_with_oid) {
Ok(_) => Ok(()), Ok(_) => Ok(()),
Err(e) => Err(format!("SPI Execution Failure: {}", e)), Err(e) => Err(format!("SPI Execution Failure: {}", e)),

View File

@ -497,6 +497,10 @@ impl Schema {
Ok(()) Ok(())
} }
/// Dynamically infers and compiles all structural database relationships between this Schema
/// and its nested children. This functions recursively traverses the JSON Schema abstract syntax
/// tree, identifies physical PostgreSQL table boundaries, and locks the resulting relation
/// constraint paths directly onto the `compiled_edges` map in O(1) memory.
pub fn compile_edges( pub fn compile_edges(
&self, &self,
db: &crate::database::Database, db: &crate::database::Database,
@ -504,19 +508,25 @@ impl Schema {
props: &std::collections::BTreeMap<String, std::sync::Arc<Schema>>, props: &std::collections::BTreeMap<String, std::sync::Arc<Schema>>,
) -> std::collections::BTreeMap<String, crate::database::edge::Edge> { ) -> std::collections::BTreeMap<String, crate::database::edge::Edge> {
let mut schema_edges = std::collections::BTreeMap::new(); let mut schema_edges = std::collections::BTreeMap::new();
// Determine the physical Database Table Name this schema structurally represents
// Plucks the polymorphic discriminator via dot-notation (e.g. extracting "person" from "full.person")
let mut parent_type_name = None; let mut parent_type_name = None;
if let Some(family) = &self.obj.family { if let Some(family) = &self.obj.family {
parent_type_name = Some(family.split('.').next_back().unwrap_or(family).to_string()); parent_type_name = Some(family.split('.').next_back().unwrap_or(family).to_string());
} else if let Some(identifier) = self.obj.identifier() { } else if let Some(identifier) = self.obj.identifier() {
parent_type_name = Some(identifier); parent_type_name = Some(identifier.split('.').next_back().unwrap_or(&identifier).to_string());
} }
if let Some(p_type) = parent_type_name { if let Some(p_type) = parent_type_name {
// Proceed only if the resolved table physically exists within the Postgres Type hierarchy
if db.types.contains_key(&p_type) { if db.types.contains_key(&p_type) {
// Iterate over all discovered schema boundaries mapped inside the object
for (prop_name, prop_schema) in props { for (prop_name, prop_schema) in props {
let mut child_type_name = None; let mut child_type_name = None;
let mut target_schema = prop_schema.clone(); let mut target_schema = prop_schema.clone();
// Structurally unpack the inner target entity if the object maps to an array list
if let Some(crate::database::schema::SchemaTypeOrArray::Single(t)) = if let Some(crate::database::schema::SchemaTypeOrArray::Single(t)) =
&prop_schema.obj.type_ &prop_schema.obj.type_
{ {
@ -527,24 +537,29 @@ impl Schema {
} }
} }
// 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(ref_id) = target_schema.obj.identifier() { } else if let Some(ref_id) = target_schema.obj.identifier() {
child_type_name = Some(ref_id); child_type_name = Some(ref_id.split('.').next_back().unwrap_or(&ref_id).to_string());
} 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() {
if let Some(ref_id) = first.obj.identifier() { if let Some(ref_id) = first.obj.identifier() {
child_type_name = Some(ref_id); child_type_name = Some(ref_id.split('.').next_back().unwrap_or(&ref_id).to_string());
} }
} }
} }
if let Some(c_type) = child_type_name { if let Some(c_type) = child_type_name {
if db.types.contains_key(&c_type) { if db.types.contains_key(&c_type) {
// Ensure the child Schema's AST has accurately compiled its own physical property keys so we can
// inject them securely for Many-to-Many Twin Deduction disambiguation matching.
target_schema.compile(db, visited); target_schema.compile(db, visited);
if let Some(compiled_target_props) = target_schema.obj.compiled_properties.get() { if let Some(compiled_target_props) = target_schema.obj.compiled_properties.get() {
let keys_for_ambiguity: Vec<String> = let keys_for_ambiguity: Vec<String> =
compiled_target_props.keys().cloned().collect(); compiled_target_props.keys().cloned().collect();
// Interrogate the Database catalog graph to discover the exact Foreign Key Constraint connecting the components
if let Some((relation, is_forward)) = if let Some((relation, is_forward)) =
resolve_relation(db, &p_type, &c_type, prop_name, Some(&keys_for_ambiguity)) resolve_relation(db, &p_type, &c_type, prop_name, Some(&keys_for_ambiguity))
{ {
@ -566,6 +581,8 @@ impl Schema {
} }
} }
/// Inspects the Postgres pg_constraint relations catalog to securely identify
/// the precise Foreign Key connecting a parent and child hierarchy path.
pub(crate) fn resolve_relation<'a>( pub(crate) fn resolve_relation<'a>(
db: &'a crate::database::Database, db: &'a crate::database::Database,
parent_type: &str, parent_type: &str,
@ -573,6 +590,8 @@ pub(crate) fn resolve_relation<'a>(
prop_name: &str, prop_name: &str,
relative_keys: Option<&Vec<String>>, relative_keys: Option<&Vec<String>>,
) -> Option<(&'a crate::database::relation::Relation, bool)> { ) -> Option<(&'a crate::database::relation::Relation, bool)> {
// Enforce graph locality by ensuring we don't accidentally crawl to pure structural entity boundaries
if parent_type == "entity" && child_type == "entity" { if parent_type == "entity" && child_type == "entity" {
return None; return None;
} }
@ -583,6 +602,9 @@ pub(crate) fn resolve_relation<'a>(
let mut matching_rels = Vec::new(); let mut matching_rels = Vec::new();
let mut directions = Vec::new(); let mut directions = Vec::new();
// Scour the complete catalog for any Edge matching the inheritance scope of the two objects
// This automatically binds polymorphic structures (e.g. recognizing a relationship targeting User
// also natively binds instances specifically typed as Person).
for rel in db.relations.values() { for rel in db.relations.values() {
let is_forward = p_def.hierarchy.contains(&rel.source_type) let is_forward = p_def.hierarchy.contains(&rel.source_type)
&& c_def.hierarchy.contains(&rel.destination_type); && c_def.hierarchy.contains(&rel.destination_type);
@ -598,10 +620,12 @@ pub(crate) fn resolve_relation<'a>(
} }
} }
// Abort relation discovery early if no hierarchical inheritance match was found
if matching_rels.is_empty() { if matching_rels.is_empty() {
return None; return None;
} }
// Ideal State: The objects only share a solitary structural relation, resolving ambiguity instantly.
if matching_rels.len() == 1 { if matching_rels.len() == 1 {
return Some((matching_rels[0], directions[0])); return Some((matching_rels[0], directions[0]));
} }
@ -609,6 +633,8 @@ pub(crate) fn resolve_relation<'a>(
let mut chosen_idx = 0; let mut chosen_idx = 0;
let mut resolved = false; let mut resolved = false;
// Exact Prefix Disambiguation: Determine if the database specifically names this constraint
// directly mapping to the JSON Schema property name (e.g., `fk_{child}_{property_name}`)
for (i, rel) in matching_rels.iter().enumerate() { for (i, rel) in matching_rels.iter().enumerate() {
if let Some(prefix) = &rel.prefix { if let Some(prefix) = &rel.prefix {
if prop_name.starts_with(prefix) if prop_name.starts_with(prefix)
@ -622,8 +648,47 @@ pub(crate) fn resolve_relation<'a>(
} }
} }
// Complex Subgraph Resolution: The database contains multiple equally explicit foreign key constraints
// linking these objects (such as pointing to `source` and `target` in Many-to-Many junction models).
if !resolved && relative_keys.is_some() {
// Twin Deduction Pass 1: We inspect the exact properties structurally defined inside the compiled payload
// to observe which explicit relation arrow the child payload natively consumes.
let keys = relative_keys.unwrap();
let mut consumed_rel_idx = None;
for (i, rel) in matching_rels.iter().enumerate() {
if let Some(prefix) = &rel.prefix {
if keys.contains(prefix) {
consumed_rel_idx = Some(i);
break; // Found the routing edge explicitly consumed by the schema payload
}
}
}
// Twin Deduction Pass 2: Knowing which arrow points outbound, we can mathematically deduce its twin
// providing the reverse ownership on the same junction boundary must be the incoming Edge to the parent.
if let Some(used_idx) = consumed_rel_idx {
let used_rel = matching_rels[used_idx];
let mut twin_ids = Vec::new();
for (i, rel) in matching_rels.iter().enumerate() {
if i != used_idx
&& rel.source_type == used_rel.source_type
&& rel.destination_type == used_rel.destination_type
&& rel.prefix.is_some()
{
twin_ids.push(i);
}
}
if twin_ids.len() == 1 {
chosen_idx = twin_ids[0];
resolved = true;
}
}
}
// Implicit Base Fallback: If no complex explicit paths resolve, but exactly one relation
// sits entirely naked (without a constraint prefix), it must be the core structural parent ownership.
if !resolved { if !resolved {
// 1. If there's EXACTLY ONE relation with a null prefix, it's the base structural edge. Pick it.
let mut null_prefix_ids = Vec::new(); let mut null_prefix_ids = Vec::new();
for (i, rel) in matching_rels.iter().enumerate() { for (i, rel) in matching_rels.iter().enumerate() {
if rel.prefix.is_none() { if rel.prefix.is_none() {
@ -632,25 +697,6 @@ pub(crate) fn resolve_relation<'a>(
} }
if null_prefix_ids.len() == 1 { if null_prefix_ids.len() == 1 {
chosen_idx = null_prefix_ids[0]; chosen_idx = null_prefix_ids[0];
resolved = true;
}
}
if !resolved && relative_keys.is_some() {
// 2. M:M Disambiguation: The child schema will explicitly define an outbound property
// matching one of the relational prefixes (e.g. "target"). We use the OTHER one (e.g. "source").
let keys = relative_keys.unwrap();
let mut missing_prefix_ids = Vec::new();
for (i, rel) in matching_rels.iter().enumerate() {
if let Some(prefix) = &rel.prefix {
if !keys.contains(prefix) {
missing_prefix_ids.push(i);
}
}
}
if missing_prefix_ids.len() == 1 {
chosen_idx = missing_prefix_ids[0];
// resolved = true;
} }
} }

View File

@ -3,8 +3,8 @@
pub mod cache; pub mod cache;
use crate::database::r#type::Type;
use crate::database::Database; use crate::database::Database;
use crate::database::r#type::Type;
use serde_json::Value; use serde_json::Value;
use std::sync::Arc; use std::sync::Arc;
@ -50,15 +50,21 @@ impl Merger {
let mut final_cause = None; let mut final_cause = None;
if let Ok(Value::Object(map)) = serde_json::from_str::<Value>(&msg) { if let Ok(Value::Object(map)) = serde_json::from_str::<Value>(&msg) {
if let (Some(Value::String(e_msg)), Some(Value::String(e_code))) = (map.get("error"), map.get("code")) { if let (Some(Value::String(e_msg)), Some(Value::String(e_code))) =
(map.get("error"), map.get("code"))
{
final_message = e_msg.clone(); final_message = e_msg.clone();
final_code = e_code.clone(); final_code = e_code.clone();
let mut cause_parts = Vec::new(); let mut cause_parts = Vec::new();
if let Some(Value::String(d)) = map.get("detail") { if let Some(Value::String(d)) = map.get("detail") {
if !d.is_empty() { cause_parts.push(d.clone()); } if !d.is_empty() {
cause_parts.push(d.clone());
}
} }
if let Some(Value::String(h)) = map.get("hint") { if let Some(Value::String(h)) = map.get("hint") {
if !h.is_empty() { cause_parts.push(h.clone()); } if !h.is_empty() {
cause_parts.push(h.clone());
}
} }
if !cause_parts.is_empty() { if !cause_parts.is_empty() {
final_cause = Some(cause_parts.join("\n")); final_cause = Some(cause_parts.join("\n"));
@ -253,9 +259,7 @@ impl Merger {
}; };
if let Some(compiled_edges) = schema.obj.compiled_edges.get() { if let Some(compiled_edges) = schema.obj.compiled_edges.get() {
println!("Compiled Edges keys for relation {}: {:?}", relation_name, compiled_edges.keys().collect::<Vec<_>>());
if let Some(edge) = compiled_edges.get(&relation_name) { if let Some(edge) = compiled_edges.get(&relation_name) {
println!("FOUND EDGE {} -> {:?}", relation_name, edge.constraint);
if let Some(relation) = self.db.relations.get(&edge.constraint) { if let Some(relation) = self.db.relations.get(&edge.constraint) {
let parent_is_source = edge.forward; let parent_is_source = edge.forward;
@ -266,15 +270,16 @@ impl Merger {
} }
} }
let mut merged_relative = match self.merge_internal(rel_schema.clone(), Value::Object(relative), notifications)? { let mut merged_relative = match self.merge_internal(
rel_schema.clone(),
Value::Object(relative),
notifications,
)? {
Value::Object(m) => m, Value::Object(m) => m,
_ => continue, _ => continue,
}; };
merged_relative.insert( merged_relative.insert("type".to_string(), Value::String(relative_type_name));
"type".to_string(),
Value::String(relative_type_name),
);
Self::apply_entity_relation( Self::apply_entity_relation(
&mut entity_fields, &mut entity_fields,
@ -297,7 +302,11 @@ impl Merger {
&entity_fields, &entity_fields,
); );
let merged_relative = match self.merge_internal(rel_schema.clone(), Value::Object(relative), notifications)? { let merged_relative = match self.merge_internal(
rel_schema.clone(),
Value::Object(relative),
notifications,
)? {
Value::Object(m) => m, Value::Object(m) => m,
_ => continue, _ => continue,
}; };
@ -360,7 +369,9 @@ impl Merger {
); );
let mut item_schema = rel_schema.clone(); let mut item_schema = rel_schema.clone();
if let Some(crate::database::schema::SchemaTypeOrArray::Single(t)) = &rel_schema.obj.type_ { if let Some(crate::database::schema::SchemaTypeOrArray::Single(t)) =
&rel_schema.obj.type_
{
if t == "array" { if t == "array" {
if let Some(items_def) = &rel_schema.obj.items { if let Some(items_def) = &rel_schema.obj.items {
item_schema = items_def.clone(); item_schema = items_def.clone();
@ -368,8 +379,11 @@ impl Merger {
} }
} }
let merged_relative = let merged_relative = match self.merge_internal(
match self.merge_internal(item_schema, Value::Object(relative_item), notifications)? { item_schema,
Value::Object(relative_item),
notifications,
)? {
Value::Object(m) => m, Value::Object(m) => m,
_ => continue, _ => continue,
}; };
@ -548,7 +562,12 @@ impl Merger {
entity_fields = new_fields; entity_fields = new_fields;
} }
Ok((entity_fields, entity_change_kind, entity_fetched, replaces_id)) Ok((
entity_fields,
entity_change_kind,
entity_fetched,
replaces_id,
))
} }
fn fetch_entity( fn fetch_entity(
@ -735,9 +754,7 @@ impl Merger {
columns.join(", "), columns.join(", "),
values.join(", ") values.join(", ")
); );
self self.db.execute(&sql, None)?;
.db
.execute(&sql, None)?;
} else if change_kind == "update" || change_kind == "delete" { } else if change_kind == "update" || change_kind == "delete" {
entity_pairs.remove("id"); entity_pairs.remove("id");
entity_pairs.remove("type"); entity_pairs.remove("type");
@ -769,9 +786,7 @@ impl Merger {
set_clauses.join(", "), set_clauses.join(", "),
Self::quote_literal(&Value::String(id_str.to_string())) Self::quote_literal(&Value::String(id_str.to_string()))
); );
self self.db.execute(&sql, None)?;
.db
.execute(&sql, None)?;
} }
} }

View File

@ -67,7 +67,10 @@ impl<'a> Compiler<'a> {
if let Some(items) = &node.schema.obj.items { if let Some(items) = &node.schema.obj.items {
let mut resolved_type = None; let mut resolved_type = None;
if let Some(family_target) = items.obj.family.as_ref() { if let Some(family_target) = items.obj.family.as_ref() {
let base_type_name = family_target.split('.').next_back().unwrap_or(family_target); let base_type_name = family_target
.split('.')
.next_back()
.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(base_type_name) = items.obj.identifier() { } else if let Some(base_type_name) = items.obj.identifier() {
resolved_type = self.db.types.get(&base_type_name); resolved_type = self.db.types.get(&base_type_name);
@ -89,7 +92,10 @@ impl<'a> Compiler<'a> {
} }
// 3. Fallback for root execution of standalone non-entity arrays // 3. Fallback for root execution of standalone non-entity arrays
Err("Cannot compile a root array without a valid entity reference or table mapped via `items`.".to_string()) Err(
"Cannot compile a root array without a valid entity reference or table mapped via `items`."
.to_string(),
)
} }
fn compile_reference(&mut self, node: Node<'a>) -> Result<(String, String), String> { fn compile_reference(&mut self, node: Node<'a>) -> Result<(String, String), String> {
@ -452,7 +458,6 @@ impl<'a> Compiler<'a> {
}, },
}; };
let (val_sql, val_type) = self.compile_node(child_node)?; let (val_sql, val_type) = self.compile_node(child_node)?;
if val_type != "abort" { if val_type != "abort" {
@ -515,7 +520,13 @@ impl<'a> Compiler<'a> {
// Determine if the property schema resolves to a physical Database Entity // Determine if the property schema resolves to a physical Database Entity
let mut bound_type_name = None; let mut bound_type_name = None;
if let Some(family_target) = prop_schema.obj.family.as_ref() { if let Some(family_target) = prop_schema.obj.family.as_ref() {
bound_type_name = Some(family_target.split('.').next_back().unwrap_or(family_target).to_string()); bound_type_name = Some(
family_target
.split('.')
.next_back()
.unwrap_or(family_target)
.to_string(),
);
} else if let Some(lookup_key) = prop_schema.obj.identifier() { } else if let Some(lookup_key) = prop_schema.obj.identifier() {
bound_type_name = Some(lookup_key); bound_type_name = Some(lookup_key);
} }
@ -536,7 +547,10 @@ impl<'a> Compiler<'a> {
} }
if let Some(col) = poly_col { if let Some(col) = poly_col {
if let Some(alias) = type_aliases.get(table_to_alias).or_else(|| type_aliases.get(&node.parent_alias)) { if let Some(alias) = type_aliases
.get(table_to_alias)
.or_else(|| type_aliases.get(&node.parent_alias))
{
where_clauses.push(format!("{}.{} = '{}'", alias, col, type_name)); where_clauses.push(format!("{}.{} = '{}'", alias, col, type_name));
} }
} }
@ -710,8 +724,6 @@ impl<'a> Compiler<'a> {
) -> Result<(), String> { ) -> Result<(), String> {
if let Some(prop_ref) = &node.property_name { if let Some(prop_ref) = &node.property_name {
let prop = prop_ref.as_str(); let prop = prop_ref.as_str();
println!("DEBUG: Eval prop: {}", prop);
let mut parent_relation_alias = node.parent_alias.clone(); let mut parent_relation_alias = node.parent_alias.clone();
let mut child_relation_alias = base_alias.to_string(); let mut child_relation_alias = base_alias.to_string();

View File

@ -1 +1 @@
1.0.98 1.0.101