Compare commits

..

26 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
dc033296d7 version: 1.0.167 2026-07-03 20:04:28 -04:00
03a871bc1a filter synthesis: named value types get their own synthesized, referenced filter
Supersedes the inline-structural approach (0d282cc): inlining erased the value
type's name, so identical nested shapes (weekly_hours at two paths) generated
duplicate leaf types downstream and Dart barrel exports collided. Now a named
non-table value type's filter is synthesized ONCE (like table-backed boundaries)
and property references point at it — mirroring how the entity side generates
one class per named type. Same filter-by-fields capability; laziness also
removes the structural-recursion hazard. A named type with no compilable
structure still gets no filter and is omitted rather than dangled.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 20:04:16 -04:00
8aa15873b0 version: 1.0.166 2026-07-03 19:24:16 -04:00
0d282cc930 filter synthesis: compile named non-table value types structurally
A property typed as a named value type (a schema-only config object like
an operating-hours schedule) previously got a dangling {type}.filter
reference — no filter is ever synthesized for a non-table-backed schema,
so the whole parent filter failed downstream (PROXY_TYPE_RESOLUTION_FAILED;
the punc generator emitted an empty filter type).

Naming a value type is a reuse choice, not a semantics choice: it now
compiles structurally into the parent filter, exactly like an inline
object, recursively (including array items). Table-backed boundaries keep
the lazy {type}.filter reference. A named type with no compilable
structure is omitted instead of dangling.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 19:23:54 -04:00
581fc8e0c0 flow update 2026-07-03 01:33:00 -04:00
34 changed files with 887 additions and 417 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. * **Scalars / Arrays / Items**: Host definitions completely override included traits.
* The `"include"` keyword is stripped, and `"traits"` maps are omitted from serialization. * 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 ## 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. * **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. * **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", "code": "CONTAINS_VIOLATED",
"values": { "values": {
"limit": "1", "count": "0",
"count": "0" "limit": "1"
}, },
"details": { "details": {
"path": "", "path": "",
@ -177,8 +177,8 @@
{ {
"code": "CONTAINS_VIOLATED", "code": "CONTAINS_VIOLATED",
"values": { "values": {
"limit": "1", "count": "0",
"count": "0" "limit": "1"
}, },
"details": { "details": {
"path": "", "path": "",
@ -227,8 +227,8 @@
{ {
"code": "CONTAINS_VIOLATED", "code": "CONTAINS_VIOLATED",
"values": { "values": {
"count": "0", "limit": "1",
"limit": "1" "count": "0"
}, },
"details": { "details": {
"path": "", "path": "",
@ -268,8 +268,8 @@
{ {
"code": "CONTAINS_VIOLATED", "code": "CONTAINS_VIOLATED",
"values": { "values": {
"count": "0", "limit": "1",
"limit": "1" "count": "0"
}, },
"details": { "details": {
"path": "", "path": "",
@ -384,8 +384,8 @@
{ {
"code": "MULTIPLE_OF_VIOLATED", "code": "MULTIPLE_OF_VIOLATED",
"values": { "values": {
"value": "9", "multiple_of": "2",
"multiple_of": "2" "value": "9"
}, },
"details": { "details": {
"path": "2", "path": "2",
@ -443,8 +443,8 @@
{ {
"code": "MULTIPLE_OF_VIOLATED", "code": "MULTIPLE_OF_VIOLATED",
"values": { "values": {
"multiple_of": "2", "value": "5",
"value": "5" "multiple_of": "2"
}, },
"details": { "details": {
"path": "1", "path": "1",

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
[ [
{ {
"description": "Queryer Execution", "description": "Queryer Suite 1",
"database": { "database": {
"puncs": [ "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", "code": "MAX_LENGTH_VIOLATED",
"values": { "values": {
"count": "26", "limit": "5",
"limit": "5" "count": "26"
}, },
"details": { "details": {
"path": "email", "path": "email",

1
flow
View File

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

2
flows

Submodule flows updated: c6af26cece...89748a246e

View File

@ -1,6 +1,7 @@
use crate::database::action::Action; use crate::database::action::Action;
use indexmap::IndexMap; use indexmap::IndexMap;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Debug, Clone, Serialize, Deserialize, Default)] #[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(default)] #[serde(default)]
@ -13,6 +14,11 @@ pub struct Page {
pub sidebar: Option<Sidebar>, pub sidebar: Option<Sidebar>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub actions: Option<IndexMap<String, Action>>, 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)] #[derive(Debug, Clone, Serialize, Deserialize, Default)]

View File

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

View File

@ -1,5 +1,6 @@
use crate::database::Database; use crate::database::Database;
use indexmap::IndexMap; use indexmap::IndexMap;
use serde_json::Value;
use std::sync::Arc; use std::sync::Arc;
pub struct Compiler<'a> { pub struct Compiler<'a> {
@ -120,6 +121,21 @@ impl<'a> Compiler<'a> {
} }
fn compile_reference(&mut self, node: Node<'a>) -> Result<(String, String), String> { 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 // Determine if this schema represents a Database Entity
let mut resolved_type = None; let mut resolved_type = None;
@ -149,16 +165,9 @@ impl<'a> Compiler<'a> {
return self.compile_entity(type_def, node.clone(), false); 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 let Some(crate::database::object::SchemaTypeOrArray::Single(t)) = &node.schema.obj.type_ {
if !crate::database::object::is_primitive_type(t) { 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)); 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_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_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(); let start_len = where_clauses.len();
self.compile_relation_conditions( self.compile_relation_conditions(
@ -865,4 +875,69 @@ impl<'a> Compiler<'a> {
} }
Ok(()) 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(); 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] #[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

@ -179,14 +179,35 @@ impl SqlFormatter {
} }
fn format_query(&mut self, query: &Query) { 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::Select(select) => self.format_select(select),
SetExpr::Query(inner_query) => { SetExpr::Query(inner_query) => {
self.push_str("("); self.push_str("(");
self.format_query(inner_query); self.format_query(inner_query);
self.push_str(")"); 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) { fn format_join(&mut self, join: &Join) {
let op = match &join.join_operator { let op_str = match &join.join_operator {
JoinOperator::Inner(_) => "JOIN", JoinOperator::Inner(_) => "JOIN",
JoinOperator::LeftOuter(_) => "LEFT JOIN", JoinOperator::Left(_) => "LEFT JOIN",
JoinOperator::Right(_) => "RIGHT JOIN",
_ => "JOIN", _ => "JOIN",
}; };
self.push_str(&format!("{} {} ON ", op, join.relation)); self.push_str(&format!("{} {}", op_str, join.relation));
match &join.join_operator { match &join.join_operator {
JoinOperator::Inner(JoinConstraint::On(expr)) => self.format_expr(expr), JoinOperator::Inner(JoinConstraint::On(expr))
JoinOperator::LeftOuter(JoinConstraint::On(expr)) => self.format_expr(expr), | JoinOperator::Left(JoinConstraint::On(expr))
JoinOperator::Join(JoinConstraint::On(expr)) => self.format_expr(expr), | JoinOperator::Right(JoinConstraint::On(expr))
_ => { | JoinOperator::Join(JoinConstraint::On(expr)) => {
println!("FALLBACK JOIN OP: {:?}", join.join_operator); 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(); let formatted_json = serde_json::to_string_pretty(&file_data).unwrap();
fs::write(path, formatted_json).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() { if env::var("UPDATE_EXPECT").is_ok() {
update_validation_fixture(path, suite_idx, case_idx, &result.errors); 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)?; expect.assert_drop(&result)?;

View File

@ -1 +1 @@
1.0.165 1.0.179