Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4a10833f50 | |||
| 46fc032026 | |||
| 7ec06b81cc | |||
| c4e8e0309f | |||
| eb91b65e65 | |||
| 8bf3649465 | |||
| 9fe5a34163 | |||
| f5bf21eb58 |
@ -142,7 +142,7 @@
|
|||||||
"errors": [
|
"errors": [
|
||||||
{
|
{
|
||||||
"code": "CONST_VIOLATED",
|
"code": "CONST_VIOLATED",
|
||||||
"path": "/con"
|
"path": "con"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -48,7 +48,7 @@
|
|||||||
"errors": [
|
"errors": [
|
||||||
{
|
{
|
||||||
"code": "TYPE_MISMATCH",
|
"code": "TYPE_MISMATCH",
|
||||||
"path": "/base_prop"
|
"path": "base_prop"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@ -109,7 +109,7 @@
|
|||||||
"errors": [
|
"errors": [
|
||||||
{
|
{
|
||||||
"code": "REQUIRED_FIELD_MISSING",
|
"code": "REQUIRED_FIELD_MISSING",
|
||||||
"path": "/a"
|
"path": "a"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@ -126,7 +126,7 @@
|
|||||||
"errors": [
|
"errors": [
|
||||||
{
|
{
|
||||||
"code": "REQUIRED_FIELD_MISSING",
|
"code": "REQUIRED_FIELD_MISSING",
|
||||||
"path": "/b"
|
"path": "b"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@ -196,7 +196,7 @@
|
|||||||
"errors": [
|
"errors": [
|
||||||
{
|
{
|
||||||
"code": "DEPENDENCY_FAILED",
|
"code": "DEPENDENCY_FAILED",
|
||||||
"path": "/base_dep"
|
"path": "base_dep"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@ -214,7 +214,7 @@
|
|||||||
"errors": [
|
"errors": [
|
||||||
{
|
{
|
||||||
"code": "DEPENDENCY_FAILED",
|
"code": "DEPENDENCY_FAILED",
|
||||||
"path": "/child_dep"
|
"path": "child_dep"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -972,7 +972,119 @@
|
|||||||
"LEFT JOIN agreego.\"user\" t2 ON t2.id = t1.id",
|
"LEFT JOIN agreego.\"user\" t2 ON t2.id = t1.id",
|
||||||
"LEFT JOIN agreego.\"organization\" t3 ON t3.id = t1.id",
|
"LEFT JOIN agreego.\"organization\" t3 ON t3.id = t1.id",
|
||||||
"LEFT JOIN agreego.\"entity\" t4 ON t4.id = t1.id",
|
"LEFT JOIN agreego.\"entity\" t4 ON t4.id = t1.id",
|
||||||
"WHERE \"first_name\" = 'LookupFirst' AND \"last_name\" = 'LookupLast' AND \"date_of_birth\" = '1990-01-01T00:00:00Z' AND \"pronouns\" = 'they/them'"
|
"WHERE (",
|
||||||
|
" \"first_name\" = 'LookupFirst'",
|
||||||
|
" AND \"last_name\" = 'LookupLast'",
|
||||||
|
" AND \"date_of_birth\" = '1990-01-01T00:00:00Z'",
|
||||||
|
" AND \"pronouns\" = 'they/them'",
|
||||||
|
")"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"UPDATE agreego.\"person\"",
|
||||||
|
"SET",
|
||||||
|
" \"contact_id\" = 'abc-contact'",
|
||||||
|
"WHERE",
|
||||||
|
" id = '22222222-2222-2222-2222-222222222222'"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"UPDATE agreego.\"entity\"",
|
||||||
|
"SET",
|
||||||
|
" \"modified_at\" = '2026-03-10T00:00:00Z',",
|
||||||
|
" \"modified_by\" = '00000000-0000-0000-0000-000000000000'",
|
||||||
|
"WHERE",
|
||||||
|
" id = '22222222-2222-2222-2222-222222222222'"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"INSERT INTO agreego.change (",
|
||||||
|
" \"old\",",
|
||||||
|
" \"new\",",
|
||||||
|
" entity_id,",
|
||||||
|
" id,",
|
||||||
|
" kind,",
|
||||||
|
" modified_at,",
|
||||||
|
" modified_by",
|
||||||
|
")",
|
||||||
|
"VALUES (",
|
||||||
|
" '{",
|
||||||
|
" \"contact_id\":\"old-contact\"",
|
||||||
|
" }',",
|
||||||
|
" '{",
|
||||||
|
" \"contact_id\":\"abc-contact\",",
|
||||||
|
" \"type\":\"person\"",
|
||||||
|
" }',",
|
||||||
|
" '22222222-2222-2222-2222-222222222222',",
|
||||||
|
" '{{uuid}}',",
|
||||||
|
" 'update',",
|
||||||
|
" '{{timestamp}}',",
|
||||||
|
" '00000000-0000-0000-0000-000000000000'",
|
||||||
|
")"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"SELECT pg_notify('entity', '{",
|
||||||
|
" \"complete\":{",
|
||||||
|
" \"contact_id\":\"abc-contact\",",
|
||||||
|
" \"date_of_birth\":\"1990-01-01T00:00:00Z\",",
|
||||||
|
" \"first_name\":\"LookupFirst\",",
|
||||||
|
" \"id\":\"22222222-2222-2222-2222-222222222222\",",
|
||||||
|
" \"last_name\":\"LookupLast\",",
|
||||||
|
" \"modified_at\":\"2026-03-10T00:00:00Z\",",
|
||||||
|
" \"modified_by\":\"00000000-0000-0000-0000-000000000000\",",
|
||||||
|
" \"pronouns\":\"they/them\",",
|
||||||
|
" \"type\":\"person\"",
|
||||||
|
" },",
|
||||||
|
" \"new\":{",
|
||||||
|
" \"contact_id\":\"abc-contact\",",
|
||||||
|
" \"type\":\"person\"",
|
||||||
|
" },",
|
||||||
|
" \"old\":{",
|
||||||
|
" \"contact_id\":\"old-contact\"",
|
||||||
|
" }",
|
||||||
|
" }')"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Update existing person with id (lookup)",
|
||||||
|
"action": "merge",
|
||||||
|
"data": {
|
||||||
|
"id": "33333333-3333-3333-3333-333333333333",
|
||||||
|
"type": "person",
|
||||||
|
"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",
|
||||||
|
"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.\"person\" t1",
|
||||||
|
"LEFT JOIN agreego.\"user\" t2 ON t2.id = t1.id",
|
||||||
|
"LEFT JOIN agreego.\"organization\" t3 ON t3.id = t1.id",
|
||||||
|
"LEFT JOIN agreego.\"entity\" t4 ON t4.id = t1.id",
|
||||||
|
"WHERE",
|
||||||
|
" t1.id = '33333333-3333-3333-3333-333333333333'",
|
||||||
|
" OR (",
|
||||||
|
" \"first_name\" = 'LookupFirst'",
|
||||||
|
" AND \"last_name\" = 'LookupLast'",
|
||||||
|
" AND \"date_of_birth\" = '1990-01-01T00:00:00Z'",
|
||||||
|
" AND \"pronouns\" = 'they/them'",
|
||||||
|
" )"
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"UPDATE agreego.\"person\"",
|
"UPDATE agreego.\"person\"",
|
||||||
@ -1484,7 +1596,7 @@
|
|||||||
"SELECT to_jsonb(t1.*) || to_jsonb(t2.*)",
|
"SELECT to_jsonb(t1.*) || to_jsonb(t2.*)",
|
||||||
"FROM agreego.\"order\" t1",
|
"FROM agreego.\"order\" t1",
|
||||||
"LEFT JOIN agreego.\"entity\" t2 ON t2.id = t1.id",
|
"LEFT JOIN agreego.\"entity\" t2 ON t2.id = t1.id",
|
||||||
"WHERE t1.id = 'abc'"
|
"WHERE t1.id = 'abc' OR (\"id\" = 'abc')"
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"INSERT INTO agreego.\"entity\" (",
|
"INSERT INTO agreego.\"entity\" (",
|
||||||
|
|||||||
214
fixtures/paths.json
Normal file
214
fixtures/paths.json
Normal file
@ -0,0 +1,214 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"description": "Hybrid Array Pathing",
|
||||||
|
"database": {
|
||||||
|
"schemas": [
|
||||||
|
{
|
||||||
|
"$id": "hybrid_pathing",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"primitives": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ad_hoc_objects": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"name"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"entities": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"deep_entities": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"nested": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"flag": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"tests": [
|
||||||
|
{
|
||||||
|
"description": "happy path passes structural validation",
|
||||||
|
"data": {
|
||||||
|
"primitives": [
|
||||||
|
"a",
|
||||||
|
"b"
|
||||||
|
],
|
||||||
|
"ad_hoc_objects": [
|
||||||
|
{
|
||||||
|
"name": "obj1"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"id": "entity-1",
|
||||||
|
"value": 15
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"deep_entities": [
|
||||||
|
{
|
||||||
|
"id": "parent-1",
|
||||||
|
"nested": [
|
||||||
|
{
|
||||||
|
"id": "child-1",
|
||||||
|
"flag": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"schema_id": "hybrid_pathing",
|
||||||
|
"action": "validate",
|
||||||
|
"expect": {
|
||||||
|
"success": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "primitive arrays use numeric indexing",
|
||||||
|
"data": {
|
||||||
|
"primitives": [
|
||||||
|
"a",
|
||||||
|
123
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"schema_id": "hybrid_pathing",
|
||||||
|
"action": "validate",
|
||||||
|
"expect": {
|
||||||
|
"success": false,
|
||||||
|
"errors": [
|
||||||
|
{
|
||||||
|
"code": "INVALID_TYPE",
|
||||||
|
"path": "primitives/1"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "ad-hoc objects without ids use numeric indexing",
|
||||||
|
"data": {
|
||||||
|
"ad_hoc_objects": [
|
||||||
|
{
|
||||||
|
"name": "valid"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"age": 30
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"schema_id": "hybrid_pathing",
|
||||||
|
"action": "validate",
|
||||||
|
"expect": {
|
||||||
|
"success": false,
|
||||||
|
"errors": [
|
||||||
|
{
|
||||||
|
"code": "REQUIRED_FIELD_MISSING",
|
||||||
|
"path": "ad_hoc_objects/1/name"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "arrays of objects with ids use topological uuid indexing",
|
||||||
|
"data": {
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"id": "entity-alpha",
|
||||||
|
"value": 20
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "entity-beta",
|
||||||
|
"value": 5
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"schema_id": "hybrid_pathing",
|
||||||
|
"action": "validate",
|
||||||
|
"expect": {
|
||||||
|
"success": false,
|
||||||
|
"errors": [
|
||||||
|
{
|
||||||
|
"code": "MINIMUM_VIOLATED",
|
||||||
|
"path": "entities/entity-beta/value"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "deeply nested entity arrays retain full topological paths",
|
||||||
|
"data": {
|
||||||
|
"deep_entities": [
|
||||||
|
{
|
||||||
|
"id": "parent-omega",
|
||||||
|
"nested": [
|
||||||
|
{
|
||||||
|
"id": "child-alpha",
|
||||||
|
"flag": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "child-beta",
|
||||||
|
"flag": "invalid-string"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"schema_id": "hybrid_pathing",
|
||||||
|
"action": "validate",
|
||||||
|
"expect": {
|
||||||
|
"success": false,
|
||||||
|
"errors": [
|
||||||
|
{
|
||||||
|
"code": "INVALID_TYPE",
|
||||||
|
"path": "deep_entities/parent-omega/nested/child-beta/flag"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
@ -20,6 +20,16 @@
|
|||||||
"$family": "base.person"
|
"$family": "base.person"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "get_orders",
|
||||||
|
"schemas": [
|
||||||
|
{
|
||||||
|
"$id": "get_orders.response",
|
||||||
|
"type": "array",
|
||||||
|
"items": { "$ref": "light.order" }
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"enums": [],
|
"enums": [],
|
||||||
@ -664,6 +674,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"$id": "light.order",
|
||||||
|
"$ref": "order",
|
||||||
|
"properties": {
|
||||||
|
"customer": {
|
||||||
|
"$ref": "base.person"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"$id": "full.order",
|
"$id": "full.order",
|
||||||
"$ref": "order",
|
"$ref": "order",
|
||||||
@ -1569,6 +1588,47 @@
|
|||||||
]
|
]
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Root Array SQL evaluation for Order fetching Light Order",
|
||||||
|
"action": "query",
|
||||||
|
"schema_id": "get_orders.response",
|
||||||
|
"expect": {
|
||||||
|
"success": true,
|
||||||
|
"sql": [
|
||||||
|
[
|
||||||
|
"(SELECT COALESCE(jsonb_agg(jsonb_build_object(",
|
||||||
|
" 'archived', entity_2.archived,",
|
||||||
|
" 'created_at', entity_2.created_at,",
|
||||||
|
" 'customer',",
|
||||||
|
" (SELECT jsonb_build_object(",
|
||||||
|
" 'age', person_3.age,",
|
||||||
|
" 'archived', entity_5.archived,",
|
||||||
|
" 'created_at', entity_5.created_at,",
|
||||||
|
" 'first_name', person_3.first_name,",
|
||||||
|
" 'id', entity_5.id,",
|
||||||
|
" 'last_name', person_3.last_name,",
|
||||||
|
" 'name', entity_5.name,",
|
||||||
|
" 'type', entity_5.type",
|
||||||
|
" )",
|
||||||
|
" FROM agreego.person person_3",
|
||||||
|
" JOIN agreego.organization organization_4 ON organization_4.id = person_3.id",
|
||||||
|
" JOIN agreego.entity entity_5 ON entity_5.id = organization_4.id",
|
||||||
|
" WHERE",
|
||||||
|
" NOT entity_5.archived",
|
||||||
|
" AND order_1.customer_id = person_3.id),",
|
||||||
|
" 'customer_id', order_1.customer_id,",
|
||||||
|
" 'id', entity_2.id,",
|
||||||
|
" 'name', entity_2.name,",
|
||||||
|
" 'total', order_1.total,",
|
||||||
|
" 'type', entity_2.type",
|
||||||
|
")), '[]'::jsonb)",
|
||||||
|
"FROM agreego.order order_1",
|
||||||
|
"JOIN agreego.entity entity_2 ON entity_2.id = order_1.id",
|
||||||
|
"WHERE NOT entity_2.archived)"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -677,7 +677,7 @@
|
|||||||
"errors": [
|
"errors": [
|
||||||
{
|
{
|
||||||
"code": "TYPE_MISMATCH",
|
"code": "TYPE_MISMATCH",
|
||||||
"path": "/type"
|
"path": "type"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@ -782,7 +782,7 @@
|
|||||||
"errors": [
|
"errors": [
|
||||||
{
|
{
|
||||||
"code": "TYPE_MISMATCH",
|
"code": "TYPE_MISMATCH",
|
||||||
"path": "/type"
|
"path": "type"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -124,42 +124,23 @@ fn parse_and_match_mocks(sql: &str, mocks: &[Value]) -> Option<Vec<Value>> {
|
|||||||
return None;
|
return None;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 2. Extract WHERE conditions
|
// 2. Extract WHERE conditions string
|
||||||
let mut conditions = Vec::new();
|
let mut where_clause = String::new();
|
||||||
if let Some(where_idx) = sql_upper.find(" WHERE ") {
|
if let Some(where_idx) = sql_upper.find(" WHERE ") {
|
||||||
let mut where_end = sql_upper.find(" ORDER BY ").unwrap_or(sql.len());
|
let mut where_end = sql_upper.find(" ORDER BY ").unwrap_or(sql_upper.len());
|
||||||
if let Some(limit_idx) = sql_upper.find(" LIMIT ") {
|
if let Some(limit_idx) = sql_upper.find(" LIMIT ") {
|
||||||
if limit_idx < where_end {
|
if limit_idx < where_end {
|
||||||
where_end = limit_idx;
|
where_end = limit_idx;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let where_clause = &sql[where_idx + 7..where_end];
|
where_clause = sql[where_idx + 7..where_end].to_string();
|
||||||
let and_regex = Regex::new(r"(?i)\s+AND\s+").ok()?;
|
|
||||||
let parts = and_regex.split(where_clause);
|
|
||||||
for part in parts {
|
|
||||||
if let Some(eq_idx) = part.find('=') {
|
|
||||||
let left = part[..eq_idx]
|
|
||||||
.trim()
|
|
||||||
.split('.')
|
|
||||||
.last()
|
|
||||||
.unwrap_or("")
|
|
||||||
.trim_matches('"');
|
|
||||||
let right = part[eq_idx + 1..].trim().trim_matches('\'');
|
|
||||||
conditions.push((left.to_string(), right.to_string()));
|
|
||||||
} else if part.to_uppercase().contains(" IS NULL") {
|
|
||||||
let left = part[..part.to_uppercase().find(" IS NULL").unwrap()]
|
|
||||||
.trim()
|
|
||||||
.split('.')
|
|
||||||
.last()
|
|
||||||
.unwrap_or("")
|
|
||||||
.replace('"', ""); // Remove quotes explicitly
|
|
||||||
conditions.push((left, "null".to_string()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Find matching mocks
|
// 3. Find matching mocks
|
||||||
let mut matches = Vec::new();
|
let mut matches = Vec::new();
|
||||||
|
let or_regex = Regex::new(r"(?i)\s+OR\s+").ok()?;
|
||||||
|
let and_regex = Regex::new(r"(?i)\s+AND\s+").ok()?;
|
||||||
|
|
||||||
for mock in mocks {
|
for mock in mocks {
|
||||||
if let Some(mock_obj) = mock.as_object() {
|
if let Some(mock_obj) = mock.as_object() {
|
||||||
if let Some(t) = mock_obj.get("type") {
|
if let Some(t) = mock_obj.get("type") {
|
||||||
@ -168,25 +149,66 @@ fn parse_and_match_mocks(sql: &str, mocks: &[Value]) -> Option<Vec<Value>> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut matches_all = true;
|
if where_clause.is_empty() {
|
||||||
for (k, v) in &conditions {
|
matches.push(mock.clone());
|
||||||
let mock_val_str = match mock_obj.get(k) {
|
continue;
|
||||||
Some(Value::String(s)) => s.clone(),
|
}
|
||||||
Some(Value::Number(n)) => n.to_string(),
|
|
||||||
Some(Value::Bool(b)) => b.to_string(),
|
let or_parts = or_regex.split(&where_clause);
|
||||||
Some(Value::Null) => "null".to_string(),
|
let mut any_branch_matched = false;
|
||||||
_ => {
|
|
||||||
matches_all = false;
|
for or_part in or_parts {
|
||||||
break;
|
let branch_str = or_part.replace('(', "").replace(')', "");
|
||||||
|
let mut branch_matches = true;
|
||||||
|
|
||||||
|
for part in and_regex.split(&branch_str) {
|
||||||
|
if let Some(eq_idx) = part.find('=') {
|
||||||
|
let left = part[..eq_idx]
|
||||||
|
.trim()
|
||||||
|
.split('.')
|
||||||
|
.last()
|
||||||
|
.unwrap_or("")
|
||||||
|
.trim_matches('"');
|
||||||
|
let right = part[eq_idx + 1..].trim().trim_matches('\'');
|
||||||
|
|
||||||
|
let mock_val_str = match mock_obj.get(left) {
|
||||||
|
Some(Value::String(s)) => s.clone(),
|
||||||
|
Some(Value::Number(n)) => n.to_string(),
|
||||||
|
Some(Value::Bool(b)) => b.to_string(),
|
||||||
|
Some(Value::Null) => "null".to_string(),
|
||||||
|
_ => "".to_string(),
|
||||||
|
};
|
||||||
|
if mock_val_str != right {
|
||||||
|
branch_matches = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else if part.to_uppercase().contains(" IS NULL") {
|
||||||
|
let left = part[..part.to_uppercase().find(" IS NULL").unwrap()]
|
||||||
|
.trim()
|
||||||
|
.split('.')
|
||||||
|
.last()
|
||||||
|
.unwrap_or("")
|
||||||
|
.trim_matches('"');
|
||||||
|
|
||||||
|
let mock_val_str = match mock_obj.get(left) {
|
||||||
|
Some(Value::Null) => "null".to_string(),
|
||||||
|
_ => "".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if mock_val_str != "null" {
|
||||||
|
branch_matches = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
if mock_val_str != *v {
|
|
||||||
matches_all = false;
|
if branch_matches {
|
||||||
|
any_branch_matched = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if matches_all {
|
if any_branch_matched {
|
||||||
matches.push(mock.clone());
|
matches.push(mock.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -585,11 +585,14 @@ impl Merger {
|
|||||||
template
|
template
|
||||||
};
|
};
|
||||||
|
|
||||||
let where_clause = if let Some(id) = id_val {
|
let mut where_parts = Vec::new();
|
||||||
format!("WHERE t1.id = {}", Self::quote_literal(id))
|
|
||||||
} else if lookup_complete {
|
|
||||||
let mut lookup_predicates = Vec::new();
|
|
||||||
|
|
||||||
|
if let Some(id) = id_val {
|
||||||
|
where_parts.push(format!("t1.id = {}", Self::quote_literal(id)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if lookup_complete {
|
||||||
|
let mut lookup_predicates = Vec::new();
|
||||||
for column in &entity_type.lookup_fields {
|
for column in &entity_type.lookup_fields {
|
||||||
let val = entity_fields.get(column).unwrap_or(&Value::Null);
|
let val = entity_fields.get(column).unwrap_or(&Value::Null);
|
||||||
if column == "type" {
|
if column == "type" {
|
||||||
@ -598,10 +601,14 @@ impl Merger {
|
|||||||
lookup_predicates.push(format!("\"{}\" = {}", column, Self::quote_literal(val)));
|
lookup_predicates.push(format!("\"{}\" = {}", column, Self::quote_literal(val)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
format!("WHERE {}", lookup_predicates.join(" AND "))
|
where_parts.push(format!("({})", lookup_predicates.join(" AND ")));
|
||||||
} else {
|
}
|
||||||
|
|
||||||
|
if where_parts.is_empty() {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
};
|
}
|
||||||
|
|
||||||
|
let where_clause = format!("WHERE {}", where_parts.join(" OR "));
|
||||||
|
|
||||||
let final_sql = format!("{} {}", fetch_sql_template, where_clause);
|
let final_sql = format!("{} {}", fetch_sql_template, where_clause);
|
||||||
|
|
||||||
|
|||||||
@ -63,33 +63,33 @@ impl<'a> Compiler<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn compile_array(&mut self, node: Node<'a>) -> Result<(String, String), String> {
|
fn compile_array(&mut self, node: Node<'a>) -> Result<(String, String), String> {
|
||||||
|
// 1. Array of DB Entities (`$ref` or `$family` pointing to a table limit)
|
||||||
if let Some(items) = &node.schema.obj.items {
|
if let Some(items) = &node.schema.obj.items {
|
||||||
let next_path = node.ast_path.clone();
|
let mut resolved_type = None;
|
||||||
|
if let Some(family_target) = items.obj.family.as_ref() {
|
||||||
if let Some(ref_id) = &items.obj.r#ref {
|
let base_type_name = family_target.split('.').next_back().unwrap_or(family_target);
|
||||||
if let Some(type_def) = self.db.types.get(ref_id) {
|
resolved_type = self.db.types.get(base_type_name);
|
||||||
let mut entity_node = node.clone();
|
} else if let Some(base_type_name) = items.obj.identifier() {
|
||||||
entity_node.ast_path = next_path;
|
resolved_type = self.db.types.get(&base_type_name);
|
||||||
entity_node.schema = std::sync::Arc::clone(items);
|
|
||||||
return self.compile_entity(type_def, entity_node, true);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut next_node = node.clone();
|
if let Some(type_def) = resolved_type {
|
||||||
next_node.depth += 1;
|
let mut entity_node = node.clone();
|
||||||
next_node.ast_path = next_path;
|
entity_node.schema = std::sync::Arc::clone(items);
|
||||||
next_node.schema = std::sync::Arc::clone(items);
|
return self.compile_entity(type_def, entity_node, true);
|
||||||
let (item_sql, _) = self.compile_node(next_node)?;
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Arrays of mapped Native Postgres Columns (e.g. `jsonb`, `text[]`)
|
||||||
|
if let Some(prop) = &node.property_name {
|
||||||
return Ok((
|
return Ok((
|
||||||
format!("(SELECT jsonb_agg({}) FROM TODO)", item_sql),
|
format!("{}.{}", node.parent_alias, prop),
|
||||||
"array".to_string(),
|
"array".to_string(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok((
|
// 3. Fallback for root execution of standalone non-entity arrays
|
||||||
"SELECT jsonb_agg(TODO) FROM TODO".to_string(),
|
Err("Cannot compile a root array without a valid entity reference or table mapped via `items`.".to_string())
|
||||||
"array".to_string(),
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn compile_reference(&mut self, node: Node<'a>) -> Result<(String, String), String> {
|
fn compile_reference(&mut self, node: Node<'a>) -> Result<(String, String), String> {
|
||||||
|
|||||||
@ -1457,6 +1457,12 @@ fn test_queryer_0_7() {
|
|||||||
crate::tests::runner::run_test_case(&path, 0, 7).unwrap();
|
crate::tests::runner::run_test_case(&path, 0, 7).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_queryer_0_8() {
|
||||||
|
let path = format!("{}/fixtures/queryer.json", env!("CARGO_MANIFEST_DIR"));
|
||||||
|
crate::tests::runner::run_test_case(&path, 0, 8).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_not_0_0() {
|
fn test_not_0_0() {
|
||||||
let path = format!("{}/fixtures/not.json", env!("CARGO_MANIFEST_DIR"));
|
let path = format!("{}/fixtures/not.json", env!("CARGO_MANIFEST_DIR"));
|
||||||
@ -2921,6 +2927,36 @@ fn test_minimum_1_6() {
|
|||||||
crate::tests::runner::run_test_case(&path, 1, 6).unwrap();
|
crate::tests::runner::run_test_case(&path, 1, 6).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_paths_0_0() {
|
||||||
|
let path = format!("{}/fixtures/paths.json", env!("CARGO_MANIFEST_DIR"));
|
||||||
|
crate::tests::runner::run_test_case(&path, 0, 0).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_paths_0_1() {
|
||||||
|
let path = format!("{}/fixtures/paths.json", env!("CARGO_MANIFEST_DIR"));
|
||||||
|
crate::tests::runner::run_test_case(&path, 0, 1).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_paths_0_2() {
|
||||||
|
let path = format!("{}/fixtures/paths.json", env!("CARGO_MANIFEST_DIR"));
|
||||||
|
crate::tests::runner::run_test_case(&path, 0, 2).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_paths_0_3() {
|
||||||
|
let path = format!("{}/fixtures/paths.json", env!("CARGO_MANIFEST_DIR"));
|
||||||
|
crate::tests::runner::run_test_case(&path, 0, 3).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_paths_0_4() {
|
||||||
|
let path = format!("{}/fixtures/paths.json", env!("CARGO_MANIFEST_DIR"));
|
||||||
|
crate::tests::runner::run_test_case(&path, 0, 4).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_one_of_0_0() {
|
fn test_one_of_0_0() {
|
||||||
let path = format!("{}/fixtures/oneOf.json", env!("CARGO_MANIFEST_DIR"));
|
let path = format!("{}/fixtures/oneOf.json", env!("CARGO_MANIFEST_DIR"));
|
||||||
@ -8560,3 +8596,9 @@ fn test_merger_0_10() {
|
|||||||
let path = format!("{}/fixtures/merger.json", env!("CARGO_MANIFEST_DIR"));
|
let path = format!("{}/fixtures/merger.json", env!("CARGO_MANIFEST_DIR"));
|
||||||
crate::tests::runner::run_test_case(&path, 0, 10).unwrap();
|
crate::tests::runner::run_test_case(&path, 0, 10).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_merger_0_11() {
|
||||||
|
let path = format!("{}/fixtures/merger.json", env!("CARGO_MANIFEST_DIR"));
|
||||||
|
crate::tests::runner::run_test_case(&path, 0, 11).unwrap();
|
||||||
|
}
|
||||||
|
|||||||
@ -134,12 +134,12 @@ fn test_library_api() {
|
|||||||
{
|
{
|
||||||
"code": "REQUIRED_FIELD_MISSING",
|
"code": "REQUIRED_FIELD_MISSING",
|
||||||
"message": "Missing name",
|
"message": "Missing name",
|
||||||
"details": { "path": "/name" }
|
"details": { "path": "name" }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"code": "STRICT_PROPERTY_VIOLATION",
|
"code": "STRICT_PROPERTY_VIOLATION",
|
||||||
"message": "Unexpected property 'wrong'",
|
"message": "Unexpected property 'wrong'",
|
||||||
"details": { "path": "/wrong" }
|
"details": { "path": "wrong" }
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|||||||
@ -41,6 +41,14 @@ impl<'a> ValidationContext<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn join_path(&self, key: &str) -> String {
|
||||||
|
if self.path.is_empty() {
|
||||||
|
key.to_string()
|
||||||
|
} else {
|
||||||
|
format!("{}/{}", self.path, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn derive(
|
pub fn derive(
|
||||||
&self,
|
&self,
|
||||||
schema: &'a Schema,
|
schema: &'a Schema,
|
||||||
|
|||||||
@ -91,12 +91,17 @@ impl<'a> ValidationContext<'a> {
|
|||||||
if let Some(ref prefix) = self.schema.prefix_items {
|
if let Some(ref prefix) = self.schema.prefix_items {
|
||||||
for (i, sub_schema) in prefix.iter().enumerate() {
|
for (i, sub_schema) in prefix.iter().enumerate() {
|
||||||
if i < len {
|
if i < len {
|
||||||
let path = format!("{}/{}", self.path, i);
|
|
||||||
if let Some(child_instance) = arr.get(i) {
|
if let Some(child_instance) = arr.get(i) {
|
||||||
|
let mut item_path = self.join_path(&i.to_string());
|
||||||
|
if let Some(obj) = child_instance.as_object() {
|
||||||
|
if let Some(id_str) = obj.get("id").and_then(|v| v.as_str()) {
|
||||||
|
item_path = self.join_path(id_str);
|
||||||
|
}
|
||||||
|
}
|
||||||
let derived = self.derive(
|
let derived = self.derive(
|
||||||
sub_schema,
|
sub_schema,
|
||||||
child_instance,
|
child_instance,
|
||||||
&path,
|
&item_path,
|
||||||
HashSet::new(),
|
HashSet::new(),
|
||||||
self.extensible,
|
self.extensible,
|
||||||
false,
|
false,
|
||||||
@ -112,12 +117,17 @@ impl<'a> ValidationContext<'a> {
|
|||||||
|
|
||||||
if let Some(ref items_schema) = self.schema.items {
|
if let Some(ref items_schema) = self.schema.items {
|
||||||
for i in validation_index..len {
|
for i in validation_index..len {
|
||||||
let path = format!("{}/{}", self.path, i);
|
|
||||||
if let Some(child_instance) = arr.get(i) {
|
if let Some(child_instance) = arr.get(i) {
|
||||||
|
let mut item_path = self.join_path(&i.to_string());
|
||||||
|
if let Some(obj) = child_instance.as_object() {
|
||||||
|
if let Some(id_str) = obj.get("id").and_then(|v| v.as_str()) {
|
||||||
|
item_path = self.join_path(id_str);
|
||||||
|
}
|
||||||
|
}
|
||||||
let derived = self.derive(
|
let derived = self.derive(
|
||||||
items_schema,
|
items_schema,
|
||||||
child_instance,
|
child_instance,
|
||||||
&path,
|
&item_path,
|
||||||
HashSet::new(),
|
HashSet::new(),
|
||||||
self.extensible,
|
self.extensible,
|
||||||
false,
|
false,
|
||||||
|
|||||||
@ -44,7 +44,7 @@ impl<'a> ValidationContext<'a> {
|
|||||||
result.errors.push(ValidationError {
|
result.errors.push(ValidationError {
|
||||||
code: "STRICT_PROPERTY_VIOLATION".to_string(),
|
code: "STRICT_PROPERTY_VIOLATION".to_string(),
|
||||||
message: format!("Unexpected property '{}'", key),
|
message: format!("Unexpected property '{}'", key),
|
||||||
path: format!("{}/{}", self.path, key),
|
path: self.join_path(key),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -53,10 +53,18 @@ impl<'a> ValidationContext<'a> {
|
|||||||
if let Some(arr) = self.instance.as_array() {
|
if let Some(arr) = self.instance.as_array() {
|
||||||
for i in 0..arr.len() {
|
for i in 0..arr.len() {
|
||||||
if !result.evaluated_indices.contains(&i) {
|
if !result.evaluated_indices.contains(&i) {
|
||||||
|
let mut item_path = self.join_path(&i.to_string());
|
||||||
|
if let Some(child_instance) = arr.get(i) {
|
||||||
|
if let Some(obj) = child_instance.as_object() {
|
||||||
|
if let Some(id_str) = obj.get("id").and_then(|v| v.as_str()) {
|
||||||
|
item_path = self.join_path(id_str);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
result.errors.push(ValidationError {
|
result.errors.push(ValidationError {
|
||||||
code: "STRICT_ITEM_VIOLATION".to_string(),
|
code: "STRICT_ITEM_VIOLATION".to_string(),
|
||||||
message: format!("Unexpected item at index {}", i),
|
message: format!("Unexpected item at index {}", i),
|
||||||
path: format!("{}/{}", self.path, i),
|
path: item_path,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -32,7 +32,7 @@ impl<'a> ValidationContext<'a> {
|
|||||||
"Type '{}' is not a valid descendant for this entity bound schema",
|
"Type '{}' is not a valid descendant for this entity bound schema",
|
||||||
type_str
|
type_str
|
||||||
),
|
),
|
||||||
path: format!("{}/type", self.path),
|
path: self.join_path("type"),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -70,7 +70,7 @@ impl<'a> ValidationContext<'a> {
|
|||||||
result.errors.push(ValidationError {
|
result.errors.push(ValidationError {
|
||||||
code: "REQUIRED_FIELD_MISSING".to_string(),
|
code: "REQUIRED_FIELD_MISSING".to_string(),
|
||||||
message: format!("Missing {}", field),
|
message: format!("Missing {}", field),
|
||||||
path: format!("{}/{}", self.path, field),
|
path: self.join_path(field),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -109,7 +109,7 @@ impl<'a> ValidationContext<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let Some(child_instance) = obj.get(key) {
|
if let Some(child_instance) = obj.get(key) {
|
||||||
let new_path = format!("{}/{}", self.path, key);
|
let new_path = self.join_path(key);
|
||||||
let is_ref = sub_schema.r#ref.is_some();
|
let is_ref = sub_schema.r#ref.is_some();
|
||||||
let next_extensible = if is_ref { false } else { self.extensible };
|
let next_extensible = if is_ref { false } else { self.extensible };
|
||||||
|
|
||||||
@ -147,7 +147,7 @@ impl<'a> ValidationContext<'a> {
|
|||||||
for (compiled_re, sub_schema) in compiled_pp {
|
for (compiled_re, sub_schema) in compiled_pp {
|
||||||
for (key, child_instance) in obj {
|
for (key, child_instance) in obj {
|
||||||
if compiled_re.0.is_match(key) {
|
if compiled_re.0.is_match(key) {
|
||||||
let new_path = format!("{}/{}", self.path, key);
|
let new_path = self.join_path(key);
|
||||||
let is_ref = sub_schema.r#ref.is_some();
|
let is_ref = sub_schema.r#ref.is_some();
|
||||||
let next_extensible = if is_ref { false } else { self.extensible };
|
let next_extensible = if is_ref { false } else { self.extensible };
|
||||||
|
|
||||||
@ -186,7 +186,7 @@ impl<'a> ValidationContext<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !locally_matched {
|
if !locally_matched {
|
||||||
let new_path = format!("{}/{}", self.path, key);
|
let new_path = self.join_path(key);
|
||||||
let is_ref = additional_schema.r#ref.is_some();
|
let is_ref = additional_schema.r#ref.is_some();
|
||||||
let next_extensible = if is_ref { false } else { self.extensible };
|
let next_extensible = if is_ref { false } else { self.extensible };
|
||||||
|
|
||||||
@ -207,7 +207,7 @@ impl<'a> ValidationContext<'a> {
|
|||||||
|
|
||||||
if let Some(ref property_names) = self.schema.property_names {
|
if let Some(ref property_names) = self.schema.property_names {
|
||||||
for key in obj.keys() {
|
for key in obj.keys() {
|
||||||
let _new_path = format!("{}/propertyNames/{}", self.path, key);
|
let _new_path = self.join_path(&format!("propertyNames/{}", key));
|
||||||
let val_str = Value::String(key.to_string());
|
let val_str = Value::String(key.to_string());
|
||||||
|
|
||||||
let ctx = ValidationContext::new(
|
let ctx = ValidationContext::new(
|
||||||
|
|||||||
Reference in New Issue
Block a user