Compare commits

..

4 Commits

24 changed files with 1066 additions and 1726 deletions

View File

@ -16,7 +16,7 @@ url = "2.5.8"
fluent-uri = "0.3.2" fluent-uri = "0.3.2"
idna = "1.1.0" idna = "1.1.0"
percent-encoding = "2.3.2" percent-encoding = "2.3.2"
uuid = { version = "1.20.0", features = ["v7", "serde"] } uuid = { version = "1.20.0", features = ["v4", "serde"] }
chrono = { version = "0.4.43", features = ["serde"] } chrono = { version = "0.4.43", features = ["serde"] }
json-pointer = "0.3.4" json-pointer = "0.3.4"
indexmap = { version = "2.13.0", features = ["serde"] } indexmap = { version = "2.13.0", features = ["serde"] }

View File

@ -175,7 +175,6 @@ In the Punc architecture, filters are automatically synthesized, strongly-typed
* **Conditions**: A condition schema is the contract defining the mathematical operations allowed on a primitive field. For example, a `string.condition` allows `$eq`, `$ne`, `$gt`, `$gte`, `$lt`, `$lte`, `$of` (IN), and `$nof` (NOT IN). * **Conditions**: A condition schema is the contract defining the mathematical operations allowed on a primitive field. For example, a `string.condition` allows `$eq`, `$ne`, `$gt`, `$gte`, `$lt`, `$lte`, `$of` (IN), and `$nof` (NOT IN).
* **Enum Conditions**: When JSPG synthesizes an enum, it dynamically generates an `<enum>.condition` (e.g., `address_kind.condition`). This strongly-typed condition perfectly mirrors the operations of a `string.condition`, but strictly limits the arrays and inputs of `$eq`, `$ne`, `$of`, and `$nof` to the exact variations defined by that Enum. This context ensures that UI generators know exactly when to render `<Select>` dropdowns instead of generic `<Text>` boxes. * **Enum Conditions**: When JSPG synthesizes an enum, it dynamically generates an `<enum>.condition` (e.g., `address_kind.condition`). This strongly-typed condition perfectly mirrors the operations of a `string.condition`, but strictly limits the arrays and inputs of `$eq`, `$ne`, `$of`, and `$nof` to the exact variations defined by that Enum. This context ensures that UI generators know exactly when to render `<Select>` dropdowns instead of generic `<Text>` boxes.
* **Pre-compiled Condition and Filter Mapping**: To prevent redundant double-wrapping of search structures, any schema property whose type is already a `.condition` or `.filter` type (such as `"string.condition"` or `"$kind.filter"`) maps directly to itself during filter synthesis rather than receiving a redundant `.filter` suffix.
* **Filters**: A filter schema (e.g., `person.filter`) is an object containing condition properties used to filter entities. It natively supports structural composition: * **Filters**: A filter schema (e.g., `person.filter`) is an object containing condition properties used to filter entities. It natively supports structural composition:
* **Inherited Properties**: Filters automatically inherit all valid database columns from their base type schema, immediately converting them to their respective `.condition` schemas. * **Inherited Properties**: Filters automatically inherit all valid database columns from their base type schema, immediately converting them to their respective `.condition` schemas.
* **Relational Proxies**: If a table has a foreign key to another table, the filter automatically generates a proxy property pointing to the related entity's filter (e.g., the `person` filter automatically gains an `organization` property that points to `organization.filter`), allowing infinitely deep nested queries natively. * **Relational Proxies**: If a table has a foreign key to another table, the filter automatically generates a proxy property pointing to the related entity's filter (e.g., the `person` filter automatically gains an `organization` property that points to `organization.filter`), allowing infinitely deep nested queries natively.
@ -296,7 +295,6 @@ The Queryer transforms Postgres into a pre-compiled Semantic Query Engine, desig
* **The Dot Convention**: When a schema requests `family: "target.schema"`, the compiler extracts the base type (e.g. `schema`) and looks up its Physical Table definition. * **The Dot Convention**: When a schema requests `family: "target.schema"`, the compiler extracts the base type (e.g. `schema`) and looks up its Physical Table definition.
* **Multi-Table Branching**: If the Physical Table is a parent to other tables (e.g. `organization` has variations `["organization", "bot", "person"]`), the compiler generates a dynamic `CASE WHEN type = '...' THEN ...` query, expanding into sub-queries for each variation. To ensure safe resolution, the compiler dynamically evaluates correlation boundaries: it attempts standard Relational Edge discovery first. If no explicit relational edge exists (indicating pure Table Inheritance rather than a standard foreign-key graph relationship), it safely invokes a **Table Parity Fallback**. This generates an explicit ID correlation constraint (`AND inner.id = outer.id`), perfectly binding the structural variations back to the parent row to eliminate Cartesian products. * **Multi-Table Branching**: If the Physical Table is a parent to other tables (e.g. `organization` has variations `["organization", "bot", "person"]`), the compiler generates a dynamic `CASE WHEN type = '...' THEN ...` query, expanding into sub-queries for each variation. To ensure safe resolution, the compiler dynamically evaluates correlation boundaries: it attempts standard Relational Edge discovery first. If no explicit relational edge exists (indicating pure Table Inheritance rather than a standard foreign-key graph relationship), it safely invokes a **Table Parity Fallback**. This generates an explicit ID correlation constraint (`AND inner.id = outer.id`), perfectly binding the structural variations back to the parent row to eliminate Cartesian products.
* **Single-Table Bypass**: If the Physical Table is a leaf node with only one variation (e.g. `person` has variations `["person"]`), the compiler cleanly bypasses `CASE` generation and compiles a simple `SELECT` across the base table, as all schema extensions (e.g. `light.person`, `full.person`) are guaranteed to reside in the exact same physical row. * **Single-Table Bypass**: If the Physical Table is a leaf node with only one variation (e.g. `person` has variations `["person"]`), the compiler cleanly bypasses `CASE` generation and compiles a simple `SELECT` across the base table, as all schema extensions (e.g. `light.person`, `full.person`) are guaranteed to reside in the exact same physical row.
* **Polymorphic Relation Type Filtering**: When a relationship maps to a polymorphic target with variations, the Queryer compiles an `IN` clause containing all allowed table variations (e.g., `counterparty_type IN ('bot', 'organization', 'person')`) rather than matching the base type literal, ensuring all polymorphic types are loaded correctly.
--- ---

View File

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

View File

@ -37,14 +37,6 @@
}, },
"filter": { "filter": {
"type": "$kind.filter" "type": "$kind.filter"
},
"conditions": {
"type": "object",
"properties": {
"new": { "type": "$kind.filter" },
"old": { "type": "$kind.filter" },
"complete": { "type": "$kind.filter" }
}
} }
} }
} }
@ -157,48 +149,7 @@
} }
] ]
} }
},
{
"description": "Valid nested filter payload",
"data": {
"kind": "person",
"conditions": {
"new": {
"age": 30
}
}
},
"schema_id": "search",
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "Invalid nested filter payload (fails constraint)",
"data": {
"kind": "person",
"conditions": {
"new": {
"age": "thirty"
}
}
},
"schema_id": "search",
"action": "validate",
"expect": {
"success": false,
"errors": [
{
"code": "INVALID_TYPE",
"details": {
"path": "conditions/new/age"
}
}
]
}
} }
] ]
} }
] ]

View File

@ -466,7 +466,7 @@
}, },
"filter": { "filter": {
"type": [ "type": [
"$kind.filter", "$kind.filter.filter",
"null" "null"
] ]
}, },

View File

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

View File

@ -219,8 +219,8 @@
} }
}, },
"hierarchy": [ "hierarchy": [
"entity", "organization",
"organization" "entity"
], ],
"fields": [ "fields": [
"id", "id",
@ -262,9 +262,9 @@
} }
}, },
"hierarchy": [ "hierarchy": [
"entity", "user",
"organization", "organization",
"user" "entity"
], ],
"fields": [ "fields": [
"id", "id",
@ -359,10 +359,10 @@
} }
}, },
"hierarchy": [ "hierarchy": [
"entity", "person",
"organization",
"user", "user",
"person" "organization",
"entity"
], ],
"fields": [ "fields": [
"id", "id",
@ -445,8 +445,8 @@
} }
}, },
"hierarchy": [ "hierarchy": [
"entity", "order",
"order" "entity"
], ],
"fields": [ "fields": [
"id", "id",
@ -504,8 +504,8 @@
} }
}, },
"hierarchy": [ "hierarchy": [
"entity", "order_line",
"order_line" "entity"
], ],
"fields": [ "fields": [
"id", "id",
@ -548,8 +548,8 @@
"name": "relationship", "name": "relationship",
"relationship": true, "relationship": true,
"hierarchy": [ "hierarchy": [
"entity", "relationship",
"relationship" "entity"
], ],
"fields": [ "fields": [
"source_id", "source_id",
@ -611,9 +611,9 @@
"name": "contact", "name": "contact",
"relationship": true, "relationship": true,
"hierarchy": [ "hierarchy": [
"entity", "contact",
"relationship", "relationship",
"contact" "entity"
], ],
"fields": [ "fields": [
"is_primary", "is_primary",
@ -683,8 +683,8 @@
{ {
"name": "phone_number", "name": "phone_number",
"hierarchy": [ "hierarchy": [
"entity", "phone_number",
"phone_number" "entity"
], ],
"fields": [ "fields": [
"number", "number",
@ -741,8 +741,8 @@
{ {
"name": "email_address", "name": "email_address",
"hierarchy": [ "hierarchy": [
"entity", "email_address",
"email_address" "entity"
], ],
"fields": [ "fields": [
"address", "address",
@ -834,8 +834,8 @@
} }
}, },
"hierarchy": [ "hierarchy": [
"entity", "attachment",
"attachment" "entity"
], ],
"fields": [ "fields": [
"id", "id",
@ -887,8 +887,8 @@
{ {
"name": "account", "name": "account",
"hierarchy": [ "hierarchy": [
"entity", "account",
"account" "entity"
], ],
"fields": [ "fields": [
"id", "id",
@ -1031,8 +1031,8 @@
} }
}, },
"hierarchy": [ "hierarchy": [
"entity", "invoice",
"invoice" "entity"
], ],
"fields": [ "fields": [
"id", "id",
@ -1107,8 +1107,8 @@
} }
}, },
"hierarchy": [ "hierarchy": [
"entity", "invoice_line",
"invoice_line" "entity"
], ],
"fields": [ "fields": [
"id", "id",
@ -1260,7 +1260,6 @@
], ],
[ [
"(SELECT pg_notify('entity', '{", "(SELECT pg_notify('entity', '{",
" \"kind\": \"create\",",
" \"complete\": {", " \"complete\": {",
" \"first_name\": \"IncompleteFirst\",", " \"first_name\": \"IncompleteFirst\",",
" \"last_name\": \"IncompleteLast\",", " \"last_name\": \"IncompleteLast\",",
@ -1309,10 +1308,10 @@
"sql": [ "sql": [
[ [
"(SELECT to_jsonb(t1.*) || to_jsonb(t2.*) || to_jsonb(t3.*) || to_jsonb(t4.*)", "(SELECT to_jsonb(t1.*) || to_jsonb(t2.*) || to_jsonb(t3.*) || to_jsonb(t4.*)",
"FROM agreego.\"entity\" t1", "FROM agreego.\"person\" t1",
"JOIN agreego.\"organization\" t2 ON ", "JOIN agreego.\"user\" t2 ON ",
"JOIN agreego.\"user\" t3 ON ", "JOIN agreego.\"organization\" t3 ON ",
"JOIN agreego.\"person\" t4 ON ", "JOIN agreego.\"entity\" t4 ON ",
"WHERE", "WHERE",
" (\"first_name\" = 'LookupFirst'", " (\"first_name\" = 'LookupFirst'",
" AND \"last_name\" = 'LookupLast'", " AND \"last_name\" = 'LookupLast'",
@ -1320,62 +1319,17 @@
" AND \"pronouns\" = 'they/them'))" " AND \"pronouns\" = 'they/them'))"
], ],
[ [
"INSERT INTO agreego.\"entity\" (", "UPDATE agreego.\"person\" SET",
" \"created_at\",", " contact_id = 'abc-contact'",
" \"created_by\",", "WHERE",
" \"id\",", " id = '{{uuid:mocks.0.id}}'"
" \"modified_at\",",
" \"modified_by\",",
" \"type\"",
")",
"VALUES (",
" '{{timestamp}}',",
" '00000000-0000-0000-0000-000000000000',",
" '{{uuid:generated_0}}',",
" '{{timestamp}}',",
" '00000000-0000-0000-0000-000000000000',",
" 'person'",
")"
], ],
[ [
"INSERT INTO agreego.\"organization\" (", "UPDATE agreego.\"entity\" SET",
" \"id\",", " modified_at = '{{timestamp}}',",
" \"type\"", " modified_by = '00000000-0000-0000-0000-000000000000'",
")", "WHERE",
"VALUES (", " id = '{{uuid:mocks.0.id}}'"
" '{{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 (", "INSERT INTO agreego.change (",
@ -1388,45 +1342,39 @@
" \"modified_by\"", " \"modified_by\"",
")", ")",
"VALUES (", "VALUES (",
" NULL,",
" '{", " '{",
" \"first_name\": \"LookupFirst\",", " \"contact_id\": \"old-contact\"",
" \"last_name\": \"LookupLast\",", " }',",
" \"date_of_birth\": \"{{timestamp}}\",", " '{",
" \"pronouns\": \"they/them\",",
" \"contact_id\": \"abc-contact\",", " \"contact_id\": \"abc-contact\",",
" \"type\": \"person\"", " \"type\": \"person\"",
" }',", " }',",
" '{{uuid:mocks.0.id}}',",
" '{{uuid:generated_0}}',", " '{{uuid:generated_0}}',",
" '{{uuid:generated_1}}',", " 'update',",
" 'create',",
" '{{timestamp}}',", " '{{timestamp}}',",
" '00000000-0000-0000-0000-000000000000'", " '00000000-0000-0000-0000-000000000000'",
")" ")"
], ],
[ [
"(SELECT pg_notify('entity', '{", "(SELECT pg_notify('entity', '{",
" \"kind\": \"create\",",
" \"complete\": {", " \"complete\": {",
" \"id\": \"{{uuid:mocks.0.id}}\",",
" \"type\": \"person\",",
" \"first_name\": \"LookupFirst\",", " \"first_name\": \"LookupFirst\",",
" \"last_name\": \"LookupLast\",", " \"last_name\": \"LookupLast\",",
" \"date_of_birth\": \"{{timestamp}}\",", " \"date_of_birth\": \"{{timestamp}}\",",
" \"pronouns\": \"they/them\",", " \"pronouns\": \"they/them\",",
" \"contact_id\": \"abc-contact\",", " \"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_by\": \"00000000-0000-0000-0000-000000000000\",",
" \"modified_at\": \"{{timestamp}}\"", " \"modified_at\": \"{{timestamp}}\"",
" },", " },",
" \"new\": {", " \"new\": {",
" \"first_name\": \"LookupFirst\",",
" \"last_name\": \"LookupLast\",",
" \"date_of_birth\": \"{{timestamp}}\",",
" \"pronouns\": \"they/them\",",
" \"contact_id\": \"abc-contact\",", " \"contact_id\": \"abc-contact\",",
" \"type\": \"person\"", " \"type\": \"person\"",
" },",
" \"old\": {",
" \"contact_id\": \"old-contact\"",
" }", " }",
"}'))" "}'))"
] ]
@ -1462,10 +1410,10 @@
"sql": [ "sql": [
[ [
"(SELECT to_jsonb(t1.*) || to_jsonb(t2.*) || to_jsonb(t3.*) || to_jsonb(t4.*)", "(SELECT to_jsonb(t1.*) || to_jsonb(t2.*) || to_jsonb(t3.*) || to_jsonb(t4.*)",
"FROM agreego.\"entity\" t1", "FROM agreego.\"person\" t1",
"JOIN agreego.\"organization\" t2 ON ", "JOIN agreego.\"user\" t2 ON ",
"JOIN agreego.\"user\" t3 ON ", "JOIN agreego.\"organization\" t3 ON ",
"JOIN agreego.\"person\" t4 ON ", "JOIN agreego.\"entity\" t4 ON ",
"WHERE", "WHERE",
" t1.id = '{{uuid:data.id}}'", " t1.id = '{{uuid:data.id}}'",
" OR (\"first_name\" = 'LookupFirst'", " OR (\"first_name\" = 'LookupFirst'",
@ -1474,62 +1422,17 @@
" AND \"pronouns\" = 'they/them'))" " AND \"pronouns\" = 'they/them'))"
], ],
[ [
"INSERT INTO agreego.\"entity\" (", "UPDATE agreego.\"person\" SET",
" \"created_at\",", " contact_id = 'abc-contact'",
" \"created_by\",", "WHERE",
" \"id\",", " id = '{{uuid:mocks.0.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\" (", "UPDATE agreego.\"entity\" SET",
" \"id\",", " modified_at = '{{timestamp}}',",
" \"type\"", " modified_by = '00000000-0000-0000-0000-000000000000'",
")", "WHERE",
"VALUES (", " id = '{{uuid:mocks.0.id}}'"
" '{{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 (", "INSERT INTO agreego.change (",
@ -1542,46 +1445,41 @@
" \"modified_by\"", " \"modified_by\"",
")", ")",
"VALUES (", "VALUES (",
" NULL,",
" '{", " '{",
" \"first_name\": \"LookupFirst\",", " \"contact_id\": \"old-contact\"",
" \"last_name\": \"LookupLast\",", " }',",
" \"date_of_birth\": \"{{timestamp}}\",", " '{",
" \"pronouns\": \"they/them\",",
" \"contact_id\": \"abc-contact\",", " \"contact_id\": \"abc-contact\",",
" \"type\": \"person\"", " \"type\": \"person\"",
" }',", " }',",
" '{{uuid:data.id}}',", " '{{uuid:mocks.0.id}}',",
" '{{uuid:generated_0}}',", " '{{uuid:generated_0}}',",
" 'create',", " 'update',",
" '{{timestamp}}',", " '{{timestamp}}',",
" '00000000-0000-0000-0000-000000000000'", " '00000000-0000-0000-0000-000000000000'",
")" ")"
], ],
[ [
"(SELECT pg_notify('entity', '{", "(SELECT pg_notify('entity', '{",
" \"kind\": \"create\",",
" \"complete\": {", " \"complete\": {",
" \"id\": \"{{uuid:mocks.0.id}}\",",
" \"type\": \"person\",",
" \"first_name\": \"LookupFirst\",", " \"first_name\": \"LookupFirst\",",
" \"last_name\": \"LookupLast\",", " \"last_name\": \"LookupLast\",",
" \"date_of_birth\": \"{{timestamp}}\",", " \"date_of_birth\": \"{{timestamp}}\",",
" \"pronouns\": \"they/them\",", " \"pronouns\": \"they/them\",",
" \"contact_id\": \"abc-contact\",", " \"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_by\": \"00000000-0000-0000-0000-000000000000\",",
" \"modified_at\": \"{{timestamp}}\"", " \"modified_at\": \"{{timestamp}}\"",
" },", " },",
" \"new\": {", " \"new\": {",
" \"first_name\": \"LookupFirst\",",
" \"last_name\": \"LookupLast\",",
" \"date_of_birth\": \"{{timestamp}}\",",
" \"pronouns\": \"they/them\",",
" \"contact_id\": \"abc-contact\",", " \"contact_id\": \"abc-contact\",",
" \"type\": \"person\"", " \"type\": \"person\"",
" }", " },",
" \"old\": {",
" \"contact_id\": \"old-contact\"",
" },",
" \"replaces\": \"{{uuid:data.id}}\"",
"}'))" "}'))"
] ]
] ]
@ -1615,10 +1513,10 @@
"sql": [ "sql": [
[ [
"(SELECT to_jsonb(t1.*) || to_jsonb(t2.*) || to_jsonb(t3.*) || to_jsonb(t4.*)", "(SELECT to_jsonb(t1.*) || to_jsonb(t2.*) || to_jsonb(t3.*) || to_jsonb(t4.*)",
"FROM agreego.\"entity\" t1", "FROM agreego.\"person\" t1",
"JOIN agreego.\"organization\" t2 ON ", "JOIN agreego.\"user\" t2 ON ",
"JOIN agreego.\"user\" t3 ON ", "JOIN agreego.\"organization\" t3 ON ",
"JOIN agreego.\"person\" t4 ON ", "JOIN agreego.\"entity\" t4 ON ",
"WHERE", "WHERE",
" t1.id = '{{uuid:data.id}}'", " t1.id = '{{uuid:data.id}}'",
" OR (\"first_name\" = 'LookupFirst'", " OR (\"first_name\" = 'LookupFirst'",
@ -1626,110 +1524,23 @@
" AND \"date_of_birth\" = '{{timestamp}}'", " AND \"date_of_birth\" = '{{timestamp}}'",
" AND \"pronouns\" = 'they/them'))" " AND \"pronouns\" = 'they/them'))"
], ],
[
"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\",",
" \"type\": \"person\"",
" }',",
" '{{uuid:data.id}}',",
" '{{uuid:generated_0}}',",
" 'create',",
" '{{timestamp}}',",
" '00000000-0000-0000-0000-000000000000'",
")"
],
[ [
"(SELECT pg_notify('entity', '{", "(SELECT pg_notify('entity', '{",
" \"kind\": \"create\",",
" \"complete\": {", " \"complete\": {",
" \"id\": \"{{uuid:mocks.0.id}}\",",
" \"type\": \"person\",",
" \"first_name\": \"LookupFirst\",", " \"first_name\": \"LookupFirst\",",
" \"last_name\": \"LookupLast\",", " \"last_name\": \"LookupLast\",",
" \"date_of_birth\": \"{{timestamp}}\",", " \"date_of_birth\": \"{{timestamp}}\",",
" \"pronouns\": \"they/them\",", " \"pronouns\": \"they/them\",",
" \"id\": \"{{uuid:data.id}}\",", " \"contact_id\": \"old-contact\",",
" \"type\": \"person\",",
" \"created_by\": \"00000000-0000-0000-0000-000000000000\",",
" \"created_at\": \"{{timestamp}}\",",
" \"modified_by\": \"00000000-0000-0000-0000-000000000000\",", " \"modified_by\": \"00000000-0000-0000-0000-000000000000\",",
" \"modified_at\": \"{{timestamp}}\"", " \"modified_at\": \"{{timestamp}}\"",
" },", " },",
" \"new\": {", " \"new\": {",
" \"first_name\": \"LookupFirst\",",
" \"last_name\": \"LookupLast\",",
" \"date_of_birth\": \"{{timestamp}}\",",
" \"pronouns\": \"they/them\",",
" \"type\": \"person\"", " \"type\": \"person\"",
" }", " },",
" \"replaces\": \"{{uuid:data.id}}\"",
"}'))" "}'))"
] ]
] ]
@ -1758,64 +1569,26 @@
"sql": [ "sql": [
[ [
"(SELECT to_jsonb(t1.*) || to_jsonb(t2.*) || to_jsonb(t3.*) || to_jsonb(t4.*)", "(SELECT to_jsonb(t1.*) || to_jsonb(t2.*) || to_jsonb(t3.*) || to_jsonb(t4.*)",
"FROM agreego.\"entity\" t1", "FROM agreego.\"person\" t1",
"JOIN agreego.\"organization\" t2 ON ", "JOIN agreego.\"user\" t2 ON ",
"JOIN agreego.\"user\" t3 ON ", "JOIN agreego.\"organization\" t3 ON ",
"JOIN agreego.\"person\" t4 ON ", "JOIN agreego.\"entity\" t4 ON ",
"WHERE", "WHERE",
" t1.id = '{{uuid:mocks.0.id}}')" " t1.id = '{{uuid:mocks.0.id}}')"
], ],
[ [
"INSERT INTO agreego.\"entity\" (", "UPDATE agreego.\"person\" SET",
" \"created_at\",", " first_name = 'NewFirst',",
" \"created_by\",", " last_name = 'NewLast'",
" \"id\",", "WHERE",
" \"modified_at\",", " id = '{{uuid:mocks.0.id}}'"
" \"modified_by\",",
" \"type\"",
")",
"VALUES (",
" '{{timestamp}}',",
" '00000000-0000-0000-0000-000000000000',",
" '{{uuid:mocks.0.id}}',",
" '{{timestamp}}',",
" '00000000-0000-0000-0000-000000000000',",
" 'person'",
")"
], ],
[ [
"INSERT INTO agreego.\"organization\" (", "UPDATE agreego.\"entity\" SET",
" \"id\",", " modified_at = '{{timestamp}}',",
" \"type\"", " modified_by = '00000000-0000-0000-0000-000000000000'",
")", "WHERE",
"VALUES (", " id = '{{uuid:mocks.0.id}}'"
" '{{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 (", "INSERT INTO agreego.change (",
@ -1828,7 +1601,10 @@
" \"modified_by\"", " \"modified_by\"",
")", ")",
"VALUES (", "VALUES (",
" NULL,", " '{",
" \"first_name\": \"OldFirst\",",
" \"last_name\": \"OldLast\"",
" }',",
" '{", " '{",
" \"first_name\": \"NewFirst\",", " \"first_name\": \"NewFirst\",",
" \"last_name\": \"NewLast\",", " \"last_name\": \"NewLast\",",
@ -1836,21 +1612,18 @@
" }',", " }',",
" '{{uuid:mocks.0.id}}',", " '{{uuid:mocks.0.id}}',",
" '{{uuid:generated_0}}',", " '{{uuid:generated_0}}',",
" 'create',", " 'update',",
" '{{timestamp}}',", " '{{timestamp}}',",
" '00000000-0000-0000-0000-000000000000'", " '00000000-0000-0000-0000-000000000000'",
")" ")"
], ],
[ [
"(SELECT pg_notify('entity', '{", "(SELECT pg_notify('entity', '{",
" \"kind\": \"create\",",
" \"complete\": {", " \"complete\": {",
" \"first_name\": \"NewFirst\",",
" \"last_name\": \"NewLast\",",
" \"id\": \"{{uuid:mocks.0.id}}\",", " \"id\": \"{{uuid:mocks.0.id}}\",",
" \"type\": \"person\",", " \"type\": \"person\",",
" \"created_by\": \"00000000-0000-0000-0000-000000000000\",", " \"first_name\": \"NewFirst\",",
" \"created_at\": \"{{timestamp}}\",", " \"last_name\": \"NewLast\",",
" \"modified_by\": \"00000000-0000-0000-0000-000000000000\",", " \"modified_by\": \"00000000-0000-0000-0000-000000000000\",",
" \"modified_at\": \"{{timestamp}}\"", " \"modified_at\": \"{{timestamp}}\"",
" },", " },",
@ -1858,6 +1631,10 @@
" \"first_name\": \"NewFirst\",", " \"first_name\": \"NewFirst\",",
" \"last_name\": \"NewLast\",", " \"last_name\": \"NewLast\",",
" \"type\": \"person\"", " \"type\": \"person\"",
" },",
" \"old\": {",
" \"first_name\": \"OldFirst\",",
" \"last_name\": \"OldLast\"",
" }", " }",
"}'))" "}'))"
] ]
@ -1881,10 +1658,10 @@
"sql": [ "sql": [
[ [
"(SELECT to_jsonb(t1.*) || to_jsonb(t2.*) || to_jsonb(t3.*) || to_jsonb(t4.*)", "(SELECT to_jsonb(t1.*) || to_jsonb(t2.*) || to_jsonb(t3.*) || to_jsonb(t4.*)",
"FROM agreego.\"entity\" t1", "FROM agreego.\"person\" t1",
"JOIN agreego.\"organization\" t2 ON ", "JOIN agreego.\"user\" t2 ON ",
"JOIN agreego.\"user\" t3 ON ", "JOIN agreego.\"organization\" t3 ON ",
"JOIN agreego.\"person\" t4 ON ", "JOIN agreego.\"entity\" t4 ON ",
"WHERE", "WHERE",
" t1.id = '123')" " t1.id = '123')"
], ],
@ -1972,7 +1749,6 @@
], ],
[ [
"(SELECT pg_notify('entity', '{", "(SELECT pg_notify('entity', '{",
" \"kind\": \"create\",",
" \"complete\": {", " \"complete\": {",
" \"first_name\": \"John\",", " \"first_name\": \"John\",",
" \"last_name\": \"Doe\",", " \"last_name\": \"Doe\",",
@ -2154,7 +1930,6 @@
], ],
[ [
"(SELECT pg_notify('entity', '{", "(SELECT pg_notify('entity', '{",
" \"kind\": \"create\",",
" \"complete\": {", " \"complete\": {",
" \"total\": 100.0,", " \"total\": 100.0,",
" \"id\": \"{{uuid:generated_3}}\",", " \"id\": \"{{uuid:generated_3}}\",",
@ -2174,7 +1949,6 @@
], ],
[ [
"(SELECT pg_notify('entity', '{", "(SELECT pg_notify('entity', '{",
" \"kind\": \"create\",",
" \"complete\": {", " \"complete\": {",
" \"first_name\": \"Bob\",", " \"first_name\": \"Bob\",",
" \"last_name\": \"Smith\",", " \"last_name\": \"Smith\",",
@ -2220,8 +1994,8 @@
"sql": [ "sql": [
[ [
"(SELECT to_jsonb(t1.*) || to_jsonb(t2.*)", "(SELECT to_jsonb(t1.*) || to_jsonb(t2.*)",
"FROM agreego.\"entity\" t1", "FROM agreego.\"order\" t1",
"JOIN agreego.\"order\" t2 ON ", "JOIN agreego.\"entity\" t2 ON ",
"WHERE", "WHERE",
" t1.id = 'abc'", " t1.id = 'abc'",
" OR (\"id\" = 'abc'))" " OR (\"id\" = 'abc'))"
@ -2340,7 +2114,6 @@
], ],
[ [
"(SELECT pg_notify('entity', '{", "(SELECT pg_notify('entity', '{",
" \"kind\": \"create\",",
" \"complete\": {", " \"complete\": {",
" \"total\": 99.0,", " \"total\": 99.0,",
" \"id\": \"abc\",", " \"id\": \"abc\",",
@ -2358,7 +2131,6 @@
], ],
[ [
"(SELECT pg_notify('entity', '{", "(SELECT pg_notify('entity', '{",
" \"kind\": \"create\",",
" \"complete\": {", " \"complete\": {",
" \"product\": \"Widget\",", " \"product\": \"Widget\",",
" \"price\": 99.0,", " \"price\": 99.0,",
@ -2847,7 +2619,6 @@
], ],
[ [
"(SELECT pg_notify('entity', '{", "(SELECT pg_notify('entity', '{",
" \"kind\": \"create\",",
" \"complete\": {", " \"complete\": {",
" \"first_name\": \"Relation\",", " \"first_name\": \"Relation\",",
" \"last_name\": \"Test\",", " \"last_name\": \"Test\",",
@ -2867,7 +2638,6 @@
], ],
[ [
"(SELECT pg_notify('entity', '{", "(SELECT pg_notify('entity', '{",
" \"kind\": \"create\",",
" \"complete\": {", " \"complete\": {",
" \"is_primary\": true,", " \"is_primary\": true,",
" \"source_id\": \"{{uuid:generated_0}}\",", " \"source_id\": \"{{uuid:generated_0}}\",",
@ -2893,7 +2663,6 @@
], ],
[ [
"(SELECT pg_notify('entity', '{", "(SELECT pg_notify('entity', '{",
" \"kind\": \"create\",",
" \"complete\": {", " \"complete\": {",
" \"number\": \"555-0001\",", " \"number\": \"555-0001\",",
" \"id\": \"{{uuid:generated_1}}\",", " \"id\": \"{{uuid:generated_1}}\",",
@ -2911,7 +2680,6 @@
], ],
[ [
"(SELECT pg_notify('entity', '{", "(SELECT pg_notify('entity', '{",
" \"kind\": \"create\",",
" \"complete\": {", " \"complete\": {",
" \"is_primary\": false,", " \"is_primary\": false,",
" \"source_id\": \"{{uuid:generated_0}}\",", " \"source_id\": \"{{uuid:generated_0}}\",",
@ -2937,7 +2705,6 @@
], ],
[ [
"(SELECT pg_notify('entity', '{", "(SELECT pg_notify('entity', '{",
" \"kind\": \"create\",",
" \"complete\": {", " \"complete\": {",
" \"address\": \"test@example.com\",", " \"address\": \"test@example.com\",",
" \"id\": \"{{uuid:generated_5}}\",", " \"id\": \"{{uuid:generated_5}}\",",
@ -2955,7 +2722,6 @@
], ],
[ [
"(SELECT pg_notify('entity', '{", "(SELECT pg_notify('entity', '{",
" \"kind\": \"create\",",
" \"complete\": {", " \"complete\": {",
" \"is_primary\": false,", " \"is_primary\": false,",
" \"source_id\": \"{{uuid:generated_0}}\",", " \"source_id\": \"{{uuid:generated_0}}\",",
@ -2981,7 +2747,6 @@
], ],
[ [
"(SELECT pg_notify('entity', '{", "(SELECT pg_notify('entity', '{",
" \"kind\": \"create\",",
" \"complete\": {", " \"complete\": {",
" \"address\": \"test2@example.com\",", " \"address\": \"test2@example.com\",",
" \"id\": \"{{uuid:generated_9}}\",", " \"id\": \"{{uuid:generated_9}}\",",
@ -3023,62 +2788,20 @@
"sql": [ "sql": [
[ [
"(SELECT to_jsonb(t1.*) || to_jsonb(t2.*) || to_jsonb(t3.*) || to_jsonb(t4.*)", "(SELECT to_jsonb(t1.*) || to_jsonb(t2.*) || to_jsonb(t3.*) || to_jsonb(t4.*)",
"FROM agreego.\"entity\" t1", "FROM agreego.\"person\" t1",
"JOIN agreego.\"organization\" t2 ON ", "JOIN agreego.\"user\" t2 ON ",
"JOIN agreego.\"user\" t3 ON ", "JOIN agreego.\"organization\" t3 ON ",
"JOIN agreego.\"person\" t4 ON ", "JOIN agreego.\"entity\" t4 ON ",
"WHERE", "WHERE",
" t1.id = 'abc-archived')" " t1.id = 'abc-archived')"
], ],
[ [
"INSERT INTO agreego.\"entity\" (", "UPDATE agreego.\"entity\" SET",
" \"archived\",", " archived = true,",
" \"created_at\",", " modified_at = '{{timestamp}}',",
" \"created_by\",", " modified_by = '00000000-0000-0000-0000-000000000000'",
" \"id\",", "WHERE",
" \"modified_at\",", " id = 'abc-archived'"
" \"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 (", "INSERT INTO agreego.change (",
@ -3091,33 +2814,37 @@
" \"modified_by\"", " \"modified_by\"",
")", ")",
"VALUES (", "VALUES (",
" NULL,", " '{",
" \"archived\": false",
" }',",
" '{", " '{",
" \"archived\": true,", " \"archived\": true,",
" \"type\": \"person\"", " \"type\": \"person\"",
" }',", " }',",
" 'abc-archived',", " 'abc-archived',",
" '{{uuid:generated_0}}',", " '{{uuid:generated_0}}',",
" 'create',", " 'delete',",
" '{{timestamp}}',", " '{{timestamp}}',",
" '00000000-0000-0000-0000-000000000000'", " '00000000-0000-0000-0000-000000000000'",
")" ")"
], ],
[ [
"(SELECT pg_notify('entity', '{", "(SELECT pg_notify('entity', '{",
" \"kind\": \"create\",",
" \"complete\": {", " \"complete\": {",
" \"archived\": true,",
" \"id\": \"abc-archived\",", " \"id\": \"abc-archived\",",
" \"type\": \"person\",", " \"type\": \"person\",",
" \"created_by\": \"00000000-0000-0000-0000-000000000000\",", " \"first_name\": \"ArchivedFirst\",",
" \"created_at\": \"{{timestamp}}\",", " \"last_name\": \"ArchivedLast\",",
" \"archived\": true,",
" \"modified_by\": \"00000000-0000-0000-0000-000000000000\",", " \"modified_by\": \"00000000-0000-0000-0000-000000000000\",",
" \"modified_at\": \"{{timestamp}}\"", " \"modified_at\": \"{{timestamp}}\"",
" },", " },",
" \"new\": {", " \"new\": {",
" \"archived\": true,", " \"archived\": true,",
" \"type\": \"person\"", " \"type\": \"person\"",
" },",
" \"old\": {",
" \"archived\": false",
" }", " }",
"}'))" "}'))"
] ]
@ -3216,7 +2943,6 @@
], ],
[ [
"(SELECT pg_notify('entity', '{", "(SELECT pg_notify('entity', '{",
" \"kind\": \"create\",",
" \"complete\": {", " \"complete\": {",
" \"flags\": [", " \"flags\": [",
" \"urgent\",", " \"urgent\",",
@ -3332,7 +3058,6 @@
], ],
[ [
"(SELECT pg_notify('entity', '{", "(SELECT pg_notify('entity', '{",
" \"kind\": \"create\",",
" \"complete\": {", " \"complete\": {",
" \"product\": \"Widget\",", " \"product\": \"Widget\",",
" \"price\": 99.0,", " \"price\": 99.0,",
@ -3376,8 +3101,8 @@
"sql": [ "sql": [
[ [
"(SELECT to_jsonb(t1.*) || to_jsonb(t2.*)", "(SELECT to_jsonb(t1.*) || to_jsonb(t2.*)",
"FROM agreego.\"entity\" t1", "FROM agreego.\"order_line\" t1",
"JOIN agreego.\"order_line\" t2 ON ", "JOIN agreego.\"entity\" t2 ON ",
"WHERE", "WHERE",
" t1.id = '{{uuid:data.lines.0.id}}')" " t1.id = '{{uuid:data.lines.0.id}}')"
], ],
@ -3442,7 +3167,6 @@
], ],
[ [
"(SELECT pg_notify('entity', '{", "(SELECT pg_notify('entity', '{",
" \"kind\": \"create\",",
" \"complete\": {", " \"complete\": {",
" \"product\": \"Widget\",", " \"product\": \"Widget\",",
" \"price\": 99.0,", " \"price\": 99.0,",
@ -3500,8 +3224,8 @@
"sql": [ "sql": [
[ [
"(SELECT to_jsonb(t1.*) || to_jsonb(t2.*)", "(SELECT to_jsonb(t1.*) || to_jsonb(t2.*)",
"FROM agreego.\"entity\" t1", "FROM agreego.\"invoice\" t1",
"JOIN agreego.\"invoice\" t2 ON ", "JOIN agreego.\"entity\" t2 ON ",
"WHERE", "WHERE",
" t1.id = '{{uuid:data.id}}'", " t1.id = '{{uuid:data.id}}'",
" OR (\"id\" = '{{uuid:data.id}}'))" " OR (\"id\" = '{{uuid:data.id}}'))"
@ -3617,8 +3341,8 @@
"sql": [ "sql": [
[ [
"(SELECT to_jsonb(t1.*) || to_jsonb(t2.*)", "(SELECT to_jsonb(t1.*) || to_jsonb(t2.*)",
"FROM agreego.\"entity\" t1", "FROM agreego.\"account\" t1",
"JOIN agreego.\"account\" t2 ON ", "JOIN agreego.\"entity\" t2 ON ",
"WHERE", "WHERE",
" t1.id = '{{uuid:data.id}}')" " t1.id = '{{uuid:data.id}}')"
], ],
@ -3680,7 +3404,6 @@
], ],
[ [
"(SELECT pg_notify('entity', '{", "(SELECT pg_notify('entity', '{",
" \"kind\": \"create\",",
" \"complete\": {", " \"complete\": {",
" \"kind\": \"checking\",", " \"kind\": \"checking\",",
" \"routing_number\": \"123456789\",", " \"routing_number\": \"123456789\",",
@ -3975,7 +3698,6 @@
], ],
[ [
"(SELECT pg_notify('entity', '{", "(SELECT pg_notify('entity', '{",
" \"kind\": \"create\",",
" \"complete\": {", " \"complete\": {",
" \"organization_id\": \"parent-org-id\",", " \"organization_id\": \"parent-org-id\",",
" \"id\": \"{{uuid:generated_3}}\",", " \"id\": \"{{uuid:generated_3}}\",",
@ -3995,7 +3717,6 @@
], ],
[ [
"(SELECT pg_notify('entity', '{", "(SELECT pg_notify('entity', '{",
" \"kind\": \"create\",",
" \"complete\": {", " \"complete\": {",
" \"first_name\": \"Const\",", " \"first_name\": \"Const\",",
" \"last_name\": \"Person\",", " \"last_name\": \"Person\",",
@ -4017,7 +3738,6 @@
], ],
[ [
"(SELECT pg_notify('entity', '{", "(SELECT pg_notify('entity', '{",
" \"kind\": \"create\",",
" \"complete\": {", " \"complete\": {",
" \"order_id\": \"{{uuid:generated_3}}\",", " \"order_id\": \"{{uuid:generated_3}}\",",
" \"id\": \"{{uuid:generated_4}}\",", " \"id\": \"{{uuid:generated_4}}\",",
@ -4037,7 +3757,6 @@
], ],
[ [
"(SELECT pg_notify('entity', '{", "(SELECT pg_notify('entity', '{",
" \"kind\": \"create\",",
" \"complete\": {", " \"complete\": {",
" \"organization_id\": \"explicit-org-id\",", " \"organization_id\": \"explicit-org-id\",",
" \"order_id\": \"{{uuid:generated_3}}\",", " \"order_id\": \"{{uuid:generated_3}}\",",

File diff suppressed because it is too large Load Diff

View File

@ -159,9 +159,7 @@ impl Schema {
}, },
"null" => None, "null" => None,
custom => { custom => {
if custom.ends_with(".condition") || custom.ends_with(".filter") { if db.enums.contains_key(custom) {
Some(vec![custom.to_string()])
} else if db.enums.contains_key(custom) {
Some(vec![format!("{}.condition", custom)]) Some(vec![format!("{}.condition", custom)])
} else { } else {
// Assume anything else is a Relational cross-boundary that already has its own .filter dynamically built // Assume anything else is a Relational cross-boundary that already has its own .filter dynamically built

View File

@ -1,7 +1,8 @@
use serde_json::Value;
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use serde_json::Value;
pub fn compose(val: &mut Value, errors: &mut Vec<crate::drop::Error>) -> Result<(), String> { pub fn compose(val: &mut Value, errors: &mut Vec<crate::drop::Error>) -> Result<(), String> {
let _ = std::fs::write("/Users/awgneo/Repositories/thoughtpatterns/cellular/jspg/traits_debug_val.json", serde_json::to_string_pretty(val).unwrap());
let mut traits = HashMap::new(); let mut traits = HashMap::new();
let mut schemas = HashMap::new(); let mut schemas = HashMap::new();
@ -73,9 +74,7 @@ fn resolve_in_place(
return; return;
} }
let include_opt = current let include_opt = current.as_object_mut().and_then(|obj| obj.remove("include"));
.as_object_mut()
.and_then(|obj| obj.remove("include"));
if let Some(include_val) = include_opt { if let Some(include_val) = include_opt {
if let Some(include_arr) = include_val.as_array() { if let Some(include_arr) = include_val.as_array() {
let mut merged_props = serde_json::Map::new(); let mut merged_props = serde_json::Map::new();
@ -146,10 +145,7 @@ fn resolve_in_place(
visited.remove(inc_name); visited.remove(inc_name);
// Merge properties (host overrides trait) // Merge properties (host overrides trait)
if let Some(target_props) = resolved_target if let Some(target_props) = resolved_target.get("properties").and_then(|v| v.as_object()) {
.get("properties")
.and_then(|v| v.as_object())
{
for (k, v) in target_props { for (k, v) in target_props {
if !merged_props.contains_key(k) { if !merged_props.contains_key(k) {
merged_props.insert(k.clone(), v.clone()); merged_props.insert(k.clone(), v.clone());
@ -158,10 +154,7 @@ fn resolve_in_place(
} }
// Merge patternProperties (host overrides trait) // Merge patternProperties (host overrides trait)
if let Some(target_pat_props) = resolved_target if let Some(target_pat_props) = resolved_target.get("patternProperties").and_then(|v| v.as_object()) {
.get("patternProperties")
.and_then(|v| v.as_object())
{
for (k, v) in target_pat_props { for (k, v) in target_pat_props {
if !merged_pattern_props.contains_key(k) { if !merged_pattern_props.contains_key(k) {
merged_pattern_props.insert(k.clone(), v.clone()); merged_pattern_props.insert(k.clone(), v.clone());
@ -188,19 +181,11 @@ fn resolve_in_place(
} }
// Merge dependencies // Merge dependencies
if let Some(target_deps) = resolved_target if let Some(target_deps) = resolved_target.get("dependencies").and_then(|v| v.as_object()) {
.get("dependencies")
.and_then(|v| v.as_object())
{
for (dep_prop, dep_val) in target_deps { for (dep_prop, dep_val) in target_deps {
if let Some(existing_val) = merged_dependencies.get_mut(dep_prop) { if let Some(existing_val) = merged_dependencies.get_mut(dep_prop) {
if let (Some(arr_existing), Some(arr_target)) = if let (Some(arr_existing), Some(arr_target)) = (existing_val.as_array_mut(), dep_val.as_array()) {
(existing_val.as_array_mut(), dep_val.as_array()) let mut set: HashSet<String> = arr_existing.iter().filter_map(|x| x.as_str().map(String::from)).collect();
{
let mut set: HashSet<String> = arr_existing
.iter()
.filter_map(|x| x.as_str().map(String::from))
.collect();
for x in arr_target { for x in arr_target {
if let Some(s) = x.as_str() { if let Some(s) = x.as_str() {
if set.insert(s.to_string()) { if set.insert(s.to_string()) {
@ -218,13 +203,7 @@ fn resolve_in_place(
// Inherit other non-merged schemas/scalars if not defined in host (type, items, cases, family, format, etc.) // Inherit other non-merged schemas/scalars if not defined in host (type, items, cases, family, format, etc.)
if let Some(obj) = current.as_object_mut() { if let Some(obj) = current.as_object_mut() {
for (k, v) in resolved_target.as_object().unwrap() { for (k, v) in resolved_target.as_object().unwrap() {
if k != "properties" if k != "properties" && k != "patternProperties" && k != "required" && k != "display" && k != "dependencies" && k != "include" {
&& k != "patternProperties"
&& k != "required"
&& k != "display"
&& k != "dependencies"
&& k != "include"
{
if !obj.contains_key(k) { if !obj.contains_key(k) {
obj.insert(k.clone(), v.clone()); obj.insert(k.clone(), v.clone());
} }
@ -250,10 +229,7 @@ fn resolve_in_place(
obj.insert("properties".to_string(), Value::Object(merged_props)); obj.insert("properties".to_string(), Value::Object(merged_props));
} }
if !merged_pattern_props.is_empty() { if !merged_pattern_props.is_empty() {
obj.insert( obj.insert("patternProperties".to_string(), Value::Object(merged_pattern_props));
"patternProperties".to_string(),
Value::Object(merged_pattern_props),
);
} }
if !merged_required.is_empty() { if !merged_required.is_empty() {
let mut req_vec: Vec<Value> = merged_required.into_iter().map(Value::String).collect(); let mut req_vec: Vec<Value> = merged_required.into_iter().map(Value::String).collect();
@ -266,10 +242,7 @@ fn resolve_in_place(
obj.insert("display".to_string(), Value::Array(disp_vec)); obj.insert("display".to_string(), Value::Array(disp_vec));
} }
if !merged_dependencies.is_empty() { if !merged_dependencies.is_empty() {
obj.insert( obj.insert("dependencies".to_string(), Value::Object(merged_dependencies));
"dependencies".to_string(),
Value::Object(merged_dependencies),
);
} }
} }
} }
@ -279,138 +252,47 @@ fn resolve_in_place(
if let Some(obj) = current.as_object_mut() { if let Some(obj) = current.as_object_mut() {
if let Some(props) = obj.get_mut("properties").and_then(|v| v.as_object_mut()) { if let Some(props) = obj.get_mut("properties").and_then(|v| v.as_object_mut()) {
for (k, v) in props { for (k, v) in props {
resolve_in_place( resolve_in_place(v, traits, schemas, errors, schema_id, &format!("{}/{}", path, k), visited);
v,
traits,
schemas,
errors,
schema_id,
&format!("{}/{}", path, k),
visited,
);
} }
} }
if let Some(pat_props) = obj if let Some(pat_props) = obj.get_mut("patternProperties").and_then(|v| v.as_object_mut()) {
.get_mut("patternProperties")
.and_then(|v| v.as_object_mut())
{
for (k, v) in pat_props { for (k, v) in pat_props {
resolve_in_place( resolve_in_place(v, traits, schemas, errors, schema_id, &format!("{}/{}", path, k), visited);
v,
traits,
schemas,
errors,
schema_id,
&format!("{}/{}", path, k),
visited,
);
} }
} }
if let Some(items) = obj.get_mut("items") { if let Some(items) = obj.get_mut("items") {
resolve_in_place( resolve_in_place(items, traits, schemas, errors, schema_id, &format!("{}/items", path), visited);
items,
traits,
schemas,
errors,
schema_id,
&format!("{}/items", path),
visited,
);
} }
if let Some(prefix_items) = obj.get_mut("prefixItems").and_then(|v| v.as_array_mut()) { if let Some(prefix_items) = obj.get_mut("prefixItems").and_then(|v| v.as_array_mut()) {
for (i, v) in prefix_items.iter_mut().enumerate() { for (i, v) in prefix_items.iter_mut().enumerate() {
resolve_in_place( resolve_in_place(v, traits, schemas, errors, schema_id, &format!("{}/prefixItems/{}", path, i), visited);
v,
traits,
schemas,
errors,
schema_id,
&format!("{}/prefixItems/{}", path, i),
visited,
);
} }
} }
if let Some(additional_props) = obj.get_mut("additionalProperties") { if let Some(additional_props) = obj.get_mut("additionalProperties") {
resolve_in_place( resolve_in_place(additional_props, traits, schemas, errors, schema_id, &format!("{}/additionalProperties", path), visited);
additional_props,
traits,
schemas,
errors,
schema_id,
&format!("{}/additionalProperties", path),
visited,
);
} }
if let Some(one_of) = obj.get_mut("oneOf").and_then(|v| v.as_array_mut()) { if let Some(one_of) = obj.get_mut("oneOf").and_then(|v| v.as_array_mut()) {
for (i, v) in one_of.iter_mut().enumerate() { for (i, v) in one_of.iter_mut().enumerate() {
resolve_in_place( resolve_in_place(v, traits, schemas, errors, schema_id, &format!("{}/oneOf/{}", path, i), visited);
v,
traits,
schemas,
errors,
schema_id,
&format!("{}/oneOf/{}", path, i),
visited,
);
} }
} }
if let Some(contains) = obj.get_mut("contains") { if let Some(contains) = obj.get_mut("contains") {
resolve_in_place( resolve_in_place(contains, traits, schemas, errors, schema_id, &format!("{}/contains", path), visited);
contains,
traits,
schemas,
errors,
schema_id,
&format!("{}/contains", path),
visited,
);
} }
if let Some(not) = obj.get_mut("not") { if let Some(not) = obj.get_mut("not") {
resolve_in_place( resolve_in_place(not, traits, schemas, errors, schema_id, &format!("{}/not", path), visited);
not,
traits,
schemas,
errors,
schema_id,
&format!("{}/not", path),
visited,
);
} }
if let Some(cases) = obj.get_mut("cases").and_then(|v| v.as_array_mut()) { if let Some(cases) = obj.get_mut("cases").and_then(|v| v.as_array_mut()) {
for (i, c_val) in cases.iter_mut().enumerate() { for (i, c_val) in cases.iter_mut().enumerate() {
if let Some(c_obj) = c_val.as_object_mut() { if let Some(c_obj) = c_val.as_object_mut() {
if let Some(when) = c_obj.get_mut("when") { if let Some(when) = c_obj.get_mut("when") {
resolve_in_place( resolve_in_place(when, traits, schemas, errors, schema_id, &format!("{}/cases/{}/when", path, i), visited);
when,
traits,
schemas,
errors,
schema_id,
&format!("{}/cases/{}/when", path, i),
visited,
);
} }
if let Some(then) = c_obj.get_mut("then") { if let Some(then) = c_obj.get_mut("then") {
resolve_in_place( resolve_in_place(then, traits, schemas, errors, schema_id, &format!("{}/cases/{}/then", path, i), visited);
then,
traits,
schemas,
errors,
schema_id,
&format!("{}/cases/{}/then", path, i),
visited,
);
} }
if let Some(else_) = c_obj.get_mut("else") { if let Some(else_) = c_obj.get_mut("else") {
resolve_in_place( resolve_in_place(else_, traits, schemas, errors, schema_id, &format!("{}/cases/{}/else", path, i), visited);
else_,
traits,
schemas,
errors,
schema_id,
&format!("{}/cases/{}/else", path, i),
visited,
);
} }
} }
} }

View File

@ -206,7 +206,6 @@ impl Database {
self.executor.timestamp() self.executor.timestamp()
} }
pub fn compile(&mut self, errors: &mut Vec<crate::drop::Error>) { pub fn compile(&mut self, errors: &mut Vec<crate::drop::Error>) {
// Phase 1: Registration // Phase 1: Registration
self.collect_schemas(errors); self.collect_schemas(errors);

View File

@ -138,9 +138,7 @@ impl Merger {
is_child: bool, is_child: bool,
) -> Result<Value, String> { ) -> Result<Value, String> {
match data { match data {
Value::Array(items) => { Value::Array(items) => self.merge_array(schema, items, notifications, parent_org_id, is_child),
self.merge_array(schema, items, notifications, parent_org_id, is_child)
}
Value::Object(map) => { Value::Object(map) => {
if let Some(options) = schema.obj.compiled_options.get() { if let Some(options) = schema.obj.compiled_options.get() {
if let Some(disc) = schema.obj.compiled_discriminator.get() { if let Some(disc) = schema.obj.compiled_discriminator.get() {
@ -212,13 +210,7 @@ impl Merger {
let mut resolved_items = Vec::new(); let mut resolved_items = Vec::new();
for item in items { for item in items {
let resolved = self.merge_internal( let resolved = self.merge_internal(item_schema.clone(), item, notifications, parent_org_id.clone(), is_child)?;
item_schema.clone(),
item,
notifications,
parent_org_id.clone(),
is_child,
)?;
resolved_items.push(resolved); resolved_items.push(resolved);
} }
Ok(Value::Array(resolved_items)) Ok(Value::Array(resolved_items))
@ -348,10 +340,7 @@ impl Merger {
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;
let org_id_to_pass = entity_fields let org_id_to_pass = entity_fields.get("organization_id").and_then(|v| v.as_str()).map(|s| s.to_string());
.get("organization_id")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
if parent_is_source { if parent_is_source {
let mut merged_relative = match self.merge_internal( let mut merged_relative = match self.merge_internal(
rel_schema.clone(), rel_schema.clone(),
@ -454,10 +443,7 @@ impl Merger {
} }
} }
let org_id_to_pass = entity_fields let org_id_to_pass = entity_fields.get("organization_id").and_then(|v| v.as_str()).map(|s| s.to_string());
.get("organization_id")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let mut relative_responses = Vec::new(); let mut relative_responses = Vec::new();
for relative_item_val in relative_arr { for relative_item_val in relative_arr {
if let Value::Object(mut relative_item) = relative_item_val { if let Value::Object(mut relative_item) = relative_item_val {
@ -588,7 +574,7 @@ impl Merger {
.and_then(|v| v.as_str()) .and_then(|v| v.as_str())
.unwrap_or(""); .unwrap_or("");
let id_val = if entity_id.is_empty() { let id_val = if entity_id.is_empty() {
Value::String(uuid::Uuid::now_v7().to_string()) Value::String(uuid::Uuid::new_v4().to_string())
} else { } else {
Value::String(entity_id.to_string()) Value::String(entity_id.to_string())
}; };
@ -791,8 +777,13 @@ impl Merger {
} }
}; };
for table_name in &entity_type.hierarchy { let mut execute_order: Vec<String> = entity_type.hierarchy.clone();
let table_fields = match grouped_fields.get(table_name).and_then(|v| v.as_array()) { 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()) {
Some(arr) => arr Some(arr) => arr
.iter() .iter()
.filter_map(|v| v.as_str().map(|s| s.to_string())) .filter_map(|v| v.as_str().map(|s| s.to_string()))
@ -956,7 +947,6 @@ impl Merger {
}; };
let mut notification = serde_json::Map::new(); let mut notification = serde_json::Map::new();
notification.insert("kind".to_string(), Value::String(change_kind.to_string()));
notification.insert("complete".to_string(), Value::Object(complete)); notification.insert("complete".to_string(), Value::Object(complete));
notification.insert("new".to_string(), new_val_obj.clone()); notification.insert("new".to_string(), new_val_obj.clone());
@ -971,11 +961,11 @@ impl Merger {
let mut notify_sql = None; let mut notify_sql = None;
if type_obj.historical && change_kind != "replace" { if type_obj.historical && change_kind != "replace" {
let change_sql = format!( let change_sql = format!(
"INSERT INTO agreego.change (\"old\", \"new\", \"entity_id\", \"id\", \"kind\", \"modified_at\", \"modified_by\") VALUES ({}, {}, {}, {}, {}, {}, {})", "INSERT INTO agreego.change (\"old\", \"new\", entity_id, id, kind, modified_at, modified_by) VALUES ({}, {}, {}, {}, {}, {}, {})",
Self::quote_literal(&old_val_obj), Self::quote_literal(&old_val_obj),
Self::quote_literal(&new_val_obj), Self::quote_literal(&new_val_obj),
Self::quote_literal(id_str), Self::quote_literal(id_str),
Self::quote_literal(&Value::String(uuid::Uuid::now_v7().to_string())), Self::quote_literal(&Value::String(uuid::Uuid::new_v4().to_string())),
Self::quote_literal(&Value::String(change_kind.to_string())), Self::quote_literal(&Value::String(change_kind.to_string())),
Self::quote_literal(&Value::String(timestamp.to_string())), Self::quote_literal(&Value::String(timestamp.to_string())),
Self::quote_literal(&Value::String(user_id.to_string())) Self::quote_literal(&Value::String(user_id.to_string()))

View File

@ -574,7 +574,7 @@ impl<'a> Compiler<'a> {
fn compile_polymorphic_bounds( fn compile_polymorphic_bounds(
&self, &self,
r#type: &crate::database::r#type::Type, _type: &crate::database::r#type::Type,
type_aliases: &std::collections::HashMap<String, String>, type_aliases: &std::collections::HashMap<String, String>,
node: &Node, node: &Node,
where_clauses: &mut Vec<String>, where_clauses: &mut Vec<String>,
@ -604,35 +604,13 @@ impl<'a> Compiler<'a> {
if let Some(type_name) = bound_type_name { if let Some(type_name) = bound_type_name {
// Ensure this type actually exists // Ensure this type actually exists
if let Some(type_def) = self.db.types.get(&type_name) { 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) { if let Some(relation) = self.db.relations.get(&edge.constraint) {
let mut poly_col = None; let mut poly_col = None;
let mut table_to_alias = ""; 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 { if edge.forward && relation.source_columns.len() > 1 {
poly_col = Some(&relation.source_columns[1]); // e.g., target_type poly_col = Some(&relation.source_columns[1]); // e.g., target_type
table_to_alias = &relation.source_type; // e.g., relationship table_to_alias = &relation.source_type; // e.g., relationship
null_tolerant = !r#type.relationship;
} else if !edge.forward && relation.destination_columns.len() > 1 { } else if !edge.forward && relation.destination_columns.len() > 1 {
poly_col = Some(&relation.destination_columns[1]); // e.g., source_type poly_col = Some(&relation.destination_columns[1]); // e.g., source_type
table_to_alias = &relation.destination_type; // e.g., relationship table_to_alias = &relation.destination_type; // e.g., relationship
@ -643,25 +621,22 @@ impl<'a> Compiler<'a> {
.get(table_to_alias) .get(table_to_alias)
.or_else(|| type_aliases.get(&node.parent_alias)) .or_else(|| type_aliases.get(&node.parent_alias))
{ {
let predicate = if type_def.variations.len() > 1 { // DEBUG: See what variations we have for this type
let quoted: Vec<String> = type_def #[cfg(not(test))]
.variations pgrx::notice!("JSPG_POLY_BOUNDS: type={}, col={}, variations_len={}, variations={:?}", type_name, col, type_def.variations.len(), type_def.variations);
.iter()
// Use IN clause with all variations to support inherited types.
// e.g., "asset" has variations ["asset", "property", "unit", ...]
// so target_type IN ('asset','property','unit') instead of = 'asset'
if type_def.variations.len() > 1 {
let quoted: Vec<String> = type_def.variations.iter()
.map(|v| format!("'{}'", v)) .map(|v| format!("'{}'", v))
.collect(); .collect();
format!("{}.{} IN ({})", alias, col, quoted.join(", ")) where_clauses.push(format!(
"{}.{} IN ({})", alias, col, quoted.join(", ")
));
} else { } else {
format!("{}.{} = '{}'", alias, col, type_name) where_clauses.push(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

@ -50,6 +50,10 @@ impl Queryer {
Err(drop) => return drop, Err(drop) => return drop,
}; };
// DEBUG: Emit compiled SQL as a NOTICE for inspection
#[cfg(not(test))]
pgrx::notice!("JSPG_SQL[{}]: {}", schema_id, sql);
// 3. Execute via Database Executor // 3. Execute via Database Executor
self.execute_sql(schema_id, &sql, args) self.execute_sql(schema_id, &sql, args)
} }

View File

@ -1277,18 +1277,6 @@ fn test_dynamic_type_0_4() {
crate::tests::runner::run_test_case(&path, 0, 4).unwrap(); crate::tests::runner::run_test_case(&path, 0, 4).unwrap();
} }
#[test]
fn test_dynamic_type_0_5() {
let path = format!("{}/fixtures/dynamicType.json", env!("CARGO_MANIFEST_DIR"));
crate::tests::runner::run_test_case(&path, 0, 5).unwrap();
}
#[test]
fn test_dynamic_type_0_6() {
let path = format!("{}/fixtures/dynamicType.json", env!("CARGO_MANIFEST_DIR"));
crate::tests::runner::run_test_case(&path, 0, 6).unwrap();
}
#[test] #[test]
fn test_property_names_0_0() { fn test_property_names_0_0() {
let path = format!("{}/fixtures/propertyNames.json", env!("CARGO_MANIFEST_DIR")); let path = format!("{}/fixtures/propertyNames.json", env!("CARGO_MANIFEST_DIR"));
@ -1517,18 +1505,6 @@ fn test_queryer_0_15() {
crate::tests::runner::run_test_case(&path, 0, 15).unwrap(); 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] #[test]
fn test_polymorphism_0_0() { fn test_polymorphism_0_0() {
let path = format!("{}/fixtures/polymorphism.json", env!("CARGO_MANIFEST_DIR")); let path = format!("{}/fixtures/polymorphism.json", env!("CARGO_MANIFEST_DIR"));

View File

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

View File

@ -96,10 +96,8 @@ impl Case {
let queries = db.executor.get_queries(); let queries = db.executor.get_queries();
if std::env::var("UPDATE_EXPECT").is_ok() { if std::env::var("UPDATE_EXPECT").is_ok() {
crate::tests::runner::update_sql_fixture(path, suite_idx, case_idx, &queries); crate::tests::runner::update_sql_fixture(path, suite_idx, case_idx, &queries);
Ok(())
} else {
expect.assert_sql(&queries)
} }
expect.assert_sql(&queries)
} else { } else {
Ok(()) Ok(())
} }
@ -130,10 +128,8 @@ impl Case {
let queries = db.executor.get_queries(); let queries = db.executor.get_queries();
if std::env::var("UPDATE_EXPECT").is_ok() { if std::env::var("UPDATE_EXPECT").is_ok() {
crate::tests::runner::update_sql_fixture(path, suite_idx, case_idx, &queries); crate::tests::runner::update_sql_fixture(path, suite_idx, case_idx, &queries);
Ok(())
} else {
expect.assert_sql(&queries)
} }
expect.assert_sql(&queries)
} else { } else {
Ok(()) Ok(())
} }

View File

@ -1,3 +1,4 @@
pub mod pattern;
pub mod sql; pub mod sql;
pub mod drop; pub mod drop;
pub mod schema; pub mod schema;

View File

@ -0,0 +1,132 @@
use super::Expect;
use regex::Regex;
use std::collections::HashMap;
impl Expect {
/// Advanced SQL execution assertion algorithm ported from `assert.go`.
/// This compares two arrays of strings, one containing {{uuid:name}} or {{timestamp}} placeholders,
/// and the other containing actual executed database queries. It ensures that placeholder UUIDs
/// are consistently mapped to the same actual UUIDs across all lines, and strictly validates line-by-line sequences.
pub fn assert_pattern(&self, actual: &[String]) -> Result<(), String> {
let patterns = match &self.sql {
Some(s) => s,
None => return Ok(()),
};
if patterns.len() != actual.len() {
return Err(format!(
"Length mismatch: expected {} SQL executions, got {}.\nActual Execution Log:\n{}",
patterns.len(),
actual.len(),
actual.join("\n")
));
}
let ws_re = Regex::new(r"\s+").unwrap();
let types = HashMap::from([
(
"uuid",
r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}",
),
(
"timestamp",
r"\d{4}-\d{2}-\d{2}(?:[ T])\d{2}:\d{2}:\d{2}(?:\.\d{1,6})?(?:Z|\+\d{2}(?::\d{2})?)?",
),
("integer", r"-?\d+"),
("float", r"-?\d+\.\d+"),
("text", r"(?:''|[^'])*"),
("json", r"(?:''|[^'])*"),
]);
let mut seen: HashMap<String, String> = HashMap::new();
let system_uuid = "00000000-0000-0000-0000-000000000000";
// Placeholder regex: {{type:name}} or {{type}}
let ph_rx = Regex::new(r"\{\{([a-z]+)(?:[:]([^}]+))?\}\}").unwrap();
let clean_str = |s: &str| -> String {
let mut s = ws_re.replace_all(s, " ").into_owned();
for token in ["(", ")", ",", "{", "}", "\"", "=", "'"] {
s = s.replace(&format!(" {}", token), token);
s = s.replace(&format!("{} ", token), token);
}
s.trim().to_string()
};
for (i, pattern_expect) in patterns.iter().enumerate() {
let aline_raw = &actual[i];
let aline = clean_str(aline_raw);
let pattern_str_raw = match pattern_expect {
super::SqlExpectation::Single(s) => s.clone(),
super::SqlExpectation::Multi(m) => m.join(" "),
};
let pattern_str = clean_str(&pattern_str_raw);
let mut pp = regex::escape(&pattern_str);
pp = pp.replace(r"\{\{", "{{").replace(r"\}\}", "}}");
let mut cap_names = HashMap::new(); // cg_X -> var_name
let mut group_idx = 0;
let mut final_rx_str = String::new();
let mut last_match = 0;
let pp_clone = pp.clone();
for caps in ph_rx.captures_iter(&pp_clone) {
let full_match = caps.get(0).unwrap();
final_rx_str.push_str(&pp[last_match..full_match.start()]);
let type_name = caps.get(1).unwrap().as_str();
let var_name = caps.get(2).map(|m| m.as_str());
if let Some(name) = var_name {
if let Some(val) = seen.get(name) {
final_rx_str.push_str(&regex::escape(val));
} else {
let type_pattern = types.get(type_name).unwrap_or(&".*?");
let cg_name = format!("cg_{}", group_idx);
final_rx_str.push_str(&format!("(?P<{}>{})", cg_name, type_pattern));
cap_names.insert(cg_name, name.to_string());
group_idx += 1;
}
} else {
let type_pattern = types.get(type_name).unwrap_or(&".*?");
final_rx_str.push_str(&format!("(?:{})", type_pattern));
}
last_match = full_match.end();
}
final_rx_str.push_str(&pp[last_match..]);
let final_rx = match Regex::new(&format!("^{}$", final_rx_str)) {
Ok(r) => r,
Err(e) => return Err(format!("Bad constructed regex: {} -> {}", final_rx_str, e)),
};
if let Some(captures) = final_rx.captures(&aline) {
for (cg_name, var_name) in cap_names {
if let Some(m) = captures.name(&cg_name) {
let matched_str = m.as_str();
if matched_str != system_uuid {
seen.insert(var_name, matched_str.to_string());
}
}
}
} else {
return Err(format!(
"Line mismatched at execution sequence {}.\nExpected Pattern: {}\nActual SQL: {}\nRegex used: {}\nVariables Mapped: {:?}",
i + 1,
pattern_str,
aline,
final_rx_str,
seen
));
}
}
Ok(())
}
}

View File

@ -1,9 +1,8 @@
use super::Expect; use super::Expect;
use regex::Regex;
use sqlparser::ast::{Expr, Query, SelectItem, Statement, TableFactor}; use sqlparser::ast::{Expr, Query, SelectItem, Statement, TableFactor};
use sqlparser::dialect::PostgreSqlDialect; use sqlparser::dialect::PostgreSqlDialect;
use sqlparser::parser::Parser; use sqlparser::parser::Parser;
use std::collections::{HashMap, HashSet}; use std::collections::HashSet;
impl Expect { impl Expect {
pub fn assert_sql(&self, actual: &[String]) -> Result<(), String> { pub fn assert_sql(&self, actual: &[String]) -> Result<(), String> {
@ -12,7 +11,6 @@ impl Expect {
return Err(e); return Err(e);
} }
} }
self.assert_pattern(actual)?;
Ok(()) Ok(())
} }
@ -205,132 +203,4 @@ impl Expect {
} }
Ok(()) Ok(())
} }
/// Advanced SQL execution assertion algorithm ported from `assert.go`.
/// This compares two arrays of strings, one containing {{uuid:name}} or {{timestamp}} placeholders,
/// and the other containing actual executed database queries. It ensures that placeholder UUIDs
/// are consistently mapped to the same actual UUIDs across all lines, and strictly validates line-by-line sequences.
pub fn assert_pattern(&self, actual: &[String]) -> Result<(), String> {
let patterns = match &self.sql {
Some(s) => s,
None => return Ok(()),
};
if patterns.len() != actual.len() {
return Err(format!(
"Length mismatch: expected {} SQL executions, got {}.\nActual Execution Log:\n{}",
patterns.len(),
actual.len(),
actual.join("\n")
));
}
let ws_re = Regex::new(r"\s+").unwrap();
let types = HashMap::from([
(
"uuid",
r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}",
),
(
"timestamp",
r"\d{4}-\d{2}-\d{2}(?:[ T])\d{2}:\d{2}:\d{2}(?:\.\d{1,6})?(?:Z|\+\d{2}(?::\d{2})?)?",
),
("integer", r"-?\d+"),
("float", r"-?\d+\.\d+"),
("text", r"(?:''|[^'])*"),
("json", r"(?:''|[^'])*"),
]);
let mut seen: HashMap<String, String> = HashMap::new();
let system_uuid = "00000000-0000-0000-0000-000000000000";
// Placeholder regex: {{type:name}} or {{type}}
let ph_rx = Regex::new(r"\{\{([a-z]+)(?:[:]([^}]+))?\}\}").unwrap();
let clean_str = |s: &str| -> String {
let mut s = ws_re.replace_all(s, " ").into_owned();
for token in ["(", ")", ",", "{", "}", "\"", "=", "'"] {
s = s.replace(&format!(" {}", token), token);
s = s.replace(&format!("{} ", token), token);
}
s.trim().to_string()
};
for (i, pattern_expect) in patterns.iter().enumerate() {
let aline_raw = &actual[i];
let formatted_actual = crate::tests::formatter::SqlFormatter::format(aline_raw).join(" ");
let aline = clean_str(&formatted_actual);
let pattern_str_raw = match pattern_expect {
super::SqlExpectation::Single(s) => s.clone(),
super::SqlExpectation::Multi(m) => m.join(" "),
};
let pattern_str = clean_str(&pattern_str_raw);
let mut pp = regex::escape(&pattern_str);
pp = pp.replace(r"\{\{", "{{").replace(r"\}\}", "}}");
let mut cap_names = HashMap::new(); // cg_X -> var_name
let mut group_idx = 0;
let mut final_rx_str = String::new();
let mut last_match = 0;
let pp_clone = pp.clone();
for caps in ph_rx.captures_iter(&pp_clone) {
let full_match = caps.get(0).unwrap();
final_rx_str.push_str(&pp[last_match..full_match.start()]);
let type_name = caps.get(1).unwrap().as_str();
let var_name = caps.get(2).map(|m| m.as_str());
if let Some(name) = var_name {
if let Some(val) = seen.get(name) {
final_rx_str.push_str(&regex::escape(val));
} else {
let type_pattern = types.get(type_name).unwrap_or(&".*?");
let cg_name = format!("cg_{}", group_idx);
final_rx_str.push_str(&format!("(?P<{}>{})", cg_name, type_pattern));
cap_names.insert(cg_name, name.to_string());
group_idx += 1;
}
} else {
let type_pattern = types.get(type_name).unwrap_or(&".*?");
final_rx_str.push_str(&format!("(?:{})", type_pattern));
}
last_match = full_match.end();
}
final_rx_str.push_str(&pp[last_match..]);
let final_rx = match Regex::new(&format!("^{}$", final_rx_str)) {
Ok(r) => r,
Err(e) => return Err(format!("Bad constructed regex: {} -> {}", final_rx_str, e)),
};
if let Some(captures) = final_rx.captures(&aline) {
for (cg_name, var_name) in cap_names {
if let Some(m) = captures.name(&cg_name) {
let matched_str = m.as_str();
if matched_str != system_uuid {
seen.insert(var_name, matched_str.to_string());
}
}
}
} else {
return Err(format!(
"Line mismatched at execution sequence {}.\nExpected Pattern: {}\nActual SQL: {}\nRegex used: {}\nVariables Mapped: {:?}",
i + 1,
pattern_str,
aline,
final_rx_str,
seen
));
}
}
Ok(())
}
} }

View File

@ -15,7 +15,7 @@ pub struct ValidationContext<'a> {
pub extensible: bool, pub extensible: bool,
pub reporter: bool, pub reporter: bool,
pub overrides: HashSet<String>, pub overrides: HashSet<String>,
pub parents: Vec<&'a serde_json::Value>, pub parent: Option<&'a serde_json::Value>,
} }
impl<'a> ValidationContext<'a> { impl<'a> ValidationContext<'a> {
@ -39,7 +39,7 @@ impl<'a> ValidationContext<'a> {
extensible: effective_extensible, extensible: effective_extensible,
reporter, reporter,
overrides, overrides,
parents: Vec::new(), parent: None,
} }
} }
@ -63,11 +63,6 @@ impl<'a> ValidationContext<'a> {
) -> Self { ) -> Self {
let effective_extensible = schema.extensible.unwrap_or(extensible); let effective_extensible = schema.extensible.unwrap_or(extensible);
let mut parents = self.parents.clone();
if let Some(p) = parent_instance {
parents.push(p);
}
Self { Self {
db: self.db, db: self.db,
root: self.root, root: self.root,
@ -78,7 +73,7 @@ impl<'a> ValidationContext<'a> {
extensible: effective_extensible, extensible: effective_extensible,
reporter, reporter,
overrides, overrides,
parents, parent: parent_instance,
} }
} }
@ -90,7 +85,7 @@ impl<'a> ValidationContext<'a> {
HashSet::new(), HashSet::new(),
self.extensible, self.extensible,
reporter, reporter,
None, self.parent,
) )
} }

View File

@ -42,13 +42,46 @@ impl Validator {
} }
pub fn validate(&self, schema_id: &str, instance: &Value) -> crate::drop::Drop { pub fn validate(&self, schema_id: &str, instance: &Value) -> crate::drop::Drop {
let schema_opt = self.db.schemas.get(schema_id); let schema_arc = if schema_id.trim().starts_with('{') {
match serde_json::from_str::<crate::database::schema::Schema>(schema_id) {
Ok(schema) => {
let mut errors = Vec::new();
schema.compile(&self.db, "inline", "inline".to_string(), &mut errors);
if !errors.is_empty() {
return crate::drop::Drop::with_errors(errors);
}
Arc::new(schema)
}
Err(e) => {
return crate::drop::Drop::with_errors(vec![crate::drop::Error {
code: "SCHEMA_PARSE_FAILED".to_string(),
message: format!("Failed to parse inline schema: {}", e),
details: crate::drop::ErrorDetails::default(),
}]);
}
}
} else {
match self.db.schemas.get(schema_id) {
Some(schema) => Arc::clone(schema),
None => {
return crate::drop::Drop::with_errors(vec![crate::drop::Error {
code: "SCHEMA_NOT_FOUND".to_string(),
message: format!("Schema {} not found", schema_id),
details: crate::drop::ErrorDetails {
path: Some("/".to_string()),
cause: None,
context: None,
schema: None,
},
}]);
}
}
};
if let Some(schema) = schema_opt {
let ctx = ValidationContext::new( let ctx = ValidationContext::new(
&self.db, &self.db,
&schema, &schema_arc,
&schema, &schema_arc,
instance, instance,
HashSet::new(), HashSet::new(),
false, false,
@ -69,7 +102,7 @@ impl Validator {
path: Some(e.path), path: Some(e.path),
cause: None, cause: None,
context: None, context: None,
schema: Some(schema_id.to_string()), schema: None,
}, },
}) })
.collect(); .collect();
@ -83,21 +116,9 @@ impl Validator {
path: Some(e.path), path: Some(e.path),
cause: None, cause: None,
context: None, context: None,
schema: Some(schema_id.to_string()), schema: None,
}, },
}]), }]),
} }
} else {
crate::drop::Drop::with_errors(vec![crate::drop::Error {
code: "SCHEMA_NOT_FOUND".to_string(),
message: format!("Schema {} not found", schema_id),
details: crate::drop::ErrorDetails {
path: Some("/".to_string()),
cause: None,
context: None,
schema: Some(schema_id.to_string()),
},
}])
}
} }
} }

View File

@ -59,13 +59,12 @@ impl<'a> ValidationContext<'a> {
}; };
let mut resolved = false; let mut resolved = false;
for parent in self.parents.iter().rev() { if let Some(parent) = self.parent {
if let Some(obj) = parent.as_object() { if let Some(obj) = parent.as_object() {
if let Some(val) = obj.get(var_name) { if let Some(val) = obj.get(var_name) {
if let Some(str_val) = val.as_str() { if let Some(str_val) = val.as_str() {
target_id = format!("{}{}", str_val, suffix); target_id = format!("{}{}", str_val, suffix);
resolved = true; resolved = true;
break;
} }
} }
} }
@ -98,7 +97,7 @@ impl<'a> ValidationContext<'a> {
new_overrides, new_overrides,
self.extensible, self.extensible,
true, // Reporter mode true, // Reporter mode
None, self.parent,
); );
shadow.root = &global_schema; shadow.root = &global_schema;
result.merge(shadow.validate()?); result.merge(shadow.validate()?);

View File

@ -1 +1 @@
1.0.161 1.0.151