diff --git a/GEMINI.md b/GEMINI.md index 1fcc0a3..e6dacb6 100644 --- a/GEMINI.md +++ b/GEMINI.md @@ -23,6 +23,12 @@ To support high-throughput operations while allowing for runtime updates (e.g., 3. **Immutable AST Caching**: The `Validator` struct immutably owns the `Database` registry. Schemas themselves are frozen structurally, but utilize `OnceLock` interior mutability during the Compilation Phase to permanently cache resolved `$ref` inheritances, properties, and `compiled_edges` directly onto their AST nodes. This guarantees strict `O(1)` relationship and property validation execution at runtime without locking or recursive DB polling. 4. **Lock-Free Reads**: Incoming operations acquire a read lock just long enough to clone the `Arc` inside an `RwLock>>`, ensuring zero blocking during schema updates. +### 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: +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. +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. +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. + ### Global API Reference These functions operate on the global `GLOBAL_JSPG` engine instance and provide administrative boundaries: diff --git a/agreego.sql b/agreego.sql new file mode 100644 index 0000000..e69de29 diff --git a/fixtures/queryer.json b/fixtures/queryer.json index e7576b4..59ee865 100644 --- a/fixtures/queryer.json +++ b/fixtures/queryer.json @@ -27,7 +27,9 @@ { "$id": "get_orders.response", "type": "array", - "items": { "$ref": "light.order" } + "items": { + "$ref": "light.order" + } } ] } @@ -77,8 +79,23 @@ "destination_type": "person", "destination_columns": [ "id" + ] + }, + { + "id": "22222222-2222-2222-2222-222222222227", + "type": "relation", + "constraint": "fk_order_counterparty_entity", + "source_type": "order", + "source_columns": [ + "counterparty_id", + "counterparty_type" ], - "prefix": "customer" + "destination_type": "entity", + "destination_columns": [ + "id", + "type" + ], + "prefix": "counterparty" }, { "id": "33333333-3333-3333-3333-333333333333", @@ -91,8 +108,7 @@ "destination_type": "order", "destination_columns": [ "id" - ], - "prefix": "lines" + ] } ], "types": [ @@ -713,14 +729,18 @@ "created_by", "modified_at", "modified_by", - "archived" + "archived", + "counterparty_id", + "counterparty_type" ], "grouped_fields": { "order": [ "id", "type", "total", - "customer_id" + "customer_id", + "counterparty_id", + "counterparty_type" ], "entity": [ "id", @@ -748,7 +768,9 @@ "created_at": "timestamptz", "created_by": "uuid", "modified_at": "timestamptz", - "modified_by": "uuid" + "modified_by": "uuid", + "counterparty_id": "uuid", + "counterparty_type": "text" }, "variations": [ "order" diff --git a/src/database/schema.rs b/src/database/schema.rs index c5191e9..cd17403 100644 --- a/src/database/schema.rs +++ b/src/database/schema.rs @@ -622,7 +622,23 @@ pub(crate) fn resolve_relation<'a>( } } + 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(); + for (i, rel) in matching_rels.iter().enumerate() { + if rel.prefix.is_none() { + null_prefix_ids.push(i); + } + } + if null_prefix_ids.len() == 1 { + 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() { @@ -634,6 +650,7 @@ pub(crate) fn resolve_relation<'a>( } if missing_prefix_ids.len() == 1 { chosen_idx = missing_prefix_ids[0]; + // resolved = true; } }