Compare commits

...

21 Commits

Author SHA1 Message Date
5b8596f7bd version: 1.0.179 2026-07-09 18:47:25 -04:00
6220a0964d fixed proxy references without null option not being treated as proxies 2026-07-09 18:47:12 -04:00
6cd5bfea7e doc update 2026-07-09 15:26:17 -04:00
a006d3f00a version: 1.0.178 2026-07-09 14:48:03 -04:00
942520a36a added static property constraints using const and enums and fixed formatter ON rendering 2026-07-09 14:47:39 -04:00
1bb9586b9b Merge branch 'main' of gitea-ssh.thoughtpatterns.ai:cellular/jspg 2026-07-09 14:46:03 -04:00
b4882bcb27 generator checkpoint 2026-07-09 14:27:50 -04:00
3bb7eb312a generator checkpoint 2026-07-09 14:26:45 -04:00
73ab6d4ce7 version: 1.0.177 2026-07-09 12:44:55 -04:00
cef65958cb reversed jsonb subquerying for now 2026-07-07 23:10:43 -04:00
95600551a7 version: 1.0.172 2026-07-07 12:45:00 -04:00
c18a8c1561 remove icon from sidebar 2026-07-07 12:44:49 -04:00
8ed588ca61 version: 1.0.171 2026-07-07 12:42:56 -04:00
edd0dd4763 jspg cleanup 2026-07-07 12:42:05 -04:00
abf1d12e43 test: mock matcher understands UNION existence reads; revive dead update-path mocks
parse_and_match_mocks matched mock.type against only the first FROM table —
always 'entity' for hierarchy existence reads — so every merger update-case
mock was silently inert and those cases snapshotted the create path. Now each
UNION arm is matched independently (a mock matching any arm returns once,
mirroring UNION dedup) and the type check accepts any FROM/JOIN table.

Regenerated the five revived cases (their snapshots now show real update /
no-op replace / archive semantics) and added the two previously untestable
probe-disagreement cases: same-row dedup (updates, no error) and id-vs-lookup
conflict (TOO_MANY_LOOKUP_ROWS).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 14:05:21 -04:00
a885713563 version: 1.0.170 2026-07-06 13:05:16 -04:00
d9b4f417f6 merger existence read: UNION the id and lk-lookup probes instead of OR-ing them across the hierarchy join — the OR is un-indexable and full-scans the subtype table on every child merge (one scan per relationship-edge write; surfaced by Castleberry onboarding); each UNION arm uses its own index (pk / lk_) and dedup preserves TOO_MANY_LOOKUP_ROWS semantics
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 20:52:29 -04:00
eae39f92d6 version: 1.0.169 2026-07-04 18:27:57 -04:00
2895f06074 page.personal is an opaque filter template (the 'mine' predicate), not a punc name — same read, scope-derived filter
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 18:27:51 -04:00
c1c16bc814 version: 1.0.168 2026-07-04 18:21:36 -04:00
5885552192 database: page carries 'personal' (the personal-scope sibling punc) and sidebar carries 'icon' — previously stripped by the whitelist
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 18:21:30 -04:00
37 changed files with 894 additions and 551 deletions

View File

@ -229,6 +229,42 @@ Traits are reusable, non-generating schema fragments used to share properties an
* **Scalars / Arrays / Items**: Host definitions completely override included traits.
* The `"include"` keyword is stripped, and `"traits"` maps are omitted from serialization.
### Static Relation Constraints (Kind Constraints)
When modeling relational properties on a schema, a developer can define a specialized subset of a related table by applying static property constraints via the `const` or `enum` validation keywords.
For example, given a general `attachment` table containing a `kind` column (e.g. `'cover'`, `'thumbnail'`, `'document'`), you can define a `cover.attachment` schema that narrows the type using a static `const` assertion:
```json
"cover.attachment": {
"type": "attachment",
"properties": {
"kind": {
"const": "cover"
}
}
}
```
A parent entity can then define a relationship using this constrained schema under a local property name (e.g. `cover_attachment`):
```json
"cover_attachment": {
"properties": {
"cover_attachment": {
"type": [
"cover.attachment",
"null"
]
}
}
}
```
**What it does:**
1. **Validation (L1)**: During payload validation (`jspg_validate`), any incoming object mapped to the constrained property is validated against the static rules (e.g. throwing `CONST_VIOLATED` if `kind` is not `"cover"`).
2. **Query Generation (L0)**: When fetching data via `jspg_query`, the Queryer automatically detects the static constraint and compiles it into the SQL subquery's `WHERE` clause (e.g. adding `AND attachment_X.kind = 'cover'`). This produces a pre-filtered view of the related entities natively at the database level.
---
## 3. Database
@ -314,6 +350,8 @@ The Queryer transforms Postgres into a pre-compiled Semantic Query Engine, desig
* **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.
* **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.
* **Static Relation Constraints (Kind Constraints)**: When a relationship (such as a nested object or array) is defined with a schema that constrains a field value statically using a `const` or `enum` keyword (for example, `kind` constrained to `"cover"` in a `cover_attachment`), the Queryer automatically extracts these static assertions during AST compilation. It injects them directly as static filters into the SQL subquery's `WHERE` clause (e.g. `AND attachment.kind = 'cover'`), allowing developers to query pre-filtered subsets of related tables natively through the schema.
* **Proxy Schema Dereferencing / Resolution**: To support punc endpoints that return non-polymorphic table-backed shapes (using `type: "full.X"` proxy schemas at the root response level), the Queryer compiler automatically dereferences non-table schema pointers to their target schemas prior to checking the types. This allows the Queryer to correctly resolve the table relationship edges pre-compiled on the full schema, while avoiding polluting the database registry with relations on ad-hoc punc response schemas during setup.
---

View File

@ -72,8 +72,8 @@
{
"code": "CONTAINS_VIOLATED",
"values": {
"limit": "1",
"count": "0"
"count": "0",
"limit": "1"
},
"details": {
"path": "",
@ -177,8 +177,8 @@
{
"code": "CONTAINS_VIOLATED",
"values": {
"limit": "1",
"count": "0"
"count": "0",
"limit": "1"
},
"details": {
"path": "",
@ -227,8 +227,8 @@
{
"code": "CONTAINS_VIOLATED",
"values": {
"count": "0",
"limit": "1"
"limit": "1",
"count": "0"
},
"details": {
"path": "",
@ -268,8 +268,8 @@
{
"code": "CONTAINS_VIOLATED",
"values": {
"count": "0",
"limit": "1"
"limit": "1",
"count": "0"
},
"details": {
"path": "",
@ -384,8 +384,8 @@
{
"code": "MULTIPLE_OF_VIOLATED",
"values": {
"value": "9",
"multiple_of": "2"
"multiple_of": "2",
"value": "9"
},
"details": {
"path": "2",
@ -443,8 +443,8 @@
{
"code": "MULTIPLE_OF_VIOLATED",
"values": {
"multiple_of": "2",
"value": "5"
"value": "5",
"multiple_of": "2"
},
"details": {
"path": "1",

View File

@ -62,8 +62,8 @@
"code": "EDGE_MISSING",
"values": {
"parent_type": "org",
"child_type": "user",
"property_name": "missing_users"
"property_name": "missing_users",
"child_type": "user"
},
"details": {
"path": "full.org/missing_users",
@ -151,9 +151,9 @@
{
"code": "EDGE_MISSING",
"values": {
"property_name": "children",
"child_type": "child",
"parent_type": "parent",
"child_type": "child"
"property_name": "children"
},
"details": {
"path": "full.parent/children",
@ -254,9 +254,9 @@
{
"code": "AMBIGUOUS_TYPE_RELATIONS",
"values": {
"parent_type": "invoice",
"child_type": "activity",
"property_name": "activities",
"parent_type": "invoice"
"property_name": "activities"
},
"details": {
"path": "full.invoice/activities",
@ -390,9 +390,9 @@
{
"code": "AMBIGUOUS_TYPE_RELATIONS",
"values": {
"property_name": "ambiguous_edge",
"parent_type": "actor",
"child_type": "junction",
"parent_type": "actor"
"property_name": "ambiguous_edge"
},
"details": {
"path": "full.actor/ambiguous_edge",
@ -470,8 +470,8 @@
{
"code": "DATABASE_TYPE_PARSE_FAILED",
"values": {
"type": "failure",
"reason": "invalid type: sequence, expected a string"
"reason": "invalid type: sequence, expected a string",
"type": "failure"
},
"details": {
"context": "failure"

View File

@ -65,8 +65,8 @@
{
"code": "DEPENDENCY_MISSING",
"values": {
"required_property": "foo",
"property_name": "bar"
"property_name": "bar",
"required_property": "foo"
},
"details": {
"path": "",
@ -276,8 +276,8 @@
{
"code": "DEPENDENCY_MISSING",
"values": {
"required_property": "foo",
"property_name": "quux"
"property_name": "quux",
"required_property": "foo"
},
"details": {
"path": "",
@ -287,8 +287,8 @@
{
"code": "DEPENDENCY_MISSING",
"values": {
"required_property": "bar",
"property_name": "quux"
"property_name": "quux",
"required_property": "bar"
},
"details": {
"path": "",
@ -362,8 +362,8 @@
{
"code": "DEPENDENCY_MISSING",
"values": {
"required_property": "foo\rbar",
"property_name": "foo\nbar"
"property_name": "foo\nbar",
"required_property": "foo\rbar"
},
"details": {
"path": "",
@ -829,8 +829,8 @@
{
"code": "MIN_PROPERTIES_VIOLATED",
"values": {
"count": "2",
"limit": "4"
"limit": "4",
"count": "2"
},
"details": {
"path": "",

View File

@ -128,8 +128,8 @@
{
"code": "PATTERN_VIOLATED",
"values": {
"value": "abcd",
"pattern": "^[a-z]{3}$"
"pattern": "^[a-z]{3}$",
"value": "abcd"
},
"details": {
"path": "keys/abcd",

View File

@ -56,8 +56,8 @@
{
"code": "EXCLUSIVE_MAXIMUM_VIOLATED",
"values": {
"limit": "3",
"value": "3.5"
"value": "3.5",
"limit": "3"
},
"details": {
"path": "",

View File

@ -34,8 +34,8 @@
{
"code": "EXCLUSIVE_MINIMUM_VIOLATED",
"values": {
"value": "1.1",
"limit": "1.1"
"limit": "1.1",
"value": "1.1"
},
"details": {
"path": "",

View File

@ -87,34 +87,6 @@
"type": "string"
}
}
},
"schedule": {
"type": [
"opening_hours",
"null"
]
}
}
},
"season": {
"type": "object",
"properties": {
"label": {
"type": "string"
}
}
},
"opening_hours": {
"type": "object",
"properties": {
"open": {
"type": "string"
},
"seasons": {
"type": "array",
"items": {
"type": "season"
}
}
}
}
@ -290,7 +262,6 @@
"uuid_field",
"tags",
"ad_hoc",
"schedule",
"$and",
"$or"
],
@ -306,7 +277,6 @@
"uuid_field",
"tags",
"ad_hoc",
"schedule",
"$and",
"$or"
],
@ -328,7 +298,6 @@
"uuid_field",
"tags",
"ad_hoc",
"schedule",
"$and",
"$or"
],
@ -397,12 +366,6 @@
"string.condition",
"null"
]
},
"schedule": {
"type": [
"opening_hours.filter",
"null"
]
}
},
"type": "filter"
@ -520,63 +483,7 @@
]
}
}
},
"opening_hours": {},
"season": {},
"opening_hours.filter": {
"type": "filter",
"compiledPropertyNames": [
"open",
"seasons",
"$and",
"$or"
],
"properties": {
"open": {
"type": [
"string.condition",
"null"
]
},
"seasons": {
"type": [
"season.filter",
"null"
]
},
"$and": {
"type": [
"array",
"null"
],
"items": {
"type": "opening_hours.filter",
"compiledPropertyNames": [
"open",
"seasons",
"$and",
"$or"
]
}
},
"$or": {
"type": [
"array",
"null"
],
"items": {
"type": "opening_hours.filter",
"compiledPropertyNames": [
"open",
"seasons",
"$and",
"$or"
]
}
}
}
},
"season.filter": {}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -70,8 +70,8 @@
{
"code": "CONTAINS_VIOLATED",
"values": {
"limit": "1",
"count": "0"
"count": "0",
"limit": "1"
},
"details": {
"path": "",
@ -144,8 +144,8 @@
{
"code": "CONTAINS_VIOLATED",
"values": {
"count": "2",
"limit": "1"
"limit": "1",
"count": "2"
},
"details": {
"path": "",

View File

@ -43,8 +43,8 @@
{
"code": "MAX_LENGTH_VIOLATED",
"values": {
"count": "3",
"limit": "2"
"limit": "2",
"count": "3"
},
"details": {
"path": "",
@ -109,8 +109,8 @@
{
"code": "MAX_LENGTH_VIOLATED",
"values": {
"count": "3",
"limit": "2"
"limit": "2",
"count": "3"
},
"details": {
"path": "",

View File

@ -139,8 +139,8 @@
{
"code": "MAX_PROPERTIES_VIOLATED",
"values": {
"count": "3",
"limit": "2"
"limit": "2",
"count": "3"
},
"details": {
"path": "",
@ -190,8 +190,8 @@
{
"code": "MAX_PROPERTIES_VIOLATED",
"values": {
"count": "1",
"limit": "0"
"limit": "0",
"count": "1"
},
"details": {
"path": "",
@ -234,8 +234,8 @@
{
"code": "MAX_PROPERTIES_VIOLATED",
"values": {
"limit": "2",
"count": "3"
"count": "3",
"limit": "2"
},
"details": {
"path": "",

View File

@ -43,8 +43,8 @@
{
"code": "MAXIMUM_VIOLATED",
"values": {
"limit": "3",
"value": "3.5"
"value": "3.5",
"limit": "3"
},
"details": {
"path": "",
@ -118,8 +118,8 @@
{
"code": "MAXIMUM_VIOLATED",
"values": {
"value": "300.5",
"limit": "300"
"limit": "300",
"value": "300.5"
},
"details": {
"path": "",

View File

@ -264,8 +264,8 @@
{
"code": "DEPENDENCY_MISSING",
"values": {
"required_property": "child_dep",
"property_name": "trigger"
"property_name": "trigger",
"required_property": "child_dep"
},
"details": {
"path": "",

View File

@ -1310,9 +1310,9 @@
[
"(SELECT to_jsonb(t1.*) || to_jsonb(t2.*) || to_jsonb(t3.*) || to_jsonb(t4.*)",
"FROM agreego.\"entity\" t1",
"JOIN agreego.\"organization\" t2 ON ",
"JOIN agreego.\"user\" t3 ON ",
"JOIN agreego.\"person\" t4 ON ",
"LEFT JOIN agreego.\"organization\" t2 ON t2.id = t1.id",
"LEFT JOIN agreego.\"user\" t3 ON t3.id = t1.id",
"LEFT JOIN agreego.\"person\" t4 ON t4.id = t1.id",
"WHERE",
" (\"first_name\" = 'LookupFirst'",
" AND \"last_name\" = 'LookupLast'",
@ -1463,12 +1463,18 @@
[
"(SELECT to_jsonb(t1.*) || to_jsonb(t2.*) || to_jsonb(t3.*) || to_jsonb(t4.*)",
"FROM agreego.\"entity\" t1",
"JOIN agreego.\"organization\" t2 ON ",
"JOIN agreego.\"user\" t3 ON ",
"JOIN agreego.\"person\" t4 ON ",
"LEFT JOIN agreego.\"organization\" t2 ON t2.id = t1.id",
"LEFT JOIN agreego.\"user\" t3 ON t3.id = t1.id",
"LEFT JOIN agreego.\"person\" t4 ON t4.id = t1.id",
"WHERE",
" t1.id = '{{uuid:data.id}}'",
" OR (\"first_name\" = 'LookupFirst'",
"UNION SELECT to_jsonb(t1.*) || to_jsonb(t2.*) || to_jsonb(t3.*) || to_jsonb(t4.*)",
"FROM agreego.\"entity\" t1",
"LEFT JOIN agreego.\"organization\" t2 ON t2.id = t1.id",
"LEFT JOIN agreego.\"user\" t3 ON t3.id = t1.id",
"LEFT JOIN agreego.\"person\" t4 ON t4.id = t1.id",
"WHERE",
" (\"first_name\" = 'LookupFirst'",
" AND \"last_name\" = 'LookupLast'",
" AND \"date_of_birth\" = '{{timestamp}}'",
" AND \"pronouns\" = 'they/them'))"
@ -1616,12 +1622,18 @@
[
"(SELECT to_jsonb(t1.*) || to_jsonb(t2.*) || to_jsonb(t3.*) || to_jsonb(t4.*)",
"FROM agreego.\"entity\" t1",
"JOIN agreego.\"organization\" t2 ON ",
"JOIN agreego.\"user\" t3 ON ",
"JOIN agreego.\"person\" t4 ON ",
"LEFT JOIN agreego.\"organization\" t2 ON t2.id = t1.id",
"LEFT JOIN agreego.\"user\" t3 ON t3.id = t1.id",
"LEFT JOIN agreego.\"person\" t4 ON t4.id = t1.id",
"WHERE",
" t1.id = '{{uuid:data.id}}'",
" OR (\"first_name\" = 'LookupFirst'",
"UNION SELECT to_jsonb(t1.*) || to_jsonb(t2.*) || to_jsonb(t3.*) || to_jsonb(t4.*)",
"FROM agreego.\"entity\" t1",
"LEFT JOIN agreego.\"organization\" t2 ON t2.id = t1.id",
"LEFT JOIN agreego.\"user\" t3 ON t3.id = t1.id",
"LEFT JOIN agreego.\"person\" t4 ON t4.id = t1.id",
"WHERE",
" (\"first_name\" = 'LookupFirst'",
" AND \"last_name\" = 'LookupLast'",
" AND \"date_of_birth\" = '{{timestamp}}'",
" AND \"pronouns\" = 'they/them'))"
@ -1759,9 +1771,9 @@
[
"(SELECT to_jsonb(t1.*) || to_jsonb(t2.*) || to_jsonb(t3.*) || to_jsonb(t4.*)",
"FROM agreego.\"entity\" t1",
"JOIN agreego.\"organization\" t2 ON ",
"JOIN agreego.\"user\" t3 ON ",
"JOIN agreego.\"person\" t4 ON ",
"LEFT JOIN agreego.\"organization\" t2 ON t2.id = t1.id",
"LEFT JOIN agreego.\"user\" t3 ON t3.id = t1.id",
"LEFT JOIN agreego.\"person\" t4 ON t4.id = t1.id",
"WHERE",
" t1.id = '{{uuid:mocks.0.id}}')"
],
@ -1882,9 +1894,9 @@
[
"(SELECT to_jsonb(t1.*) || to_jsonb(t2.*) || to_jsonb(t3.*) || to_jsonb(t4.*)",
"FROM agreego.\"entity\" t1",
"JOIN agreego.\"organization\" t2 ON ",
"JOIN agreego.\"user\" t3 ON ",
"JOIN agreego.\"person\" t4 ON ",
"LEFT JOIN agreego.\"organization\" t2 ON t2.id = t1.id",
"LEFT JOIN agreego.\"user\" t3 ON t3.id = t1.id",
"LEFT JOIN agreego.\"person\" t4 ON t4.id = t1.id",
"WHERE",
" t1.id = '123')"
],
@ -2221,10 +2233,14 @@
[
"(SELECT to_jsonb(t1.*) || to_jsonb(t2.*)",
"FROM agreego.\"entity\" t1",
"JOIN agreego.\"order\" t2 ON ",
"LEFT JOIN agreego.\"order\" t2 ON t2.id = t1.id",
"WHERE",
" t1.id = 'abc'",
" OR (\"id\" = 'abc'))"
"UNION SELECT to_jsonb(t1.*) || to_jsonb(t2.*)",
"FROM agreego.\"entity\" t1",
"LEFT JOIN agreego.\"order\" t2 ON t2.id = t1.id",
"WHERE",
" (\"id\" = 'abc'))"
],
[
"INSERT INTO agreego.\"entity\" (",
@ -3024,9 +3040,9 @@
[
"(SELECT to_jsonb(t1.*) || to_jsonb(t2.*) || to_jsonb(t3.*) || to_jsonb(t4.*)",
"FROM agreego.\"entity\" t1",
"JOIN agreego.\"organization\" t2 ON ",
"JOIN agreego.\"user\" t3 ON ",
"JOIN agreego.\"person\" t4 ON ",
"LEFT JOIN agreego.\"organization\" t2 ON t2.id = t1.id",
"LEFT JOIN agreego.\"user\" t3 ON t3.id = t1.id",
"LEFT JOIN agreego.\"person\" t4 ON t4.id = t1.id",
"WHERE",
" t1.id = 'abc-archived')"
],
@ -3377,7 +3393,7 @@
[
"(SELECT to_jsonb(t1.*) || to_jsonb(t2.*)",
"FROM agreego.\"entity\" t1",
"JOIN agreego.\"order_line\" t2 ON ",
"LEFT JOIN agreego.\"order_line\" t2 ON t2.id = t1.id",
"WHERE",
" t1.id = '{{uuid:data.lines.0.id}}')"
],
@ -3501,10 +3517,14 @@
[
"(SELECT to_jsonb(t1.*) || to_jsonb(t2.*)",
"FROM agreego.\"entity\" t1",
"JOIN agreego.\"invoice\" t2 ON ",
"LEFT JOIN agreego.\"invoice\" t2 ON t2.id = t1.id",
"WHERE",
" t1.id = '{{uuid:data.id}}'",
" OR (\"id\" = '{{uuid:data.id}}'))"
"UNION SELECT to_jsonb(t1.*) || to_jsonb(t2.*)",
"FROM agreego.\"entity\" t1",
"LEFT JOIN agreego.\"invoice\" t2 ON t2.id = t1.id",
"WHERE",
" (\"id\" = '{{uuid:data.id}}'))"
],
[
"INSERT INTO agreego.\"entity\" (",
@ -3618,7 +3638,7 @@
[
"(SELECT to_jsonb(t1.*) || to_jsonb(t2.*)",
"FROM agreego.\"entity\" t1",
"JOIN agreego.\"account\" t2 ON ",
"LEFT JOIN agreego.\"account\" t2 ON t2.id = t1.id",
"WHERE",
" t1.id = '{{uuid:data.id}}')"
],

View File

@ -67,8 +67,8 @@
{
"code": "CONTAINS_VIOLATED",
"values": {
"limit": "1",
"count": "0"
"count": "0",
"limit": "1"
},
"details": {
"path": "",
@ -91,8 +91,8 @@
{
"code": "CONTAINS_VIOLATED",
"values": {
"limit": "1",
"count": "0"
"count": "0",
"limit": "1"
},
"details": {
"path": "",
@ -218,8 +218,8 @@
{
"code": "CONTAINS_VIOLATED",
"values": {
"limit": "2",
"count": "1"
"count": "1",
"limit": "2"
},
"details": {
"path": "",
@ -357,8 +357,8 @@
{
"code": "CONTAINS_VIOLATED",
"values": {
"count": "0",
"limit": "2"
"limit": "2",
"count": "0"
},
"details": {
"path": "",
@ -381,8 +381,8 @@
{
"code": "CONTAINS_VIOLATED",
"values": {
"limit": "2",
"count": "1"
"count": "1",
"limit": "2"
},
"details": {
"path": "",
@ -407,8 +407,8 @@
{
"code": "CONTAINS_VIOLATED",
"values": {
"limit": "2",
"count": "3"
"count": "3",
"limit": "2"
},
"details": {
"path": "",
@ -463,8 +463,8 @@
{
"code": "CONTAINS_VIOLATED",
"values": {
"count": "0",
"limit": "3"
"limit": "3",
"count": "0"
},
"details": {
"path": "",
@ -487,8 +487,8 @@
{
"code": "CONTAINS_VIOLATED",
"values": {
"count": "1",
"limit": "3"
"limit": "3",
"count": "1"
},
"details": {
"path": "",
@ -513,8 +513,8 @@
{
"code": "CONTAINS_VIOLATED",
"values": {
"count": "3",
"limit": "1"
"limit": "1",
"count": "3"
},
"details": {
"path": "",
@ -538,8 +538,8 @@
{
"code": "CONTAINS_VIOLATED",
"values": {
"limit": "3",
"count": "2"
"count": "2",
"limit": "3"
},
"details": {
"path": "",
@ -549,8 +549,8 @@
{
"code": "CONTAINS_VIOLATED",
"values": {
"limit": "1",
"count": "2"
"count": "2",
"limit": "1"
},
"details": {
"path": "",
@ -657,8 +657,8 @@
{
"code": "CONTAINS_VIOLATED",
"values": {
"count": "2",
"limit": "1"
"limit": "1",
"count": "2"
},
"details": {
"path": "",

View File

@ -48,8 +48,8 @@
{
"code": "MIN_ITEMS_VIOLATED",
"values": {
"limit": "1",
"count": "0"
"count": "0",
"limit": "1"
},
"details": {
"path": "",
@ -108,8 +108,8 @@
{
"code": "MIN_ITEMS_VIOLATED",
"values": {
"count": "0",
"limit": "1"
"limit": "1",
"count": "0"
},
"details": {
"path": "",

View File

@ -43,8 +43,8 @@
{
"code": "MINIMUM_VIOLATED",
"values": {
"value": "0.6",
"limit": "1.1"
"limit": "1.1",
"value": "0.6"
},
"details": {
"path": "",
@ -127,8 +127,8 @@
{
"code": "MINIMUM_VIOLATED",
"values": {
"value": "-2.0001",
"limit": "-2"
"limit": "-2",
"value": "-2.0001"
},
"details": {
"path": "",

View File

@ -100,8 +100,8 @@
{
"code": "MULTIPLE_OF_VIOLATED",
"values": {
"multiple_of": "1.5",
"value": "35"
"value": "35",
"multiple_of": "1.5"
},
"details": {
"path": "",
@ -148,8 +148,8 @@
{
"code": "MULTIPLE_OF_VIOLATED",
"values": {
"value": "0.00751",
"multiple_of": "0.0001"
"multiple_of": "0.0001",
"value": "0.00751"
},
"details": {
"path": "",

View File

@ -156,8 +156,8 @@
{
"code": "MAXIMUM_VIOLATED",
"values": {
"value": "60",
"limit": "50"
"limit": "50",
"value": "60"
},
"details": {
"path": "max",
@ -191,6 +191,9 @@
},
{
"name": "request",
"field_types": {
"inv": "jsonb"
},
"schemas": {
"request": {
"type": "object",

View File

@ -34,8 +34,8 @@
{
"code": "PATTERN_VIOLATED",
"values": {
"pattern": "^a*$",
"value": "abc"
"value": "abc",
"pattern": "^a*$"
},
"details": {
"path": "",

View File

@ -148,8 +148,8 @@
{
"code": "NO_FAMILY_MATCH",
"values": {
"discriminator": "type",
"value": "alien"
"value": "alien",
"discriminator": "type"
},
"details": {
"path": "",
@ -284,8 +284,8 @@
{
"code": "NO_FAMILY_MATCH",
"values": {
"discriminator": "type",
"value": "bot"
"value": "bot",
"discriminator": "type"
},
"details": {
"path": "",

View File

@ -43,8 +43,8 @@
{
"code": "MAX_LENGTH_VIOLATED",
"values": {
"limit": "3",
"count": "6"
"count": "6",
"limit": "3"
},
"details": {
"path": "",
@ -431,8 +431,8 @@
{
"code": "MAX_LENGTH_VIOLATED",
"values": {
"count": "6",
"limit": "3"
"limit": "3",
"count": "6"
},
"details": {
"path": "",

View File

@ -1,6 +1,6 @@
[
{
"description": "Queryer Execution",
"description": "Queryer Suite 1",
"database": {
"puncs": [
{
@ -2439,5 +2439,273 @@
}
}
]
},
{
"description": "Queryer Suite 2",
"database": {
"puncs": [
{
"name": "get_pet",
"schemas": {
"get_pet.response": {
"type": "full.pet"
}
}
}
],
"enums": [],
"relations": [
{
"type": "relation",
"id": "00000000-0000-0000-0000-000000000001",
"constraint": "fk_attachment_attachable_entity",
"source_type": "attachment",
"source_columns": [
"attachable_id"
],
"destination_type": "entity",
"destination_columns": [
"id"
],
"prefix": "attachable"
}
],
"types": [
{
"name": "entity",
"hierarchy": [
"entity"
],
"grouped_fields": {
"entity": [
"id",
"type",
"archived"
]
},
"field_types": {
"id": "uuid",
"archived": "boolean",
"type": "text"
},
"schemas": {
"entity": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"type": {
"type": "string"
},
"archived": {
"type": "boolean"
}
}
}
},
"fields": [
"id",
"type",
"archived"
],
"variations": [
"entity",
"pet",
"attachment"
]
},
{
"name": "pet",
"hierarchy": [
"entity",
"pet"
],
"grouped_fields": {
"entity": [
"id",
"type",
"archived"
],
"pet": [
"name"
]
},
"field_types": {
"name": "text"
},
"schemas": {
"pet": {
"type": "entity",
"properties": {
"name": {
"type": "string"
}
}
},
"full.pet": {
"type": "pet",
"properties": {
"cover_attachment": {
"type": "attachment",
"properties": {
"kind": {
"const": "cover"
}
}
},
"cover_attachments": {
"type": "array",
"items": {
"type": "attachment",
"properties": {
"kind": {
"const": "cover"
}
}
}
},
"media_attachments": {
"type": "array",
"items": {
"type": "attachment",
"properties": {
"kind": {
"enum": [
"cover",
"thumbnail"
]
}
}
}
}
}
}
},
"fields": [
"id",
"type",
"archived",
"name"
],
"variations": [
"pet"
]
},
{
"name": "attachment",
"hierarchy": [
"entity",
"attachment"
],
"grouped_fields": {
"entity": [
"id",
"type",
"archived"
],
"attachment": [
"attachable_id",
"name",
"kind"
]
},
"field_types": {
"attachable_id": "uuid",
"name": "text",
"kind": "text"
},
"schemas": {
"attachment": {
"type": "entity",
"properties": {
"attachable_id": {
"type": "string"
},
"name": {
"type": "string"
},
"kind": {
"type": "string"
}
}
}
},
"fields": [
"id",
"type",
"archived",
"attachable_id",
"name",
"kind"
],
"variations": [
"attachment"
]
}
]
},
"tests": [
{
"description": "Select query on pet with static properties constraints on attachments",
"action": "query",
"schema_id": "get_pet.response",
"expect": {
"success": true,
"sql": [
[
"((SELECT jsonb_strip_nulls((",
" SELECT jsonb_build_object(",
" 'name', pet_2.name,",
" 'cover_attachment', (",
" SELECT jsonb_build_object(",
" 'attachable_id', attachment_4.attachable_id,",
" 'name', attachment_4.name,",
" 'kind', attachment_4.kind",
" )",
" FROM agreego.entity entity_3",
" JOIN agreego.attachment attachment_4 ON attachment_4.id = entity_3.id",
" WHERE",
" NOT entity_3.archived",
" AND attachment_4.kind = 'cover'",
" AND attachment_4.attachable_id = entity_1.id",
" ),",
" 'cover_attachments', (",
" SELECT COALESCE(jsonb_agg(jsonb_build_object(",
" 'attachable_id', attachment_6.attachable_id,",
" 'name', attachment_6.name,",
" 'kind', attachment_6.kind",
" )), '[]'::jsonb)",
" FROM agreego.entity entity_5",
" JOIN agreego.attachment attachment_6 ON attachment_6.id = entity_5.id",
" WHERE",
" NOT entity_5.archived",
" AND attachment_6.kind = 'cover'",
" AND attachment_6.attachable_id = entity_1.id",
" ),",
" 'media_attachments', (",
" SELECT COALESCE(jsonb_agg(jsonb_build_object(",
" 'attachable_id', attachment_8.attachable_id,",
" 'name', attachment_8.name,",
" 'kind', attachment_8.kind",
" )), '[]'::jsonb)",
" FROM agreego.entity entity_7",
" JOIN agreego.attachment attachment_8 ON attachment_8.id = entity_7.id",
" WHERE",
" NOT entity_7.archived",
" AND attachment_8.kind IN ('cover', 'thumbnail')",
" AND attachment_8.attachable_id = entity_1.id",
" )",
" )",
" FROM agreego.entity entity_1",
" JOIN agreego.pet pet_2 ON pet_2.id = entity_1.id",
" WHERE",
" NOT entity_1.archived",
"))))"
]
]
}
}
]
}
]

View File

@ -139,8 +139,8 @@
{
"code": "MAX_LENGTH_VIOLATED",
"values": {
"count": "26",
"limit": "5"
"limit": "5",
"count": "26"
},
"details": {
"path": "email",

1
flow
View File

@ -36,7 +36,6 @@ pgrx-down() {
info "Taking pgrx down..."
}
build() {
local version
version=$(get-version) || return $?

2
flows

Submodule flows updated: 0d9bd8644e...89748a246e

View File

@ -165,20 +165,8 @@ impl Schema {
} else if db.enums.contains_key(custom) {
Some(vec![format!("{}.condition", custom)])
} else {
// A named type gets a reference to its dynamically built .filter — either a
// Relational cross-boundary (table-backed) or a named value type, whose filter
// is likewise synthesized (see Database::compile_filters). A named type with no
// compilable structure gets no filter — omit it rather than dangle a reference.
let base = custom.split('.').next_back().unwrap_or(custom);
let has_value_filter = db
.schemas
.get(custom)
.map_or(false, |s| Database::is_value_filter_candidate(custom, s));
if db.types.contains_key(base) || has_value_filter {
// Assume anything else is a Relational cross-boundary that already has its own .filter dynamically built
Some(vec![format!("{}.filter", custom)])
} else {
None
}
}
}
}

View File

@ -262,23 +262,15 @@ impl Database {
}
}
/// Synthesizes Composed Filter References for all table-backed boundaries — and for
/// named non-table value types (schema-only objects, e.g. an operating-hours config),
/// so a property reference resolves to ONE named filter instead of inlining anonymous
/// per-path copies (which duplicate identical leaf type names for downstream codegen).
/// Synthesizes Composed Filter References for all table-backed boundaries.
fn compile_filters(&mut self, errors: &mut Vec<crate::drop::Error>) -> Vec<(String, String)> {
let mut filter_schemas = Vec::new();
let mut seen_value_ids = std::collections::HashSet::new();
for (type_name, type_def) in &self.types {
for (id, schema_arc) in &type_def.schemas {
// Run synthesis on structured table-backed boundaries and named value types.
// Exclude subschemas!
// Only run synthesis on actual structured, table-backed boundaries. Exclude subschemas!
let base_name = id.split('.').last().unwrap_or(id);
let is_table_backed = base_name == type_def.name;
let is_value_type = !is_table_backed
&& Self::is_value_filter_candidate(id, schema_arc)
&& seen_value_ids.insert(id.clone());
if (is_table_backed || is_value_type) && !id.contains('/') {
if is_table_backed && !id.contains('/') {
if let Some(filter_schema) = schema_arc.compile_filter(self, id, errors) {
filter_schemas.push((
type_name.clone(),
@ -301,20 +293,6 @@ impl Database {
filter_ids
}
/// A named non-table value type that earns its own synthesized filter: a bare-named
/// (dotless) schema-only object with compiled properties. The base `filter`/`condition`
/// schemas are infrastructure, not value types.
pub fn is_value_filter_candidate(id: &str, schema: &Arc<Schema>) -> bool {
!id.contains('.')
&& id != "filter"
&& id != "condition"
&& schema
.obj
.compiled_properties
.get()
.map_or(false, |props| !props.is_empty())
}
/// Synthesizes strong Enum Conditions mirroring the string.condition capabilities.
fn compile_conditions(&mut self) -> Vec<(String, String)> {
let mut enum_conditions = Vec::new();

View File

@ -1,6 +1,7 @@
use crate::database::action::Action;
use indexmap::IndexMap;
use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(default)]
@ -13,6 +14,11 @@ pub struct Page {
pub sidebar: Option<Sidebar>,
#[serde(skip_serializing_if = "Option::is_none")]
pub actions: Option<IndexMap<String, Action>>,
/// The personal-scope filter template ("one surface, two scopes"): the
/// "mine" predicate the page merges into its filter when the app is in
/// personal scope, e.g. {"reserved_by": "$me"}. Opaque to the engine.
#[serde(skip_serializing_if = "Option::is_none")]
pub personal: Option<Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]

View File

@ -744,9 +744,15 @@ impl Merger {
return Ok(None);
}
let where_clause = format!("WHERE {}", where_parts.join(" OR "));
let final_sql = format!("{} {}", fetch_sql_template, where_clause);
let final_sql = if where_parts.len() == 1 {
format!("{} WHERE {}", fetch_sql_template, where_parts[0])
} else {
where_parts
.iter()
.map(|p| format!("{} WHERE {}", fetch_sql_template, p))
.collect::<Vec<_>>()
.join(" UNION ")
};
let fetched = match self.db.query(&final_sql, None) {
Ok(Value::Array(table)) => {

View File

@ -1,5 +1,6 @@
use crate::database::Database;
use indexmap::IndexMap;
use serde_json::Value;
use std::sync::Arc;
pub struct Compiler<'a> {
@ -120,6 +121,21 @@ impl<'a> Compiler<'a> {
}
fn compile_reference(&mut self, node: Node<'a>) -> Result<(String, String), String> {
// Handle Direct Refs via type pointer first
if let Some(crate::database::object::SchemaTypeOrArray::Single(t)) = &node.schema.obj.type_ {
if !crate::database::object::is_primitive_type(t) {
if !self.db.types.contains_key(t) {
// If it's just an ad-hoc struct ref, we should resolve it
if let Some(target_schema) = self.db.schemas.get(t).cloned() {
let mut ref_node = node.clone();
ref_node.schema = target_schema.clone();
ref_node.schema_id = Some(t.clone());
return self.compile_node(ref_node);
}
}
}
}
// Determine if this schema represents a Database Entity
let mut resolved_type = None;
@ -149,16 +165,9 @@ impl<'a> Compiler<'a> {
return self.compile_entity(type_def, node.clone(), false);
}
// Handle Direct Refs via type pointer
// Fallback error if the schema pointer was unresolved
if let Some(crate::database::object::SchemaTypeOrArray::Single(t)) = &node.schema.obj.type_ {
if !crate::database::object::is_primitive_type(t) {
// If it's just an ad-hoc struct ref, we should resolve it
if let Some(target_schema) = self.db.schemas.get(t).cloned() {
let mut ref_node = node.clone();
ref_node.schema = target_schema.clone();
ref_node.schema_id = Some(t.clone());
return self.compile_node(ref_node);
}
return Err(format!("Unresolved schema type pointer: {}", t));
}
}
@ -551,6 +560,7 @@ impl<'a> Compiler<'a> {
self.compile_filter_conditions(r#type, type_aliases, &node, &base_alias, &mut where_clauses);
self.compile_polymorphic_bounds(r#type, type_aliases, &node, &mut where_clauses);
self.compile_static_property_conditions(r#type, type_aliases, &node, &base_alias, &mut where_clauses);
let start_len = where_clauses.len();
self.compile_relation_conditions(
@ -865,4 +875,69 @@ impl<'a> Compiler<'a> {
}
Ok(())
}
fn compile_static_property_conditions(
&self,
r#type: &crate::database::r#type::Type,
type_aliases: &std::collections::HashMap<String, String>,
node: &Node,
base_alias: &str,
where_clauses: &mut Vec<String>,
) {
if let Some(props) = node.schema.obj.properties.as_ref() {
for (prop_name, prop_schema) in props {
let filter_alias = Self::resolve_filter_alias(r#type, type_aliases, base_alias, prop_name);
if let Some(const_val) = prop_schema.obj.const_.as_ref() {
let sql_val = Self::quote_literal(const_val);
where_clauses.push(format!("{}.{} = {}", filter_alias, prop_name, sql_val));
}
if let Some(enum_vals) = prop_schema.obj.enum_.as_ref() {
if !enum_vals.is_empty() {
let sql_vals: Vec<String> = enum_vals
.iter()
.map(|v| Self::quote_literal(v))
.collect();
where_clauses.push(format!(
"{}.{} IN ({})",
filter_alias,
prop_name,
sql_vals.join(", ")
));
}
}
}
}
}
fn quote_literal(val: &Value) -> String {
match val {
Value::Null => "NULL".to_string(),
Value::Bool(b) => {
if *b {
"true".to_string()
} else {
"false".to_string()
}
}
Value::Number(n) => {
if let Some(f) = n.as_f64() {
if f.fract() == 0.0 {
return f.trunc().to_string();
}
}
n.to_string()
}
Value::String(s) => {
if s.is_empty() {
"NULL".to_string()
} else {
format!("'{}'", s.replace('\'', "''"))
}
}
_ => format!(
"'{}'",
serde_json::to_string(val).unwrap().replace('\'', "''")
),
}
}
}

View File

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

View File

@ -179,14 +179,35 @@ impl SqlFormatter {
}
fn format_query(&mut self, query: &Query) {
match &*query.body {
self.format_set_expr(&query.body);
}
fn format_set_expr(&mut self, set_expr: &SetExpr) {
match set_expr {
SetExpr::Select(select) => self.format_select(select),
SetExpr::Query(inner_query) => {
self.push_str("(");
self.format_query(inner_query);
self.push_str(")");
}
_ => self.push_str(&query.to_string()),
SetExpr::SetOperation { op, left, right, set_quantifier } => {
self.format_set_expr(left);
let op_str = match op {
sqlparser::ast::SetOperator::Union => "UNION",
sqlparser::ast::SetOperator::Intersect => "INTERSECT",
sqlparser::ast::SetOperator::Except => "EXCEPT",
_ => "UNION",
};
let quant_str = match set_quantifier {
sqlparser::ast::SetQuantifier::All => " ALL",
sqlparser::ast::SetQuantifier::Distinct => " DISTINCT",
sqlparser::ast::SetQuantifier::None => "",
_ => "",
};
self.push_line(&format!("{}{} ", op_str, quant_str));
self.format_set_expr(right);
}
_ => self.push_str(&set_expr.to_string()),
}
}
@ -236,20 +257,23 @@ impl SqlFormatter {
}
fn format_join(&mut self, join: &Join) {
let op = match &join.join_operator {
let op_str = match &join.join_operator {
JoinOperator::Inner(_) => "JOIN",
JoinOperator::LeftOuter(_) => "LEFT JOIN",
JoinOperator::Left(_) => "LEFT JOIN",
JoinOperator::Right(_) => "RIGHT JOIN",
_ => "JOIN",
};
self.push_str(&format!("{} {} ON ", op, join.relation));
self.push_str(&format!("{} {}", op_str, join.relation));
match &join.join_operator {
JoinOperator::Inner(JoinConstraint::On(expr)) => self.format_expr(expr),
JoinOperator::LeftOuter(JoinConstraint::On(expr)) => self.format_expr(expr),
JoinOperator::Join(JoinConstraint::On(expr)) => self.format_expr(expr),
_ => {
println!("FALLBACK JOIN OP: {:?}", join.join_operator);
JoinOperator::Inner(JoinConstraint::On(expr))
| JoinOperator::Left(JoinConstraint::On(expr))
| JoinOperator::Right(JoinConstraint::On(expr))
| JoinOperator::Join(JoinConstraint::On(expr)) => {
self.push_str(" ON ");
self.format_expr(expr);
}
_ => {}
}
}

View File

@ -259,3 +259,25 @@ pub fn update_sql_fixture(path: &str, suite_idx: usize, case_idx: usize, queries
let formatted_json = serde_json::to_string_pretty(&file_data).unwrap();
fs::write(path, formatted_json).unwrap();
}
pub fn update_schemas_fixture(path: &str, suite_idx: usize, case_idx: usize, db: &crate::database::Database) {
let content = fs::read_to_string(path).unwrap();
let mut file_data: Value = serde_json::from_str(&content).unwrap();
if let Some(expect) = file_data[suite_idx]["tests"][case_idx].get_mut("expect") {
if let Some(schemas_map) = expect.get_mut("schemas").and_then(|v| v.as_object_mut()) {
for (key, expected_val) in schemas_map {
if expected_val.is_object() && expected_val.as_object().unwrap().is_empty() {
continue;
}
if let Some(actual_ast) = db.schemas.get(key) {
let actual_val = serde_json::to_value(actual_ast).unwrap();
*expected_val = actual_val;
}
}
}
}
let formatted_json = serde_json::to_string_pretty(&file_data).unwrap();
fs::write(path, formatted_json).unwrap();
}

View File

@ -57,6 +57,9 @@ impl Case {
if env::var("UPDATE_EXPECT").is_ok() {
update_validation_fixture(path, suite_idx, case_idx, &result.errors);
if let Ok(db) = db_res {
crate::tests::runner::update_schemas_fixture(path, suite_idx, case_idx, db);
}
}
expect.assert_drop(&result)?;

View File

@ -1 +1 @@
1.0.167
1.0.179