Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ea8c5febf2 | |||
| ea5770aebe | |||
| e25890461d | |||
| c4bf6bde58 | |||
| 1fedf5253c | |||
| 9ef59422b8 | |||
| b49b216b36 | |||
| 13b28387c2 |
12
GEMINI.md
12
GEMINI.md
@ -240,6 +240,7 @@ For example, given a general `attachment` table containing a `kind` column (e.g.
|
||||
"type": "attachment",
|
||||
"properties": {
|
||||
"kind": {
|
||||
"type": "string",
|
||||
"const": "cover"
|
||||
}
|
||||
}
|
||||
@ -252,10 +253,7 @@ A parent entity can then define a relationship using this constrained schema und
|
||||
"cover_attachment": {
|
||||
"properties": {
|
||||
"cover_attachment": {
|
||||
"type": [
|
||||
"cover.attachment",
|
||||
"null"
|
||||
]
|
||||
"type": "cover.attachment"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -316,13 +314,13 @@ The Merger provides an automated, high-performance graph synchronization engine.
|
||||
|
||||
### Core Features
|
||||
|
||||
* **Caching Strategy**: The Merger leverages the native `compiled_edges` permanently cached onto the Schema AST via `OnceLock` to instantly resolve Foreign Key mapping graphs natively in absolute `O(1)` time. It additionally utilizes the concurrent `GLOBAL_JSPG` application memory (`DashMap`) to cache statically constructed SQL `SELECT` strings used during deduplication (`lk_`) and difference tracking calculations.
|
||||
* **Caching Strategy**: The Merger leverages the native `compiled_edges` permanently cached onto the Schema AST via `OnceLock` to instantly resolve Foreign Key mapping graphs natively in absolute `O(1)` time. It additionally utilizes the concurrent `GLOBAL_JSPG` application memory (`DashMap`) to cache statically constructed SQL `SELECT` strings used during deduplication (`lookup_fields`) and difference tracking calculations.
|
||||
* **Deep Graph Merging**: The Merger walks arbitrary levels of deeply nested JSON schemas (e.g. tracking an `order`, its `customer`, and an array of its `lines`). It intelligently discovers the correct parent-to-child or child-to-parent Foreign Keys stored in the registry and automatically maps the UUIDs across the relationships during UPSERT.
|
||||
* **Prefix Foreign Key Matching**: Handles scenario where multiple relations point to the same table by using database Foreign Key constraint prefixes (`fk_`). For example, if a schema has `shipping_address` and `billing_address`, the merger resolves against `fk_shipping_address_entity` vs `fk_billing_address_entity` automatically to correctly route object properties.
|
||||
* **Dynamic Deduplication & Lookups**: If a nested object is provided without an `id`, the Merger utilizes Postgres `lk_` index constraints defined in the schema registry (e.g. `lk_person` mapped to `first_name` and `last_name`). It dynamically queries these unique matching constraints to discover the correct UUID to perform an UPDATE, preventing data duplication.
|
||||
* **Dynamic Deduplication & Lookups**: If a nested object is provided without an `id`, the Merger utilizes custom `lookup_fields` declared directly in the schema registry JSON comments. It validates at setup compile-time that a corresponding unique index exists in PostgreSQL for these fields. When merging, it dynamically builds query predicates for any satisfied `lookup_fields` sets in the entity's type hierarchy (checking child-to-parent hierarchies order-independently and combining satisfied keys with `UNION` queries) to discover the correct UUID to perform an UPDATE, preventing data duplication.
|
||||
* **Hierarchical Table Inheritance**: The Punc system uses distributed table inheritance (e.g. `person` inherits `user` inherits `organization` inherits `entity`). The Merger splits the incoming JSON payload and performs atomic row updates across *all* relevant tables in the lineage map.
|
||||
* **The Archive Paradigm**: Data is never deleted in the Punc system. The Merger securely enforces referential integrity by toggling the `archived` Boolean flag on the base `entity` table rather than issuing SQL `DELETE` commands.
|
||||
* **Change Tracking & Reactivity**: The Merger diffs the incoming JSON against the existing database row (utilizing static, `DashMap`-cached `lk_` SELECT string templates). Every detected change is recorded into the `agreego.change` audit table, tracking the user mapping. It then natively uses `pg_notify` to broadcast a completely flat row-level diff out to the Go WebSocket server for O(1) routing.
|
||||
* **Change Tracking & Reactivity**: The Merger diffs the incoming JSON against the existing database row (utilizing static, `DashMap`-cached `lookup` SELECT string templates). Every detected change is recorded into the `agreego.change` audit table, tracking the user mapping. It then natively uses `pg_notify` to broadcast a completely flat row-level diff out to the Go WebSocket server for O(1) routing.
|
||||
* **Flat Structural Beats (Unidirectional Flow)**: The Merger purposefully DOES NOT trace or hydrate outbound Foreign Keys or nested parent structures during writes. It emits completely flat, mathematically perfect structural deltas via `pg_notify` representing only the exact Postgres rows that changed. This guarantees the write-path remains O(1) lightning fast. It is the strict responsibility of the upstream Punc Framework (the Go `Speaker`) to intercept these flat beats, evaluate them against active Websocket Schema Topologies, and dynamically issue targeted `jspg_query` reads to hydrate the exact contextual subgraphs required by listening clients.
|
||||
* **Pre-Order Notification Traversal**: To support proper topological hydration on the upstream Go Framework, the Merger decouples the `pg_notify` execution from the physical database write execution. The engine collects structural changes and explicitly fires `pg_notify` SQL statements in strict **Pre-Order** (Parent -> Relations -> Children). This guarantees that WebSocket clients receive the parent entity `Beat` prior to any nested child entities, ensuring stable unidirectional data flows without hydration race conditions.
|
||||
* **Many-to-Many Graph Edge Management**: Operates seamlessly with the global `agreego.relationship` table, allowing the system to represent and merge arbitrary reified M:M relationships directionally between any two entities.
|
||||
|
||||
@ -227,8 +227,8 @@
|
||||
{
|
||||
"code": "CONTAINS_VIOLATED",
|
||||
"values": {
|
||||
"limit": "1",
|
||||
"count": "0"
|
||||
"count": "0",
|
||||
"limit": "1"
|
||||
},
|
||||
"details": {
|
||||
"path": "",
|
||||
@ -268,8 +268,8 @@
|
||||
{
|
||||
"code": "CONTAINS_VIOLATED",
|
||||
"values": {
|
||||
"limit": "1",
|
||||
"count": "0"
|
||||
"count": "0",
|
||||
"limit": "1"
|
||||
},
|
||||
"details": {
|
||||
"path": "",
|
||||
@ -347,8 +347,8 @@
|
||||
{
|
||||
"code": "CONTAINS_VIOLATED",
|
||||
"values": {
|
||||
"limit": "1",
|
||||
"count": "0"
|
||||
"count": "0",
|
||||
"limit": "1"
|
||||
},
|
||||
"details": {
|
||||
"path": "",
|
||||
@ -373,8 +373,8 @@
|
||||
{
|
||||
"code": "MULTIPLE_OF_VIOLATED",
|
||||
"values": {
|
||||
"multiple_of": "2",
|
||||
"value": "3"
|
||||
"value": "3",
|
||||
"multiple_of": "2"
|
||||
},
|
||||
"details": {
|
||||
"path": "0",
|
||||
@ -384,8 +384,8 @@
|
||||
{
|
||||
"code": "MULTIPLE_OF_VIOLATED",
|
||||
"values": {
|
||||
"multiple_of": "2",
|
||||
"value": "9"
|
||||
"value": "9",
|
||||
"multiple_of": "2"
|
||||
},
|
||||
"details": {
|
||||
"path": "2",
|
||||
@ -432,8 +432,8 @@
|
||||
{
|
||||
"code": "MULTIPLE_OF_VIOLATED",
|
||||
"values": {
|
||||
"multiple_of": "2",
|
||||
"value": "1"
|
||||
"value": "1",
|
||||
"multiple_of": "2"
|
||||
},
|
||||
"details": {
|
||||
"path": "0",
|
||||
|
||||
@ -62,8 +62,8 @@
|
||||
"code": "EDGE_MISSING",
|
||||
"values": {
|
||||
"parent_type": "org",
|
||||
"property_name": "missing_users",
|
||||
"child_type": "user"
|
||||
"child_type": "user",
|
||||
"property_name": "missing_users"
|
||||
},
|
||||
"details": {
|
||||
"path": "full.org/missing_users",
|
||||
@ -151,8 +151,8 @@
|
||||
{
|
||||
"code": "EDGE_MISSING",
|
||||
"values": {
|
||||
"child_type": "child",
|
||||
"parent_type": "parent",
|
||||
"child_type": "child",
|
||||
"property_name": "children"
|
||||
},
|
||||
"details": {
|
||||
@ -470,8 +470,8 @@
|
||||
{
|
||||
"code": "DATABASE_TYPE_PARSE_FAILED",
|
||||
"values": {
|
||||
"reason": "invalid type: sequence, expected a string",
|
||||
"type": "failure"
|
||||
"type": "failure",
|
||||
"reason": "invalid type: sequence, expected a string"
|
||||
},
|
||||
"details": {
|
||||
"context": "failure"
|
||||
@ -911,9 +911,7 @@
|
||||
"archived"
|
||||
]
|
||||
},
|
||||
"lookup_fields": [
|
||||
"id"
|
||||
],
|
||||
"lookup_fields": [],
|
||||
"historical": true,
|
||||
"relationship": false,
|
||||
"field_types": {
|
||||
@ -1018,5 +1016,100 @@
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Validation - lookup fields without matching index",
|
||||
"database": {
|
||||
"types": [
|
||||
{
|
||||
"id": "22222222-2222-2222-2222-222222222222",
|
||||
"type": "type",
|
||||
"name": "user",
|
||||
"module": "test",
|
||||
"source": "test",
|
||||
"hierarchy": ["user"],
|
||||
"variations": ["user"],
|
||||
"fields": ["id", "email"],
|
||||
"lookup_fields": ["email"],
|
||||
"schemas": {
|
||||
"user": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": { "type": "string", "format": "uuid" },
|
||||
"email": { "type": "string", "format": "email" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "Fails setup compilation with MISSING_LOOKUP_INDEX if unique index is missing",
|
||||
"action": "compile",
|
||||
"expect": {
|
||||
"success": false,
|
||||
"errors": [
|
||||
{
|
||||
"code": "MISSING_LOOKUP_INDEX",
|
||||
"values": {
|
||||
"type": "user",
|
||||
"lookup_fields": "[\"email\"]"
|
||||
},
|
||||
"details": {
|
||||
"path": "/types/user",
|
||||
"schema": "user"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Validation - lookup fields with matching index",
|
||||
"database": {
|
||||
"indexes": [
|
||||
{
|
||||
"table": "user",
|
||||
"columns": ["email"]
|
||||
}
|
||||
],
|
||||
"types": [
|
||||
{
|
||||
"id": "22222222-2222-2222-2222-222222222222",
|
||||
"type": "type",
|
||||
"name": "user",
|
||||
"module": "test",
|
||||
"source": "test",
|
||||
"hierarchy": ["user"],
|
||||
"variations": ["user"],
|
||||
"fields": ["id", "email"],
|
||||
"lookup_fields": ["email"],
|
||||
"schemas": {
|
||||
"user": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": { "type": "string", "format": "uuid" },
|
||||
"email": { "type": "string", "format": "email" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"description": "Compiles successfully when unique index matches lookup_fields",
|
||||
"action": "compile",
|
||||
"expect": {
|
||||
"success": true,
|
||||
"schemas": {
|
||||
"user": {},
|
||||
"user.filter": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@ -227,8 +227,8 @@
|
||||
{
|
||||
"code": "DEPENDENCY_MISSING",
|
||||
"values": {
|
||||
"required_property": "bar",
|
||||
"property_name": "quux"
|
||||
"property_name": "quux",
|
||||
"required_property": "bar"
|
||||
},
|
||||
"details": {
|
||||
"path": "",
|
||||
@ -386,8 +386,8 @@
|
||||
{
|
||||
"code": "DEPENDENCY_MISSING",
|
||||
"values": {
|
||||
"required_property": "foo'bar",
|
||||
"property_name": "foo\"bar"
|
||||
"property_name": "foo\"bar",
|
||||
"required_property": "foo'bar"
|
||||
},
|
||||
"details": {
|
||||
"path": "",
|
||||
@ -829,8 +829,8 @@
|
||||
{
|
||||
"code": "MIN_PROPERTIES_VIOLATED",
|
||||
"values": {
|
||||
"limit": "4",
|
||||
"count": "2"
|
||||
"count": "2",
|
||||
"limit": "4"
|
||||
},
|
||||
"details": {
|
||||
"path": "",
|
||||
|
||||
@ -34,8 +34,8 @@
|
||||
{
|
||||
"code": "EXCLUSIVE_MAXIMUM_VIOLATED",
|
||||
"values": {
|
||||
"limit": "3",
|
||||
"value": "3"
|
||||
"value": "3",
|
||||
"limit": "3"
|
||||
},
|
||||
"details": {
|
||||
"path": "",
|
||||
|
||||
@ -34,8 +34,8 @@
|
||||
{
|
||||
"code": "EXCLUSIVE_MINIMUM_VIOLATED",
|
||||
"values": {
|
||||
"limit": "1.1",
|
||||
"value": "1.1"
|
||||
"value": "1.1",
|
||||
"limit": "1.1"
|
||||
},
|
||||
"details": {
|
||||
"path": "",
|
||||
|
||||
@ -211,13 +211,6 @@
|
||||
"gender": {},
|
||||
"gender.condition": {
|
||||
"type": "condition",
|
||||
"compiledPropertyNames": [
|
||||
"kind",
|
||||
"$eq",
|
||||
"$ne",
|
||||
"$of",
|
||||
"$nof"
|
||||
],
|
||||
"properties": {
|
||||
"$eq": {
|
||||
"type": [
|
||||
@ -231,15 +224,6 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"$nof": {
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"type": "gender"
|
||||
}
|
||||
},
|
||||
"$of": {
|
||||
"type": [
|
||||
"array",
|
||||
@ -248,80 +232,32 @@
|
||||
"items": {
|
||||
"type": "gender"
|
||||
}
|
||||
},
|
||||
"$nof": {
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"type": "gender"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"compiledPropertyNames": [
|
||||
"kind",
|
||||
"$eq",
|
||||
"$ne",
|
||||
"$of",
|
||||
"$nof"
|
||||
]
|
||||
},
|
||||
"person": {},
|
||||
"person.filter": {
|
||||
"compiledPropertyNames": [
|
||||
"first_name",
|
||||
"age",
|
||||
"billing_address",
|
||||
"gender",
|
||||
"birth_date",
|
||||
"uuid_field",
|
||||
"tags",
|
||||
"ad_hoc",
|
||||
"$and",
|
||||
"$or"
|
||||
],
|
||||
"type": "filter",
|
||||
"properties": {
|
||||
"$and": {
|
||||
"items": {
|
||||
"compiledPropertyNames": [
|
||||
"first_name",
|
||||
"age",
|
||||
"billing_address",
|
||||
"gender",
|
||||
"birth_date",
|
||||
"uuid_field",
|
||||
"tags",
|
||||
"ad_hoc",
|
||||
"$and",
|
||||
"$or"
|
||||
],
|
||||
"type": "person.filter"
|
||||
},
|
||||
"first_name": {
|
||||
"type": [
|
||||
"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",
|
||||
"string.condition",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
@ -337,6 +273,12 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"gender": {
|
||||
"type": [
|
||||
"gender.condition",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"birth_date": {
|
||||
"type": [
|
||||
"date.condition",
|
||||
@ -349,48 +291,48 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"first_name": {
|
||||
"type": [
|
||||
"string.condition",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"gender": {
|
||||
"type": [
|
||||
"gender.condition",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"tags": {
|
||||
"type": [
|
||||
"string.condition",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "filter"
|
||||
},
|
||||
"address": {},
|
||||
"address.filter": {
|
||||
"type": "filter",
|
||||
"compiledPropertyNames": [
|
||||
"city",
|
||||
"$and",
|
||||
"$or"
|
||||
],
|
||||
"properties": {
|
||||
},
|
||||
"ad_hoc": {
|
||||
"type": [
|
||||
"filter",
|
||||
"null"
|
||||
],
|
||||
"properties": {
|
||||
"foo": {
|
||||
"type": [
|
||||
"string.condition",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"compiledPropertyNames": [
|
||||
"foo"
|
||||
]
|
||||
},
|
||||
"$and": {
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"type": "person.filter",
|
||||
"compiledPropertyNames": [
|
||||
"city",
|
||||
"first_name",
|
||||
"age",
|
||||
"billing_address",
|
||||
"gender",
|
||||
"birth_date",
|
||||
"uuid_field",
|
||||
"tags",
|
||||
"ad_hoc",
|
||||
"$and",
|
||||
"$or"
|
||||
],
|
||||
"type": "address.filter"
|
||||
]
|
||||
}
|
||||
},
|
||||
"$or": {
|
||||
@ -399,21 +341,79 @@
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"type": "person.filter",
|
||||
"compiledPropertyNames": [
|
||||
"city",
|
||||
"first_name",
|
||||
"age",
|
||||
"billing_address",
|
||||
"gender",
|
||||
"birth_date",
|
||||
"uuid_field",
|
||||
"tags",
|
||||
"ad_hoc",
|
||||
"$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,52 +424,7 @@
|
||||
"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",
|
||||
@ -481,8 +436,53 @@
|
||||
"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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -106,8 +106,8 @@
|
||||
{
|
||||
"code": "CONTAINS_VIOLATED",
|
||||
"values": {
|
||||
"limit": "1",
|
||||
"count": "2"
|
||||
"count": "2",
|
||||
"limit": "1"
|
||||
},
|
||||
"details": {
|
||||
"path": "",
|
||||
@ -144,8 +144,8 @@
|
||||
{
|
||||
"code": "CONTAINS_VIOLATED",
|
||||
"values": {
|
||||
"limit": "1",
|
||||
"count": "2"
|
||||
"count": "2",
|
||||
"limit": "1"
|
||||
},
|
||||
"details": {
|
||||
"path": "",
|
||||
@ -284,8 +284,8 @@
|
||||
{
|
||||
"code": "CONTAINS_VIOLATED",
|
||||
"values": {
|
||||
"limit": "3",
|
||||
"count": "4"
|
||||
"count": "4",
|
||||
"limit": "3"
|
||||
},
|
||||
"details": {
|
||||
"path": "",
|
||||
|
||||
@ -52,8 +52,8 @@
|
||||
{
|
||||
"code": "MAX_ITEMS_VIOLATED",
|
||||
"values": {
|
||||
"limit": "2",
|
||||
"count": "3"
|
||||
"count": "3",
|
||||
"limit": "2"
|
||||
},
|
||||
"details": {
|
||||
"path": "",
|
||||
|
||||
@ -43,8 +43,8 @@
|
||||
{
|
||||
"code": "MAX_LENGTH_VIOLATED",
|
||||
"values": {
|
||||
"limit": "2",
|
||||
"count": "3"
|
||||
"count": "3",
|
||||
"limit": "2"
|
||||
},
|
||||
"details": {
|
||||
"path": "",
|
||||
@ -109,8 +109,8 @@
|
||||
{
|
||||
"code": "MAX_LENGTH_VIOLATED",
|
||||
"values": {
|
||||
"limit": "2",
|
||||
"count": "3"
|
||||
"count": "3",
|
||||
"limit": "2"
|
||||
},
|
||||
"details": {
|
||||
"path": "",
|
||||
|
||||
@ -139,8 +139,8 @@
|
||||
{
|
||||
"code": "MAX_PROPERTIES_VIOLATED",
|
||||
"values": {
|
||||
"limit": "2",
|
||||
"count": "3"
|
||||
"count": "3",
|
||||
"limit": "2"
|
||||
},
|
||||
"details": {
|
||||
"path": "",
|
||||
@ -190,8 +190,8 @@
|
||||
{
|
||||
"code": "MAX_PROPERTIES_VIOLATED",
|
||||
"values": {
|
||||
"limit": "0",
|
||||
"count": "1"
|
||||
"count": "1",
|
||||
"limit": "0"
|
||||
},
|
||||
"details": {
|
||||
"path": "",
|
||||
|
||||
@ -118,8 +118,8 @@
|
||||
{
|
||||
"code": "MAXIMUM_VIOLATED",
|
||||
"values": {
|
||||
"limit": "300",
|
||||
"value": "300.5"
|
||||
"value": "300.5",
|
||||
"limit": "300"
|
||||
},
|
||||
"details": {
|
||||
"path": "",
|
||||
|
||||
@ -239,8 +239,8 @@
|
||||
{
|
||||
"code": "DEPENDENCY_MISSING",
|
||||
"values": {
|
||||
"required_property": "base_dep",
|
||||
"property_name": "trigger"
|
||||
"property_name": "trigger",
|
||||
"required_property": "base_dep"
|
||||
},
|
||||
"details": {
|
||||
"path": "",
|
||||
|
||||
@ -117,6 +117,16 @@
|
||||
]
|
||||
}
|
||||
],
|
||||
"indexes": [
|
||||
{
|
||||
"table": "person",
|
||||
"columns": ["first_name", "last_name", "date_of_birth", "pronouns"]
|
||||
},
|
||||
{
|
||||
"table": "user",
|
||||
"columns": ["name"]
|
||||
}
|
||||
],
|
||||
"types": [
|
||||
{
|
||||
"name": "entity",
|
||||
@ -296,7 +306,9 @@
|
||||
"archived"
|
||||
]
|
||||
},
|
||||
"lookup_fields": [],
|
||||
"lookup_fields": [
|
||||
"name"
|
||||
],
|
||||
"historical": true,
|
||||
"notify": true,
|
||||
"relationship": false
|
||||
@ -478,9 +490,7 @@
|
||||
"organization_id"
|
||||
]
|
||||
},
|
||||
"lookup_fields": [
|
||||
"id"
|
||||
],
|
||||
"lookup_fields": [],
|
||||
"historical": true,
|
||||
"notify": true,
|
||||
"relationship": false
|
||||
@ -1068,9 +1078,7 @@
|
||||
"archived"
|
||||
]
|
||||
},
|
||||
"lookup_fields": [
|
||||
"id"
|
||||
],
|
||||
"lookup_fields": [],
|
||||
"historical": true,
|
||||
"relationship": false,
|
||||
"field_types": {
|
||||
@ -1593,6 +1601,180 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "Update existing person with id and multiple inherited lookup keys",
|
||||
"action": "merge",
|
||||
"data": {
|
||||
"id": "33333333-3333-3333-3333-333333333333",
|
||||
"type": "person",
|
||||
"name": "LookupName",
|
||||
"first_name": "LookupFirst",
|
||||
"last_name": "LookupLast",
|
||||
"date_of_birth": "1990-01-01T00:00:00Z",
|
||||
"pronouns": "they/them",
|
||||
"contact_id": "abc-contact"
|
||||
},
|
||||
"mocks": [
|
||||
{
|
||||
"id": "22222222-2222-2222-2222-222222222222",
|
||||
"type": "person",
|
||||
"name": "LookupName",
|
||||
"first_name": "LookupFirst",
|
||||
"last_name": "LookupLast",
|
||||
"date_of_birth": "1990-01-01T00:00:00Z",
|
||||
"pronouns": "they/them",
|
||||
"contact_id": "old-contact"
|
||||
}
|
||||
],
|
||||
"schema_id": "person",
|
||||
"expect": {
|
||||
"success": true,
|
||||
"sql": [
|
||||
[
|
||||
"(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",
|
||||
" t1.id = '{{uuid:data.id}}'",
|
||||
"UNION SELECT to_jsonb(t1.*) || to_jsonb(t2.*) || to_jsonb(t3.*) || to_jsonb(t4.*)",
|
||||
"FROM agreego.\"entity\" t1",
|
||||
"LEFT JOIN agreego.\"organization\" t2 ON t2.id = t1.id",
|
||||
"LEFT JOIN agreego.\"user\" t3 ON t3.id = t1.id",
|
||||
"LEFT JOIN agreego.\"person\" t4 ON t4.id = t1.id",
|
||||
"WHERE",
|
||||
" (\"first_name\" = 'LookupFirst'",
|
||||
" AND \"last_name\" = 'LookupLast'",
|
||||
" AND \"date_of_birth\" = '{{timestamp}}'",
|
||||
" AND \"pronouns\" = 'they/them')",
|
||||
"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",
|
||||
" (\"name\" = 'LookupName'))"
|
||||
],
|
||||
[
|
||||
"INSERT INTO agreego.\"entity\" (",
|
||||
" \"created_at\",",
|
||||
" \"created_by\",",
|
||||
" \"id\",",
|
||||
" \"modified_at\",",
|
||||
" \"modified_by\",",
|
||||
" \"type\"",
|
||||
")",
|
||||
"VALUES (",
|
||||
" '{{timestamp}}',",
|
||||
" '00000000-0000-0000-0000-000000000000',",
|
||||
" '{{uuid:data.id}}',",
|
||||
" '{{timestamp}}',",
|
||||
" '00000000-0000-0000-0000-000000000000',",
|
||||
" 'person'",
|
||||
")"
|
||||
],
|
||||
[
|
||||
"INSERT INTO agreego.\"organization\" (",
|
||||
" \"id\",",
|
||||
" \"name\",",
|
||||
" \"type\"",
|
||||
")",
|
||||
"VALUES (",
|
||||
" '{{uuid:data.id}}',",
|
||||
" 'LookupName',",
|
||||
" 'person'",
|
||||
")"
|
||||
],
|
||||
[
|
||||
"INSERT INTO agreego.\"user\" (",
|
||||
" \"id\",",
|
||||
" \"type\"",
|
||||
")",
|
||||
"VALUES (",
|
||||
" '{{uuid:data.id}}',",
|
||||
" 'person'",
|
||||
")"
|
||||
],
|
||||
[
|
||||
"INSERT INTO agreego.\"person\" (",
|
||||
" \"contact_id\",",
|
||||
" \"date_of_birth\",",
|
||||
" \"first_name\",",
|
||||
" \"id\",",
|
||||
" \"last_name\",",
|
||||
" \"pronouns\",",
|
||||
" \"type\"",
|
||||
")",
|
||||
"VALUES (",
|
||||
" 'abc-contact',",
|
||||
" '{{timestamp}}',",
|
||||
" 'LookupFirst',",
|
||||
" '{{uuid:data.id}}',",
|
||||
" 'LookupLast',",
|
||||
" 'they/them',",
|
||||
" 'person'",
|
||||
")"
|
||||
],
|
||||
[
|
||||
"INSERT INTO agreego.change (",
|
||||
" \"old\",",
|
||||
" \"new\",",
|
||||
" \"entity_id\",",
|
||||
" \"id\",",
|
||||
" \"kind\",",
|
||||
" \"modified_at\",",
|
||||
" \"modified_by\"",
|
||||
")",
|
||||
"VALUES (",
|
||||
" NULL,",
|
||||
" '{",
|
||||
" \"name\": \"LookupName\",",
|
||||
" \"first_name\": \"LookupFirst\",",
|
||||
" \"last_name\": \"LookupLast\",",
|
||||
" \"date_of_birth\": \"{{timestamp}}\",",
|
||||
" \"pronouns\": \"they/them\",",
|
||||
" \"contact_id\": \"abc-contact\",",
|
||||
" \"type\": \"person\"",
|
||||
" }',",
|
||||
" '{{uuid:data.id}}',",
|
||||
" '{{uuid:generated_0}}',",
|
||||
" 'create',",
|
||||
" '{{timestamp}}',",
|
||||
" '00000000-0000-0000-0000-000000000000'",
|
||||
")"
|
||||
],
|
||||
[
|
||||
"(SELECT pg_notify('entity', '{",
|
||||
" \"kind\": \"create\",",
|
||||
" \"complete\": {",
|
||||
" \"name\": \"LookupName\",",
|
||||
" \"first_name\": \"LookupFirst\",",
|
||||
" \"last_name\": \"LookupLast\",",
|
||||
" \"date_of_birth\": \"{{timestamp}}\",",
|
||||
" \"pronouns\": \"they/them\",",
|
||||
" \"contact_id\": \"abc-contact\",",
|
||||
" \"id\": \"{{uuid:data.id}}\",",
|
||||
" \"type\": \"person\",",
|
||||
" \"created_by\": \"00000000-0000-0000-0000-000000000000\",",
|
||||
" \"created_at\": \"{{timestamp}}\",",
|
||||
" \"modified_by\": \"00000000-0000-0000-0000-000000000000\",",
|
||||
" \"modified_at\": \"{{timestamp}}\"",
|
||||
" },",
|
||||
" \"new\": {",
|
||||
" \"name\": \"LookupName\",",
|
||||
" \"first_name\": \"LookupFirst\",",
|
||||
" \"last_name\": \"LookupLast\",",
|
||||
" \"date_of_birth\": \"{{timestamp}}\",",
|
||||
" \"pronouns\": \"they/them\",",
|
||||
" \"contact_id\": \"abc-contact\",",
|
||||
" \"type\": \"person\"",
|
||||
" }",
|
||||
"}'))"
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "Replace existing person with id and no changes (lookup)",
|
||||
"action": "merge",
|
||||
@ -2235,12 +2417,7 @@
|
||||
"FROM agreego.\"entity\" t1",
|
||||
"LEFT JOIN agreego.\"order\" t2 ON t2.id = t1.id",
|
||||
"WHERE",
|
||||
" t1.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'))"
|
||||
" t1.id = 'abc')"
|
||||
],
|
||||
[
|
||||
"INSERT INTO agreego.\"entity\" (",
|
||||
@ -3519,12 +3696,7 @@
|
||||
"FROM agreego.\"entity\" t1",
|
||||
"LEFT JOIN agreego.\"invoice\" t2 ON t2.id = t1.id",
|
||||
"WHERE",
|
||||
" t1.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}}'))"
|
||||
" t1.id = '{{uuid:data.id}}')"
|
||||
],
|
||||
[
|
||||
"INSERT INTO agreego.\"entity\" (",
|
||||
|
||||
@ -169,8 +169,8 @@
|
||||
{
|
||||
"code": "CONTAINS_VIOLATED",
|
||||
"values": {
|
||||
"limit": "2",
|
||||
"count": "0"
|
||||
"count": "0",
|
||||
"limit": "2"
|
||||
},
|
||||
"details": {
|
||||
"path": "",
|
||||
@ -357,8 +357,8 @@
|
||||
{
|
||||
"code": "CONTAINS_VIOLATED",
|
||||
"values": {
|
||||
"limit": "2",
|
||||
"count": "0"
|
||||
"count": "0",
|
||||
"limit": "2"
|
||||
},
|
||||
"details": {
|
||||
"path": "",
|
||||
@ -463,8 +463,8 @@
|
||||
{
|
||||
"code": "CONTAINS_VIOLATED",
|
||||
"values": {
|
||||
"limit": "3",
|
||||
"count": "0"
|
||||
"count": "0",
|
||||
"limit": "3"
|
||||
},
|
||||
"details": {
|
||||
"path": "",
|
||||
@ -487,8 +487,8 @@
|
||||
{
|
||||
"code": "CONTAINS_VIOLATED",
|
||||
"values": {
|
||||
"limit": "3",
|
||||
"count": "1"
|
||||
"count": "1",
|
||||
"limit": "3"
|
||||
},
|
||||
"details": {
|
||||
"path": "",
|
||||
@ -513,8 +513,8 @@
|
||||
{
|
||||
"code": "CONTAINS_VIOLATED",
|
||||
"values": {
|
||||
"limit": "1",
|
||||
"count": "3"
|
||||
"count": "3",
|
||||
"limit": "1"
|
||||
},
|
||||
"details": {
|
||||
"path": "",
|
||||
@ -657,8 +657,8 @@
|
||||
{
|
||||
"code": "CONTAINS_VIOLATED",
|
||||
"values": {
|
||||
"limit": "1",
|
||||
"count": "2"
|
||||
"count": "2",
|
||||
"limit": "1"
|
||||
},
|
||||
"details": {
|
||||
"path": "",
|
||||
|
||||
@ -108,8 +108,8 @@
|
||||
{
|
||||
"code": "MIN_ITEMS_VIOLATED",
|
||||
"values": {
|
||||
"limit": "1",
|
||||
"count": "0"
|
||||
"count": "0",
|
||||
"limit": "1"
|
||||
},
|
||||
"details": {
|
||||
"path": "",
|
||||
|
||||
@ -43,8 +43,8 @@
|
||||
{
|
||||
"code": "MIN_LENGTH_VIOLATED",
|
||||
"values": {
|
||||
"limit": "2",
|
||||
"count": "1"
|
||||
"count": "1",
|
||||
"limit": "2"
|
||||
},
|
||||
"details": {
|
||||
"path": "",
|
||||
|
||||
@ -43,8 +43,8 @@
|
||||
{
|
||||
"code": "MINIMUM_VIOLATED",
|
||||
"values": {
|
||||
"limit": "1.1",
|
||||
"value": "0.6"
|
||||
"value": "0.6",
|
||||
"limit": "1.1"
|
||||
},
|
||||
"details": {
|
||||
"path": "",
|
||||
@ -127,8 +127,8 @@
|
||||
{
|
||||
"code": "MINIMUM_VIOLATED",
|
||||
"values": {
|
||||
"limit": "-2",
|
||||
"value": "-2.0001"
|
||||
"value": "-2.0001",
|
||||
"limit": "-2"
|
||||
},
|
||||
"details": {
|
||||
"path": "",
|
||||
@ -149,8 +149,8 @@
|
||||
{
|
||||
"code": "MINIMUM_VIOLATED",
|
||||
"values": {
|
||||
"limit": "-2",
|
||||
"value": "-3"
|
||||
"value": "-3",
|
||||
"limit": "-2"
|
||||
},
|
||||
"details": {
|
||||
"path": "",
|
||||
|
||||
@ -148,8 +148,8 @@
|
||||
{
|
||||
"code": "MULTIPLE_OF_VIOLATED",
|
||||
"values": {
|
||||
"multiple_of": "0.0001",
|
||||
"value": "0.00751"
|
||||
"value": "0.00751",
|
||||
"multiple_of": "0.0001"
|
||||
},
|
||||
"details": {
|
||||
"path": "",
|
||||
|
||||
@ -156,8 +156,8 @@
|
||||
{
|
||||
"code": "MAXIMUM_VIOLATED",
|
||||
"values": {
|
||||
"limit": "50",
|
||||
"value": "60"
|
||||
"value": "60",
|
||||
"limit": "50"
|
||||
},
|
||||
"details": {
|
||||
"path": "max",
|
||||
|
||||
@ -200,8 +200,8 @@
|
||||
{
|
||||
"code": "MINIMUM_VIOLATED",
|
||||
"values": {
|
||||
"limit": "10",
|
||||
"value": "5"
|
||||
"value": "5",
|
||||
"limit": "10"
|
||||
},
|
||||
"details": {
|
||||
"path": "entities/entity-beta/value",
|
||||
|
||||
@ -34,8 +34,8 @@
|
||||
{
|
||||
"code": "PATTERN_VIOLATED",
|
||||
"values": {
|
||||
"value": "abc",
|
||||
"pattern": "^a*$"
|
||||
"pattern": "^a*$",
|
||||
"value": "abc"
|
||||
},
|
||||
"details": {
|
||||
"path": "",
|
||||
|
||||
@ -148,8 +148,8 @@
|
||||
{
|
||||
"code": "NO_FAMILY_MATCH",
|
||||
"values": {
|
||||
"value": "alien",
|
||||
"discriminator": "type"
|
||||
"discriminator": "type",
|
||||
"value": "alien"
|
||||
},
|
||||
"details": {
|
||||
"path": "",
|
||||
@ -284,8 +284,8 @@
|
||||
{
|
||||
"code": "NO_FAMILY_MATCH",
|
||||
"values": {
|
||||
"value": "bot",
|
||||
"discriminator": "type"
|
||||
"discriminator": "type",
|
||||
"value": "bot"
|
||||
},
|
||||
"details": {
|
||||
"path": "",
|
||||
@ -591,8 +591,8 @@
|
||||
{
|
||||
"code": "NO_ONEOF_MATCH",
|
||||
"values": {
|
||||
"value": "alien",
|
||||
"discriminator": "type"
|
||||
"discriminator": "type",
|
||||
"value": "alien"
|
||||
},
|
||||
"details": {
|
||||
"path": "",
|
||||
@ -931,8 +931,8 @@
|
||||
{
|
||||
"code": "NO_FAMILY_MATCH",
|
||||
"values": {
|
||||
"value": "unknown_panel",
|
||||
"discriminator": "kind"
|
||||
"discriminator": "kind",
|
||||
"value": "unknown_panel"
|
||||
},
|
||||
"details": {
|
||||
"path": "",
|
||||
|
||||
@ -431,8 +431,8 @@
|
||||
{
|
||||
"code": "MAX_LENGTH_VIOLATED",
|
||||
"values": {
|
||||
"limit": "3",
|
||||
"count": "6"
|
||||
"count": "6",
|
||||
"limit": "3"
|
||||
},
|
||||
"details": {
|
||||
"path": "",
|
||||
|
||||
@ -258,9 +258,7 @@
|
||||
"name": "text",
|
||||
"created_at": "timestamptz"
|
||||
},
|
||||
"lookup_fields": [
|
||||
"id"
|
||||
],
|
||||
"lookup_fields": [],
|
||||
"null_fields": [],
|
||||
"default_fields": [
|
||||
"id",
|
||||
@ -806,9 +804,7 @@
|
||||
"archived"
|
||||
]
|
||||
},
|
||||
"lookup_fields": [
|
||||
"id"
|
||||
],
|
||||
"lookup_fields": [],
|
||||
"historical": true,
|
||||
"relationship": false,
|
||||
"field_types": {
|
||||
@ -1106,9 +1102,7 @@
|
||||
"archived"
|
||||
]
|
||||
},
|
||||
"lookup_fields": [
|
||||
"id"
|
||||
],
|
||||
"lookup_fields": [],
|
||||
"historical": true,
|
||||
"relationship": false,
|
||||
"field_types": {
|
||||
@ -2461,11 +2455,13 @@
|
||||
"constraint": "fk_attachment_attachable_entity",
|
||||
"source_type": "attachment",
|
||||
"source_columns": [
|
||||
"attachable_id"
|
||||
"attachable_id",
|
||||
"attachable_type"
|
||||
],
|
||||
"destination_type": "entity",
|
||||
"destination_columns": [
|
||||
"id"
|
||||
"id",
|
||||
"type"
|
||||
],
|
||||
"prefix": "attachable"
|
||||
}
|
||||
@ -2669,6 +2665,7 @@
|
||||
" NOT entity_3.archived",
|
||||
" AND attachment_4.kind = 'cover'",
|
||||
" AND attachment_4.attachable_id = entity_1.id",
|
||||
" LIMIT 1",
|
||||
" ),",
|
||||
" 'cover_attachments', (",
|
||||
" SELECT COALESCE(jsonb_agg(jsonb_build_object(",
|
||||
|
||||
@ -139,8 +139,8 @@
|
||||
{
|
||||
"code": "MAX_LENGTH_VIOLATED",
|
||||
"values": {
|
||||
"limit": "5",
|
||||
"count": "26"
|
||||
"count": "26",
|
||||
"limit": "5"
|
||||
},
|
||||
"details": {
|
||||
"path": "email",
|
||||
|
||||
@ -2,7 +2,7 @@ use crate::database::schema::Schema;
|
||||
#[allow(unused_imports)]
|
||||
use crate::drop::{Error, ErrorDetails};
|
||||
#[allow(unused_imports)]
|
||||
use std::collections::HashMap;
|
||||
use indexmap::IndexMap;
|
||||
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(HashMap::from([
|
||||
values: Some(IndexMap::from([
|
||||
("character".to_string(), c.to_string()),
|
||||
("field_name".to_string(), field_name.to_string()),
|
||||
("identifier".to_string(), id.to_string()),
|
||||
|
||||
@ -7,7 +7,6 @@ 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(
|
||||
@ -64,7 +63,7 @@ impl Schema {
|
||||
if custom_type_count > 1 {
|
||||
errors.push(Error {
|
||||
code: "MULTIPLE_INHERITANCE_PROHIBITED".to_string(),
|
||||
values: Some(HashMap::from([("types".to_string(), types.join(", "))])),
|
||||
values: Some(IndexMap::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 std::collections::HashMap;
|
||||
use indexmap::IndexMap;
|
||||
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(HashMap::from([("value".to_string(), val.to_string())])),
|
||||
values: Some(IndexMap::from([("value".to_string(), val.to_string())])),
|
||||
details: ErrorDetails {
|
||||
path: Some(path.to_string()),
|
||||
schema: Some(root_id.to_string()),
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
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();
|
||||
@ -114,7 +115,7 @@ fn resolve_in_place(
|
||||
if visited.contains(inc_name) {
|
||||
errors.push(Error {
|
||||
code: "CIRCULAR_INCLUDE_DETECTED".to_string(),
|
||||
values: Some(HashMap::from([(
|
||||
values: Some(IndexMap::from([(
|
||||
"include".to_string(),
|
||||
inc_name.to_string(),
|
||||
)])),
|
||||
@ -218,7 +219,7 @@ fn resolve_in_place(
|
||||
} else {
|
||||
errors.push(Error {
|
||||
code: "TRAIT_NOT_FOUND".to_string(),
|
||||
values: Some(HashMap::from([(
|
||||
values: Some(IndexMap::from([(
|
||||
"include".to_string(),
|
||||
inc_name.to_string(),
|
||||
)])),
|
||||
|
||||
@ -85,6 +85,10 @@ impl DatabaseExecutor for MockExecutor {
|
||||
Ok("2026-03-10T00:00:00Z".to_string())
|
||||
}
|
||||
|
||||
fn is_mock(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn get_queries(&self) -> Vec<String> {
|
||||
MOCK_STATE.with(|state| state.borrow().captured_queries.clone())
|
||||
|
||||
@ -20,6 +20,11 @@ pub trait DatabaseExecutor: Send + Sync {
|
||||
/// Returns the current transaction timestamp
|
||||
fn timestamp(&self) -> Result<String, String>;
|
||||
|
||||
/// Returns whether this is a mock executor (bypassing pg_catalog index checks)
|
||||
fn is_mock(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn get_queries(&self) -> Vec<String>;
|
||||
|
||||
|
||||
@ -21,16 +21,22 @@ use executors::pgrx::SpiExecutor;
|
||||
#[cfg(test)]
|
||||
use executors::mock::MockExecutor;
|
||||
|
||||
use indexmap::IndexMap;
|
||||
use punc::Punc;
|
||||
use relation::Relation;
|
||||
use schema::Schema;
|
||||
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, serde::Deserialize, Debug, Clone)]
|
||||
pub struct IndexInfo {
|
||||
pub table: String,
|
||||
pub columns: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
pub struct Database {
|
||||
pub enums: IndexMap<String, Enum>,
|
||||
@ -38,6 +44,8 @@ pub struct Database {
|
||||
pub puncs: IndexMap<String, Punc>,
|
||||
pub relations: IndexMap<String, Relation>,
|
||||
#[serde(skip)]
|
||||
pub indexes: Vec<IndexInfo>,
|
||||
#[serde(skip)]
|
||||
pub schemas: IndexMap<String, Arc<Schema>>,
|
||||
#[serde(skip)]
|
||||
pub executor: Box<dyn DatabaseExecutor + Send + Sync>,
|
||||
@ -50,6 +58,7 @@ impl Database {
|
||||
types: IndexMap::new(),
|
||||
relations: IndexMap::new(),
|
||||
puncs: IndexMap::new(),
|
||||
indexes: Vec::new(),
|
||||
schemas: IndexMap::new(),
|
||||
#[cfg(not(test))]
|
||||
executor: Box::new(SpiExecutor::new()),
|
||||
@ -76,7 +85,7 @@ impl Database {
|
||||
Err(e) => {
|
||||
errors.push(Error {
|
||||
code: "DATABASE_ENUM_PARSE_FAILED".to_string(),
|
||||
values: Some(HashMap::from([
|
||||
values: Some(IndexMap::from([
|
||||
("enum".to_string(), name.clone()),
|
||||
("reason".to_string(), e.to_string()),
|
||||
])),
|
||||
@ -104,7 +113,7 @@ impl Database {
|
||||
Err(e) => {
|
||||
errors.push(Error {
|
||||
code: "DATABASE_TYPE_PARSE_FAILED".to_string(),
|
||||
values: Some(HashMap::from([
|
||||
values: Some(IndexMap::from([
|
||||
("type".to_string(), name.clone()),
|
||||
("reason".to_string(), e.to_string()),
|
||||
])),
|
||||
@ -136,7 +145,7 @@ impl Database {
|
||||
Err(e) => {
|
||||
errors.push(Error {
|
||||
code: "DATABASE_RELATION_PARSE_FAILED".to_string(),
|
||||
values: Some(HashMap::from([
|
||||
values: Some(IndexMap::from([
|
||||
("relation".to_string(), constraint.clone()),
|
||||
("reason".to_string(), e.to_string()),
|
||||
])),
|
||||
@ -164,7 +173,7 @@ impl Database {
|
||||
Err(e) => {
|
||||
errors.push(Error {
|
||||
code: "DATABASE_PUNC_PARSE_FAILED".to_string(),
|
||||
values: Some(HashMap::from([
|
||||
values: Some(IndexMap::from([
|
||||
("punc".to_string(), name.clone()),
|
||||
("reason".to_string(), e.to_string()),
|
||||
])),
|
||||
@ -177,6 +186,25 @@ impl Database {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(serde_json::Value::Array(arr)) = map.remove("indexes") {
|
||||
for item in arr {
|
||||
match serde_json::from_value::<IndexInfo>(item) {
|
||||
Ok(idx) => {
|
||||
db.indexes.push(idx);
|
||||
}
|
||||
Err(e) => {
|
||||
errors.push(Error {
|
||||
code: "DATABASE_INDEX_PARSE_FAILED".to_string(),
|
||||
values: Some(IndexMap::from([("reason".to_string(), e.to_string())])),
|
||||
details: ErrorDetails {
|
||||
..Default::default()
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
db.compile(&mut errors);
|
||||
@ -214,7 +242,6 @@ impl Database {
|
||||
self.executor.timestamp()
|
||||
}
|
||||
|
||||
|
||||
pub fn compile(&mut self, errors: &mut Vec<crate::drop::Error>) {
|
||||
// Phase 1: Registration
|
||||
self.collect_schemas(errors);
|
||||
@ -260,6 +287,43 @@ impl Database {
|
||||
.compile(self, root_id, id.clone(), errors);
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 5: Verify unique indexes for defined lookup fields
|
||||
for (_, type_def) in &self.types {
|
||||
if !type_def.lookup_fields.is_empty() {
|
||||
let mut index_found = false;
|
||||
for index in &self.indexes {
|
||||
if index.table == type_def.name {
|
||||
if index.columns.len() == type_def.lookup_fields.len()
|
||||
&& index
|
||||
.columns
|
||||
.iter()
|
||||
.all(|c| type_def.lookup_fields.contains(c))
|
||||
{
|
||||
index_found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if !index_found {
|
||||
errors.push(Error {
|
||||
code: "MISSING_LOOKUP_INDEX".to_string(),
|
||||
values: Some(IndexMap::from([
|
||||
("type".to_string(), type_def.name.clone()),
|
||||
(
|
||||
"lookup_fields".to_string(),
|
||||
format!("{:?}", type_def.lookup_fields),
|
||||
),
|
||||
])),
|
||||
details: ErrorDetails {
|
||||
path: Some(format!("/types/{}", type_def.name)),
|
||||
schema: Some(type_def.name.clone()),
|
||||
..Default::default()
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Synthesizes Composed Filter References for all table-backed boundaries.
|
||||
@ -395,7 +459,6 @@ impl Database {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Inspects the Postgres pg_constraint relations catalog to securely identify
|
||||
/// the precise Foreign Key connecting a parent and child hierarchy path.
|
||||
pub fn resolve_relation<'a>(
|
||||
@ -461,7 +524,7 @@ impl Database {
|
||||
|
||||
errors.push(Error {
|
||||
code: "EDGE_MISSING".to_string(),
|
||||
values: Some(HashMap::from([
|
||||
values: Some(IndexMap::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 +626,7 @@ impl Database {
|
||||
|
||||
errors.push(Error {
|
||||
code: "AMBIGUOUS_TYPE_RELATIONS".to_string(),
|
||||
values: Some(HashMap::from([
|
||||
values: Some(IndexMap::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 std::collections::HashMap;
|
||||
use indexmap::IndexMap;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct Error {
|
||||
pub code: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub values: Option<HashMap<String, String>>,
|
||||
pub values: Option<IndexMap<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 std::collections::HashMap;
|
||||
use indexmap::IndexMap;
|
||||
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(HashMap::from([
|
||||
values: Some(IndexMap::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(HashMap::from([
|
||||
values: Some(IndexMap::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(HashMap::from([
|
||||
values: Some(IndexMap::from([
|
||||
("error".to_string(), e.to_string()),
|
||||
])),
|
||||
details: ErrorDetails {
|
||||
@ -677,25 +677,32 @@ impl Merger {
|
||||
let id_val = entity_fields.get("id");
|
||||
let entity_type_name = entity_type.name.as_str();
|
||||
|
||||
let mut lookup_complete = false;
|
||||
if !entity_type.lookup_fields.is_empty() {
|
||||
lookup_complete = true;
|
||||
for column in &entity_type.lookup_fields {
|
||||
match entity_fields.get(column) {
|
||||
Some(Value::Null) | None => {
|
||||
lookup_complete = false;
|
||||
break;
|
||||
let mut lookup_satisfied_keys = Vec::new();
|
||||
for parent_type_name in entity_type.hierarchy.iter().rev() {
|
||||
if let Some(parent_type) = self.db.types.get(parent_type_name) {
|
||||
if !parent_type.lookup_fields.is_empty() {
|
||||
let mut lookup_complete = true;
|
||||
for column in &parent_type.lookup_fields {
|
||||
match entity_fields.get(column) {
|
||||
Some(Value::Null) | None => {
|
||||
lookup_complete = false;
|
||||
break;
|
||||
}
|
||||
Some(Value::String(s)) if s.is_empty() => {
|
||||
lookup_complete = false;
|
||||
break;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Some(Value::String(s)) if s.is_empty() => {
|
||||
lookup_complete = false;
|
||||
break;
|
||||
if lookup_complete {
|
||||
lookup_satisfied_keys.push(&parent_type.lookup_fields);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if id_val.is_none() && !lookup_complete {
|
||||
if id_val.is_none() && lookup_satisfied_keys.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
@ -727,9 +734,9 @@ impl Merger {
|
||||
where_parts.push(format!("t1.id = {}", Self::quote_literal(id)));
|
||||
}
|
||||
|
||||
if lookup_complete {
|
||||
for lookup_fields in lookup_satisfied_keys {
|
||||
let mut lookup_predicates = Vec::new();
|
||||
for column in &entity_type.lookup_fields {
|
||||
for column in lookup_fields {
|
||||
let val = entity_fields.get(column).unwrap_or(&Value::Null);
|
||||
if column == "type" {
|
||||
lookup_predicates.push(format!("t1.\"{}\" = {}", column, Self::quote_literal(val)));
|
||||
|
||||
@ -240,7 +240,7 @@ impl<'a> Compiler<'a> {
|
||||
};
|
||||
|
||||
// 3. Build WHERE clauses
|
||||
let where_clauses = self.compile_where_clause(r#type, &table_aliases, node)?;
|
||||
let where_clauses = self.compile_where_clause(r#type, &table_aliases, node.clone())?;
|
||||
|
||||
let selection = if is_array {
|
||||
format!("COALESCE(jsonb_agg({}), '[]'::jsonb)", jsonb_obj_sql)
|
||||
@ -248,11 +248,38 @@ impl<'a> Compiler<'a> {
|
||||
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 ")
|
||||
where_clauses.join(" AND "),
|
||||
limit_clause
|
||||
);
|
||||
|
||||
Ok((
|
||||
@ -621,9 +648,6 @@ 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 {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
use crate::database::Database;
|
||||
use crate::drop::{Drop, Error, ErrorDetails};
|
||||
use std::collections::HashMap;
|
||||
use indexmap::IndexMap;
|
||||
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(HashMap::from([
|
||||
values: Some(IndexMap::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(HashMap::from([
|
||||
values: Some(IndexMap::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(HashMap::from([
|
||||
values: Some(IndexMap::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(HashMap::from([
|
||||
values: Some(IndexMap::from([
|
||||
("error".to_string(), e.to_string()),
|
||||
])),
|
||||
details: ErrorDetails {
|
||||
|
||||
@ -3407,6 +3407,18 @@ fn test_database_6_0() {
|
||||
crate::tests::runner::run_test_case(&path, 6, 0).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_database_7_0() {
|
||||
let path = format!("{}/fixtures/database.json", env!("CARGO_MANIFEST_DIR"));
|
||||
crate::tests::runner::run_test_case(&path, 7, 0).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_database_8_0() {
|
||||
let path = format!("{}/fixtures/database.json", env!("CARGO_MANIFEST_DIR"));
|
||||
crate::tests::runner::run_test_case(&path, 8, 0).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cases_0_0() {
|
||||
let path = format!("{}/fixtures/cases.json", env!("CARGO_MANIFEST_DIR"));
|
||||
@ -7666,3 +7678,9 @@ fn test_merger_0_15() {
|
||||
let path = format!("{}/fixtures/merger.json", env!("CARGO_MANIFEST_DIR"));
|
||||
crate::tests::runner::run_test_case(&path, 0, 15).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_merger_0_16() {
|
||||
let path = format!("{}/fixtures/merger.json", env!("CARGO_MANIFEST_DIR"));
|
||||
crate::tests::runner::run_test_case(&path, 0, 16).unwrap();
|
||||
}
|
||||
|
||||
@ -180,6 +180,9 @@ 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) {
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
use std::collections::HashMap;
|
||||
use indexmap::IndexMap;
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
pub struct ValidationError {
|
||||
pub code: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub values: Option<HashMap<String, String>>,
|
||||
pub values: Option<IndexMap<String, String>>,
|
||||
pub path: String,
|
||||
}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::collections::HashSet;
|
||||
use indexmap::IndexMap;
|
||||
|
||||
pub mod context;
|
||||
pub mod error;
|
||||
@ -92,7 +93,7 @@ impl Validator {
|
||||
} else {
|
||||
Drop::with_errors(vec![Error {
|
||||
code: "SCHEMA_NOT_FOUND".to_string(),
|
||||
values: Some(HashMap::from([
|
||||
values: Some(IndexMap::from([
|
||||
("schema".to_string(), schema_id.to_string()),
|
||||
])),
|
||||
details: ErrorDetails {
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::collections::HashSet;
|
||||
use indexmap::IndexMap;
|
||||
|
||||
use serde_json::Value;
|
||||
|
||||
@ -18,7 +19,7 @@ impl<'a> ValidationContext<'a> {
|
||||
{
|
||||
result.errors.push(ValidationError {
|
||||
code: "MIN_ITEMS_VIOLATED".to_string(),
|
||||
values: Some(HashMap::from([
|
||||
values: Some(IndexMap::from([
|
||||
("count".to_string(), arr.len().to_string()),
|
||||
("limit".to_string(), min.to_string()),
|
||||
])),
|
||||
@ -30,7 +31,7 @@ impl<'a> ValidationContext<'a> {
|
||||
{
|
||||
result.errors.push(ValidationError {
|
||||
code: "MAX_ITEMS_VIOLATED".to_string(),
|
||||
values: Some(HashMap::from([
|
||||
values: Some(IndexMap::from([
|
||||
("count".to_string(), arr.len().to_string()),
|
||||
("limit".to_string(), max.to_string()),
|
||||
])),
|
||||
@ -77,7 +78,7 @@ impl<'a> ValidationContext<'a> {
|
||||
if _match_count < min {
|
||||
result.errors.push(ValidationError {
|
||||
code: "CONTAINS_VIOLATED".to_string(),
|
||||
values: Some(HashMap::from([
|
||||
values: Some(IndexMap::from([
|
||||
("count".to_string(), _match_count.to_string()),
|
||||
("limit".to_string(), min.to_string()),
|
||||
])),
|
||||
@ -89,7 +90,7 @@ impl<'a> ValidationContext<'a> {
|
||||
{
|
||||
result.errors.push(ValidationError {
|
||||
code: "CONTAINS_VIOLATED".to_string(),
|
||||
values: Some(HashMap::from([
|
||||
values: Some(IndexMap::from([
|
||||
("count".to_string(), _match_count.to_string()),
|
||||
("limit".to_string(), max.to_string()),
|
||||
])),
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
use std::collections::HashMap;
|
||||
use indexmap::IndexMap;
|
||||
|
||||
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(HashMap::from([
|
||||
values: Some(IndexMap::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(HashMap::from([
|
||||
values: Some(IndexMap::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(HashMap::from([
|
||||
values: Some(IndexMap::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(HashMap::from([
|
||||
values: Some(IndexMap::from([
|
||||
("expected".to_string(), format!("{:?}", enum_vals)),
|
||||
])),
|
||||
path: self.path.to_string(),
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
use std::collections::HashMap;
|
||||
use indexmap::IndexMap;
|
||||
|
||||
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(HashMap::from([
|
||||
values: Some(IndexMap::from([
|
||||
("property_name".to_string(), key.to_string()),
|
||||
])),
|
||||
path: self.join_path(key),
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
use std::collections::HashMap;
|
||||
use indexmap::IndexMap;
|
||||
|
||||
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(HashMap::from([
|
||||
values: Some(IndexMap::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(HashMap::from([
|
||||
values: Some(IndexMap::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 std::collections::HashMap;
|
||||
use indexmap::IndexMap;
|
||||
|
||||
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(HashMap::from([
|
||||
values: Some(IndexMap::from([
|
||||
("limit".to_string(), 100.to_string()),
|
||||
])),
|
||||
path: self.path.to_string(),
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
use std::collections::HashMap;
|
||||
use indexmap::IndexMap;
|
||||
|
||||
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(HashMap::from([
|
||||
values: Some(IndexMap::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(HashMap::from([
|
||||
values: Some(IndexMap::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(HashMap::from([
|
||||
values: Some(IndexMap::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(HashMap::from([
|
||||
values: Some(IndexMap::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(HashMap::from([
|
||||
values: Some(IndexMap::from([
|
||||
("value".to_string(), num.to_string()),
|
||||
("multiple_of".to_string(), multiple_of.to_string()),
|
||||
])),
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::collections::HashSet;
|
||||
use indexmap::IndexMap;
|
||||
|
||||
use serde_json::Value;
|
||||
|
||||
@ -38,7 +39,7 @@ impl<'a> ValidationContext<'a> {
|
||||
} else {
|
||||
result.errors.push(ValidationError {
|
||||
code: "CONST_VIOLATED".to_string(), // Aligning with original const override errors natively
|
||||
values: Some(HashMap::from([
|
||||
values: Some(IndexMap::from([
|
||||
("value".to_string(), type_str.to_string()),
|
||||
])),
|
||||
path: self.join_path("type"),
|
||||
@ -49,7 +50,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(HashMap::from([
|
||||
values: Some(IndexMap::from([
|
||||
("expected".to_string(), expected_type.to_string()),
|
||||
])),
|
||||
path: self.path.clone(), // Empty boundary
|
||||
@ -104,7 +105,7 @@ impl<'a> ValidationContext<'a> {
|
||||
{
|
||||
result.errors.push(ValidationError {
|
||||
code: "MIN_PROPERTIES_VIOLATED".to_string(),
|
||||
values: Some(HashMap::from([
|
||||
values: Some(IndexMap::from([
|
||||
("count".to_string(), obj.len().to_string()),
|
||||
("limit".to_string(), min.to_string()),
|
||||
])),
|
||||
@ -117,7 +118,7 @@ impl<'a> ValidationContext<'a> {
|
||||
{
|
||||
result.errors.push(ValidationError {
|
||||
code: "MAX_PROPERTIES_VIOLATED".to_string(),
|
||||
values: Some(HashMap::from([
|
||||
values: Some(IndexMap::from([
|
||||
("count".to_string(), obj.len().to_string()),
|
||||
("limit".to_string(), max.to_string()),
|
||||
])),
|
||||
@ -131,7 +132,7 @@ impl<'a> ValidationContext<'a> {
|
||||
if field == "type" {
|
||||
result.errors.push(ValidationError {
|
||||
code: "MISSING_TYPE".to_string(),
|
||||
values: Some(HashMap::from([
|
||||
values: Some(IndexMap::from([
|
||||
("field_name".to_string(), "type".to_string()),
|
||||
])),
|
||||
path: self.join_path(field),
|
||||
@ -139,7 +140,7 @@ impl<'a> ValidationContext<'a> {
|
||||
} else {
|
||||
result.errors.push(ValidationError {
|
||||
code: "REQUIRED_FIELD_MISSING".to_string(),
|
||||
values: Some(HashMap::from([
|
||||
values: Some(IndexMap::from([
|
||||
("field_name".to_string(), field.to_string()),
|
||||
])),
|
||||
path: self.join_path(field),
|
||||
@ -158,7 +159,7 @@ impl<'a> ValidationContext<'a> {
|
||||
if !obj.contains_key(req_prop) {
|
||||
result.errors.push(ValidationError {
|
||||
code: "DEPENDENCY_MISSING".to_string(),
|
||||
values: Some(HashMap::from([
|
||||
values: Some(IndexMap::from([
|
||||
("property_name".to_string(), prop.to_string()),
|
||||
("required_property".to_string(), req_prop.to_string()),
|
||||
])),
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::validator::context::ValidationContext;
|
||||
use crate::validator::error::ValidationError;
|
||||
use crate::validator::result::ValidationResult;
|
||||
@ -55,10 +53,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);
|
||||
}
|
||||
}
|
||||
@ -110,9 +108,10 @@ impl<'a> ValidationContext<'a> {
|
||||
} else {
|
||||
result.errors.push(ValidationError {
|
||||
code: "MISSING_COMPILED_SCHEMA".to_string(),
|
||||
values: Some(HashMap::from([
|
||||
("target_id".to_string(), target_id.to_string()),
|
||||
])),
|
||||
values: Some(IndexMap::from([(
|
||||
"target_id".to_string(),
|
||||
target_id.to_string(),
|
||||
)])),
|
||||
path: self.path.to_string(),
|
||||
});
|
||||
return Ok(false);
|
||||
@ -132,9 +131,7 @@ impl<'a> ValidationContext<'a> {
|
||||
} else {
|
||||
result.errors.push(ValidationError {
|
||||
code: "MISSING_COMPILED_SCHEMA".to_string(),
|
||||
values: Some(HashMap::from([
|
||||
("index".to_string(), idx.to_string()),
|
||||
])),
|
||||
values: Some(IndexMap::from([("index".to_string(), idx.to_string())])),
|
||||
path: self.path.to_string(),
|
||||
});
|
||||
return Ok(false);
|
||||
@ -144,14 +141,12 @@ impl<'a> ValidationContext<'a> {
|
||||
}
|
||||
} else {
|
||||
let values = if let Some(d) = self.schema.compiled_discriminator.get() {
|
||||
Some(HashMap::from([
|
||||
Some(IndexMap::from([
|
||||
("discriminator".to_string(), d.to_string()),
|
||||
("value".to_string(), val.to_string()),
|
||||
]))
|
||||
} else {
|
||||
Some(HashMap::from([
|
||||
("primitive".to_string(), val.to_string()),
|
||||
]))
|
||||
Some(IndexMap::from([("primitive".to_string(), val.to_string())]))
|
||||
};
|
||||
result.errors.push(ValidationError {
|
||||
code: if self.schema.family.is_some() {
|
||||
@ -168,9 +163,10 @@ impl<'a> ValidationContext<'a> {
|
||||
if let Some(d) = self.schema.compiled_discriminator.get() {
|
||||
result.errors.push(ValidationError {
|
||||
code: "MISSING_TYPE".to_string(),
|
||||
values: Some(HashMap::from([
|
||||
("discriminator".to_string(), d.to_string()),
|
||||
])),
|
||||
values: Some(IndexMap::from([(
|
||||
"discriminator".to_string(),
|
||||
d.to_string(),
|
||||
)])),
|
||||
path: self.path.to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
use std::collections::HashMap;
|
||||
use indexmap::IndexMap;
|
||||
|
||||
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(HashMap::from([
|
||||
values: Some(IndexMap::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(HashMap::from([
|
||||
values: Some(IndexMap::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(HashMap::from([
|
||||
values: Some(IndexMap::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(HashMap::from([
|
||||
values: Some(IndexMap::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 std::collections::HashMap;
|
||||
use indexmap::IndexMap;
|
||||
|
||||
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(HashMap::from([
|
||||
values: Some(IndexMap::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(HashMap::from([
|
||||
values: Some(IndexMap::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(HashMap::from([
|
||||
values: Some(IndexMap::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(HashMap::from([
|
||||
values: Some(IndexMap::from([
|
||||
("pointer".to_string(), target_id.to_string()),
|
||||
])),
|
||||
path: self.path.to_string(),
|
||||
|
||||
Reference in New Issue
Block a user