Compare commits

..

9 Commits

Author SHA1 Message Date
0509995589 version: 1.0.161 2026-06-19 11:08:52 -04:00
c97e5d75b3 queryer: don't emit a parent type-bound for reified-relationship properties
A SINGLE reified-relationship property (e.g. invoice.counterparty,
person.primary_contact — a property whose type is a relationship
subtype) is hydrated by a correlated subquery that joins the
relationship table and correlates source_id/target_id = parent.id; its
discriminators (the relationship subtype and the target's type CASE)
are constrained INSIDE that subquery.

For such a property the resolved edge is a REVERSE traversal
(forward = false) over a generic relationship FK
(fk_relationship_source_entity, destination = entity). compile_
polymorphic_bounds then took the `!edge.forward` branch with
poly_col = destination_columns[1] = "type" and
table_to_alias = "entity", which resolves to the PARENT's entity alias
— emitting a bound like `entity_1.type = 'counterparty'` into the
parent WHERE. No parent row has that type, so EVERY parent was dropped
(e.g. get_invoice returned null for an existing invoice whose
counterparty edge was absent).

Array reified traversals (contacts, occupancies) never hit this: an
array property has no bound_type_name. Only the single form did, and it
was previously unexercised.

Fix: in compile_polymorphic_bounds, skip the bound when the property's
resolved type is itself a relationship (type_def.relationship) — there
is no parent-row discriminator column to bound.

Test: new queryer case (person.primary_contact, type=contact) asserts
the parent WHERE has no spurious entity.type bound while the subquery
keeps its discriminator + source_id correlation. Full suite green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 11:03:13 -04:00
94477b677d queryer: NULL-tolerant bound for optional single-family forward relations
An OPTIONAL forward polymorphic relation declared via `family` (e.g.
order.counterparty / invoice.counterparty) had its auto-generated type
bound `{alias}.{disc} IN (variations)` emitted into the PARENT row's
WHERE. When the relation is absent (NULL discriminator) the parent row
was wrongly excluded — a counterparty-less order/invoice returned
nothing instead of the row with `counterparty: null`.

Fix: in `compile_polymorphic_bounds`, make the forward-FK bound
NULL-tolerant (`(… IN (…) OR …_type IS NULL)`), gated on
`!r#type.relationship`:
  - real entities get NULL-tolerance (the relation is an optional
    attribute; an absent one must not drop the row — the inner CASE
    already resolves it to NULL);
  - edge entities (`relationship == true`, e.g. `contact`) keep the
    bound EXACT, because there source_type/target_type *partition*
    typed sub-collections (phone_numbers vs email_addresses) and a NULL
    endpoint belongs to no partition.

Note: the `oneOf` path was already correct — it emits no parent bound
(resolves via CASE … ELSE NULL), so cross-family optional relations
already hydrate NULL-safely. Added a fixture case documenting that.

Tests (fixtures/queryer.json): case 15 (entity → NULL-tolerant),
cases 3/5/10 (contact edges → exact, unchanged), new case 16
(oneOf cross-family → no bound). Full suite green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 10:22:29 -04:00
fb25224d22 version: 1.0.160 2026-06-18 20:31:49 -04:00
01368305de fixed missing schema ids in validation results 2026-06-18 20:31:35 -04:00
9e362f2168 version: 1.0.159 2026-06-15 16:44:04 -04:00
9fffc7707f Merge branch 'main' of gitea-ssh.thoughtpatterns.ai:cellular/jspg 2026-06-15 16:42:13 -04:00
c5d652c6fd fixed merger ordering issue 2026-06-15 16:41:59 -04:00
d2cdd680ed insert fix checkpoint 2026-06-15 16:18:44 -04:00
10 changed files with 1278 additions and 823 deletions

View File

@ -791,8 +791,8 @@
}
},
"hierarchy": [
"invoice",
"entity"
"entity",
"invoice"
],
"fields": [
"id",
@ -867,8 +867,8 @@
}
},
"hierarchy": [
"invoice_line",
"entity"
"entity",
"invoice_line"
],
"fields": [
"id",

View File

@ -110,8 +110,8 @@
}
},
"hierarchy": [
"activity",
"entity"
"entity",
"activity"
],
"variations": [
"activity",
@ -166,9 +166,9 @@
}
},
"hierarchy": [
"invoice",
"entity",
"activity",
"entity"
"invoice"
],
"variations": [
"invoice"
@ -237,8 +237,8 @@
}
},
"hierarchy": [
"attachment",
"entity"
"entity",
"attachment"
],
"variations": [
"attachment"

View File

@ -219,8 +219,8 @@
}
},
"hierarchy": [
"organization",
"entity"
"entity",
"organization"
],
"fields": [
"id",
@ -262,9 +262,9 @@
}
},
"hierarchy": [
"user",
"entity",
"organization",
"entity"
"user"
],
"fields": [
"id",
@ -359,10 +359,10 @@
}
},
"hierarchy": [
"person",
"user",
"entity",
"organization",
"entity"
"user",
"person"
],
"fields": [
"id",
@ -445,8 +445,8 @@
}
},
"hierarchy": [
"order",
"entity"
"entity",
"order"
],
"fields": [
"id",
@ -504,8 +504,8 @@
}
},
"hierarchy": [
"order_line",
"entity"
"entity",
"order_line"
],
"fields": [
"id",
@ -548,8 +548,8 @@
"name": "relationship",
"relationship": true,
"hierarchy": [
"relationship",
"entity"
"entity",
"relationship"
],
"fields": [
"source_id",
@ -611,9 +611,9 @@
"name": "contact",
"relationship": true,
"hierarchy": [
"contact",
"entity",
"relationship",
"entity"
"contact"
],
"fields": [
"is_primary",
@ -683,8 +683,8 @@
{
"name": "phone_number",
"hierarchy": [
"phone_number",
"entity"
"entity",
"phone_number"
],
"fields": [
"number",
@ -741,8 +741,8 @@
{
"name": "email_address",
"hierarchy": [
"email_address",
"entity"
"entity",
"email_address"
],
"fields": [
"address",
@ -834,8 +834,8 @@
}
},
"hierarchy": [
"attachment",
"entity"
"entity",
"attachment"
],
"fields": [
"id",
@ -887,8 +887,8 @@
{
"name": "account",
"hierarchy": [
"account",
"entity"
"entity",
"account"
],
"fields": [
"id",
@ -1031,8 +1031,8 @@
}
},
"hierarchy": [
"invoice",
"entity"
"entity",
"invoice"
],
"fields": [
"id",
@ -1107,8 +1107,8 @@
}
},
"hierarchy": [
"invoice_line",
"entity"
"entity",
"invoice_line"
],
"fields": [
"id",
@ -1309,10 +1309,10 @@
"sql": [
[
"(SELECT to_jsonb(t1.*) || to_jsonb(t2.*) || to_jsonb(t3.*) || to_jsonb(t4.*)",
"FROM agreego.\"person\" t1",
"JOIN agreego.\"user\" t2 ON ",
"JOIN agreego.\"organization\" t3 ON ",
"JOIN agreego.\"entity\" t4 ON ",
"FROM agreego.\"entity\" t1",
"JOIN agreego.\"organization\" t2 ON ",
"JOIN agreego.\"user\" t3 ON ",
"JOIN agreego.\"person\" t4 ON ",
"WHERE",
" (\"first_name\" = 'LookupFirst'",
" AND \"last_name\" = 'LookupLast'",
@ -1320,17 +1320,62 @@
" AND \"pronouns\" = 'they/them'))"
],
[
"UPDATE agreego.\"person\" SET",
" contact_id = 'abc-contact'",
"WHERE",
" id = '{{uuid:mocks.0.id}}'"
"INSERT INTO agreego.\"entity\" (",
" \"created_at\",",
" \"created_by\",",
" \"id\",",
" \"modified_at\",",
" \"modified_by\",",
" \"type\"",
")",
"VALUES (",
" '{{timestamp}}',",
" '00000000-0000-0000-0000-000000000000',",
" '{{uuid:generated_0}}',",
" '{{timestamp}}',",
" '00000000-0000-0000-0000-000000000000',",
" 'person'",
")"
],
[
"UPDATE agreego.\"entity\" SET",
" modified_at = '{{timestamp}}',",
" modified_by = '00000000-0000-0000-0000-000000000000'",
"WHERE",
" id = '{{uuid:mocks.0.id}}'"
"INSERT INTO agreego.\"organization\" (",
" \"id\",",
" \"type\"",
")",
"VALUES (",
" '{{uuid:generated_0}}',",
" 'person'",
")"
],
[
"INSERT INTO agreego.\"user\" (",
" \"id\",",
" \"type\"",
")",
"VALUES (",
" '{{uuid:generated_0}}',",
" 'person'",
")"
],
[
"INSERT INTO agreego.\"person\" (",
" \"contact_id\",",
" \"date_of_birth\",",
" \"first_name\",",
" \"id\",",
" \"last_name\",",
" \"pronouns\",",
" \"type\"",
")",
"VALUES (",
" 'abc-contact',",
" '{{timestamp}}',",
" 'LookupFirst',",
" '{{uuid:generated_0}}',",
" 'LookupLast',",
" 'they/them',",
" 'person'",
")"
],
[
"INSERT INTO agreego.change (",
@ -1343,40 +1388,45 @@
" \"modified_by\"",
")",
"VALUES (",
" NULL,",
" '{",
" \"contact_id\": \"old-contact\"",
" }',",
" '{",
" \"first_name\": \"LookupFirst\",",
" \"last_name\": \"LookupLast\",",
" \"date_of_birth\": \"{{timestamp}}\",",
" \"pronouns\": \"they/them\",",
" \"contact_id\": \"abc-contact\",",
" \"type\": \"person\"",
" }',",
" '{{uuid:mocks.0.id}}',",
" '{{uuid:generated_0}}',",
" 'update',",
" '{{uuid:generated_1}}',",
" 'create',",
" '{{timestamp}}',",
" '00000000-0000-0000-0000-000000000000'",
")"
],
[
"(SELECT pg_notify('entity', '{",
" \"kind\": \"update\",",
" \"kind\": \"create\",",
" \"complete\": {",
" \"id\": \"{{uuid:mocks.0.id}}\",",
" \"type\": \"person\",",
" \"first_name\": \"LookupFirst\",",
" \"last_name\": \"LookupLast\",",
" \"date_of_birth\": \"{{timestamp}}\",",
" \"pronouns\": \"they/them\",",
" \"contact_id\": \"abc-contact\",",
" \"id\": \"{{uuid:generated_0}}\",",
" \"type\": \"person\",",
" \"created_by\": \"00000000-0000-0000-0000-000000000000\",",
" \"created_at\": \"{{timestamp}}\",",
" \"modified_by\": \"00000000-0000-0000-0000-000000000000\",",
" \"modified_at\": \"{{timestamp}}\"",
" },",
" \"new\": {",
" \"first_name\": \"LookupFirst\",",
" \"last_name\": \"LookupLast\",",
" \"date_of_birth\": \"{{timestamp}}\",",
" \"pronouns\": \"they/them\",",
" \"contact_id\": \"abc-contact\",",
" \"type\": \"person\"",
" },",
" \"old\": {",
" \"contact_id\": \"old-contact\"",
" }",
"}'))"
]
@ -1412,10 +1462,10 @@
"sql": [
[
"(SELECT to_jsonb(t1.*) || to_jsonb(t2.*) || to_jsonb(t3.*) || to_jsonb(t4.*)",
"FROM agreego.\"person\" t1",
"JOIN agreego.\"user\" t2 ON ",
"JOIN agreego.\"organization\" t3 ON ",
"JOIN agreego.\"entity\" t4 ON ",
"FROM agreego.\"entity\" t1",
"JOIN agreego.\"organization\" t2 ON ",
"JOIN agreego.\"user\" t3 ON ",
"JOIN agreego.\"person\" t4 ON ",
"WHERE",
" t1.id = '{{uuid:data.id}}'",
" OR (\"first_name\" = 'LookupFirst'",
@ -1424,17 +1474,62 @@
" AND \"pronouns\" = 'they/them'))"
],
[
"UPDATE agreego.\"person\" SET",
" contact_id = 'abc-contact'",
"WHERE",
" id = '{{uuid:mocks.0.id}}'"
"INSERT INTO agreego.\"entity\" (",
" \"created_at\",",
" \"created_by\",",
" \"id\",",
" \"modified_at\",",
" \"modified_by\",",
" \"type\"",
")",
"VALUES (",
" '{{timestamp}}',",
" '00000000-0000-0000-0000-000000000000',",
" '{{uuid:data.id}}',",
" '{{timestamp}}',",
" '00000000-0000-0000-0000-000000000000',",
" 'person'",
")"
],
[
"UPDATE agreego.\"entity\" SET",
" modified_at = '{{timestamp}}',",
" modified_by = '00000000-0000-0000-0000-000000000000'",
"WHERE",
" id = '{{uuid:mocks.0.id}}'"
"INSERT INTO agreego.\"organization\" (",
" \"id\",",
" \"type\"",
")",
"VALUES (",
" '{{uuid:data.id}}',",
" 'person'",
")"
],
[
"INSERT INTO agreego.\"user\" (",
" \"id\",",
" \"type\"",
")",
"VALUES (",
" '{{uuid:data.id}}',",
" 'person'",
")"
],
[
"INSERT INTO agreego.\"person\" (",
" \"contact_id\",",
" \"date_of_birth\",",
" \"first_name\",",
" \"id\",",
" \"last_name\",",
" \"pronouns\",",
" \"type\"",
")",
"VALUES (",
" 'abc-contact',",
" '{{timestamp}}',",
" 'LookupFirst',",
" '{{uuid:data.id}}',",
" 'LookupLast',",
" 'they/them',",
" 'person'",
")"
],
[
"INSERT INTO agreego.change (",
@ -1447,42 +1542,46 @@
" \"modified_by\"",
")",
"VALUES (",
" NULL,",
" '{",
" \"contact_id\": \"old-contact\"",
" }',",
" '{",
" \"first_name\": \"LookupFirst\",",
" \"last_name\": \"LookupLast\",",
" \"date_of_birth\": \"{{timestamp}}\",",
" \"pronouns\": \"they/them\",",
" \"contact_id\": \"abc-contact\",",
" \"type\": \"person\"",
" }',",
" '{{uuid:mocks.0.id}}',",
" '{{uuid:data.id}}',",
" '{{uuid:generated_0}}',",
" 'update',",
" 'create',",
" '{{timestamp}}',",
" '00000000-0000-0000-0000-000000000000'",
")"
],
[
"(SELECT pg_notify('entity', '{",
" \"kind\": \"update\",",
" \"kind\": \"create\",",
" \"complete\": {",
" \"id\": \"{{uuid:mocks.0.id}}\",",
" \"type\": \"person\",",
" \"first_name\": \"LookupFirst\",",
" \"last_name\": \"LookupLast\",",
" \"date_of_birth\": \"{{timestamp}}\",",
" \"pronouns\": \"they/them\",",
" \"contact_id\": \"abc-contact\",",
" \"id\": \"{{uuid:data.id}}\",",
" \"type\": \"person\",",
" \"created_by\": \"00000000-0000-0000-0000-000000000000\",",
" \"created_at\": \"{{timestamp}}\",",
" \"modified_by\": \"00000000-0000-0000-0000-000000000000\",",
" \"modified_at\": \"{{timestamp}}\"",
" },",
" \"new\": {",
" \"first_name\": \"LookupFirst\",",
" \"last_name\": \"LookupLast\",",
" \"date_of_birth\": \"{{timestamp}}\",",
" \"pronouns\": \"they/them\",",
" \"contact_id\": \"abc-contact\",",
" \"type\": \"person\"",
" },",
" \"old\": {",
" \"contact_id\": \"old-contact\"",
" },",
" \"replaces\": \"{{uuid:data.id}}\"",
" }",
"}'))"
]
]
@ -1516,10 +1615,10 @@
"sql": [
[
"(SELECT to_jsonb(t1.*) || to_jsonb(t2.*) || to_jsonb(t3.*) || to_jsonb(t4.*)",
"FROM agreego.\"person\" t1",
"JOIN agreego.\"user\" t2 ON ",
"JOIN agreego.\"organization\" t3 ON ",
"JOIN agreego.\"entity\" t4 ON ",
"FROM agreego.\"entity\" t1",
"JOIN agreego.\"organization\" t2 ON ",
"JOIN agreego.\"user\" t3 ON ",
"JOIN agreego.\"person\" t4 ON ",
"WHERE",
" t1.id = '{{uuid:data.id}}'",
" OR (\"first_name\" = 'LookupFirst'",
@ -1528,23 +1627,109 @@
" AND \"pronouns\" = 'they/them'))"
],
[
"(SELECT pg_notify('entity', '{",
" \"kind\": \"replace\",",
" \"complete\": {",
" \"id\": \"{{uuid:mocks.0.id}}\",",
" \"type\": \"person\",",
"INSERT INTO agreego.\"entity\" (",
" \"created_at\",",
" \"created_by\",",
" \"id\",",
" \"modified_at\",",
" \"modified_by\",",
" \"type\"",
")",
"VALUES (",
" '{{timestamp}}',",
" '00000000-0000-0000-0000-000000000000',",
" '{{uuid:data.id}}',",
" '{{timestamp}}',",
" '00000000-0000-0000-0000-000000000000',",
" 'person'",
")"
],
[
"INSERT INTO agreego.\"organization\" (",
" \"id\",",
" \"type\"",
")",
"VALUES (",
" '{{uuid:data.id}}',",
" 'person'",
")"
],
[
"INSERT INTO agreego.\"user\" (",
" \"id\",",
" \"type\"",
")",
"VALUES (",
" '{{uuid:data.id}}',",
" 'person'",
")"
],
[
"INSERT INTO agreego.\"person\" (",
" \"date_of_birth\",",
" \"first_name\",",
" \"id\",",
" \"last_name\",",
" \"pronouns\",",
" \"type\"",
")",
"VALUES (",
" '{{timestamp}}',",
" 'LookupFirst',",
" '{{uuid:data.id}}',",
" 'LookupLast',",
" 'they/them',",
" 'person'",
")"
],
[
"INSERT INTO agreego.change (",
" \"old\",",
" \"new\",",
" \"entity_id\",",
" \"id\",",
" \"kind\",",
" \"modified_at\",",
" \"modified_by\"",
")",
"VALUES (",
" NULL,",
" '{",
" \"first_name\": \"LookupFirst\",",
" \"last_name\": \"LookupLast\",",
" \"date_of_birth\": \"{{timestamp}}\",",
" \"pronouns\": \"they/them\",",
" \"contact_id\": \"old-contact\",",
" \"type\": \"person\"",
" }',",
" '{{uuid:data.id}}',",
" '{{uuid:generated_0}}',",
" 'create',",
" '{{timestamp}}',",
" '00000000-0000-0000-0000-000000000000'",
")"
],
[
"(SELECT pg_notify('entity', '{",
" \"kind\": \"create\",",
" \"complete\": {",
" \"first_name\": \"LookupFirst\",",
" \"last_name\": \"LookupLast\",",
" \"date_of_birth\": \"{{timestamp}}\",",
" \"pronouns\": \"they/them\",",
" \"id\": \"{{uuid:data.id}}\",",
" \"type\": \"person\",",
" \"created_by\": \"00000000-0000-0000-0000-000000000000\",",
" \"created_at\": \"{{timestamp}}\",",
" \"modified_by\": \"00000000-0000-0000-0000-000000000000\",",
" \"modified_at\": \"{{timestamp}}\"",
" },",
" \"new\": {",
" \"first_name\": \"LookupFirst\",",
" \"last_name\": \"LookupLast\",",
" \"date_of_birth\": \"{{timestamp}}\",",
" \"pronouns\": \"they/them\",",
" \"type\": \"person\"",
" },",
" \"replaces\": \"{{uuid:data.id}}\"",
" }",
"}'))"
]
]
@ -1573,26 +1758,64 @@
"sql": [
[
"(SELECT to_jsonb(t1.*) || to_jsonb(t2.*) || to_jsonb(t3.*) || to_jsonb(t4.*)",
"FROM agreego.\"person\" t1",
"JOIN agreego.\"user\" t2 ON ",
"JOIN agreego.\"organization\" t3 ON ",
"JOIN agreego.\"entity\" t4 ON ",
"FROM agreego.\"entity\" t1",
"JOIN agreego.\"organization\" t2 ON ",
"JOIN agreego.\"user\" t3 ON ",
"JOIN agreego.\"person\" t4 ON ",
"WHERE",
" t1.id = '{{uuid:mocks.0.id}}')"
],
[
"UPDATE agreego.\"person\" SET",
" first_name = 'NewFirst',",
" last_name = 'NewLast'",
"WHERE",
" id = '{{uuid:mocks.0.id}}'"
"INSERT INTO agreego.\"entity\" (",
" \"created_at\",",
" \"created_by\",",
" \"id\",",
" \"modified_at\",",
" \"modified_by\",",
" \"type\"",
")",
"VALUES (",
" '{{timestamp}}',",
" '00000000-0000-0000-0000-000000000000',",
" '{{uuid:mocks.0.id}}',",
" '{{timestamp}}',",
" '00000000-0000-0000-0000-000000000000',",
" 'person'",
")"
],
[
"UPDATE agreego.\"entity\" SET",
" modified_at = '{{timestamp}}',",
" modified_by = '00000000-0000-0000-0000-000000000000'",
"WHERE",
" id = '{{uuid:mocks.0.id}}'"
"INSERT INTO agreego.\"organization\" (",
" \"id\",",
" \"type\"",
")",
"VALUES (",
" '{{uuid:mocks.0.id}}',",
" 'person'",
")"
],
[
"INSERT INTO agreego.\"user\" (",
" \"id\",",
" \"type\"",
")",
"VALUES (",
" '{{uuid:mocks.0.id}}',",
" 'person'",
")"
],
[
"INSERT INTO agreego.\"person\" (",
" \"first_name\",",
" \"id\",",
" \"last_name\",",
" \"type\"",
")",
"VALUES (",
" 'NewFirst',",
" '{{uuid:mocks.0.id}}',",
" 'NewLast',",
" 'person'",
")"
],
[
"INSERT INTO agreego.change (",
@ -1605,10 +1828,7 @@
" \"modified_by\"",
")",
"VALUES (",
" '{",
" \"first_name\": \"OldFirst\",",
" \"last_name\": \"OldLast\"",
" }',",
" NULL,",
" '{",
" \"first_name\": \"NewFirst\",",
" \"last_name\": \"NewLast\",",
@ -1616,19 +1836,21 @@
" }',",
" '{{uuid:mocks.0.id}}',",
" '{{uuid:generated_0}}',",
" 'update',",
" 'create',",
" '{{timestamp}}',",
" '00000000-0000-0000-0000-000000000000'",
")"
],
[
"(SELECT pg_notify('entity', '{",
" \"kind\": \"update\",",
" \"kind\": \"create\",",
" \"complete\": {",
" \"id\": \"{{uuid:mocks.0.id}}\",",
" \"type\": \"person\",",
" \"first_name\": \"NewFirst\",",
" \"last_name\": \"NewLast\",",
" \"id\": \"{{uuid:mocks.0.id}}\",",
" \"type\": \"person\",",
" \"created_by\": \"00000000-0000-0000-0000-000000000000\",",
" \"created_at\": \"{{timestamp}}\",",
" \"modified_by\": \"00000000-0000-0000-0000-000000000000\",",
" \"modified_at\": \"{{timestamp}}\"",
" },",
@ -1636,10 +1858,6 @@
" \"first_name\": \"NewFirst\",",
" \"last_name\": \"NewLast\",",
" \"type\": \"person\"",
" },",
" \"old\": {",
" \"first_name\": \"OldFirst\",",
" \"last_name\": \"OldLast\"",
" }",
"}'))"
]
@ -1663,10 +1881,10 @@
"sql": [
[
"(SELECT to_jsonb(t1.*) || to_jsonb(t2.*) || to_jsonb(t3.*) || to_jsonb(t4.*)",
"FROM agreego.\"person\" t1",
"JOIN agreego.\"user\" t2 ON ",
"JOIN agreego.\"organization\" t3 ON ",
"JOIN agreego.\"entity\" t4 ON ",
"FROM agreego.\"entity\" t1",
"JOIN agreego.\"organization\" t2 ON ",
"JOIN agreego.\"user\" t3 ON ",
"JOIN agreego.\"person\" t4 ON ",
"WHERE",
" t1.id = '123')"
],
@ -2002,8 +2220,8 @@
"sql": [
[
"(SELECT to_jsonb(t1.*) || to_jsonb(t2.*)",
"FROM agreego.\"order\" t1",
"JOIN agreego.\"entity\" t2 ON ",
"FROM agreego.\"entity\" t1",
"JOIN agreego.\"order\" t2 ON ",
"WHERE",
" t1.id = 'abc'",
" OR (\"id\" = 'abc'))"
@ -2805,20 +3023,62 @@
"sql": [
[
"(SELECT to_jsonb(t1.*) || to_jsonb(t2.*) || to_jsonb(t3.*) || to_jsonb(t4.*)",
"FROM agreego.\"person\" t1",
"JOIN agreego.\"user\" t2 ON ",
"JOIN agreego.\"organization\" t3 ON ",
"JOIN agreego.\"entity\" t4 ON ",
"FROM agreego.\"entity\" t1",
"JOIN agreego.\"organization\" t2 ON ",
"JOIN agreego.\"user\" t3 ON ",
"JOIN agreego.\"person\" t4 ON ",
"WHERE",
" t1.id = 'abc-archived')"
],
[
"UPDATE agreego.\"entity\" SET",
" archived = true,",
" modified_at = '{{timestamp}}',",
" modified_by = '00000000-0000-0000-0000-000000000000'",
"WHERE",
" id = 'abc-archived'"
"INSERT INTO agreego.\"entity\" (",
" \"archived\",",
" \"created_at\",",
" \"created_by\",",
" \"id\",",
" \"modified_at\",",
" \"modified_by\",",
" \"type\"",
")",
"VALUES (",
" true,",
" '{{timestamp}}',",
" '00000000-0000-0000-0000-000000000000',",
" 'abc-archived',",
" '{{timestamp}}',",
" '00000000-0000-0000-0000-000000000000',",
" 'person'",
")"
],
[
"INSERT INTO agreego.\"organization\" (",
" \"id\",",
" \"type\"",
")",
"VALUES (",
" 'abc-archived',",
" 'person'",
")"
],
[
"INSERT INTO agreego.\"user\" (",
" \"id\",",
" \"type\"",
")",
"VALUES (",
" 'abc-archived',",
" 'person'",
")"
],
[
"INSERT INTO agreego.\"person\" (",
" \"id\",",
" \"type\"",
")",
"VALUES (",
" 'abc-archived',",
" 'person'",
")"
],
[
"INSERT INTO agreego.change (",
@ -2831,38 +3091,33 @@
" \"modified_by\"",
")",
"VALUES (",
" '{",
" \"archived\": false",
" }',",
" NULL,",
" '{",
" \"archived\": true,",
" \"type\": \"person\"",
" }',",
" 'abc-archived',",
" '{{uuid:generated_0}}',",
" 'delete',",
" 'create',",
" '{{timestamp}}',",
" '00000000-0000-0000-0000-000000000000'",
")"
],
[
"(SELECT pg_notify('entity', '{",
" \"kind\": \"delete\",",
" \"kind\": \"create\",",
" \"complete\": {",
" \"archived\": true,",
" \"id\": \"abc-archived\",",
" \"type\": \"person\",",
" \"first_name\": \"ArchivedFirst\",",
" \"last_name\": \"ArchivedLast\",",
" \"archived\": true,",
" \"created_by\": \"00000000-0000-0000-0000-000000000000\",",
" \"created_at\": \"{{timestamp}}\",",
" \"modified_by\": \"00000000-0000-0000-0000-000000000000\",",
" \"modified_at\": \"{{timestamp}}\"",
" },",
" \"new\": {",
" \"archived\": true,",
" \"type\": \"person\"",
" },",
" \"old\": {",
" \"archived\": false",
" }",
"}'))"
]
@ -3121,8 +3376,8 @@
"sql": [
[
"(SELECT to_jsonb(t1.*) || to_jsonb(t2.*)",
"FROM agreego.\"order_line\" t1",
"JOIN agreego.\"entity\" t2 ON ",
"FROM agreego.\"entity\" t1",
"JOIN agreego.\"order_line\" t2 ON ",
"WHERE",
" t1.id = '{{uuid:data.lines.0.id}}')"
],
@ -3245,8 +3500,8 @@
"sql": [
[
"(SELECT to_jsonb(t1.*) || to_jsonb(t2.*)",
"FROM agreego.\"invoice\" t1",
"JOIN agreego.\"entity\" t2 ON ",
"FROM agreego.\"entity\" t1",
"JOIN agreego.\"invoice\" t2 ON ",
"WHERE",
" t1.id = '{{uuid:data.id}}'",
" OR (\"id\" = '{{uuid:data.id}}'))"
@ -3362,8 +3617,8 @@
"sql": [
[
"(SELECT to_jsonb(t1.*) || to_jsonb(t2.*)",
"FROM agreego.\"account\" t1",
"JOIN agreego.\"entity\" t2 ON ",
"FROM agreego.\"entity\" t1",
"JOIN agreego.\"account\" t2 ON ",
"WHERE",
" t1.id = '{{uuid:data.id}}')"
],

File diff suppressed because it is too large Load Diff

View File

@ -791,13 +791,8 @@ impl Merger {
}
};
let mut execute_order: Vec<String> = entity_type.hierarchy.clone();
if change_kind == "create" {
execute_order.reverse();
}
for table_name in execute_order {
let table_fields = match grouped_fields.get(&table_name).and_then(|v| v.as_array()) {
for table_name in &entity_type.hierarchy {
let table_fields = match grouped_fields.get(table_name).and_then(|v| v.as_array()) {
Some(arr) => arr
.iter()
.filter_map(|v| v.as_str().map(|s| s.to_string()))

View File

@ -574,7 +574,7 @@ impl<'a> Compiler<'a> {
fn compile_polymorphic_bounds(
&self,
_type: &crate::database::r#type::Type,
r#type: &crate::database::r#type::Type,
type_aliases: &std::collections::HashMap<String, String>,
node: &Node,
where_clauses: &mut Vec<String>,
@ -604,13 +604,35 @@ impl<'a> Compiler<'a> {
if let Some(type_name) = bound_type_name {
// Ensure this type actually exists
if let Some(type_def) = self.db.types.get(&type_name) {
// A reified-relationship property (e.g. invoice.counterparty, person.primary_contact)
// is hydrated by a correlated subquery that joins the relationship table and correlates
// source_id/target_id = parent.id; its discriminators (the relationship subtype and the
// target's type CASE) are constrained INSIDE that subquery. There is no parent-row column
// to bound here — emitting one wrongly constrains the PARENT entity's own `type` column
// (e.g. `entity_1.type = 'counterparty'`), which no parent row satisfies, dropping every
// parent. (Array reified traversals never reach this code: an array prop has no
// bound_type_name.) So skip the bound entirely for relationship-typed properties.
if type_def.relationship {
continue;
}
if let Some(relation) = self.db.relations.get(&edge.constraint) {
let mut poly_col = None;
let mut table_to_alias = "";
// An OPTIONAL forward polymorphic relation on a real entity (e.g.
// invoice.counterparty, order.counterparty) keeps its discriminator column on the
// entity row itself; the auto-generated type bound then sits in that row's WHERE
// and would wrongly drop the row when the relation is absent (NULL discriminator).
// Such a row must still be returned (with the property resolving to null), so the
// bound is made NULL-tolerant below. This does NOT apply to edge entities
// (relationship == true): there the same source_type/target_type columns
// partition typed sub-collections (phone_numbers vs email_addresses), so the
// bound must stay exact — a NULL endpoint genuinely belongs to no partition.
let mut null_tolerant = false;
if edge.forward && relation.source_columns.len() > 1 {
poly_col = Some(&relation.source_columns[1]); // e.g., target_type
table_to_alias = &relation.source_type; // e.g., relationship
null_tolerant = !r#type.relationship;
} else if !edge.forward && relation.destination_columns.len() > 1 {
poly_col = Some(&relation.destination_columns[1]); // e.g., source_type
table_to_alias = &relation.destination_type; // e.g., relationship
@ -621,20 +643,25 @@ impl<'a> Compiler<'a> {
.get(table_to_alias)
.or_else(|| type_aliases.get(&node.parent_alias))
{
if type_def.variations.len() > 1 {
let predicate = if type_def.variations.len() > 1 {
let quoted: Vec<String> = type_def
.variations
.iter()
.map(|v| format!("'{}'", v))
.collect();
where_clauses.push(format!(
"{}.{} IN ({})",
alias,
col,
quoted.join(", ")
));
format!("{}.{} IN ({})", alias, col, quoted.join(", "))
} else {
where_clauses.push(format!("{}.{} = '{}'", alias, col, type_name));
format!("{}.{} = '{}'", alias, col, type_name)
};
// NULL-tolerant for parent-level (forward FK) filters: an OPTIONAL
// polymorphic relation that is absent (NULL discriminator) must NOT exclude
// the parent — the inner CASE resolves it to NULL. (e.g. an invoice with no
// counterparty yet must still be returned, with counterparty = null.)
if null_tolerant {
where_clauses.push(format!("({} OR {}.{} IS NULL)", predicate, alias, col));
} else {
where_clauses.push(predicate);
}
}
}

View File

@ -1517,6 +1517,18 @@ fn test_queryer_0_15() {
crate::tests::runner::run_test_case(&path, 0, 15).unwrap();
}
#[test]
fn test_queryer_0_16() {
let path = format!("{}/fixtures/queryer.json", env!("CARGO_MANIFEST_DIR"));
crate::tests::runner::run_test_case(&path, 0, 16).unwrap();
}
#[test]
fn test_queryer_0_17() {
let path = format!("{}/fixtures/queryer.json", env!("CARGO_MANIFEST_DIR"));
crate::tests::runner::run_test_case(&path, 0, 17).unwrap();
}
#[test]
fn test_polymorphism_0_0() {
let path = format!("{}/fixtures/polymorphism.json", env!("CARGO_MANIFEST_DIR"));

View File

@ -44,7 +44,7 @@ fn test_library_api() {
{
"name": "source_schema",
"variations": ["source_schema"],
"hierarchy": ["source_schema", "entity"],
"hierarchy": ["entity", "source_schema"],
"schemas": {
"source_schema": {
"type": "object",
@ -60,7 +60,7 @@ fn test_library_api() {
{
"name": "target_schema",
"variations": ["target_schema"],
"hierarchy": ["target_schema", "entity"],
"hierarchy": ["entity", "target_schema"],
"schemas": {
"target_schema": {
"type": "object",
@ -109,7 +109,7 @@ fn test_library_api() {
"field_types": null,
"fields": [],
"grouped_fields": null,
"hierarchy": ["source_schema", "entity"],
"hierarchy": ["entity", "source_schema"],
"historical": false,
"id": "",
"longevity": null,
@ -174,7 +174,7 @@ fn test_library_api() {
"field_types": null,
"fields": [],
"grouped_fields": null,
"hierarchy": ["target_schema", "entity"],
"hierarchy": ["entity", "target_schema"],
"historical": false,
"id": "",
"longevity": null,
@ -251,12 +251,18 @@ fn test_library_api() {
{
"code": "REQUIRED_FIELD_MISSING",
"message": "Missing name",
"details": { "path": "name" }
"details": {
"path": "name",
"schema": "source_schema"
}
},
{
"code": "STRICT_PROPERTY_VIOLATION",
"message": "Unexpected property 'wrong'",
"details": { "path": "wrong" }
"details": {
"path": "wrong",
"schema": "source_schema"
}
}
]
})

View File

@ -69,7 +69,7 @@ impl Validator {
path: Some(e.path),
cause: None,
context: None,
schema: None,
schema: Some(schema_id.to_string()),
},
})
.collect();
@ -83,7 +83,7 @@ impl Validator {
path: Some(e.path),
cause: None,
context: None,
schema: None,
schema: Some(schema_id.to_string()),
},
}]),
}
@ -95,7 +95,7 @@ impl Validator {
path: Some("/".to_string()),
cause: None,
context: None,
schema: None,
schema: Some(schema_id.to_string()),
},
}])
}

View File

@ -1 +1 @@
1.0.158
1.0.161