Compare commits

..

17 Commits

14 changed files with 675 additions and 190 deletions

View File

@ -111,6 +111,10 @@ Polymorphism is how an object boundary can dynamically take on entirely differen
* *Setup*: `{ "family": "widget" }` (Where `widget` is a table type but has no external variations). * *Setup*: `{ "family": "widget" }` (Where `widget` is a table type but has no external variations).
* *Execution*: The engine queries `db.types.get("widget").variations` and finds only `["widget"]`. Since it lacks table inheritance, it is treated as STI. The engine scans the specific, confined `schemas` array directly under `db.types.get("widget")` for any registered key terminating in the base `.widget` (e.g., `stock.widget`). The `family` automatically uses `kind` as the discriminator. * *Execution*: The engine queries `db.types.get("widget").variations` and finds only `["widget"]`. Since it lacks table inheritance, it is treated as STI. The engine scans the specific, confined `schemas` array directly under `db.types.get("widget")` for any registered key terminating in the base `.widget` (e.g., `stock.widget`). The `family` automatically uses `kind` as the discriminator.
* *Options*: `stock` -> `stock.widget`, `tasks` -> `tasks.widget`. * *Options*: `stock` -> `stock.widget`, `tasks` -> `tasks.widget`.
* **Scenario D: JSONB Bubble Inheritance (Field-Backed)**
* *Setup*: `{ "family": "panel" }` (Where `panel` is NOT a table type, but rather an isolated JSONB boundary defined within another table's `schemas`).
* *Execution*: The engine observes `panel` is not in `db.types` (because it has no physical table). It falls back to scanning the global `db.schemas` registry for any registered key terminating in the base `.panel` (e.g., `balance.panel`, `units.panel`). The `family` automatically uses `kind` as the discriminator.
* *Options*: `balance` -> `balance.panel`, `units` -> `units.panel`.
* **`oneOf` (Strict Tagged Unions)**: A hardcoded list of candidate schemas. Unlike `family` which relies on global DB metadata, `oneOf` forces pure mathematical structural evaluation of the provided candidates. It strictly bans typical JSON Schema "Union of Sets" fallback searches. Every candidate MUST possess a mathematically unique discriminator payload to allow $O(1)$ routing. * **`oneOf` (Strict Tagged Unions)**: A hardcoded list of candidate schemas. Unlike `family` which relies on global DB metadata, `oneOf` forces pure mathematical structural evaluation of the provided candidates. It strictly bans typical JSON Schema "Union of Sets" fallback searches. Every candidate MUST possess a mathematically unique discriminator payload to allow $O(1)$ routing.
* **Disjoint Types**: `oneOf: [{ "type": "person" }, { "type": "widget" }]`. The engine succeeds because the native `type` acts as a unique discriminator (`"person"` vs `"widget"`). * **Disjoint Types**: `oneOf: [{ "type": "person" }, { "type": "widget" }]`. The engine succeeds because the native `type` acts as a unique discriminator (`"person"` vs `"widget"`).

View File

@ -138,6 +138,9 @@
} }
} }
}, },
"filter": {
"type": "object"
},
"condition": { "condition": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -300,7 +303,7 @@
} }
}, },
"type": [ "type": [
"object", "filter",
"null" "null"
] ]
}, },
@ -341,11 +344,11 @@
] ]
} }
}, },
"type": "object" "type": "filter"
}, },
"address": {}, "address": {},
"address.filter": { "address.filter": {
"type": "object", "type": "filter",
"compiledPropertyNames": [ "compiledPropertyNames": [
"$and", "$and",
"$or", "$or",
@ -389,12 +392,13 @@
} }
}, },
"condition": {}, "condition": {},
"filter": {},
"string.condition": {}, "string.condition": {},
"integer.condition": {}, "integer.condition": {},
"date.condition": {}, "date.condition": {},
"search": {}, "search": {},
"search.filter": { "search.filter": {
"type": "object", "type": "filter",
"compiledPropertyNames": [ "compiledPropertyNames": [
"$and", "$and",
"$or", "$or",

View File

@ -146,6 +146,9 @@
"modified_at": { "modified_at": {
"type": "string", "type": "string",
"format": "date-time" "format": "date-time"
},
"organization_id": {
"type": "string"
} }
}, },
"required": [ "required": [
@ -168,7 +171,8 @@
"created_by", "created_by",
"modified_at", "modified_at",
"modified_by", "modified_by",
"archived" "archived",
"organization_id"
], ],
"grouped_fields": { "grouped_fields": {
"entity": [ "entity": [
@ -178,7 +182,8 @@
"created_by", "created_by",
"modified_at", "modified_at",
"modified_by", "modified_by",
"archived" "archived",
"organization_id"
] ]
}, },
"lookup_fields": [], "lookup_fields": [],
@ -345,6 +350,10 @@
} }
} }
} }
},
"organization_id": {
"type": "string",
"const": "ffffffff-ffff-ffff-ffff-ffffffffffff"
} }
} }
} }
@ -368,7 +377,8 @@
"created_by", "created_by",
"modified_at", "modified_at",
"modified_by", "modified_by",
"archived" "archived",
"organization_id"
], ],
"grouped_fields": { "grouped_fields": {
"person": [ "person": [
@ -396,7 +406,8 @@
"created_by", "created_by",
"modified_at", "modified_at",
"modified_by", "modified_by",
"archived" "archived",
"organization_id"
] ]
}, },
"lookup_fields": [ "lookup_fields": [
@ -446,7 +457,8 @@
"created_by", "created_by",
"modified_at", "modified_at",
"modified_by", "modified_by",
"archived" "archived",
"organization_id"
], ],
"grouped_fields": { "grouped_fields": {
"order": [ "order": [
@ -462,7 +474,8 @@
"created_by", "created_by",
"modified_at", "modified_at",
"modified_by", "modified_by",
"archived" "archived",
"organization_id"
] ]
}, },
"lookup_fields": [ "lookup_fields": [
@ -504,7 +517,8 @@
"created_by", "created_by",
"modified_at", "modified_at",
"modified_by", "modified_by",
"archived" "archived",
"organization_id"
], ],
"grouped_fields": { "grouped_fields": {
"order_line": [ "order_line": [
@ -521,7 +535,8 @@
"created_by", "created_by",
"modified_at", "modified_at",
"modified_by", "modified_by",
"archived" "archived",
"organization_id"
] ]
}, },
"lookup_fields": [], "lookup_fields": [],
@ -1791,6 +1806,7 @@
" \"id\",", " \"id\",",
" \"modified_at\",", " \"modified_at\",",
" \"modified_by\",", " \"modified_by\",",
" \"organization_id\",",
" \"type\"", " \"type\"",
")", ")",
"VALUES (", "VALUES (",
@ -1799,6 +1815,7 @@
" '{{uuid:customer_id}}',", " '{{uuid:customer_id}}',",
" '{{timestamp}}',", " '{{timestamp}}',",
" '00000000-0000-0000-0000-000000000000',", " '00000000-0000-0000-0000-000000000000',",
" 'ffffffff-ffff-ffff-ffff-ffffffffffff',",
" 'person'", " 'person'",
")" ")"
], ],
@ -1854,6 +1871,7 @@
" \"date_of_birth\":\"2000-01-01\",", " \"date_of_birth\":\"2000-01-01\",",
" \"first_name\":\"Bob\",", " \"first_name\":\"Bob\",",
" \"last_name\":\"Smith\",", " \"last_name\":\"Smith\",",
" \"organization_id\":\"ffffffff-ffff-ffff-ffff-ffffffffffff\",",
" \"type\":\"person\"", " \"type\":\"person\"",
" }',", " }',",
" '{{uuid:customer_id}}',", " '{{uuid:customer_id}}',",
@ -1949,12 +1967,14 @@
" \"last_name\":\"Smith\",", " \"last_name\":\"Smith\",",
" \"modified_at\":\"{{timestamp}}\",", " \"modified_at\":\"{{timestamp}}\",",
" \"modified_by\":\"00000000-0000-0000-0000-000000000000\",", " \"modified_by\":\"00000000-0000-0000-0000-000000000000\",",
" \"organization_id\":\"ffffffff-ffff-ffff-ffff-ffffffffffff\",",
" \"type\":\"person\"", " \"type\":\"person\"",
" },", " },",
" \"new\":{", " \"new\":{",
" \"date_of_birth\":\"2000-01-01\",", " \"date_of_birth\":\"2000-01-01\",",
" \"first_name\":\"Bob\",", " \"first_name\":\"Bob\",",
" \"last_name\":\"Smith\",", " \"last_name\":\"Smith\",",
" \"organization_id\":\"ffffffff-ffff-ffff-ffff-ffffffffffff\",",
" \"type\":\"person\"", " \"type\":\"person\"",
" }", " }",
" }')" " }')"
@ -3126,10 +3146,26 @@
"type": "invoice", "type": "invoice",
"number": "INV-1001", "number": "INV-1001",
"total": 200.0, "total": 200.0,
"metadata_line": {"price": 50}, "metadata_line": {
"metadata_lines": [{"price": 25}], "price": 50
"metadata_nested_line": {"line": {"price": 75}}, },
"metadata_nested_lines": {"lines": [{"price": 100}]} "metadata_lines": [
{
"price": 25
}
],
"metadata_nested_line": {
"line": {
"price": 75
}
},
"metadata_nested_lines": {
"lines": [
{
"price": 100
}
]
}
}, },
"expect": { "expect": {
"success": true, "success": true,
@ -3304,6 +3340,359 @@
] ]
] ]
} }
},
{
"description": "Test organization_id syntactic sugar permutations",
"action": "merge",
"data": {
"type": "order",
"organization_id": "parent-org-id",
"customer": {
"type": "person",
"first_name": "Const",
"last_name": "Person"
},
"lines": [
{
"type": "order_line"
},
{
"type": "order_line",
"organization_id": "explicit-org-id"
}
]
},
"schema_id": "order",
"expect": {
"success": true,
"sql": [
[
"INSERT INTO agreego.\"entity\" (",
" \"created_at\",",
" \"created_by\",",
" \"id\",",
" \"modified_at\",",
" \"modified_by\",",
" \"organization_id\",",
" \"type\"",
")",
"VALUES (",
" '{{timestamp}}',",
" '{{uuid}}',",
" '{{uuid:person_id}}',",
" '{{timestamp}}',",
" '{{uuid}}',",
" 'ffffffff-ffff-ffff-ffff-ffffffffffff',",
" 'person'",
")"
],
[
"INSERT INTO agreego.\"organization\" (",
" \"id\",",
" \"type\"",
")",
"VALUES (",
" '{{uuid:person_id}}',",
" 'person'",
")"
],
[
"INSERT INTO agreego.\"user\" (",
" \"id\",",
" \"type\"",
")",
"VALUES (",
" '{{uuid:person_id}}',",
" 'person'",
")"
],
[
"INSERT INTO agreego.\"person\" (",
" \"first_name\",",
" \"id\",",
" \"last_name\",",
" \"type\"",
")",
"VALUES (",
" 'Const',",
" '{{uuid:person_id}}',",
" 'Person',",
" 'person'",
")"
],
[
"INSERT INTO agreego.change (",
" \"old\",",
" \"new\",",
" entity_id,",
" id,",
" kind,",
" modified_at,",
" modified_by",
")",
"VALUES (",
" NULL,",
" '{",
" \"first_name\":\"Const\",",
" \"last_name\":\"Person\",",
" \"organization_id\":\"ffffffff-ffff-ffff-ffff-ffffffffffff\",",
" \"type\":\"person\"",
" }',",
" '{{uuid:person_id}}',",
" '{{uuid}}',",
" 'create',",
" '{{timestamp}}',",
" '{{uuid}}'",
")"
],
[
"INSERT INTO agreego.\"entity\" (",
" \"created_at\",",
" \"created_by\",",
" \"id\",",
" \"modified_at\",",
" \"modified_by\",",
" \"organization_id\",",
" \"type\"",
")",
"VALUES (",
" '{{timestamp}}',",
" '{{uuid}}',",
" '{{uuid:order_id}}',",
" '{{timestamp}}',",
" '{{uuid}}',",
" 'parent-org-id',",
" 'order'",
")"
],
[
"INSERT INTO agreego.\"order\" (",
" \"customer_id\",",
" \"id\",",
" \"type\"",
")",
"VALUES (",
" '{{uuid:person_id}}',",
" '{{uuid:order_id}}',",
" 'order'",
")"
],
[
"INSERT INTO agreego.\"entity\" (",
" \"created_at\",",
" \"created_by\",",
" \"id\",",
" \"modified_at\",",
" \"modified_by\",",
" \"organization_id\",",
" \"type\"",
")",
"VALUES (",
" '{{timestamp}}',",
" '{{uuid}}',",
" '{{uuid:line1_id}}',",
" '{{timestamp}}',",
" '{{uuid}}',",
" 'parent-org-id',",
" 'order_line'",
")"
],
[
"INSERT INTO agreego.\"order_line\" (",
" \"id\",",
" \"order_id\",",
" \"type\"",
")",
"VALUES (",
" '{{uuid:line1_id}}',",
" '{{uuid:order_id}}',",
" 'order_line'",
")"
],
[
"INSERT INTO agreego.change (",
" \"old\",",
" \"new\",",
" entity_id,",
" id,",
" kind,",
" modified_at,",
" modified_by",
")",
"VALUES (",
" NULL,",
" '{",
" \"order_id\":\"{{uuid:order_id}}\",",
" \"organization_id\":\"parent-org-id\",",
" \"type\":\"order_line\"",
" }',",
" '{{uuid:line1_id}}',",
" '{{uuid}}',",
" 'create',",
" '{{timestamp}}',",
" '{{uuid}}'",
")"
],
[
"INSERT INTO agreego.\"entity\" (",
" \"created_at\",",
" \"created_by\",",
" \"id\",",
" \"modified_at\",",
" \"modified_by\",",
" \"organization_id\",",
" \"type\"",
")",
"VALUES (",
" '{{timestamp}}',",
" '{{uuid}}',",
" '{{uuid:line2_id}}',",
" '{{timestamp}}',",
" '{{uuid}}',",
" 'explicit-org-id',",
" 'order_line'",
")"
],
[
"INSERT INTO agreego.\"order_line\" (",
" \"id\",",
" \"order_id\",",
" \"type\"",
")",
"VALUES (",
" '{{uuid:line2_id}}',",
" '{{uuid:order_id}}',",
" 'order_line'",
")"
],
[
"INSERT INTO agreego.change (",
" \"old\",",
" \"new\",",
" entity_id,",
" id,",
" kind,",
" modified_at,",
" modified_by",
")",
"VALUES (",
" NULL,",
" '{",
" \"order_id\":\"{{uuid:order_id}}\",",
" \"organization_id\":\"explicit-org-id\",",
" \"type\":\"order_line\"",
" }',",
" '{{uuid:line2_id}}',",
" '{{uuid}}',",
" 'create',",
" '{{timestamp}}',",
" '{{uuid}}'",
")"
],
[
"INSERT INTO agreego.change (",
" \"old\",",
" \"new\",",
" entity_id,",
" id,",
" kind,",
" modified_at,",
" modified_by",
")",
"VALUES (",
" NULL,",
" '{",
" \"customer_id\":\"{{uuid:person_id}}\",",
" \"organization_id\":\"parent-org-id\",",
" \"type\":\"order\"",
" }',",
" '{{uuid:order_id}}',",
" '{{uuid}}',",
" 'create',",
" '{{timestamp}}',",
" '{{uuid}}'",
")"
],
[
"SELECT pg_notify('entity', '{",
" \"complete\":{",
" \"created_at\":\"{{timestamp}}\",",
" \"created_by\":\"00000000-0000-0000-0000-000000000000\",",
" \"customer_id\":\"{{uuid:person_id}}\",",
" \"id\":\"{{uuid:order_id}}\",",
" \"modified_at\":\"{{timestamp}}\",",
" \"modified_by\":\"00000000-0000-0000-0000-000000000000\",",
" \"organization_id\":\"parent-org-id\",",
" \"type\":\"order\"",
" },",
" \"new\":{",
" \"customer_id\":\"{{uuid:person_id}}\",",
" \"organization_id\":\"parent-org-id\",",
" \"type\":\"order\"",
" }",
" }')"
],
[
"SELECT pg_notify('entity', '{",
" \"complete\":{",
" \"created_at\":\"{{timestamp}}\",",
" \"created_by\":\"00000000-0000-0000-0000-000000000000\",",
" \"first_name\":\"Const\",",
" \"id\":\"{{uuid:person_id}}\",",
" \"last_name\":\"Person\",",
" \"modified_at\":\"{{timestamp}}\",",
" \"modified_by\":\"00000000-0000-0000-0000-000000000000\",",
" \"organization_id\":\"ffffffff-ffff-ffff-ffff-ffffffffffff\",",
" \"type\":\"person\"",
" },",
" \"new\":{",
" \"first_name\":\"Const\",",
" \"last_name\":\"Person\",",
" \"organization_id\":\"ffffffff-ffff-ffff-ffff-ffffffffffff\",",
" \"type\":\"person\"",
" }",
" }')"
],
[
"SELECT pg_notify('entity', '{",
" \"complete\":{",
" \"created_at\":\"{{timestamp}}\",",
" \"created_by\":\"00000000-0000-0000-0000-000000000000\",",
" \"id\":\"{{uuid:line1_id}}\",",
" \"modified_at\":\"{{timestamp}}\",",
" \"modified_by\":\"00000000-0000-0000-0000-000000000000\",",
" \"order_id\":\"{{uuid:order_id}}\",",
" \"organization_id\":\"parent-org-id\",",
" \"type\":\"order_line\"",
" },",
" \"new\":{",
" \"order_id\":\"{{uuid:order_id}}\",",
" \"organization_id\":\"parent-org-id\",",
" \"type\":\"order_line\"",
" }",
" }')"
],
[
"SELECT pg_notify('entity', '{",
" \"complete\":{",
" \"created_at\":\"{{timestamp}}\",",
" \"created_by\":\"00000000-0000-0000-0000-000000000000\",",
" \"id\":\"{{uuid:line2_id}}\",",
" \"modified_at\":\"{{timestamp}}\",",
" \"modified_by\":\"00000000-0000-0000-0000-000000000000\",",
" \"order_id\":\"{{uuid:order_id}}\",",
" \"organization_id\":\"explicit-org-id\",",
" \"type\":\"order_line\"",
" },",
" \"new\":{",
" \"order_id\":\"{{uuid:order_id}}\",",
" \"organization_id\":\"explicit-org-id\",",
" \"type\":\"order_line\"",
" }",
" }')"
]
]
}
} }
] ]
} }

View File

@ -1,6 +1,6 @@
[ [
{ {
"description": "Vertical family Routing (Across Tables)", "description": "Vertical family Routing (Scenario A)",
"database": { "database": {
"types": [ "types": [
{ {
@ -153,7 +153,7 @@
] ]
}, },
{ {
"description": "Matrix family Routing (Vertical + Horizontal Intersections)", "description": "Matrix family Routing (Scenario B)",
"database": { "database": {
"types": [ "types": [
{ {
@ -284,7 +284,7 @@
] ]
}, },
{ {
"description": "Horizontal family Routing (Virtual Variations)", "description": "Horizontal family Routing (Scenario C)",
"database": { "database": {
"types": [ "types": [
{ {
@ -776,5 +776,123 @@
} }
} }
] ]
},
{
"description": "JSONB Field Bubble family Routing (Scenario D)",
"database": {
"types": [
{
"name": "dashboard",
"variations": [
"dashboard"
],
"schemas": {
"dashboard": {
"type": "object",
"properties": {
"type": {
"type": "string"
}
}
},
"panel": {
"type": "object",
"required": [
"id",
"kind"
],
"properties": {
"id": {
"type": "string"
},
"kind": {
"type": "string"
}
}
},
"balance.panel": {
"type": "panel",
"properties": {
"amount": {
"type": "integer"
}
}
},
"units.panel": {
"type": "panel",
"properties": {
"count": {
"type": "integer"
}
}
}
}
},
{
"name": "family_panel",
"schemas": {
"family_panel": {
"family": "panel"
}
}
}
]
},
"tests": [
{
"description": "Successfully routes to nested balance panel",
"schema_id": "family_panel",
"data": {
"id": "123",
"kind": "balance",
"amount": 500
},
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "Fails validation on routed schema due to invalid property type",
"schema_id": "family_panel",
"data": {
"id": "123",
"kind": "balance",
"amount": "not_an_int"
},
"action": "validate",
"expect": {
"success": false,
"errors": [
{
"code": "INVALID_TYPE",
"details": {
"path": "amount"
}
}
]
}
},
{
"description": "Fails when discriminator does not match any bubble schema",
"schema_id": "family_panel",
"data": {
"id": "123",
"kind": "unknown_panel"
},
"action": "validate",
"expect": {
"success": false,
"errors": [
{
"code": "NO_FAMILY_MATCH",
"details": {
"path": ""
}
}
]
}
}
]
} }
] ]

View File

@ -1320,7 +1320,7 @@
" 'id', entity_11.id,", " 'id', entity_11.id,",
" 'is_primary', contact_9.is_primary,", " 'is_primary', contact_9.is_primary,",
" 'target', CASE", " 'target', CASE",
" WHEN entity_11.target_type = 'address' THEN", " WHEN relationship_10.target_type = 'address' THEN",
" ((SELECT jsonb_build_object(", " ((SELECT jsonb_build_object(",
" 'archived', entity_13.archived,", " 'archived', entity_13.archived,",
" 'city', address_12.city,", " 'city', address_12.city,",
@ -1333,7 +1333,7 @@
" WHERE", " WHERE",
" NOT entity_13.archived", " NOT entity_13.archived",
" AND relationship_10.target_id = entity_13.id))", " AND relationship_10.target_id = entity_13.id))",
" WHEN entity_11.target_type = 'email_address' THEN", " WHEN relationship_10.target_type = 'email_address' THEN",
" ((SELECT jsonb_build_object(", " ((SELECT jsonb_build_object(",
" 'address', email_address_14.address,", " 'address', email_address_14.address,",
" 'archived', entity_15.archived,", " 'archived', entity_15.archived,",
@ -1346,7 +1346,7 @@
" WHERE", " WHERE",
" NOT entity_15.archived", " NOT entity_15.archived",
" AND relationship_10.target_id = entity_15.id))", " AND relationship_10.target_id = entity_15.id))",
" WHEN entity_11.target_type = 'phone_number' THEN", " WHEN relationship_10.target_type = 'phone_number' THEN",
" ((SELECT jsonb_build_object(", " ((SELECT jsonb_build_object(",
" 'archived', entity_17.archived,", " 'archived', entity_17.archived,",
" 'created_at', entity_17.created_at,", " 'created_at', entity_17.created_at,",
@ -1556,7 +1556,7 @@
" 'id', entity_11.id,", " 'id', entity_11.id,",
" 'is_primary', contact_9.is_primary,", " 'is_primary', contact_9.is_primary,",
" 'target', CASE", " 'target', CASE",
" WHEN entity_11.target_type = 'address' THEN", " WHEN relationship_10.target_type = 'address' THEN",
" ((SELECT jsonb_build_object(", " ((SELECT jsonb_build_object(",
" 'archived', entity_13.archived,", " 'archived', entity_13.archived,",
" 'city', address_12.city,", " 'city', address_12.city,",
@ -1569,7 +1569,7 @@
" WHERE", " WHERE",
" NOT entity_13.archived", " NOT entity_13.archived",
" AND relationship_10.target_id = entity_13.id))", " AND relationship_10.target_id = entity_13.id))",
" WHEN entity_11.target_type = 'email_address' THEN", " WHEN relationship_10.target_type = 'email_address' THEN",
" ((SELECT jsonb_build_object(", " ((SELECT jsonb_build_object(",
" 'address', email_address_14.address,", " 'address', email_address_14.address,",
" 'archived', entity_15.archived,", " 'archived', entity_15.archived,",
@ -1582,7 +1582,7 @@
" WHERE", " WHERE",
" NOT entity_15.archived", " NOT entity_15.archived",
" AND relationship_10.target_id = entity_15.id))", " AND relationship_10.target_id = entity_15.id))",
" WHEN entity_11.target_type = 'phone_number' THEN", " WHEN relationship_10.target_type = 'phone_number' THEN",
" ((SELECT jsonb_build_object(", " ((SELECT jsonb_build_object(",
" 'archived', entity_17.archived,", " 'archived', entity_17.archived,",
" 'created_at', entity_17.created_at,", " 'created_at', entity_17.created_at,",
@ -1989,7 +1989,7 @@
" 'is_primary', contact_11.is_primary,", " 'is_primary', contact_11.is_primary,",
" 'target',", " 'target',",
" CASE", " CASE",
" WHEN entity_13.target_type = 'address' THEN (", " WHEN relationship_12.target_type = 'address' THEN (",
" (SELECT jsonb_build_object(", " (SELECT jsonb_build_object(",
" 'archived', entity_15.archived,", " 'archived', entity_15.archived,",
" 'city', address_14.city,", " 'city', address_14.city,",
@ -2003,7 +2003,7 @@
" NOT entity_15.archived", " NOT entity_15.archived",
" AND relationship_12.target_id = entity_15.id)", " AND relationship_12.target_id = entity_15.id)",
" )", " )",
" WHEN entity_13.target_type = 'email_address' THEN (", " WHEN relationship_12.target_type = 'email_address' THEN (",
" (SELECT jsonb_build_object(", " (SELECT jsonb_build_object(",
" 'address', email_address_16.address,", " 'address', email_address_16.address,",
" 'archived', entity_17.archived,", " 'archived', entity_17.archived,",
@ -2017,7 +2017,7 @@
" NOT entity_17.archived", " NOT entity_17.archived",
" AND relationship_12.target_id = entity_17.id)", " AND relationship_12.target_id = entity_17.id)",
" )", " )",
" WHEN entity_13.target_type = 'phone_number' THEN (", " WHEN relationship_12.target_type = 'phone_number' THEN (",
" (SELECT jsonb_build_object(", " (SELECT jsonb_build_object(",
" 'archived', entity_19.archived,", " 'archived', entity_19.archived,",
" 'created_at', entity_19.created_at,", " 'created_at', entity_19.created_at,",

2
flows

Submodule flows updated: 4d61e13e00...0d9bd8644e

View File

@ -34,7 +34,7 @@ impl Schema {
if let Some(mut inline_schema) = structural_filter { if let Some(mut inline_schema) = structural_filter {
inline_schema.obj.type_ = Some(SchemaTypeOrArray::Multiple(vec![ inline_schema.obj.type_ = Some(SchemaTypeOrArray::Multiple(vec![
"object".to_string(), "filter".to_string(),
"null".to_string(), "null".to_string(),
])); ]));
@ -104,8 +104,8 @@ impl Schema {
} }
let mut wrapper_obj = SchemaObject::default(); let mut wrapper_obj = SchemaObject::default();
// Filters are just plain objects containing conditions, no inheritance required // Filters now inherit from the base 'filter' type
wrapper_obj.type_ = Some(SchemaTypeOrArray::Single("object".to_string())); wrapper_obj.type_ = Some(SchemaTypeOrArray::Single("filter".to_string()));
wrapper_obj.properties = Some(filter_props); wrapper_obj.properties = Some(filter_props);
return Some(Schema { return Some(Schema {

View File

@ -9,7 +9,7 @@ impl Schema {
errors: &mut Vec<crate::drop::Error>, errors: &mut Vec<crate::drop::Error>,
) { ) {
let mut options = std::collections::BTreeMap::new(); let mut options = std::collections::BTreeMap::new();
let mut strategy = String::new(); let strategy: &str;
if let Some(family) = &self.obj.family { if let Some(family) = &self.obj.family {
// Formalize the <Variant>.<Base> topology // Formalize the <Variant>.<Base> topology
@ -24,7 +24,7 @@ impl Schema {
if let Some(type_def) = db.types.get(&family_base) { if let Some(type_def) = db.types.get(&family_base) {
if type_def.variations.len() > 1 && type_def.variations.iter().any(|v| v != &family_base) { if type_def.variations.len() > 1 && type_def.variations.iter().any(|v| v != &family_base) {
// Scenario A / B: Table Variations // Scenario A / B: Table Variations
strategy = "type".to_string(); strategy = "type";
for var in &type_def.variations { for var in &type_def.variations {
let target_id = if family_prefix.is_empty() { let target_id = if family_prefix.is_empty() {
var.to_string() var.to_string()
@ -38,7 +38,7 @@ impl Schema {
} }
} else { } else {
// Scenario C: Single Table Inheritance (Horizontal) // Scenario C: Single Table Inheritance (Horizontal)
strategy = "kind".to_string(); strategy = "kind";
let suffix = format!(".{}", family_base); let suffix = format!(".{}", family_base);
@ -50,6 +50,19 @@ impl Schema {
} }
} }
} }
} else {
// Scenario D: Field-Backed JSONB Bubble STI (No explicit table representation)
strategy = "kind";
let suffix = format!(".{}", family_base);
// Scan the entire database schemas registry for matching suffixes
for (id, schema) in &db.schemas {
if id.ends_with(&suffix) || id == &family_base {
if let Some(kind_val) = schema.obj.get_discriminator_value("kind", id) {
options.insert(kind_val, (None, Some(id.to_string())));
}
}
}
} }
} else if let Some(one_of) = &self.obj.one_of { } else if let Some(one_of) = &self.obj.one_of {
let mut type_vals = std::collections::HashSet::new(); let mut type_vals = std::collections::HashSet::new();
@ -84,7 +97,7 @@ impl Schema {
} }
if disjoint_base && structural_types.len() == one_of.len() { if disjoint_base && structural_types.len() == one_of.len() {
strategy = "".to_string(); strategy = "";
for (i, c) in one_of.iter().enumerate() { for (i, c) in one_of.iter().enumerate() {
if let Some(crate::database::object::SchemaTypeOrArray::Single(t)) = &c.obj.type_ { if let Some(crate::database::object::SchemaTypeOrArray::Single(t)) = &c.obj.type_ {
if crate::database::object::is_primitive_type(t) { if crate::database::object::is_primitive_type(t) {
@ -96,11 +109,11 @@ impl Schema {
} }
} else { } else {
strategy = if type_vals.len() > 1 && type_vals.len() == one_of.len() { strategy = if type_vals.len() > 1 && type_vals.len() == one_of.len() {
"type".to_string() "type"
} else if kind_vals.len() > 1 && kind_vals.len() == one_of.len() { } else if kind_vals.len() > 1 && kind_vals.len() == one_of.len() {
"kind".to_string() "kind"
} else { } else {
"".to_string() ""
}; };
if strategy.is_empty() { if strategy.is_empty() {
@ -148,7 +161,7 @@ impl Schema {
if !options.is_empty() { if !options.is_empty() {
if !strategy.is_empty() { if !strategy.is_empty() {
let _ = self.obj.compiled_discriminator.set(strategy); let _ = self.obj.compiled_discriminator.set(strategy.to_string());
} }
let _ = self.obj.compiled_options.set(options); let _ = self.obj.compiled_options.set(options);
} }

View File

@ -40,7 +40,7 @@ impl Merger {
} }
}; };
let result = self.merge_internal(target_schema, data, &mut notifications_queue); let result = self.merge_internal(target_schema, data, &mut notifications_queue, None, false);
let val_resolved = match result { let val_resolved = match result {
Ok(val) => val, Ok(val) => val,
@ -134,9 +134,11 @@ impl Merger {
mut schema: Arc<crate::database::schema::Schema>, mut schema: Arc<crate::database::schema::Schema>,
data: Value, data: Value,
notifications: &mut Vec<String>, notifications: &mut Vec<String>,
parent_org_id: Option<String>,
is_child: bool,
) -> Result<Value, String> { ) -> Result<Value, String> {
match data { match data {
Value::Array(items) => self.merge_array(schema, items, notifications), Value::Array(items) => 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() {
@ -144,9 +146,7 @@ impl Merger {
if let Some(v) = val { if let Some(v) = val {
if let Some((idx_opt, target_id_opt)) = options.get(v) { if let Some((idx_opt, target_id_opt)) = options.get(v) {
if let Some(target_id) = target_id_opt { if let Some(target_id) = target_id_opt {
if let Some(target_schema) = if let Some(target_schema) = self.db.schemas.get(target_id) {
self.db.schemas.get(target_id)
{
schema = target_schema.clone(); schema = target_schema.clone();
} else { } else {
return Err(format!( return Err(format!(
@ -185,7 +185,7 @@ impl Merger {
} }
} }
} }
self.merge_object(schema, map, notifications) self.merge_object(schema, map, notifications, parent_org_id, is_child)
} }
_ => Err("Invalid merge payload: root must be an Object or Array".to_string()), _ => Err("Invalid merge payload: root must be an Object or Array".to_string()),
} }
@ -196,6 +196,8 @@ impl Merger {
schema: Arc<crate::database::schema::Schema>, schema: Arc<crate::database::schema::Schema>,
items: Vec<Value>, items: Vec<Value>,
notifications: &mut Vec<String>, notifications: &mut Vec<String>,
parent_org_id: Option<String>,
is_child: bool,
) -> Result<Value, String> { ) -> Result<Value, String> {
let mut item_schema = schema.clone(); let mut item_schema = schema.clone();
if let Some(crate::database::object::SchemaTypeOrArray::Single(t)) = &schema.obj.type_ { if let Some(crate::database::object::SchemaTypeOrArray::Single(t)) = &schema.obj.type_ {
@ -208,7 +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(item_schema.clone(), item, notifications)?; let resolved = self.merge_internal(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))
@ -219,6 +221,8 @@ impl Merger {
schema: Arc<crate::database::schema::Schema>, schema: Arc<crate::database::schema::Schema>,
obj: serde_json::Map<String, Value>, obj: serde_json::Map<String, Value>,
notifications: &mut Vec<String>, notifications: &mut Vec<String>,
parent_org_id: Option<String>,
is_child: bool,
) -> Result<Value, String> { ) -> Result<Value, String> {
let queue_start = notifications.len(); let queue_start = notifications.len();
@ -278,6 +282,20 @@ impl Merger {
} }
} }
let mut current_org_id = None;
if let Some(compiled_props) = schema.obj.compiled_properties.get() {
if let Some(org_schema) = compiled_props.get("organization_id") {
if let Some(c) = &org_schema.obj.const_ {
if let Some(c_str) = c.as_str() {
current_org_id = Some(c_str.to_string());
}
}
}
}
if current_org_id.is_none() {
current_org_id = parent_org_id.clone();
}
let user_id = self.db.auth_user_id()?; let user_id = self.db.auth_user_id()?;
let timestamp = self.db.timestamp()?; let timestamp = self.db.timestamp()?;
@ -292,6 +310,16 @@ impl Merger {
entity_change_kind = kind; entity_change_kind = kind;
entity_fetched = fetched; entity_fetched = fetched;
entity_replaces = replaces; entity_replaces = replaces;
if entity_change_kind.as_deref() == Some("create") {
if is_child {
if !entity_fields.contains_key("organization_id") {
if let Some(ref org_id) = current_org_id {
entity_fields.insert("organization_id".to_string(), Value::String(org_id.clone()));
}
}
}
}
} }
let mut entity_response = serde_json::Map::new(); let mut entity_response = serde_json::Map::new();
@ -312,17 +340,14 @@ 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.get("organization_id").and_then(|v| v.as_str()).map(|s| s.to_string());
if parent_is_source { if parent_is_source {
if !relative.contains_key("organization_id") {
if let Some(org_id) = entity_fields.get("organization_id") {
relative.insert("organization_id".to_string(), org_id.clone());
}
}
let mut merged_relative = match self.merge_internal( let mut merged_relative = match self.merge_internal(
rel_schema.clone(), rel_schema.clone(),
Value::Object(relative), Value::Object(relative),
notifications, notifications,
org_id_to_pass.clone(),
true,
)? { )? {
Value::Object(m) => m, Value::Object(m) => m,
_ => continue, _ => continue,
@ -338,12 +363,6 @@ impl Merger {
); );
entity_response.insert(relation_name, Value::Object(merged_relative)); entity_response.insert(relation_name, Value::Object(merged_relative));
} else { } else {
if !relative.contains_key("organization_id") {
if let Some(org_id) = entity_fields.get("organization_id") {
relative.insert("organization_id".to_string(), org_id.clone());
}
}
Self::apply_entity_relation( Self::apply_entity_relation(
&mut relative, &mut relative,
&relation.source_columns, &relation.source_columns,
@ -355,6 +374,8 @@ impl Merger {
rel_schema.clone(), rel_schema.clone(),
Value::Object(relative), Value::Object(relative),
notifications, notifications,
org_id_to_pass.clone(),
true,
)? { )? {
Value::Object(m) => m, Value::Object(m) => m,
_ => continue, _ => continue,
@ -374,6 +395,16 @@ impl Merger {
entity_change_kind = kind; entity_change_kind = kind;
entity_fetched = fetched; entity_fetched = fetched;
entity_replaces = replaces; entity_replaces = replaces;
if entity_change_kind.as_deref() == Some("create") {
if is_child {
if !entity_fields.contains_key("organization_id") {
if let Some(ref org_id) = current_org_id {
entity_fields.insert("organization_id".to_string(), Value::String(org_id.clone()));
}
}
}
}
} }
self.merge_entity_fields( self.merge_entity_fields(
@ -401,15 +432,21 @@ impl Merger {
if let Some(compiled_edges) = schema.obj.compiled_edges.get() { if let Some(compiled_edges) = schema.obj.compiled_edges.get() {
if let Some(edge) = compiled_edges.get(&relation_name) { if let Some(edge) = compiled_edges.get(&relation_name) {
if let Some(relation) = self.db.relations.get(&edge.constraint) { if let Some(relation) = self.db.relations.get(&edge.constraint) {
let mut item_schema = rel_schema.clone();
if let Some(crate::database::object::SchemaTypeOrArray::Single(t)) =
&rel_schema.obj.type_
{
if t == "array" {
if let Some(items_def) = &rel_schema.obj.items {
item_schema = items_def.clone();
}
}
}
let org_id_to_pass = entity_fields.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 {
if !relative_item.contains_key("organization_id") {
if let Some(org_id) = entity_fields.get("organization_id") {
relative_item.insert("organization_id".to_string(), org_id.clone());
}
}
Self::apply_entity_relation( Self::apply_entity_relation(
&mut relative_item, &mut relative_item,
&relation.source_columns, &relation.source_columns,
@ -417,21 +454,12 @@ impl Merger {
&entity_fields, &entity_fields,
); );
let mut item_schema = rel_schema.clone();
if let Some(crate::database::object::SchemaTypeOrArray::Single(t)) =
&rel_schema.obj.type_
{
if t == "array" {
if let Some(items_def) = &rel_schema.obj.items {
item_schema = items_def.clone();
}
}
}
let merged_relative = match self.merge_internal( let merged_relative = match self.merge_internal(
item_schema, item_schema.clone(),
Value::Object(relative_item), Value::Object(relative_item),
notifications, notifications,
org_id_to_pass.clone(),
true,
)? { )? {
Value::Object(m) => m, Value::Object(m) => m,
_ => continue, _ => continue,

View File

@ -461,10 +461,24 @@ impl<'a> Compiler<'a> {
.cloned() .cloned()
.unwrap_or_else(|| format!("{}_t_err", node.parent_alias)); .unwrap_or_else(|| format!("{}_t_err", node.parent_alias));
let mut lookup_key = prop_key.as_str();
if let Some(edges) = node.schema.obj.compiled_edges.get() {
if let Some(edge) = edges.get(prop_key) {
if let Some(relation) = self.db.relations.get(&edge.constraint) {
if edge.forward {
lookup_key = &relation.source_columns[0];
} else {
lookup_key = &relation.destination_columns[0];
}
}
}
}
if let Some(gf) = grouped_fields { if let Some(gf) = grouped_fields {
for (t_name, fields_val) in gf { for (t_name, fields_val) in gf {
if let Some(fields_arr) = fields_val.as_array() { if let Some(fields_arr) = fields_val.as_array() {
if fields_arr.iter().any(|v| v.as_str() == Some(prop_key)) { if fields_arr.iter().any(|v| v.as_str() == Some(lookup_key)) {
owner_alias = table_aliases owner_alias = table_aliases
.get(t_name) .get(t_name)
.cloned() .cloned()

View File

@ -1619,6 +1619,24 @@ fn test_polymorphism_5_2() {
crate::tests::runner::run_test_case(&path, 5, 2).unwrap(); crate::tests::runner::run_test_case(&path, 5, 2).unwrap();
} }
#[test]
fn test_polymorphism_6_0() {
let path = format!("{}/fixtures/polymorphism.json", env!("CARGO_MANIFEST_DIR"));
crate::tests::runner::run_test_case(&path, 6, 0).unwrap();
}
#[test]
fn test_polymorphism_6_1() {
let path = format!("{}/fixtures/polymorphism.json", env!("CARGO_MANIFEST_DIR"));
crate::tests::runner::run_test_case(&path, 6, 1).unwrap();
}
#[test]
fn test_polymorphism_6_2() {
let path = format!("{}/fixtures/polymorphism.json", env!("CARGO_MANIFEST_DIR"));
crate::tests::runner::run_test_case(&path, 6, 2).unwrap();
}
#[test] #[test]
fn test_not_0_0() { fn test_not_0_0() {
let path = format!("{}/fixtures/not.json", env!("CARGO_MANIFEST_DIR")); let path = format!("{}/fixtures/not.json", env!("CARGO_MANIFEST_DIR"));
@ -8170,3 +8188,9 @@ fn test_merger_0_14() {
let path = format!("{}/fixtures/merger.json", env!("CARGO_MANIFEST_DIR")); let path = format!("{}/fixtures/merger.json", env!("CARGO_MANIFEST_DIR"));
crate::tests::runner::run_test_case(&path, 0, 14).unwrap(); crate::tests::runner::run_test_case(&path, 0, 14).unwrap();
} }
#[test]
fn test_merger_0_15() {
let path = format!("{}/fixtures/merger.json", env!("CARGO_MANIFEST_DIR"));
crate::tests::runner::run_test_case(&path, 0, 15).unwrap();
}

View File

@ -160,7 +160,7 @@ fn test_library_api() {
"target": { "type": ["target_schema.filter", "null"] }, "target": { "type": ["target_schema.filter", "null"] },
"type": { "type": ["string.condition", "null"] } "type": { "type": ["string.condition", "null"] }
}, },
"type": "object" "type": "filter"
} }
}, },
"sensitive": false, "sensitive": false,
@ -211,7 +211,7 @@ fn test_library_api() {
}, },
"value": { "type": ["number.condition", "null"] } "value": { "type": ["number.condition", "null"] }
}, },
"type": "object" "type": "filter"
} }
}, },
"sensitive": false, "sensitive": false,

View File

@ -1,109 +0,0 @@
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.43s
Running unittests src/lib.rs (target/debug/deps/jspg-d3f18ff3a7e2b386)
running 1 test
test tests::test_filter_0_0 ... FAILED
failures:
---- tests::test_filter_0_0 stdout ----
TEST COMPILE ERROR FOR 'Assert filter generation map accurately represents strongly typed conditions natively.': Detailed Schema Match Failure for 'gender.condition'!
Expected:
{
"compiledPropertyNames": [
"$eq",
"$ne",
"$nof",
"$of"
],
"properties": {
"$eq": {
"type": [
"gender",
"null"
]
},
"$ne": {
"type": [
"gender",
"null"
]
},
"$nof": {
"items": {
"type": "gender"
},
"type": [
"array",
"null"
]
},
"$of": {
"items": {
"type": "gender"
},
"type": [
"array",
"null"
]
}
},
"type": "object"
}
Actual:
{
"compiledPropertyNames": [
"$eq",
"$ne",
"$nof",
"$of",
"kind"
],
"properties": {
"$eq": {
"type": [
"gender",
"null"
]
},
"$ne": {
"type": [
"gender",
"null"
]
},
"$nof": {
"items": {
"type": "gender"
},
"type": [
"array",
"null"
]
},
"$of": {
"items": {
"type": "gender"
},
"type": [
"array",
"null"
]
}
},
"type": "condition"
}
thread 'tests::test_filter_0_0' (118346550) panicked at src/tests/fixtures.rs:539:54:
called `Result::unwrap()` on an `Err` value: "[Filter Synthesis Object-Oriented Composition] Compile Test 'Assert filter generation map accurately represents strongly typed conditions natively.' failed. Error: Detailed Schema Match Failure for 'gender.condition'!\n\nExpected:\n{\n \"compiledPropertyNames\": [\n \"$eq\",\n \"$ne\",\n \"$nof\",\n \"$of\"\n ],\n \"properties\": {\n \"$eq\": {\n \"type\": [\n \"gender\",\n \"null\"\n ]\n },\n \"$ne\": {\n \"type\": [\n \"gender\",\n \"null\"\n ]\n },\n \"$nof\": {\n \"items\": {\n \"type\": \"gender\"\n },\n \"type\": [\n \"array\",\n \"null\"\n ]\n },\n \"$of\": {\n \"items\": {\n \"type\": \"gender\"\n },\n \"type\": [\n \"array\",\n \"null\"\n ]\n }\n },\n \"type\": \"object\"\n}\n\nActual:\n{\n \"compiledPropertyNames\": [\n \"$eq\",\n \"$ne\",\n \"$nof\",\n \"$of\",\n \"kind\"\n ],\n \"properties\": {\n \"$eq\": {\n \"type\": [\n \"gender\",\n \"null\"\n ]\n },\n \"$ne\": {\n \"type\": [\n \"gender\",\n \"null\"\n ]\n },\n \"$nof\": {\n \"items\": {\n \"type\": \"gender\"\n },\n \"type\": [\n \"array\",\n \"null\"\n ]\n },\n \"$of\": {\n \"items\": {\n \"type\": \"gender\"\n },\n \"type\": [\n \"array\",\n \"null\"\n ]\n }\n },\n \"type\": \"condition\"\n}"
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
failures:
tests::test_filter_0_0
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 1362 filtered out; finished in 0.00s
error: test failed, to rerun pass `--lib`

View File

@ -1 +1 @@
1.0.132 1.0.139