Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 29d8dfb608 | |||
| 5b36ecf06c | |||
| 76467a6fed | |||
| 930d0513cd | |||
| cad651dbd8 | |||
| ea9ac8469c | |||
| ebcdb661fa | |||
| c893e29c59 | |||
| 7523431007 |
@ -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:
|
||||||
|
|||||||
@ -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\"",
|
||||||
@ -2373,7 +2360,7 @@
|
|||||||
" \"target_type\":\"email_address\",",
|
" \"target_type\":\"email_address\",",
|
||||||
" \"type\":\"contact\"",
|
" \"type\":\"contact\"",
|
||||||
" }",
|
" }",
|
||||||
" }')"
|
"}')"
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"SELECT pg_notify('entity', '{",
|
"SELECT pg_notify('entity', '{",
|
||||||
@ -2390,7 +2377,49 @@
|
|||||||
" \"address\":\"test@example.com\",",
|
" \"address\":\"test@example.com\",",
|
||||||
" \"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\"",
|
||||||
|
" }",
|
||||||
|
"}')"
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
2
flows
Submodule flows updated: a7b0f5dc4d...4d61e13e00
@ -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());
|
||||||
|
|||||||
@ -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)),
|
||||||
|
|||||||
@ -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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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)?;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user