Compare commits
9 Commits
1.0.181
...
review-202
| Author | SHA1 | Date | |
|---|---|---|---|
| ddc86f1ba0 | |||
| 562e52e0eb | |||
| 569eb1d2ea | |||
| 6cebce3740 | |||
| d3de3961c1 | |||
| d0ae6eeddd | |||
| e741a7197f | |||
| cf3cabd1f5 | |||
| d608f5e963 |
36
GEMINI.md
36
GEMINI.md
@ -229,40 +229,6 @@ 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": {
|
||||
"type": "string",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**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
|
||||
@ -348,8 +314,6 @@ 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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@ -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": "",
|
||||
@ -347,8 +347,8 @@
|
||||
{
|
||||
"code": "CONTAINS_VIOLATED",
|
||||
"values": {
|
||||
"count": "0",
|
||||
"limit": "1"
|
||||
"limit": "1",
|
||||
"count": "0"
|
||||
},
|
||||
"details": {
|
||||
"path": "",
|
||||
@ -373,8 +373,8 @@
|
||||
{
|
||||
"code": "MULTIPLE_OF_VIOLATED",
|
||||
"values": {
|
||||
"value": "3",
|
||||
"multiple_of": "2"
|
||||
"multiple_of": "2",
|
||||
"value": "3"
|
||||
},
|
||||
"details": {
|
||||
"path": "0",
|
||||
@ -384,8 +384,8 @@
|
||||
{
|
||||
"code": "MULTIPLE_OF_VIOLATED",
|
||||
"values": {
|
||||
"value": "9",
|
||||
"multiple_of": "2"
|
||||
"multiple_of": "2",
|
||||
"value": "9"
|
||||
},
|
||||
"details": {
|
||||
"path": "2",
|
||||
@ -432,8 +432,8 @@
|
||||
{
|
||||
"code": "MULTIPLE_OF_VIOLATED",
|
||||
"values": {
|
||||
"value": "1",
|
||||
"multiple_of": "2"
|
||||
"multiple_of": "2",
|
||||
"value": "1"
|
||||
},
|
||||
"details": {
|
||||
"path": "0",
|
||||
|
||||
@ -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,8 +151,8 @@
|
||||
{
|
||||
"code": "EDGE_MISSING",
|
||||
"values": {
|
||||
"parent_type": "parent",
|
||||
"child_type": "child",
|
||||
"parent_type": "parent",
|
||||
"property_name": "children"
|
||||
},
|
||||
"details": {
|
||||
@ -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"
|
||||
|
||||
@ -227,8 +227,8 @@
|
||||
{
|
||||
"code": "DEPENDENCY_MISSING",
|
||||
"values": {
|
||||
"property_name": "quux",
|
||||
"required_property": "bar"
|
||||
"required_property": "bar",
|
||||
"property_name": "quux"
|
||||
},
|
||||
"details": {
|
||||
"path": "",
|
||||
@ -386,8 +386,8 @@
|
||||
{
|
||||
"code": "DEPENDENCY_MISSING",
|
||||
"values": {
|
||||
"property_name": "foo\"bar",
|
||||
"required_property": "foo'bar"
|
||||
"required_property": "foo'bar",
|
||||
"property_name": "foo\"bar"
|
||||
},
|
||||
"details": {
|
||||
"path": "",
|
||||
@ -829,8 +829,8 @@
|
||||
{
|
||||
"code": "MIN_PROPERTIES_VIOLATED",
|
||||
"values": {
|
||||
"count": "2",
|
||||
"limit": "4"
|
||||
"limit": "4",
|
||||
"count": "2"
|
||||
},
|
||||
"details": {
|
||||
"path": "",
|
||||
|
||||
@ -34,8 +34,8 @@
|
||||
{
|
||||
"code": "EXCLUSIVE_MAXIMUM_VIOLATED",
|
||||
"values": {
|
||||
"value": "3",
|
||||
"limit": "3"
|
||||
"limit": "3",
|
||||
"value": "3"
|
||||
},
|
||||
"details": {
|
||||
"path": "",
|
||||
|
||||
@ -34,8 +34,8 @@
|
||||
{
|
||||
"code": "EXCLUSIVE_MINIMUM_VIOLATED",
|
||||
"values": {
|
||||
"value": "1.1",
|
||||
"limit": "1.1"
|
||||
"limit": "1.1",
|
||||
"value": "1.1"
|
||||
},
|
||||
"details": {
|
||||
"path": "",
|
||||
|
||||
@ -87,6 +87,34 @@
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"schedule": {
|
||||
"type": [
|
||||
"opening_hours",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"opening_hours": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"open": {
|
||||
"type": "string"
|
||||
},
|
||||
"seasons": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "season"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"season": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"label": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -211,6 +239,13 @@
|
||||
"gender": {},
|
||||
"gender.condition": {
|
||||
"type": "condition",
|
||||
"compiledPropertyNames": [
|
||||
"kind",
|
||||
"$eq",
|
||||
"$ne",
|
||||
"$of",
|
||||
"$nof"
|
||||
],
|
||||
"properties": {
|
||||
"$eq": {
|
||||
"type": [
|
||||
@ -224,15 +259,6 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"$of": {
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"type": "gender"
|
||||
}
|
||||
},
|
||||
"$nof": {
|
||||
"type": [
|
||||
"array",
|
||||
@ -241,23 +267,89 @@
|
||||
"items": {
|
||||
"type": "gender"
|
||||
}
|
||||
},
|
||||
"$of": {
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"type": "gender"
|
||||
}
|
||||
}
|
||||
},
|
||||
"compiledPropertyNames": [
|
||||
"kind",
|
||||
"$eq",
|
||||
"$ne",
|
||||
"$of",
|
||||
"$nof"
|
||||
]
|
||||
}
|
||||
},
|
||||
"person": {},
|
||||
"person.filter": {
|
||||
"type": "filter",
|
||||
"compiledPropertyNames": [
|
||||
"first_name",
|
||||
"age",
|
||||
"billing_address",
|
||||
"gender",
|
||||
"birth_date",
|
||||
"uuid_field",
|
||||
"tags",
|
||||
"ad_hoc",
|
||||
"$and",
|
||||
"$or"
|
||||
],
|
||||
"properties": {
|
||||
"first_name": {
|
||||
"$and": {
|
||||
"items": {
|
||||
"compiledPropertyNames": [
|
||||
"first_name",
|
||||
"age",
|
||||
"billing_address",
|
||||
"gender",
|
||||
"birth_date",
|
||||
"uuid_field",
|
||||
"tags",
|
||||
"ad_hoc",
|
||||
"$and",
|
||||
"$or"
|
||||
],
|
||||
"type": "person.filter"
|
||||
},
|
||||
"type": [
|
||||
"string.condition",
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"$or": {
|
||||
"items": {
|
||||
"compiledPropertyNames": [
|
||||
"first_name",
|
||||
"age",
|
||||
"billing_address",
|
||||
"gender",
|
||||
"birth_date",
|
||||
"uuid_field",
|
||||
"tags",
|
||||
"ad_hoc",
|
||||
"$and",
|
||||
"$or"
|
||||
],
|
||||
"type": "person.filter"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"ad_hoc": {
|
||||
"compiledPropertyNames": [
|
||||
"foo"
|
||||
],
|
||||
"properties": {
|
||||
"foo": {
|
||||
"type": [
|
||||
"string.condition",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": [
|
||||
"filter",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
@ -273,12 +365,6 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"gender": {
|
||||
"type": [
|
||||
"gender.condition",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"birth_date": {
|
||||
"type": [
|
||||
"date.condition",
|
||||
@ -291,48 +377,48 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"first_name": {
|
||||
"type": [
|
||||
"string.condition",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"gender": {
|
||||
"type": [
|
||||
"gender.condition",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"tags": {
|
||||
"type": [
|
||||
"string.condition",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"ad_hoc": {
|
||||
"type": [
|
||||
"filter",
|
||||
"null"
|
||||
],
|
||||
"properties": {
|
||||
"foo": {
|
||||
"type": [
|
||||
"string.condition",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"compiledPropertyNames": [
|
||||
"foo"
|
||||
]
|
||||
},
|
||||
}
|
||||
},
|
||||
"type": "filter"
|
||||
},
|
||||
"address": {},
|
||||
"address.filter": {
|
||||
"type": "filter",
|
||||
"compiledPropertyNames": [
|
||||
"city",
|
||||
"$and",
|
||||
"$or"
|
||||
],
|
||||
"properties": {
|
||||
"$and": {
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"type": "person.filter",
|
||||
"compiledPropertyNames": [
|
||||
"first_name",
|
||||
"age",
|
||||
"billing_address",
|
||||
"gender",
|
||||
"birth_date",
|
||||
"uuid_field",
|
||||
"tags",
|
||||
"ad_hoc",
|
||||
"city",
|
||||
"$and",
|
||||
"$or"
|
||||
]
|
||||
],
|
||||
"type": "address.filter"
|
||||
}
|
||||
},
|
||||
"$or": {
|
||||
@ -341,79 +427,21 @@
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"type": "person.filter",
|
||||
"compiledPropertyNames": [
|
||||
"first_name",
|
||||
"age",
|
||||
"billing_address",
|
||||
"gender",
|
||||
"birth_date",
|
||||
"uuid_field",
|
||||
"tags",
|
||||
"ad_hoc",
|
||||
"city",
|
||||
"$and",
|
||||
"$or"
|
||||
]
|
||||
],
|
||||
"type": "address.filter"
|
||||
}
|
||||
}
|
||||
},
|
||||
"compiledPropertyNames": [
|
||||
"first_name",
|
||||
"age",
|
||||
"billing_address",
|
||||
"gender",
|
||||
"birth_date",
|
||||
"uuid_field",
|
||||
"tags",
|
||||
"ad_hoc",
|
||||
"$and",
|
||||
"$or"
|
||||
]
|
||||
},
|
||||
"address": {},
|
||||
"address.filter": {
|
||||
"type": "filter",
|
||||
"properties": {
|
||||
},
|
||||
"city": {
|
||||
"type": [
|
||||
"string.condition",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"$and": {
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"type": "address.filter",
|
||||
"compiledPropertyNames": [
|
||||
"city",
|
||||
"$and",
|
||||
"$or"
|
||||
]
|
||||
}
|
||||
},
|
||||
"$or": {
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"type": "address.filter",
|
||||
"compiledPropertyNames": [
|
||||
"city",
|
||||
"$and",
|
||||
"$or"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"compiledPropertyNames": [
|
||||
"city",
|
||||
"$and",
|
||||
"$or"
|
||||
]
|
||||
}
|
||||
},
|
||||
"condition": {},
|
||||
"filter": {},
|
||||
@ -424,7 +452,52 @@
|
||||
"search": {},
|
||||
"search.filter": {
|
||||
"type": "filter",
|
||||
"compiledPropertyNames": [
|
||||
"kind",
|
||||
"name",
|
||||
"filter",
|
||||
"$and",
|
||||
"$or"
|
||||
],
|
||||
"properties": {
|
||||
"$and": {
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"compiledPropertyNames": [
|
||||
"kind",
|
||||
"name",
|
||||
"filter",
|
||||
"$and",
|
||||
"$or"
|
||||
],
|
||||
"type": "search.filter"
|
||||
}
|
||||
},
|
||||
"$or": {
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"compiledPropertyNames": [
|
||||
"kind",
|
||||
"name",
|
||||
"filter",
|
||||
"$and",
|
||||
"$or"
|
||||
],
|
||||
"type": "search.filter"
|
||||
}
|
||||
},
|
||||
"filter": {
|
||||
"type": [
|
||||
"$kind.filter",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"kind": {
|
||||
"type": [
|
||||
"string.condition",
|
||||
@ -436,54 +509,11 @@
|
||||
"string.condition",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"filter": {
|
||||
"type": [
|
||||
"$kind.filter",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"$and": {
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"type": "search.filter",
|
||||
"compiledPropertyNames": [
|
||||
"kind",
|
||||
"name",
|
||||
"filter",
|
||||
"$and",
|
||||
"$or"
|
||||
]
|
||||
}
|
||||
},
|
||||
"$or": {
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"type": "search.filter",
|
||||
"compiledPropertyNames": [
|
||||
"kind",
|
||||
"name",
|
||||
"filter",
|
||||
"$and",
|
||||
"$or"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"compiledPropertyNames": [
|
||||
"kind",
|
||||
"name",
|
||||
"filter",
|
||||
"$and",
|
||||
"$or"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"opening_hours": {},
|
||||
"season": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -106,8 +106,8 @@
|
||||
{
|
||||
"code": "CONTAINS_VIOLATED",
|
||||
"values": {
|
||||
"count": "2",
|
||||
"limit": "1"
|
||||
"limit": "1",
|
||||
"count": "2"
|
||||
},
|
||||
"details": {
|
||||
"path": "",
|
||||
@ -144,8 +144,8 @@
|
||||
{
|
||||
"code": "CONTAINS_VIOLATED",
|
||||
"values": {
|
||||
"count": "2",
|
||||
"limit": "1"
|
||||
"limit": "1",
|
||||
"count": "2"
|
||||
},
|
||||
"details": {
|
||||
"path": "",
|
||||
@ -284,8 +284,8 @@
|
||||
{
|
||||
"code": "CONTAINS_VIOLATED",
|
||||
"values": {
|
||||
"count": "4",
|
||||
"limit": "3"
|
||||
"limit": "3",
|
||||
"count": "4"
|
||||
},
|
||||
"details": {
|
||||
"path": "",
|
||||
|
||||
@ -52,8 +52,8 @@
|
||||
{
|
||||
"code": "MAX_ITEMS_VIOLATED",
|
||||
"values": {
|
||||
"count": "3",
|
||||
"limit": "2"
|
||||
"limit": "2",
|
||||
"count": "3"
|
||||
},
|
||||
"details": {
|
||||
"path": "",
|
||||
|
||||
@ -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": "",
|
||||
|
||||
@ -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": "",
|
||||
|
||||
@ -118,8 +118,8 @@
|
||||
{
|
||||
"code": "MAXIMUM_VIOLATED",
|
||||
"values": {
|
||||
"value": "300.5",
|
||||
"limit": "300"
|
||||
"limit": "300",
|
||||
"value": "300.5"
|
||||
},
|
||||
"details": {
|
||||
"path": "",
|
||||
|
||||
@ -239,8 +239,8 @@
|
||||
{
|
||||
"code": "DEPENDENCY_MISSING",
|
||||
"values": {
|
||||
"property_name": "trigger",
|
||||
"required_property": "base_dep"
|
||||
"required_property": "base_dep",
|
||||
"property_name": "trigger"
|
||||
},
|
||||
"details": {
|
||||
"path": "",
|
||||
|
||||
@ -169,8 +169,8 @@
|
||||
{
|
||||
"code": "CONTAINS_VIOLATED",
|
||||
"values": {
|
||||
"count": "0",
|
||||
"limit": "2"
|
||||
"limit": "2",
|
||||
"count": "0"
|
||||
},
|
||||
"details": {
|
||||
"path": "",
|
||||
@ -357,8 +357,8 @@
|
||||
{
|
||||
"code": "CONTAINS_VIOLATED",
|
||||
"values": {
|
||||
"count": "0",
|
||||
"limit": "2"
|
||||
"limit": "2",
|
||||
"count": "0"
|
||||
},
|
||||
"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": "",
|
||||
@ -657,8 +657,8 @@
|
||||
{
|
||||
"code": "CONTAINS_VIOLATED",
|
||||
"values": {
|
||||
"count": "2",
|
||||
"limit": "1"
|
||||
"limit": "1",
|
||||
"count": "2"
|
||||
},
|
||||
"details": {
|
||||
"path": "",
|
||||
|
||||
@ -108,8 +108,8 @@
|
||||
{
|
||||
"code": "MIN_ITEMS_VIOLATED",
|
||||
"values": {
|
||||
"count": "0",
|
||||
"limit": "1"
|
||||
"limit": "1",
|
||||
"count": "0"
|
||||
},
|
||||
"details": {
|
||||
"path": "",
|
||||
|
||||
@ -43,8 +43,8 @@
|
||||
{
|
||||
"code": "MIN_LENGTH_VIOLATED",
|
||||
"values": {
|
||||
"count": "1",
|
||||
"limit": "2"
|
||||
"limit": "2",
|
||||
"count": "1"
|
||||
},
|
||||
"details": {
|
||||
"path": "",
|
||||
|
||||
@ -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": "",
|
||||
@ -149,8 +149,8 @@
|
||||
{
|
||||
"code": "MINIMUM_VIOLATED",
|
||||
"values": {
|
||||
"value": "-3",
|
||||
"limit": "-2"
|
||||
"limit": "-2",
|
||||
"value": "-3"
|
||||
},
|
||||
"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": "",
|
||||
|
||||
@ -156,8 +156,8 @@
|
||||
{
|
||||
"code": "MAXIMUM_VIOLATED",
|
||||
"values": {
|
||||
"value": "60",
|
||||
"limit": "50"
|
||||
"limit": "50",
|
||||
"value": "60"
|
||||
},
|
||||
"details": {
|
||||
"path": "max",
|
||||
@ -191,9 +191,6 @@
|
||||
},
|
||||
{
|
||||
"name": "request",
|
||||
"field_types": {
|
||||
"inv": "jsonb"
|
||||
},
|
||||
"schemas": {
|
||||
"request": {
|
||||
"type": "object",
|
||||
|
||||
@ -200,8 +200,8 @@
|
||||
{
|
||||
"code": "MINIMUM_VIOLATED",
|
||||
"values": {
|
||||
"value": "5",
|
||||
"limit": "10"
|
||||
"limit": "10",
|
||||
"value": "5"
|
||||
},
|
||||
"details": {
|
||||
"path": "entities/entity-beta/value",
|
||||
|
||||
@ -34,8 +34,8 @@
|
||||
{
|
||||
"code": "PATTERN_VIOLATED",
|
||||
"values": {
|
||||
"pattern": "^a*$",
|
||||
"value": "abc"
|
||||
"value": "abc",
|
||||
"pattern": "^a*$"
|
||||
},
|
||||
"details": {
|
||||
"path": "",
|
||||
|
||||
@ -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": "",
|
||||
@ -591,8 +591,8 @@
|
||||
{
|
||||
"code": "NO_ONEOF_MATCH",
|
||||
"values": {
|
||||
"discriminator": "type",
|
||||
"value": "alien"
|
||||
"value": "alien",
|
||||
"discriminator": "type"
|
||||
},
|
||||
"details": {
|
||||
"path": "",
|
||||
@ -931,8 +931,8 @@
|
||||
{
|
||||
"code": "NO_FAMILY_MATCH",
|
||||
"values": {
|
||||
"discriminator": "kind",
|
||||
"value": "unknown_panel"
|
||||
"value": "unknown_panel",
|
||||
"discriminator": "kind"
|
||||
},
|
||||
"details": {
|
||||
"path": "",
|
||||
|
||||
@ -431,8 +431,8 @@
|
||||
{
|
||||
"code": "MAX_LENGTH_VIOLATED",
|
||||
"values": {
|
||||
"count": "6",
|
||||
"limit": "3"
|
||||
"limit": "3",
|
||||
"count": "6"
|
||||
},
|
||||
"details": {
|
||||
"path": "",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -139,8 +139,8 @@
|
||||
{
|
||||
"code": "MAX_LENGTH_VIOLATED",
|
||||
"values": {
|
||||
"count": "26",
|
||||
"limit": "5"
|
||||
"limit": "5",
|
||||
"count": "26"
|
||||
},
|
||||
"details": {
|
||||
"path": "email",
|
||||
|
||||
1
flow
1
flow
@ -36,6 +36,7 @@ pgrx-down() {
|
||||
info "Taking pgrx down..."
|
||||
}
|
||||
|
||||
|
||||
build() {
|
||||
local version
|
||||
version=$(get-version) || return $?
|
||||
|
||||
@ -2,7 +2,7 @@ use crate::database::schema::Schema;
|
||||
#[allow(unused_imports)]
|
||||
use crate::drop::{Error, ErrorDetails};
|
||||
#[allow(unused_imports)]
|
||||
use indexmap::IndexMap;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
impl Schema {
|
||||
@ -19,7 +19,7 @@ impl Schema {
|
||||
if !c.is_ascii_lowercase() && !c.is_ascii_digit() && c != '_' && c != '.' && c != '$' {
|
||||
errors.push(Error {
|
||||
code: "INVALID_IDENTIFIER".to_string(),
|
||||
values: Some(IndexMap::from([
|
||||
values: Some(HashMap::from([
|
||||
("character".to_string(), c.to_string()),
|
||||
("field_name".to_string(), field_name.to_string()),
|
||||
("identifier".to_string(), id.to_string()),
|
||||
|
||||
@ -165,8 +165,16 @@ impl Schema {
|
||||
} else if db.enums.contains_key(custom) {
|
||||
Some(vec![format!("{}.condition", custom)])
|
||||
} else {
|
||||
// Assume anything else is a Relational cross-boundary that already has its own .filter dynamically built
|
||||
Some(vec![format!("{}.filter", custom)])
|
||||
// Only a Table-Backed boundary has a synthesized Composed Filter to proxy to.
|
||||
// A Field-Backed JSONB Bubble has none — omit it like an inline object rather
|
||||
// than emit a dangling proxy reference, which breaks eager consumers of the
|
||||
// exported registry (downstream code generators).
|
||||
let base = custom.split('.').next_back().unwrap_or(custom);
|
||||
if db.types.contains_key(base) {
|
||||
Some(vec![format!("{}.filter", custom)])
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,6 +7,7 @@ pub mod polymorphism;
|
||||
use crate::database::schema::Schema;
|
||||
use crate::drop::{Error, ErrorDetails};
|
||||
use indexmap::IndexMap;
|
||||
use std::collections::HashMap;
|
||||
|
||||
impl Schema {
|
||||
pub fn compile(
|
||||
@ -63,7 +64,7 @@ impl Schema {
|
||||
if custom_type_count > 1 {
|
||||
errors.push(Error {
|
||||
code: "MULTIPLE_INHERITANCE_PROHIBITED".to_string(),
|
||||
values: Some(IndexMap::from([("types".to_string(), types.join(", "))])),
|
||||
values: Some(HashMap::from([("types".to_string(), types.join(", "))])),
|
||||
details: ErrorDetails {
|
||||
path: Some(path.clone()),
|
||||
schema: Some(root_id.to_string()),
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
use crate::drop::{Error, ErrorDetails};
|
||||
use indexmap::IndexSet;
|
||||
use indexmap::IndexMap;
|
||||
use std::collections::HashMap;
|
||||
use crate::database::schema::Schema;
|
||||
|
||||
impl Schema {
|
||||
@ -144,7 +144,7 @@ impl Schema {
|
||||
if options.contains_key(&val) {
|
||||
errors.push(Error {
|
||||
code: "POLYMORPHIC_COLLISION".to_string(),
|
||||
values: Some(IndexMap::from([("value".to_string(), val.to_string())])),
|
||||
values: Some(HashMap::from([("value".to_string(), val.to_string())])),
|
||||
details: ErrorDetails {
|
||||
path: Some(path.to_string()),
|
||||
schema: Some(root_id.to_string()),
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
use crate::drop::{Error, ErrorDetails};
|
||||
use serde_json::Value;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use indexmap::IndexMap;
|
||||
|
||||
pub fn compose(val: &mut Value, errors: &mut Vec<Error>) {
|
||||
let mut traits = HashMap::new();
|
||||
@ -115,7 +114,7 @@ fn resolve_in_place(
|
||||
if visited.contains(inc_name) {
|
||||
errors.push(Error {
|
||||
code: "CIRCULAR_INCLUDE_DETECTED".to_string(),
|
||||
values: Some(IndexMap::from([(
|
||||
values: Some(HashMap::from([(
|
||||
"include".to_string(),
|
||||
inc_name.to_string(),
|
||||
)])),
|
||||
@ -219,7 +218,7 @@ fn resolve_in_place(
|
||||
} else {
|
||||
errors.push(Error {
|
||||
code: "TRAIT_NOT_FOUND".to_string(),
|
||||
values: Some(IndexMap::from([(
|
||||
values: Some(HashMap::from([(
|
||||
"include".to_string(),
|
||||
inc_name.to_string(),
|
||||
)])),
|
||||
|
||||
@ -28,7 +28,7 @@ use serde_json::Value;
|
||||
use indexmap::IndexMap;
|
||||
use std::sync::Arc;
|
||||
use r#type::Type;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use crate::drop::{Drop, Error, ErrorDetails};
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
@ -76,7 +76,7 @@ impl Database {
|
||||
Err(e) => {
|
||||
errors.push(Error {
|
||||
code: "DATABASE_ENUM_PARSE_FAILED".to_string(),
|
||||
values: Some(IndexMap::from([
|
||||
values: Some(HashMap::from([
|
||||
("enum".to_string(), name.clone()),
|
||||
("reason".to_string(), e.to_string()),
|
||||
])),
|
||||
@ -104,7 +104,7 @@ impl Database {
|
||||
Err(e) => {
|
||||
errors.push(Error {
|
||||
code: "DATABASE_TYPE_PARSE_FAILED".to_string(),
|
||||
values: Some(IndexMap::from([
|
||||
values: Some(HashMap::from([
|
||||
("type".to_string(), name.clone()),
|
||||
("reason".to_string(), e.to_string()),
|
||||
])),
|
||||
@ -136,7 +136,7 @@ impl Database {
|
||||
Err(e) => {
|
||||
errors.push(Error {
|
||||
code: "DATABASE_RELATION_PARSE_FAILED".to_string(),
|
||||
values: Some(IndexMap::from([
|
||||
values: Some(HashMap::from([
|
||||
("relation".to_string(), constraint.clone()),
|
||||
("reason".to_string(), e.to_string()),
|
||||
])),
|
||||
@ -164,7 +164,7 @@ impl Database {
|
||||
Err(e) => {
|
||||
errors.push(Error {
|
||||
code: "DATABASE_PUNC_PARSE_FAILED".to_string(),
|
||||
values: Some(IndexMap::from([
|
||||
values: Some(HashMap::from([
|
||||
("punc".to_string(), name.clone()),
|
||||
("reason".to_string(), e.to_string()),
|
||||
])),
|
||||
@ -461,7 +461,7 @@ impl Database {
|
||||
|
||||
errors.push(Error {
|
||||
code: "EDGE_MISSING".to_string(),
|
||||
values: Some(IndexMap::from([
|
||||
values: Some(HashMap::from([
|
||||
("parent_type".to_string(), parent_type.to_string()),
|
||||
("child_type".to_string(), child_type.to_string()),
|
||||
("property_name".to_string(), prop_name.to_string()),
|
||||
@ -563,7 +563,7 @@ impl Database {
|
||||
|
||||
errors.push(Error {
|
||||
code: "AMBIGUOUS_TYPE_RELATIONS".to_string(),
|
||||
values: Some(IndexMap::from([
|
||||
values: Some(HashMap::from([
|
||||
("parent_type".to_string(), parent_type.to_string()),
|
||||
("child_type".to_string(), child_type.to_string()),
|
||||
("property_name".to_string(), prop_name.to_string()),
|
||||
|
||||
@ -57,13 +57,13 @@ impl Drop {
|
||||
}
|
||||
}
|
||||
|
||||
use indexmap::IndexMap;
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct Error {
|
||||
pub code: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub values: Option<IndexMap<String, String>>,
|
||||
pub values: Option<HashMap<String, String>>,
|
||||
pub details: ErrorDetails,
|
||||
}
|
||||
|
||||
|
||||
@ -7,7 +7,7 @@ use crate::database::Database;
|
||||
use crate::database::r#type::Type;
|
||||
use crate::drop::{Drop, Error, ErrorDetails};
|
||||
use serde_json::Value;
|
||||
use indexmap::IndexMap;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub struct Merger {
|
||||
@ -31,7 +31,7 @@ impl Merger {
|
||||
None => {
|
||||
return Drop::with_errors(vec![Error {
|
||||
code: "SCHEMA_NOT_FOUND".to_string(),
|
||||
values: Some(IndexMap::from([
|
||||
values: Some(HashMap::from([
|
||||
("schema".to_string(), schema_id.to_string()),
|
||||
])),
|
||||
details: ErrorDetails {
|
||||
@ -78,7 +78,7 @@ impl Merger {
|
||||
|
||||
return Drop::with_errors(vec![Error {
|
||||
code: final_code,
|
||||
values: Some(IndexMap::from([
|
||||
values: Some(HashMap::from([
|
||||
("error".to_string(), final_message),
|
||||
])),
|
||||
details: ErrorDetails {
|
||||
@ -96,7 +96,7 @@ impl Merger {
|
||||
if let Err(e) = self.db.execute(¬ify_sql, None) {
|
||||
return Drop::with_errors(vec![Error {
|
||||
code: "MERGE_FAILED".to_string(),
|
||||
values: Some(IndexMap::from([
|
||||
values: Some(HashMap::from([
|
||||
("error".to_string(), e.to_string()),
|
||||
])),
|
||||
details: ErrorDetails {
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
use crate::database::Database;
|
||||
use indexmap::IndexMap;
|
||||
use serde_json::Value;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub struct Compiler<'a> {
|
||||
@ -121,21 +120,6 @@ 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;
|
||||
|
||||
@ -165,9 +149,16 @@ impl<'a> Compiler<'a> {
|
||||
return self.compile_entity(type_def, node.clone(), false);
|
||||
}
|
||||
|
||||
// Fallback error if the schema pointer was unresolved
|
||||
// Handle Direct Refs via type pointer
|
||||
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));
|
||||
}
|
||||
}
|
||||
@ -240,46 +231,29 @@ impl<'a> Compiler<'a> {
|
||||
};
|
||||
|
||||
// 3. Build WHERE clauses
|
||||
let where_clauses = self.compile_where_clause(r#type, &table_aliases, node.clone())?;
|
||||
let where_clauses = self.compile_where_clause(r#type, &table_aliases, node)?;
|
||||
|
||||
let selection = if is_array {
|
||||
format!("COALESCE(jsonb_agg({}), '[]'::jsonb)", jsonb_obj_sql)
|
||||
// Deterministic order: aggregation over an unordered heap made result
|
||||
// order vary run-to-run (flaky reads, unstable grids). Creation order
|
||||
// with id as the tiebreaker; the entity alias exists for every
|
||||
// entity-family type.
|
||||
match table_aliases.get("entity") {
|
||||
Some(entity_alias) => format!(
|
||||
"COALESCE(jsonb_agg({} ORDER BY {}.created_at, {}.id), '[]'::jsonb)",
|
||||
jsonb_obj_sql, entity_alias, entity_alias
|
||||
),
|
||||
None => format!("COALESCE(jsonb_agg({}), '[]'::jsonb)", jsonb_obj_sql),
|
||||
}
|
||||
} else {
|
||||
jsonb_obj_sql
|
||||
};
|
||||
|
||||
let mut limit_clause = "";
|
||||
if !is_array {
|
||||
let is_reverse = if let Some(prop_ref) = &node.property_name {
|
||||
let prop = prop_ref.as_str();
|
||||
if let Some(parent_schema) = &node.parent_schema {
|
||||
if let Some(compiled_edges) = parent_schema.obj.compiled_edges.get() {
|
||||
if let Some(edge) = compiled_edges.get(prop) {
|
||||
!edge.forward
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
if is_reverse {
|
||||
limit_clause = " LIMIT 1";
|
||||
}
|
||||
}
|
||||
|
||||
let full_sql = format!(
|
||||
"(SELECT {} FROM {} WHERE {}{})",
|
||||
"(SELECT {} FROM {} WHERE {})",
|
||||
selection,
|
||||
from_clauses.join(" "),
|
||||
where_clauses.join(" AND "),
|
||||
limit_clause
|
||||
where_clauses.join(" AND ")
|
||||
);
|
||||
|
||||
Ok((
|
||||
@ -585,9 +559,8 @@ impl<'a> Compiler<'a> {
|
||||
where_clauses.push(format!("NOT {}.archived", entity_alias));
|
||||
}
|
||||
|
||||
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_static_property_conditions(r#type, type_aliases, &node, &base_alias, &mut where_clauses);
|
||||
|
||||
let start_len = where_clauses.len();
|
||||
self.compile_relation_conditions(
|
||||
@ -648,6 +621,9 @@ impl<'a> Compiler<'a> {
|
||||
if edge.forward && relation.source_columns.len() > 1 {
|
||||
poly_col = Some(&relation.source_columns[1]); // e.g., target_type
|
||||
table_to_alias = &relation.source_type; // e.g., relationship
|
||||
} else if !edge.forward && relation.destination_columns.len() > 1 {
|
||||
poly_col = Some(&relation.destination_columns[1]); // e.g., source_type
|
||||
table_to_alias = &relation.destination_type; // e.g., relationship
|
||||
}
|
||||
|
||||
if let Some(col) = poly_col {
|
||||
@ -723,6 +699,11 @@ impl<'a> Compiler<'a> {
|
||||
|| pg_type.contains("int")
|
||||
|| pg_type == "real"
|
||||
|| pg_type == "double precision"
|
||||
// pg catalog typnames for real / double precision — the SQL
|
||||
// names above never appear in field_types (they come from
|
||||
// pg_type.typname), so these are what actually arrives
|
||||
|| pg_type == "float4"
|
||||
|| pg_type == "float8"
|
||||
{
|
||||
cast = "::numeric";
|
||||
} else if pg_type == "text" || pg_type.contains("char") {
|
||||
@ -749,7 +730,7 @@ impl<'a> Compiler<'a> {
|
||||
node: &Node,
|
||||
base_alias: &str,
|
||||
where_clauses: &mut Vec<String>,
|
||||
) {
|
||||
) -> Result<(), String> {
|
||||
for (i, filter_key) in self.filter_keys.iter().enumerate() {
|
||||
let mut parts = filter_key.split(':');
|
||||
let full_field_path = parts.next().unwrap_or(filter_key);
|
||||
@ -779,6 +760,95 @@ impl<'a> Compiler<'a> {
|
||||
let param_index = i + 1;
|
||||
let p_val = format!("${}#>>'{{}}'", param_index);
|
||||
|
||||
// jsonb columns never type-check against the text parameter, and for
|
||||
// array-valued properties (tag lists) the meaning of a condition is
|
||||
// CONTAINMENT, not equality. Compile them explicitly; reject the rest
|
||||
// loudly at compile time instead of failing at execution.
|
||||
let is_jsonb = r#type
|
||||
.field_types
|
||||
.as_ref()
|
||||
.and_then(|v| v.as_object())
|
||||
.and_then(|ft| ft.get(field_name))
|
||||
.and_then(|v| v.as_str())
|
||||
== Some("jsonb");
|
||||
if is_jsonb {
|
||||
// The node schema may be a punc-response proxy without properties —
|
||||
// fall back to the type's own schema. Compiled schemas normalize to
|
||||
// the list form ("type": ["array"]), hand-written ones may use the
|
||||
// single form — accept both.
|
||||
let prop_schema = node
|
||||
.schema
|
||||
.obj
|
||||
.properties
|
||||
.as_ref()
|
||||
.and_then(|p| p.get(field_name))
|
||||
.cloned()
|
||||
.or_else(|| {
|
||||
r#type
|
||||
.schemas
|
||||
.get(&r#type.name)
|
||||
.and_then(|s| s.obj.properties.as_ref())
|
||||
.and_then(|p| p.get(field_name))
|
||||
.cloned()
|
||||
});
|
||||
let is_array_prop = prop_schema
|
||||
.map(|ps| match &ps.obj.type_ {
|
||||
Some(crate::database::object::SchemaTypeOrArray::Single(t)) => t == "array",
|
||||
Some(crate::database::object::SchemaTypeOrArray::Multiple(ts)) => {
|
||||
ts.iter().any(|t| t == "array") && ts.iter().all(|t| t == "array" || t == "null")
|
||||
}
|
||||
None => false,
|
||||
})
|
||||
.unwrap_or(false);
|
||||
|
||||
if is_array_prop {
|
||||
match op {
|
||||
// "the array contains this value"
|
||||
"$eq" => where_clauses.push(format!(
|
||||
"{}.{} ? ({})",
|
||||
filter_alias, field_name, p_val
|
||||
)),
|
||||
"$ne" => where_clauses.push(format!(
|
||||
"NOT ({}.{} ? ({}))",
|
||||
filter_alias, field_name, p_val
|
||||
)),
|
||||
// "the array contains ANY of these values"
|
||||
"$of" => where_clauses.push(format!(
|
||||
"{}.{} ?| ARRAY(SELECT jsonb_array_elements_text(({})::jsonb))",
|
||||
filter_alias, field_name, p_val
|
||||
)),
|
||||
"$nof" => where_clauses.push(format!(
|
||||
"NOT ({}.{} ?| ARRAY(SELECT jsonb_array_elements_text(({})::jsonb)))",
|
||||
filter_alias, field_name, p_val
|
||||
)),
|
||||
other => {
|
||||
return Err(format!(
|
||||
"operator {} is not supported on array property '{}' (jsonb containment supports $eq/$ne/$of/$nof)",
|
||||
other, field_name
|
||||
))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
match op {
|
||||
"$eq" => where_clauses.push(format!(
|
||||
"{}.{} = ({})::jsonb",
|
||||
filter_alias, field_name, p_val
|
||||
)),
|
||||
"$ne" => where_clauses.push(format!(
|
||||
"{}.{} != ({})::jsonb",
|
||||
filter_alias, field_name, p_val
|
||||
)),
|
||||
other => {
|
||||
return Err(format!(
|
||||
"operator {} is not supported on jsonb property '{}' (only $eq/$ne)",
|
||||
other, field_name
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if op == "$of" || op == "$nof" {
|
||||
let sql_op = if op == "$of" { "IN" } else { "NOT IN" };
|
||||
let subquery = format!(
|
||||
@ -830,6 +900,7 @@ impl<'a> Compiler<'a> {
|
||||
));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn compile_relation_conditions(
|
||||
@ -899,69 +970,4 @@ 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('\'', "''")
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
use crate::database::Database;
|
||||
use crate::drop::{Drop, Error, ErrorDetails};
|
||||
use indexmap::IndexMap;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub mod compiler;
|
||||
@ -33,7 +33,7 @@ impl Queryer {
|
||||
Err(msg) => {
|
||||
return Drop::with_errors(vec![Error {
|
||||
code: "FILTER_PARSE_FAILED".to_string(),
|
||||
values: Some(IndexMap::from([
|
||||
values: Some(HashMap::from([
|
||||
("error".to_string(), msg.clone()),
|
||||
])),
|
||||
details: ErrorDetails {
|
||||
@ -140,7 +140,7 @@ impl Queryer {
|
||||
}
|
||||
Err(e) => Err(Drop::with_errors(vec![Error {
|
||||
code: "QUERY_COMPILATION_FAILED".to_string(),
|
||||
values: Some(IndexMap::from([
|
||||
values: Some(HashMap::from([
|
||||
("error".to_string(), e.clone()),
|
||||
])),
|
||||
details: ErrorDetails {
|
||||
@ -169,7 +169,7 @@ impl Queryer {
|
||||
}
|
||||
Ok(other) => Drop::with_errors(vec![Error {
|
||||
code: "QUERY_FAILED".to_string(),
|
||||
values: Some(IndexMap::from([
|
||||
values: Some(HashMap::from([
|
||||
("error".to_string(), format!("Expected array from generic query, got: {:?}", other)),
|
||||
])),
|
||||
details: ErrorDetails {
|
||||
@ -181,7 +181,7 @@ impl Queryer {
|
||||
}]),
|
||||
Err(e) => Drop::with_errors(vec![Error {
|
||||
code: "QUERY_FAILED".to_string(),
|
||||
values: Some(IndexMap::from([
|
||||
values: Some(HashMap::from([
|
||||
("error".to_string(), e.to_string()),
|
||||
])),
|
||||
details: ErrorDetails {
|
||||
|
||||
@ -1302,9 +1302,15 @@ fn test_queryer_0_15() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_queryer_1_0() {
|
||||
fn test_queryer_0_16() {
|
||||
let path = format!("{}/fixtures/queryer.json", env!("CARGO_MANIFEST_DIR"));
|
||||
crate::tests::runner::run_test_case(&path, 1, 0).unwrap();
|
||||
crate::tests::runner::run_test_case(&path, 0, 16).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_queryer_0_17() {
|
||||
let path = format!("{}/fixtures/queryer.json", env!("CARGO_MANIFEST_DIR"));
|
||||
crate::tests::runner::run_test_case(&path, 0, 17).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@ -180,9 +180,6 @@ impl SqlFormatter {
|
||||
|
||||
fn format_query(&mut self, query: &Query) {
|
||||
self.format_set_expr(&query.body);
|
||||
if let Some(limit_clause) = &query.limit_clause {
|
||||
self.push_line(&format!("{}", limit_clause.to_string().trim()));
|
||||
}
|
||||
}
|
||||
|
||||
fn format_set_expr(&mut self, set_expr: &SetExpr) {
|
||||
@ -271,8 +268,7 @@ impl SqlFormatter {
|
||||
match &join.join_operator {
|
||||
JoinOperator::Inner(JoinConstraint::On(expr))
|
||||
| JoinOperator::Left(JoinConstraint::On(expr))
|
||||
| JoinOperator::Right(JoinConstraint::On(expr))
|
||||
| JoinOperator::Join(JoinConstraint::On(expr)) => {
|
||||
| JoinOperator::Right(JoinConstraint::On(expr)) => {
|
||||
self.push_str(" ON ");
|
||||
self.format_expr(expr);
|
||||
}
|
||||
@ -393,6 +389,7 @@ impl SqlFormatter {
|
||||
i += 2;
|
||||
}
|
||||
self.indent -= 2;
|
||||
self.format_function_clauses(list);
|
||||
self.push_line(")");
|
||||
} else {
|
||||
for (i, arg) in list.args.iter().enumerate() {
|
||||
@ -400,6 +397,7 @@ impl SqlFormatter {
|
||||
self.format_function_arg(arg);
|
||||
self.push_str(comma);
|
||||
}
|
||||
self.format_function_clauses(list);
|
||||
self.push_str(")");
|
||||
}
|
||||
} else {
|
||||
@ -407,6 +405,25 @@ impl SqlFormatter {
|
||||
}
|
||||
}
|
||||
|
||||
// Aggregate clauses (e.g. jsonb_agg(x ORDER BY y)) — without this the
|
||||
// snapshot silently drops the ORDER BY the compiler emits.
|
||||
fn format_function_clauses(&mut self, list: &sqlparser::ast::FunctionArgumentList) {
|
||||
for clause in &list.clauses {
|
||||
if let sqlparser::ast::FunctionArgumentClause::OrderBy(order) = clause {
|
||||
self.push_str(" ORDER BY ");
|
||||
for (i, ob) in order.iter().enumerate() {
|
||||
if i > 0 {
|
||||
self.push_str(", ");
|
||||
}
|
||||
self.format_expr(&ob.expr);
|
||||
if let Some(asc) = ob.options.asc {
|
||||
self.push_str(if asc { " ASC" } else { " DESC" });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn format_function_arg(&mut self, arg: &FunctionArg) {
|
||||
match arg {
|
||||
FunctionArg::Unnamed(sqlparser::ast::FunctionArgExpr::Expr(expr)) => self.format_expr(expr),
|
||||
|
||||
@ -259,25 +259,3 @@ 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();
|
||||
}
|
||||
|
||||
@ -57,9 +57,6 @@ 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)?;
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
use indexmap::IndexMap;
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
pub struct ValidationError {
|
||||
pub code: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub values: Option<IndexMap<String, String>>,
|
||||
pub values: Option<HashMap<String, String>>,
|
||||
pub path: String,
|
||||
}
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
use std::collections::HashSet;
|
||||
use indexmap::IndexMap;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
pub mod context;
|
||||
pub mod error;
|
||||
@ -93,7 +92,7 @@ impl Validator {
|
||||
} else {
|
||||
Drop::with_errors(vec![Error {
|
||||
code: "SCHEMA_NOT_FOUND".to_string(),
|
||||
values: Some(IndexMap::from([
|
||||
values: Some(HashMap::from([
|
||||
("schema".to_string(), schema_id.to_string()),
|
||||
])),
|
||||
details: ErrorDetails {
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
use std::collections::HashSet;
|
||||
use indexmap::IndexMap;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use serde_json::Value;
|
||||
|
||||
@ -19,7 +18,7 @@ impl<'a> ValidationContext<'a> {
|
||||
{
|
||||
result.errors.push(ValidationError {
|
||||
code: "MIN_ITEMS_VIOLATED".to_string(),
|
||||
values: Some(IndexMap::from([
|
||||
values: Some(HashMap::from([
|
||||
("count".to_string(), arr.len().to_string()),
|
||||
("limit".to_string(), min.to_string()),
|
||||
])),
|
||||
@ -31,7 +30,7 @@ impl<'a> ValidationContext<'a> {
|
||||
{
|
||||
result.errors.push(ValidationError {
|
||||
code: "MAX_ITEMS_VIOLATED".to_string(),
|
||||
values: Some(IndexMap::from([
|
||||
values: Some(HashMap::from([
|
||||
("count".to_string(), arr.len().to_string()),
|
||||
("limit".to_string(), max.to_string()),
|
||||
])),
|
||||
@ -78,7 +77,7 @@ impl<'a> ValidationContext<'a> {
|
||||
if _match_count < min {
|
||||
result.errors.push(ValidationError {
|
||||
code: "CONTAINS_VIOLATED".to_string(),
|
||||
values: Some(IndexMap::from([
|
||||
values: Some(HashMap::from([
|
||||
("count".to_string(), _match_count.to_string()),
|
||||
("limit".to_string(), min.to_string()),
|
||||
])),
|
||||
@ -90,7 +89,7 @@ impl<'a> ValidationContext<'a> {
|
||||
{
|
||||
result.errors.push(ValidationError {
|
||||
code: "CONTAINS_VIOLATED".to_string(),
|
||||
values: Some(IndexMap::from([
|
||||
values: Some(HashMap::from([
|
||||
("count".to_string(), _match_count.to_string()),
|
||||
("limit".to_string(), max.to_string()),
|
||||
])),
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
use indexmap::IndexMap;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::validator::Validator;
|
||||
use crate::validator::context::ValidationContext;
|
||||
@ -19,7 +19,7 @@ impl<'a> ValidationContext<'a> {
|
||||
if !Validator::check_type(t, current) {
|
||||
result.errors.push(ValidationError {
|
||||
code: "INVALID_TYPE".to_string(),
|
||||
values: Some(IndexMap::from([
|
||||
values: Some(HashMap::from([
|
||||
("expected".to_string(), t.to_string()),
|
||||
])),
|
||||
path: self.path.to_string(),
|
||||
@ -37,7 +37,7 @@ impl<'a> ValidationContext<'a> {
|
||||
if !valid {
|
||||
result.errors.push(ValidationError {
|
||||
code: "INVALID_TYPE".to_string(),
|
||||
values: Some(IndexMap::from([
|
||||
values: Some(HashMap::from([
|
||||
("expected".to_string(), format!("{:?}", types)),
|
||||
])),
|
||||
path: self.path.to_string(),
|
||||
@ -51,7 +51,7 @@ impl<'a> ValidationContext<'a> {
|
||||
if !equals(current, const_val) {
|
||||
result.errors.push(ValidationError {
|
||||
code: "CONST_VIOLATED".to_string(),
|
||||
values: Some(IndexMap::from([
|
||||
values: Some(HashMap::from([
|
||||
("expected".to_string(), format!("{:?}", const_val)),
|
||||
])),
|
||||
path: self.path.to_string(),
|
||||
@ -74,7 +74,7 @@ impl<'a> ValidationContext<'a> {
|
||||
if !found {
|
||||
result.errors.push(ValidationError {
|
||||
code: "ENUM_MISMATCH".to_string(),
|
||||
values: Some(IndexMap::from([
|
||||
values: Some(HashMap::from([
|
||||
("expected".to_string(), format!("{:?}", enum_vals)),
|
||||
])),
|
||||
path: self.path.to_string(),
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
use indexmap::IndexMap;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::database::object::{is_primitive_type, SchemaTypeOrArray};
|
||||
use crate::database::schema::Schema;
|
||||
@ -75,7 +75,7 @@ impl<'a> ValidationContext<'a> {
|
||||
if !result.evaluated_keys.contains(key) && !self.overrides.contains(key) {
|
||||
result.errors.push(ValidationError {
|
||||
code: "STRICT_PROPERTY_VIOLATION".to_string(),
|
||||
values: Some(IndexMap::from([
|
||||
values: Some(HashMap::from([
|
||||
("property_name".to_string(), key.to_string()),
|
||||
])),
|
||||
path: self.join_path(key),
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
use indexmap::IndexMap;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::validator::context::ValidationContext;
|
||||
use crate::validator::error::ValidationError;
|
||||
@ -21,7 +21,7 @@ impl<'a> ValidationContext<'a> {
|
||||
if should && let Err(e) = f(current) {
|
||||
result.errors.push(ValidationError {
|
||||
code: "FORMAT_MISMATCH".to_string(),
|
||||
values: Some(IndexMap::from([
|
||||
values: Some(HashMap::from([
|
||||
("format".to_string(), self.schema.format.clone().unwrap_or_default()),
|
||||
("error".to_string(), e.to_string()),
|
||||
])),
|
||||
@ -35,7 +35,7 @@ impl<'a> ValidationContext<'a> {
|
||||
{
|
||||
result.errors.push(ValidationError {
|
||||
code: "FORMAT_MISMATCH".to_string(),
|
||||
values: Some(IndexMap::from([
|
||||
values: Some(HashMap::from([
|
||||
("format".to_string(), self.schema.format.clone().unwrap_or_default()),
|
||||
])),
|
||||
path: self.path.to_string(),
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
use crate::validator::context::ValidationContext;
|
||||
use crate::validator::error::ValidationError;
|
||||
use crate::validator::result::ValidationResult;
|
||||
use indexmap::IndexMap;
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub mod array;
|
||||
pub mod cases;
|
||||
@ -64,7 +64,7 @@ impl<'a> ValidationContext<'a> {
|
||||
if self.depth > 100 {
|
||||
Err(ValidationError {
|
||||
code: "RECURSION_LIMIT_EXCEEDED".to_string(),
|
||||
values: Some(IndexMap::from([
|
||||
values: Some(HashMap::from([
|
||||
("limit".to_string(), 100.to_string()),
|
||||
])),
|
||||
path: self.path.to_string(),
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
use indexmap::IndexMap;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::validator::context::ValidationContext;
|
||||
use crate::validator::error::ValidationError;
|
||||
@ -16,7 +16,7 @@ impl<'a> ValidationContext<'a> {
|
||||
{
|
||||
result.errors.push(ValidationError {
|
||||
code: "MINIMUM_VIOLATED".to_string(),
|
||||
values: Some(IndexMap::from([
|
||||
values: Some(HashMap::from([
|
||||
("value".to_string(), num.to_string()),
|
||||
("limit".to_string(), min.to_string()),
|
||||
])),
|
||||
@ -28,7 +28,7 @@ impl<'a> ValidationContext<'a> {
|
||||
{
|
||||
result.errors.push(ValidationError {
|
||||
code: "MAXIMUM_VIOLATED".to_string(),
|
||||
values: Some(IndexMap::from([
|
||||
values: Some(HashMap::from([
|
||||
("value".to_string(), num.to_string()),
|
||||
("limit".to_string(), max.to_string()),
|
||||
])),
|
||||
@ -40,7 +40,7 @@ impl<'a> ValidationContext<'a> {
|
||||
{
|
||||
result.errors.push(ValidationError {
|
||||
code: "EXCLUSIVE_MINIMUM_VIOLATED".to_string(),
|
||||
values: Some(IndexMap::from([
|
||||
values: Some(HashMap::from([
|
||||
("value".to_string(), num.to_string()),
|
||||
("limit".to_string(), ex_min.to_string()),
|
||||
])),
|
||||
@ -52,7 +52,7 @@ impl<'a> ValidationContext<'a> {
|
||||
{
|
||||
result.errors.push(ValidationError {
|
||||
code: "EXCLUSIVE_MAXIMUM_VIOLATED".to_string(),
|
||||
values: Some(IndexMap::from([
|
||||
values: Some(HashMap::from([
|
||||
("value".to_string(), num.to_string()),
|
||||
("limit".to_string(), ex_max.to_string()),
|
||||
])),
|
||||
@ -64,7 +64,7 @@ impl<'a> ValidationContext<'a> {
|
||||
if (val - val.round()).abs() > f64::EPSILON {
|
||||
result.errors.push(ValidationError {
|
||||
code: "MULTIPLE_OF_VIOLATED".to_string(),
|
||||
values: Some(IndexMap::from([
|
||||
values: Some(HashMap::from([
|
||||
("value".to_string(), num.to_string()),
|
||||
("multiple_of".to_string(), multiple_of.to_string()),
|
||||
])),
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
use std::collections::HashSet;
|
||||
use indexmap::IndexMap;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use serde_json::Value;
|
||||
|
||||
@ -39,7 +38,7 @@ impl<'a> ValidationContext<'a> {
|
||||
} else {
|
||||
result.errors.push(ValidationError {
|
||||
code: "CONST_VIOLATED".to_string(), // Aligning with original const override errors natively
|
||||
values: Some(IndexMap::from([
|
||||
values: Some(HashMap::from([
|
||||
("value".to_string(), type_str.to_string()),
|
||||
])),
|
||||
path: self.join_path("type"),
|
||||
@ -50,7 +49,7 @@ impl<'a> ValidationContext<'a> {
|
||||
// Because it's a global entity target, the payload must structurally provide a discriminator natively
|
||||
result.errors.push(ValidationError {
|
||||
code: "MISSING_TYPE".to_string(),
|
||||
values: Some(IndexMap::from([
|
||||
values: Some(HashMap::from([
|
||||
("expected".to_string(), expected_type.to_string()),
|
||||
])),
|
||||
path: self.path.clone(), // Empty boundary
|
||||
@ -105,7 +104,7 @@ impl<'a> ValidationContext<'a> {
|
||||
{
|
||||
result.errors.push(ValidationError {
|
||||
code: "MIN_PROPERTIES_VIOLATED".to_string(),
|
||||
values: Some(IndexMap::from([
|
||||
values: Some(HashMap::from([
|
||||
("count".to_string(), obj.len().to_string()),
|
||||
("limit".to_string(), min.to_string()),
|
||||
])),
|
||||
@ -118,7 +117,7 @@ impl<'a> ValidationContext<'a> {
|
||||
{
|
||||
result.errors.push(ValidationError {
|
||||
code: "MAX_PROPERTIES_VIOLATED".to_string(),
|
||||
values: Some(IndexMap::from([
|
||||
values: Some(HashMap::from([
|
||||
("count".to_string(), obj.len().to_string()),
|
||||
("limit".to_string(), max.to_string()),
|
||||
])),
|
||||
@ -132,7 +131,7 @@ impl<'a> ValidationContext<'a> {
|
||||
if field == "type" {
|
||||
result.errors.push(ValidationError {
|
||||
code: "MISSING_TYPE".to_string(),
|
||||
values: Some(IndexMap::from([
|
||||
values: Some(HashMap::from([
|
||||
("field_name".to_string(), "type".to_string()),
|
||||
])),
|
||||
path: self.join_path(field),
|
||||
@ -140,7 +139,7 @@ impl<'a> ValidationContext<'a> {
|
||||
} else {
|
||||
result.errors.push(ValidationError {
|
||||
code: "REQUIRED_FIELD_MISSING".to_string(),
|
||||
values: Some(IndexMap::from([
|
||||
values: Some(HashMap::from([
|
||||
("field_name".to_string(), field.to_string()),
|
||||
])),
|
||||
path: self.join_path(field),
|
||||
@ -159,7 +158,7 @@ impl<'a> ValidationContext<'a> {
|
||||
if !obj.contains_key(req_prop) {
|
||||
result.errors.push(ValidationError {
|
||||
code: "DEPENDENCY_MISSING".to_string(),
|
||||
values: Some(IndexMap::from([
|
||||
values: Some(HashMap::from([
|
||||
("property_name".to_string(), prop.to_string()),
|
||||
("required_property".to_string(), req_prop.to_string()),
|
||||
])),
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::validator::context::ValidationContext;
|
||||
use crate::validator::error::ValidationError;
|
||||
use crate::validator::result::ValidationResult;
|
||||
@ -53,10 +55,10 @@ impl<'a> ValidationContext<'a> {
|
||||
return self.execute_polymorph(options, result);
|
||||
} else {
|
||||
result.errors.push(ValidationError {
|
||||
code: "UNCOMPILED_ONEOF".to_string(),
|
||||
values: None,
|
||||
path: self.path.to_string(),
|
||||
});
|
||||
code: "UNCOMPILED_ONEOF".to_string(),
|
||||
values: None,
|
||||
path: self.path.to_string(),
|
||||
});
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
@ -108,10 +110,9 @@ impl<'a> ValidationContext<'a> {
|
||||
} else {
|
||||
result.errors.push(ValidationError {
|
||||
code: "MISSING_COMPILED_SCHEMA".to_string(),
|
||||
values: Some(IndexMap::from([(
|
||||
"target_id".to_string(),
|
||||
target_id.to_string(),
|
||||
)])),
|
||||
values: Some(HashMap::from([
|
||||
("target_id".to_string(), target_id.to_string()),
|
||||
])),
|
||||
path: self.path.to_string(),
|
||||
});
|
||||
return Ok(false);
|
||||
@ -131,7 +132,9 @@ impl<'a> ValidationContext<'a> {
|
||||
} else {
|
||||
result.errors.push(ValidationError {
|
||||
code: "MISSING_COMPILED_SCHEMA".to_string(),
|
||||
values: Some(IndexMap::from([("index".to_string(), idx.to_string())])),
|
||||
values: Some(HashMap::from([
|
||||
("index".to_string(), idx.to_string()),
|
||||
])),
|
||||
path: self.path.to_string(),
|
||||
});
|
||||
return Ok(false);
|
||||
@ -141,12 +144,14 @@ impl<'a> ValidationContext<'a> {
|
||||
}
|
||||
} else {
|
||||
let values = if let Some(d) = self.schema.compiled_discriminator.get() {
|
||||
Some(IndexMap::from([
|
||||
Some(HashMap::from([
|
||||
("discriminator".to_string(), d.to_string()),
|
||||
("value".to_string(), val.to_string()),
|
||||
]))
|
||||
} else {
|
||||
Some(IndexMap::from([("primitive".to_string(), val.to_string())]))
|
||||
Some(HashMap::from([
|
||||
("primitive".to_string(), val.to_string()),
|
||||
]))
|
||||
};
|
||||
result.errors.push(ValidationError {
|
||||
code: if self.schema.family.is_some() {
|
||||
@ -163,10 +168,9 @@ impl<'a> ValidationContext<'a> {
|
||||
if let Some(d) = self.schema.compiled_discriminator.get() {
|
||||
result.errors.push(ValidationError {
|
||||
code: "MISSING_TYPE".to_string(),
|
||||
values: Some(IndexMap::from([(
|
||||
"discriminator".to_string(),
|
||||
d.to_string(),
|
||||
)])),
|
||||
values: Some(HashMap::from([
|
||||
("discriminator".to_string(), d.to_string()),
|
||||
])),
|
||||
path: self.path.to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
use indexmap::IndexMap;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::validator::context::ValidationContext;
|
||||
use crate::validator::error::ValidationError;
|
||||
@ -17,7 +17,7 @@ impl<'a> ValidationContext<'a> {
|
||||
{
|
||||
result.errors.push(ValidationError {
|
||||
code: "MIN_LENGTH_VIOLATED".to_string(),
|
||||
values: Some(IndexMap::from([
|
||||
values: Some(HashMap::from([
|
||||
("count".to_string(), s.chars().count().to_string()),
|
||||
("limit".to_string(), min.to_string()),
|
||||
])),
|
||||
@ -29,7 +29,7 @@ impl<'a> ValidationContext<'a> {
|
||||
{
|
||||
result.errors.push(ValidationError {
|
||||
code: "MAX_LENGTH_VIOLATED".to_string(),
|
||||
values: Some(IndexMap::from([
|
||||
values: Some(HashMap::from([
|
||||
("count".to_string(), s.chars().count().to_string()),
|
||||
("limit".to_string(), max.to_string()),
|
||||
])),
|
||||
@ -40,7 +40,7 @@ impl<'a> ValidationContext<'a> {
|
||||
if !compiled_re.0.is_match(s) {
|
||||
result.errors.push(ValidationError {
|
||||
code: "PATTERN_VIOLATED".to_string(),
|
||||
values: Some(IndexMap::from([
|
||||
values: Some(HashMap::from([
|
||||
("pattern".to_string(), self.schema.pattern.clone().unwrap_or_default()),
|
||||
("value".to_string(), s.to_string()),
|
||||
])),
|
||||
@ -53,7 +53,7 @@ impl<'a> ValidationContext<'a> {
|
||||
{
|
||||
result.errors.push(ValidationError {
|
||||
code: "PATTERN_VIOLATED".to_string(),
|
||||
values: Some(IndexMap::from([
|
||||
values: Some(HashMap::from([
|
||||
("pattern".to_string(), pattern.to_string()),
|
||||
("value".to_string(), s.to_string()),
|
||||
])),
|
||||
|
||||
@ -2,7 +2,7 @@ use crate::database::object::{is_primitive_type, SchemaTypeOrArray};
|
||||
use crate::validator::context::ValidationContext;
|
||||
use crate::validator::error::ValidationError;
|
||||
use crate::validator::result::ValidationResult;
|
||||
use indexmap::IndexMap;
|
||||
use std::collections::HashMap;
|
||||
|
||||
impl<'a> ValidationContext<'a> {
|
||||
pub(crate) fn validate_type(
|
||||
@ -76,7 +76,7 @@ impl<'a> ValidationContext<'a> {
|
||||
if !resolved {
|
||||
result.errors.push(ValidationError {
|
||||
code: "DYNAMIC_TYPE_RESOLUTION_FAILED".to_string(),
|
||||
values: Some(IndexMap::from([
|
||||
values: Some(HashMap::from([
|
||||
("pointer".to_string(), t.clone()),
|
||||
("discriminator".to_string(), var_name.to_string()),
|
||||
])),
|
||||
@ -109,7 +109,7 @@ impl<'a> ValidationContext<'a> {
|
||||
if t.starts_with('$') {
|
||||
result.errors.push(ValidationError {
|
||||
code: "DYNAMIC_TYPE_RESOLUTION_FAILED".to_string(),
|
||||
values: Some(IndexMap::from([
|
||||
values: Some(HashMap::from([
|
||||
("pointer".to_string(), target_id.to_string()),
|
||||
])),
|
||||
path: self.path.to_string(),
|
||||
@ -117,7 +117,7 @@ impl<'a> ValidationContext<'a> {
|
||||
} else if self.schema.is_proxy() {
|
||||
result.errors.push(ValidationError {
|
||||
code: "PROXY_TYPE_RESOLUTION_FAILED".to_string(),
|
||||
values: Some(IndexMap::from([
|
||||
values: Some(HashMap::from([
|
||||
("pointer".to_string(), target_id.to_string()),
|
||||
])),
|
||||
path: self.path.to_string(),
|
||||
@ -125,7 +125,7 @@ impl<'a> ValidationContext<'a> {
|
||||
} else {
|
||||
result.errors.push(ValidationError {
|
||||
code: "INHERITANCE_RESOLUTION_FAILED".to_string(),
|
||||
values: Some(IndexMap::from([
|
||||
values: Some(HashMap::from([
|
||||
("pointer".to_string(), target_id.to_string()),
|
||||
])),
|
||||
path: self.path.to_string(),
|
||||
|
||||
Reference in New Issue
Block a user