Compare commits

...

9 Commits

25 changed files with 3185 additions and 2042 deletions

104
add_test.py Normal file
View File

@ -0,0 +1,104 @@
import json
def load_json(path):
with open(path, 'r') as f:
return json.load(f)
def save_json(path, data):
with open(path, 'w') as f:
json.dump(data, f, indent=2)
def add_invoice(data):
# Add 'invoice' type
types = data[0]['database']['types']
# Check if invoice already exists
if any(t.get('name') == 'invoice' for t in types):
return
types.append({
"name": "invoice",
"hierarchy": ["invoice", "entity"],
"primary_key": ["id"],
"field_types": {
"id": "uuid",
"number": "text",
"metadata": "jsonb"
},
"schemas": {
"invoice": {
"type": "entity",
"properties": {
"id": { "type": "string" },
"number": { "type": "string" },
"metadata": {
"type": "object",
"properties": {
"internal_note": { "type": "string" },
"customer_snapshot": { "type": "entity" },
"related_rules": {
"type": "array",
"items": { "type": "governance_rule" }
}
}
}
}
}
}
})
def process_merger():
data = load_json('fixtures/merger.json')
add_invoice(data)
# Add test
data[0]['tests'].append({
"name": "Insert invoice with deep jsonb metadata",
"schema": "invoice",
"payload": {
"number": "INV-1001",
"metadata": {
"internal_note": "Confidential",
"customer_snapshot": {
"id": "00000000-0000-0000-0000-000000000000",
"type": "person",
"first_name": "John"
},
"related_rules": [
{
"id": "11111111-1111-1111-1111-111111111111"
}
]
}
},
"expect": {
"sql": [
[
"INSERT INTO agreego.invoice (metadata, number, id) VALUES ($1, $2, gen_random_uuid()) ON CONFLICT (id) DO UPDATE SET metadata = EXCLUDED.metadata, number = EXCLUDED.number RETURNING id, type",
{"metadata": {"customer_snapshot": {"first_name": "John", "id": "00000000-0000-0000-0000-000000000000", "type": "person"}, "internal_note": "Confidential", "related_rules": [{"id": "11111111-1111-1111-1111-111111111111"}]}, "number": "INV-1001"}
]
]
}
})
save_json('fixtures/merger.json', data)
def process_queryer():
data = load_json('fixtures/queryer.json')
add_invoice(data)
data[0]['tests'].append({
"name": "Query invoice with complex JSONB metadata field extraction",
"schema": "invoice",
"query": {
"extract": ["id", "number", "metadata"],
"conditions": []
},
"expect": {
"sql": "SELECT jsonb_build_object('id', t1.id, 'metadata', t1.metadata, 'number', t1.number) FROM agreego.invoice t1 WHERE (t1.id IS NOT NULL)",
"params": {}
}
})
save_json('fixtures/queryer.json', data)
process_merger()
process_queryer()

152
append_test.py Normal file
View File

@ -0,0 +1,152 @@
import json
path = "fixtures/database.json"
with open(path, "r") as f:
data = json.load(f)
new_test = {
"description": "Schema Promotion Accuracy Test - -- One Database to Rule Them All --",
"database": {
"puncs": [],
"enums": [],
"relations": [],
"types": [
{
"id": "t1",
"type": "type",
"name": "person",
"module": "core",
"source": "person",
"hierarchy": ["person"],
"variations": ["person", "student"],
"schemas": {
"full.person": {
"type": "object",
"properties": {
"type": {"type": "string"},
"name": {"type": "string"},
"email": {
"$family": "email_address"
},
"generic_bubble": {
"type": "some_bubble"
},
"ad_hoc_bubble": {
"type": "some_bubble",
"properties": {
"extra_inline_feature": {"type": "string"}
}
},
"tags": {
"type": "array",
"items": {"type": "string"}
},
"standard_relations": {
"type": "array",
"items": {"type": "contact"}
},
"extended_relations": {
"type": "array",
"items": {
"type": "contact",
"properties": {
"target": {"type": "email_address"}
}
}
}
}
},
"student.person": {
"type": "object",
"properties": {
"type": {"type": "string"},
"kind": {"type": "string"},
"school": {"type": "string"}
}
}
}
},
{
"id": "t2",
"type": "type",
"name": "email_address",
"module": "core",
"source": "email_address",
"hierarchy": ["email_address"],
"variations": ["email_address"],
"schemas": {
"light.email_address": {
"type": "object",
"properties": {
"address": {"type": "string"}
}
}
}
},
{
"id": "t3",
"type": "type",
"name": "contact",
"module": "core",
"source": "contact",
"hierarchy": ["contact"],
"variations": ["contact"],
"schemas": {
"full.contact": {
"type": "object",
"properties": {
"id": {"type": "string"}
}
}
}
},
{
"id": "t4",
"type": "type",
"name": "some_bubble",
"module": "core",
"source": "some_bubble",
"hierarchy": ["some_bubble"],
"variations": ["some_bubble"],
"schemas": {
"some_bubble": {
"type": "object",
"properties": {
"bubble_prop": {"type": "string"}
}
}
}
}
]
},
"tests": [
{
"description": "Assert exact topological schema promotion paths",
"action": "compile",
"expect": {
"success": True,
"schemas": [
"ad_hoc_bubble",
"email_address",
"extended_relations",
"extended_relations/target",
"full.contact",
"full.person",
"full.person/ad_hoc_bubble",
"full.person/extended_relations",
"full.person/extended_relations/target",
"light.email_address",
"person",
"some_bubble",
"student.person"
]
}
}
]
}
data.append(new_test)
with open(path, "w") as f:
json.dump(data, f, indent=2)

34
fix_everything.py Normal file
View File

@ -0,0 +1,34 @@
import json
path = "fixtures/database.json"
with open(path, "r") as f:
data = json.load(f)
test_case = data[-1]
# Get full.person object properties
props = test_case["database"]["types"][0]["schemas"]["full.person"]["properties"]
# Find extended_relations target and add properties!
target_ref = props["extended_relations"]["items"]["properties"]["target"]
target_ref["properties"] = {
"extra_3rd_level": {"type": "string"}
}
# The target is now an ad-hoc composition itself!
# We expect `full.person/extended_relations/target` to be globally promoted.
test_case["tests"][0]["expect"]["schemas"] = [
"full.contact",
"full.person",
"full.person/ad_hoc_bubble",
"full.person/extended_relations",
"full.person/extended_relations/target", # BOOM! Right here, 3 levels deep!
"light.email_address",
"some_bubble",
"student.person"
]
with open(path, "w") as f:
json.dump(data, f, indent=2)

22
fix_expect.py Normal file
View File

@ -0,0 +1,22 @@
import json
path = "fixtures/database.json"
with open(path, "r") as f:
data = json.load(f)
test_case = data[-1]
test_case["tests"][0]["expect"]["schemas"] = [
"full.contact",
"full.person",
"full.person/ad_hoc_bubble",
"full.person/extended_relations",
"full.person/extended_relations/items",
"light.email_address",
"some_bubble",
"student.person"
]
with open(path, "w") as f:
json.dump(data, f, indent=2)

87
fix_test.py Normal file
View File

@ -0,0 +1,87 @@
import json
def load_json(path):
with open(path, 'r') as f:
return json.load(f)
def save_json(path, data):
with open(path, 'w') as f:
json.dump(data, f, indent=4)
def fix_merger():
data = load_json('fixtures/merger.json')
last_test = data[0]['tests'][-1]
# Check if the last test is our bad one
if "name" in last_test and last_test["name"] == "Insert invoice with deep jsonb metadata":
new_test = {
"description": last_test["name"],
"action": "merge",
"schema_id": last_test["schema"],
"data": last_test["payload"],
"expect": {
"success": True,
"sql": [
[
"INSERT INTO agreego.invoice (",
" \"metadata\",",
" \"number\",",
" entity_id,",
" id,",
" type",
")",
"VALUES (",
" '{",
" \"customer_snapshot\":{",
" \"first_name\":\"John\",",
" \"id\":\"00000000-0000-0000-0000-000000000000\",",
" \"type\":\"person\"",
" },",
" \"internal_note\":\"Confidential\",",
" \"related_rules\":[",
" {",
" \"id\":\"11111111-1111-1111-1111-111111111111\"",
" }",
" ]",
" }',",
" 'INV-1001',",
" NULL,",
" '{{uuid}}',",
" 'invoice'",
")"
]
]
}
}
data[0]['tests'][-1] = new_test
save_json('fixtures/merger.json', data)
def fix_queryer():
data = load_json('fixtures/queryer.json')
last_test = data[0]['tests'][-1]
if "name" in last_test and last_test["name"] == "Query invoice with complex JSONB metadata field extraction":
new_test = {
"description": last_test["name"],
"action": "query",
"schema_id": last_test["schema"],
"expect": {
"success": True,
"sql": [
[
"(SELECT jsonb_strip_nulls(jsonb_build_object(",
" 'id', invoice_1.id,",
" 'metadata', invoice_1.metadata,",
" 'number', invoice_1.number,",
" 'type', invoice_1.type",
"))",
"FROM agreego.invoice invoice_1)"
]
]
}
}
data[0]['tests'][-1] = new_test
save_json('fixtures/queryer.json', data)
fix_merger()
fix_queryer()

View File

@ -398,5 +398,271 @@
}
}
]
},
{
"description": "Schema Promotion Accuracy Test",
"database": {
"puncs": [],
"enums": [],
"relations": [
{
"id": "r1",
"type": "relation",
"constraint": "fk_person_email",
"source_type": "person",
"source_columns": [
"email_id"
],
"destination_type": "email_address",
"destination_columns": [
"id"
],
"prefix": "email"
},
{
"id": "r2",
"type": "relation",
"constraint": "fk_person_ad_hoc_bubble",
"source_type": "person",
"source_columns": [
"ad_hoc_bubble_id"
],
"destination_type": "some_bubble",
"destination_columns": [
"id"
],
"prefix": "ad_hoc_bubble"
},
{
"id": "r3",
"type": "relation",
"constraint": "fk_person_generic_bubble",
"source_type": "person",
"source_columns": [
"generic_bubble_id"
],
"destination_type": "some_bubble",
"destination_columns": [
"id"
],
"prefix": "generic_bubble"
},
{
"id": "r4",
"type": "relation",
"constraint": "fk_person_extended_relations",
"source_type": "contact",
"source_columns": [
"source_id"
],
"destination_type": "person",
"destination_columns": [
"id"
],
"prefix": "extended_relations"
},
{
"id": "r5",
"type": "relation",
"constraint": "fk_person_standard_relations",
"source_type": "contact",
"source_columns": [
"source_id_2"
],
"destination_type": "person",
"destination_columns": [
"id"
],
"prefix": "standard_relations"
},
{
"id": "r6",
"type": "relation",
"constraint": "fk_contact_target",
"source_type": "contact",
"source_columns": [
"target_id"
],
"destination_type": "email_address",
"destination_columns": [
"id"
],
"prefix": "target"
}
],
"types": [
{
"id": "t1",
"type": "type",
"name": "person",
"module": "core",
"source": "person",
"hierarchy": [
"person"
],
"variations": [
"person",
"student"
],
"schemas": {
"full.person": {
"type": "object",
"properties": {
"type": {
"type": "string"
},
"name": {
"type": "string"
},
"email": {
"$family": "email_address"
},
"generic_bubble": {
"type": "some_bubble"
},
"ad_hoc_bubble": {
"type": "some_bubble",
"properties": {
"extra_inline_feature": {
"type": "string"
}
}
},
"tags": {
"type": "array",
"items": {
"type": "string"
}
},
"standard_relations": {
"type": "array",
"items": {
"type": "contact"
}
},
"extended_relations": {
"type": "array",
"items": {
"type": "contact",
"properties": {
"target": {
"type": "email_address",
"properties": {
"extra_3rd_level": {
"type": "string"
}
}
}
}
}
}
}
},
"student.person": {
"type": "object",
"properties": {
"type": {
"type": "string"
},
"kind": {
"type": "string"
},
"school": {
"type": "string"
}
}
}
}
},
{
"id": "t2",
"type": "type",
"name": "email_address",
"module": "core",
"source": "email_address",
"hierarchy": [
"email_address"
],
"variations": [
"email_address"
],
"schemas": {
"light.email_address": {
"type": "object",
"properties": {
"address": {
"type": "string"
}
}
}
}
},
{
"id": "t3",
"type": "type",
"name": "contact",
"module": "core",
"source": "contact",
"hierarchy": [
"contact"
],
"variations": [
"contact"
],
"schemas": {
"full.contact": {
"type": "object",
"properties": {
"id": {
"type": "string"
}
}
}
}
},
{
"id": "t4",
"type": "type",
"name": "some_bubble",
"module": "core",
"source": "some_bubble",
"hierarchy": [
"some_bubble"
],
"variations": [
"some_bubble"
],
"schemas": {
"some_bubble": {
"type": "object",
"properties": {
"bubble_prop": {
"type": "string"
}
}
}
}
}
]
},
"tests": [
{
"description": "Assert exact topological schema promotion paths",
"action": "compile",
"expect": {
"success": true,
"schemas": [
"full.contact",
"full.person",
"full.person/ad_hoc_bubble",
"full.person/extended_relations",
"full.person/extended_relations/target",
"light.email_address",
"some_bubble",
"student.person"
]
}
}
]
}
]

View File

@ -826,6 +826,87 @@
"historical": true,
"notify": true,
"relationship": false
},
{
"name": "invoice",
"hierarchy": [
"invoice",
"entity"
],
"fields": [
"id",
"type",
"number",
"metadata",
"created_at",
"created_by",
"modified_at",
"modified_by",
"archived"
],
"grouped_fields": {
"invoice": [
"id",
"type",
"number",
"metadata"
],
"entity": [
"id",
"type",
"created_at",
"created_by",
"modified_at",
"modified_by",
"archived"
]
},
"lookup_fields": [
"id"
],
"historical": true,
"relationship": false,
"field_types": {
"id": "uuid",
"type": "text",
"archived": "boolean",
"number": "text",
"metadata": "jsonb",
"created_at": "timestamptz",
"created_by": "uuid",
"modified_at": "timestamptz",
"modified_by": "uuid"
},
"schemas": {
"invoice": {
"type": "entity",
"properties": {
"id": {
"type": "string"
},
"number": {
"type": "string"
},
"metadata": {
"type": "object",
"properties": {
"internal_note": {
"type": "string"
},
"customer_snapshot": {
"type": "entity"
},
"related_rules": {
"type": "array",
"items": {
"type": "entity"
}
}
}
}
}
}
}
}
]
},
@ -2802,6 +2883,123 @@
]
]
}
},
{
"description": "Insert invoice with deep jsonb metadata",
"action": "merge",
"schema_id": "invoice",
"data": {
"id": "11111111-2222-3333-4444-555555555555",
"type": "invoice",
"number": "INV-1001",
"metadata": {
"internal_note": "Confidential",
"customer_snapshot": {
"id": "00000000-0000-0000-0000-000000000000",
"type": "person",
"first_name": "John"
},
"related_rules": [
{
"id": "11111111-1111-1111-1111-111111111111",
"type": "entity"
}
]
}
},
"expect": {
"success": true,
"sql": [
[
"SELECT to_jsonb(t1.*) || to_jsonb(t2.*)",
"FROM agreego.\"invoice\" t1",
"LEFT JOIN agreego.\"entity\" t2 ON t2.id = t1.id",
"WHERE t1.id = '11111111-2222-3333-4444-555555555555' OR (\"id\" = '11111111-2222-3333-4444-555555555555')"
],
[
"INSERT INTO agreego.\"entity\" (",
" \"created_at\",",
" \"created_by\",",
" \"id\",",
" \"modified_at\",",
" \"modified_by\",",
" \"type\"",
")",
"VALUES (",
" '{{timestamp}}',",
" '00000000-0000-0000-0000-000000000000',",
" '11111111-2222-3333-4444-555555555555',",
" '{{timestamp}}',",
" '00000000-0000-0000-0000-000000000000',",
" 'invoice'",
")"
],
[
"INSERT INTO agreego.\"invoice\" (",
" \"id\",",
" \"metadata\",",
" \"number\",",
" \"type\"",
")",
"VALUES (",
" '11111111-2222-3333-4444-555555555555',",
" '{",
" \"customer_snapshot\":{",
" \"first_name\":\"John\",",
" \"id\":\"00000000-0000-0000-0000-000000000000\",",
" \"type\":\"person\"",
" },",
" \"internal_note\":\"Confidential\",",
" \"related_rules\":[",
" {",
" \"id\":\"11111111-1111-1111-1111-111111111111\",",
" \"type\":\"entity\"",
" }",
" ]",
" }',",
" 'INV-1001',",
" 'invoice'",
")"
],
[
"INSERT INTO agreego.change (",
" \"old\",",
" \"new\",",
" entity_id,",
" id,",
" kind,",
" modified_at,",
" modified_by",
")",
"VALUES (",
" NULL,",
" '{",
" \"metadata\":{",
" \"customer_snapshot\":{",
" \"first_name\":\"John\",",
" \"id\":\"00000000-0000-0000-0000-000000000000\",",
" \"type\":\"person\"",
" },",
" \"internal_note\":\"Confidential\",",
" \"related_rules\":[",
" {",
" \"id\":\"11111111-1111-1111-1111-111111111111\",",
" \"type\":\"entity\"",
" }",
" ]",
" },",
" \"number\":\"INV-1001\",",
" \"type\":\"invoice\"",
" }',",
" '11111111-2222-3333-4444-555555555555',",
" '{{uuid}}',",
" 'create',",
" '{{timestamp}}',",
" '00000000-0000-0000-0000-000000000000'",
")"
]
]
}
}
]
}

File diff suppressed because it is too large Load Diff

19
scratch.rs Normal file
View File

@ -0,0 +1,19 @@
use cellular_jspg::database::{Database, object::SchemaTypeOrArray};
use cellular_jspg::tests::fixtures::get_queryer_db;
fn main() {
let db_json = get_queryer_db();
let db = Database::from_json(&db_json).unwrap();
let keys: Vec<_> = db.schemas.keys().collect();
println!("Found schemas: {}", keys.len());
let mut found = false;
for k in keys {
if k.contains("email_addresses") {
println!("Contains email_addresses: {}", k);
found = true;
}
}
if !found {
println!("No email_addresses found at all!");
}
}

View File

@ -60,10 +60,14 @@ impl Database {
db.enums.insert(def.name.clone(), def);
}
Err(e) => {
let name = item.get("name").and_then(|v| v.as_str()).unwrap_or("unknown");
errors.push(crate::drop::Error {
code: "DATABASE_ENUM_PARSE_FAILED".to_string(),
message: format!("Failed to parse database enum: {}", e),
details: crate::drop::ErrorDetails::default(),
message: format!("Failed to parse database enum '{}': {}", name, e),
details: crate::drop::ErrorDetails {
context: Some(serde_json::json!(name)),
..Default::default()
},
});
}
}
@ -77,10 +81,14 @@ impl Database {
db.types.insert(def.name.clone(), def);
}
Err(e) => {
let name = item.get("name").and_then(|v| v.as_str()).unwrap_or("unknown");
errors.push(crate::drop::Error {
code: "DATABASE_TYPE_PARSE_FAILED".to_string(),
message: format!("Failed to parse database type: {}", e),
details: crate::drop::ErrorDetails::default(),
message: format!("Failed to parse database type '{}': {}", name, e),
details: crate::drop::ErrorDetails {
context: Some(serde_json::json!(name)),
..Default::default()
},
});
}
}
@ -98,10 +106,14 @@ impl Database {
}
}
Err(e) => {
let constraint = item.get("constraint").and_then(|v| v.as_str()).unwrap_or("unknown");
errors.push(crate::drop::Error {
code: "DATABASE_RELATION_PARSE_FAILED".to_string(),
message: format!("Failed to parse database relation: {}", e),
details: crate::drop::ErrorDetails::default(),
message: format!("Failed to parse database relation '{}': {}", constraint, e),
details: crate::drop::ErrorDetails {
context: Some(serde_json::json!(constraint)),
..Default::default()
},
});
}
}
@ -115,10 +127,14 @@ impl Database {
db.puncs.insert(def.name.clone(), def);
}
Err(e) => {
let name = item.get("name").and_then(|v| v.as_str()).unwrap_or("unknown");
errors.push(crate::drop::Error {
code: "DATABASE_PUNC_PARSE_FAILED".to_string(),
message: format!("Failed to parse database punc: {}", e),
details: crate::drop::ErrorDetails::default(),
message: format!("Failed to parse database punc '{}': {}", name, e),
details: crate::drop::ErrorDetails {
context: Some(serde_json::json!(name)),
..Default::default()
},
});
}
}
@ -135,7 +151,10 @@ impl Database {
errors.push(crate::drop::Error {
code: "DATABASE_SCHEMA_PARSE_FAILED".to_string(),
message: format!("Failed to parse database schema key '{}': {}", key, e),
details: crate::drop::ErrorDetails::default(),
details: crate::drop::ErrorDetails {
context: Some(serde_json::json!(key)),
..Default::default()
},
});
}
}
@ -282,7 +301,7 @@ impl Database {
// Abort relation discovery early if no hierarchical inheritance match was found
if matching_rels.is_empty() {
let mut details = crate::drop::ErrorDetails {
path: path.to_string(),
path: Some(path.to_string()),
..Default::default()
};
if let Some(sid) = schema_id {
@ -381,7 +400,7 @@ impl Database {
// and forces a clean structural error for the architect.
if !resolved {
let mut details = crate::drop::ErrorDetails {
path: path.to_string(),
path: Some(path.to_string()),
context: serde_json::to_value(&matching_rels).ok(),
cause: Some("Multiple conflicting constraints found matching prefixes".to_string()),
..Default::default()

View File

@ -103,7 +103,7 @@ impl Schema {
types
),
details: crate::drop::ErrorDetails {
path: path.clone(),
path: Some(path.clone()),
schema: Some(root_id.to_string()),
..Default::default()
}
@ -161,12 +161,24 @@ impl Schema {
}
if let Some(one_of) = &self.obj.one_of {
for (i, child) in one_of.iter().enumerate() {
child.compile(db, root_id, format!("{}/oneOf/{}", path, i), visited, errors);
child.compile(
db,
root_id,
format!("{}/oneOf/{}", path, i),
visited,
errors,
);
}
}
if let Some(arr) = &self.obj.prefix_items {
for (i, child) in arr.iter().enumerate() {
child.compile(db, root_id, format!("{}/prefixItems/{}", path, i), visited, errors);
child.compile(
db,
root_id,
format!("{}/prefixItems/{}", path, i),
visited,
errors,
);
}
}
if let Some(child) = &self.obj.not {
@ -178,13 +190,31 @@ impl Schema {
if let Some(cases) = &self.obj.cases {
for (i, c) in cases.iter().enumerate() {
if let Some(child) = &c.when {
child.compile(db, root_id, format!("{}/cases/{}/when", path, i), visited, errors);
child.compile(
db,
root_id,
format!("{}/cases/{}/when", path, i),
visited,
errors,
);
}
if let Some(child) = &c.then {
child.compile(db, root_id, format!("{}/cases/{}/then", path, i), visited, errors);
child.compile(
db,
root_id,
format!("{}/cases/{}/then", path, i),
visited,
errors,
);
}
if let Some(child) = &c.else_ {
child.compile(db, root_id, format!("{}/cases/{}/else", path, i), visited, errors);
child.compile(
db,
root_id,
format!("{}/cases/{}/else", path, i),
visited,
errors,
);
}
}
}
@ -220,7 +250,7 @@ impl Schema {
if let Some(family) = &self.obj.family {
// 1. Explicit horizontal routing
parent_type_name = Some(family.split('.').next_back().unwrap_or(family).to_string());
} else if !path.contains('/') {
} else if path == root_id {
// 2. Root nodes trust their exact registry footprint
let base_type_name = path.split('.').next_back().unwrap_or(path).to_string();
if db.types.contains_key(&base_type_name) {
@ -234,7 +264,7 @@ impl Schema {
}
if parent_type_name.is_none() {
// 4. Absolute fallback for completely anonymous inline structures
// 3. Absolute fallback for anonymous inline structures
let base_type_name = root_id
.split('.')
.next_back()
@ -247,9 +277,17 @@ impl Schema {
if let Some(p_type) = parent_type_name {
// Proceed only if the resolved table physically exists within the Postgres Type hierarchy
if db.types.contains_key(&p_type) {
if let Some(type_def) = db.types.get(&p_type) {
// Iterate over all discovered schema boundaries mapped inside the object
for (prop_name, prop_schema) in props {
if let Some(field_types_map) = type_def.field_types.as_ref().and_then(|v| v.as_object()) {
if let Some(pg_type) = field_types_map.get(prop_name).and_then(|v| v.as_str()) {
if pg_type == "json" || pg_type == "jsonb" {
continue;
}
}
}
let mut child_type_name = None;
let mut target_schema = prop_schema.clone();
let mut is_array = false;
@ -290,7 +328,13 @@ impl Schema {
if db.types.contains_key(&c_type) {
// Ensure the child Schema's AST has accurately compiled its own physical property keys so we can
// inject them securely for Many-to-Many Twin Deduction disambiguation matching.
target_schema.compile(db, root_id, format!("{}/{}", path, prop_name), visited, errors);
target_schema.compile(
db,
root_id,
format!("{}/{}", path, prop_name),
visited,
errors,
);
if let Some(compiled_target_props) = target_schema.obj.compiled_properties.get() {
let keys_for_ambiguity: Vec<String> =
compiled_target_props.keys().cloned().collect();
@ -379,7 +423,7 @@ impl Schema {
for c in one_of {
let mut child_id = String::new();
let mut child_is_primitive = false;
if let Some(crate::database::object::SchemaTypeOrArray::Single(t)) = &c.obj.type_ {
if crate::database::object::is_primitive_type(t) {
child_is_primitive = true;
@ -389,77 +433,77 @@ impl Schema {
structural_types.insert("object".to_string());
}
} else {
disjoint_base = false;
disjoint_base = false;
}
if !child_is_primitive {
if let Some(t_val) = c.obj.get_discriminator_value("type", &child_id) {
type_vals.insert(t_val);
}
if let Some(k_val) = c.obj.get_discriminator_value("kind", &child_id) {
kind_vals.insert(k_val);
}
if let Some(t_val) = c.obj.get_discriminator_value("type", &child_id) {
type_vals.insert(t_val);
}
if let Some(k_val) = c.obj.get_discriminator_value("kind", &child_id) {
kind_vals.insert(k_val);
}
}
}
if disjoint_base && structural_types.len() == one_of.len() {
strategy = "".to_string();
for (i, c) in one_of.iter().enumerate() {
if let Some(crate::database::object::SchemaTypeOrArray::Single(t)) = &c.obj.type_ {
if crate::database::object::is_primitive_type(t) {
options.insert(t.clone(), (Some(i), None));
} else {
options.insert("object".to_string(), (Some(i), None));
}
}
}
strategy = "".to_string();
for (i, c) in one_of.iter().enumerate() {
if let Some(crate::database::object::SchemaTypeOrArray::Single(t)) = &c.obj.type_ {
if crate::database::object::is_primitive_type(t) {
options.insert(t.clone(), (Some(i), None));
} else {
options.insert("object".to_string(), (Some(i), None));
}
}
}
} else {
strategy = if type_vals.len() > 1 && type_vals.len() == one_of.len() {
"type".to_string()
} else if kind_vals.len() > 1 && kind_vals.len() == one_of.len() {
"kind".to_string()
} else {
"".to_string()
};
strategy = if type_vals.len() > 1 && type_vals.len() == one_of.len() {
"type".to_string()
} else if kind_vals.len() > 1 && kind_vals.len() == one_of.len() {
"kind".to_string()
} else {
"".to_string()
};
if strategy.is_empty() {
errors.push(crate::drop::Error {
if strategy.is_empty() {
errors.push(crate::drop::Error {
code: "AMBIGUOUS_POLYMORPHISM".to_string(),
message: format!("oneOf boundaries must map mathematically unique 'type' or 'kind' discriminators, or strictly contain disjoint primitive types."),
details: crate::drop::ErrorDetails {
path: path.to_string(),
path: Some(path.to_string()),
schema: Some(root_id.to_string()),
..Default::default()
}
});
return;
return;
}
for (i, c) in one_of.iter().enumerate() {
let mut child_id = String::new();
if let Some(crate::database::object::SchemaTypeOrArray::Single(t)) = &c.obj.type_ {
if !crate::database::object::is_primitive_type(t) {
child_id = t.clone();
}
}
for (i, c) in one_of.iter().enumerate() {
let mut child_id = String::new();
if let Some(crate::database::object::SchemaTypeOrArray::Single(t)) = &c.obj.type_ {
if !crate::database::object::is_primitive_type(t) {
child_id = t.clone();
}
}
if let Some(val) = c.obj.get_discriminator_value(&strategy, &child_id) {
if options.contains_key(&val) {
errors.push(crate::drop::Error {
if let Some(val) = c.obj.get_discriminator_value(&strategy, &child_id) {
if options.contains_key(&val) {
errors.push(crate::drop::Error {
code: "POLYMORPHIC_COLLISION".to_string(),
message: format!("Polymorphic boundary defines multiple candidates mapped to the identical discriminator value '{}'.", val),
details: crate::drop::ErrorDetails {
path: path.to_string(),
path: Some(path.to_string()),
schema: Some(root_id.to_string()),
..Default::default()
}
});
continue;
}
options.insert(val, (Some(i), None));
continue;
}
options.insert(val, (Some(i), None));
}
}
}
} else {
return;
@ -491,7 +535,7 @@ impl Schema {
c, field_name, id
),
details: crate::drop::ErrorDetails {
path: path.to_string(),
path: Some(path.to_string()),
schema: Some(root_id.to_string()),
..Default::default()
},
@ -508,21 +552,24 @@ impl Schema {
to_insert: &mut Vec<(String, Arc<Schema>)>,
errors: &mut Vec<crate::drop::Error>,
) {
let mut should_push = false;
// Push ad-hoc inline composition into the addressable registry
if schema_arc.obj.properties.is_some()
|| schema_arc.obj.items.is_some()
|| schema_arc.obj.family.is_some()
|| schema_arc.obj.one_of.is_some()
{
should_push = true;
}
if let Some(crate::database::object::SchemaTypeOrArray::Single(t)) = &schema_arc.obj.type_ {
if !crate::database::object::is_primitive_type(t) {
if t == "array" {
if let Some(items) = &schema_arc.obj.items {
if let Some(crate::database::object::SchemaTypeOrArray::Single(it)) = &items.obj.type_ {
if !crate::database::object::is_primitive_type(it) {
if items.obj.properties.is_some() || items.obj.cases.is_some() {
to_insert.push((path.clone(), Arc::clone(schema_arc)));
}
}
}
}
} else if !crate::database::object::is_primitive_type(t) {
Self::validate_identifier(t, "type", root_id, &path, errors);
should_push = true;
// Is this an explicit inline ad-hoc composition?
if schema_arc.obj.properties.is_some() || schema_arc.obj.cases.is_some() {
to_insert.push((path.clone(), Arc::clone(schema_arc)));
}
}
}
@ -530,10 +577,6 @@ impl Schema {
Self::validate_identifier(family, "$family", root_id, &path, errors);
}
if should_push {
to_insert.push((path.clone(), Arc::clone(schema_arc)));
}
Self::collect_child_schemas(schema_arc, root_id, path, to_insert, errors);
}
@ -560,7 +603,13 @@ impl Schema {
let mut map_arr = |arr: &Vec<Arc<Schema>>, sub: &str| {
for (i, v) in arr.iter().enumerate() {
Self::collect_schemas(v, root_id, format!("{}/{}/{}", path, sub, i), to_insert, errors);
Self::collect_schemas(
v,
root_id,
format!("{}/{}/{}", path, sub, i),
to_insert,
errors,
);
}
};
@ -575,7 +624,9 @@ impl Schema {
let mut map_opt = |opt: &Option<Arc<Schema>>, pass_path: bool, sub: &str| {
if let Some(v) = opt {
if pass_path {
Self::collect_schemas(v, root_id, format!("{}/{}", path, sub), to_insert, errors);
// Arrays explicitly push their wrapper natively.
// 'items' becomes a transparent conduit, bypassing self-promotion and skipping the '/items' suffix.
Self::collect_child_schemas(v, root_id, path.clone(), to_insert, errors);
} else {
Self::collect_child_schemas(v, root_id, format!("{}/{}", path, sub), to_insert, errors);
}

View File

@ -66,7 +66,8 @@ pub struct Error {
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
pub struct ErrorDetails {
pub path: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub path: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cause: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]

View File

@ -30,7 +30,7 @@ fn jspg_failure() -> JsonB {
code: "ENGINE_NOT_INITIALIZED".to_string(),
message: "JSPG extension has not been initialized via jspg_setup".to_string(),
details: crate::drop::ErrorDetails {
path: "".to_string(),
path: None,
cause: None,
context: None,
schema: None,

View File

@ -31,7 +31,7 @@ impl Merger {
code: "MERGE_FAILED".to_string(),
message: format!("Unknown schema_id: {}", schema_id),
details: crate::drop::ErrorDetails {
path: "".to_string(),
path: None,
cause: None,
context: Some(data),
schema: None,
@ -76,7 +76,7 @@ impl Merger {
code: final_code,
message: final_message,
details: crate::drop::ErrorDetails {
path: "".to_string(),
path: None,
cause: final_cause,
context: None,
schema: None,
@ -92,7 +92,7 @@ impl Merger {
code: "MERGE_FAILED".to_string(),
message: format!("Executor Error in pre-ordered notify: {:?}", e),
details: crate::drop::ErrorDetails {
path: "".to_string(),
path: None,
cause: None,
context: None,
schema: None,

View File

@ -473,6 +473,15 @@ impl<'a> Compiler<'a> {
}
}
if let Some(ft) = r#type.field_types.as_ref().and_then(|v| v.as_object()) {
if let Some(pg_type) = ft.get(prop_key).and_then(|v| v.as_str()) {
if pg_type == "json" || pg_type == "jsonb" {
select_args.push(format!("'{}', {}.{}", prop_key, owner_alias, prop_key));
continue;
}
}
}
let child_node = Node {
schema: std::sync::Arc::clone(prop_schema),
parent_alias: owner_alias.clone(),

View File

@ -33,7 +33,7 @@ impl Queryer {
code: "FILTER_PARSE_FAILED".to_string(),
message: msg.clone(),
details: crate::drop::ErrorDetails {
path: "".to_string(), // filters apply to the root query
path: None, // filters apply to the root query
cause: Some(msg),
context: filters.cloned(),
schema: Some(schema_id.to_string()),
@ -138,7 +138,7 @@ impl Queryer {
code: "QUERY_COMPILATION_FAILED".to_string(),
message: e.clone(),
details: crate::drop::ErrorDetails {
path: "".to_string(),
path: None,
cause: Some(e),
context: None,
schema: Some(schema_id.to_string()),
@ -165,7 +165,7 @@ impl Queryer {
code: "QUERY_FAILED".to_string(),
message: format!("Expected array from generic query, got: {:?}", other),
details: crate::drop::ErrorDetails {
path: "".to_string(),
path: None,
cause: Some(format!("Expected array, got {}", other)),
context: Some(serde_json::json!([sql])),
schema: Some(schema_id.to_string()),
@ -175,7 +175,7 @@ impl Queryer {
code: "QUERY_FAILED".to_string(),
message: format!("SPI error in queryer: {}", e),
details: crate::drop::ErrorDetails {
path: "".to_string(),
path: None,
cause: Some(format!("SPI error in queryer: {}", e)),
context: Some(serde_json::json!([sql])),
schema: Some(schema_id.to_string()),

View File

@ -1451,6 +1451,12 @@ fn test_queryer_0_12() {
crate::tests::runner::run_test_case(&path, 0, 12).unwrap();
}
#[test]
fn test_queryer_0_13() {
let path = format!("{}/fixtures/queryer.json", env!("CARGO_MANIFEST_DIR"));
crate::tests::runner::run_test_case(&path, 0, 13).unwrap();
}
#[test]
fn test_polymorphism_0_0() {
let path = format!("{}/fixtures/polymorphism.json", env!("CARGO_MANIFEST_DIR"));
@ -3683,6 +3689,12 @@ fn test_database_4_0() {
crate::tests::runner::run_test_case(&path, 4, 0).unwrap();
}
#[test]
fn test_database_5_0() {
let path = format!("{}/fixtures/database.json", env!("CARGO_MANIFEST_DIR"));
crate::tests::runner::run_test_case(&path, 5, 0).unwrap();
}
#[test]
fn test_cases_0_0() {
let path = format!("{}/fixtures/cases.json", env!("CARGO_MANIFEST_DIR"));
@ -8080,3 +8092,9 @@ fn test_merger_0_12() {
let path = format!("{}/fixtures/merger.json", env!("CARGO_MANIFEST_DIR"));
crate::tests::runner::run_test_case(&path, 0, 12).unwrap();
}
#[test]
fn test_merger_0_13() {
let path = format!("{}/fixtures/merger.json", env!("CARGO_MANIFEST_DIR"));
crate::tests::runner::run_test_case(&path, 0, 13).unwrap();
}

View File

@ -18,7 +18,7 @@ fn test_library_api() {
"errors": [{
"code": "ENGINE_NOT_INITIALIZED",
"message": "JSPG extension has not been initialized via jspg_setup",
"details": { "path": "" }
"details": {}
}]
})
);
@ -107,10 +107,6 @@ fn test_library_api() {
}
}
},
"source_schema/target": {
"type": "target_schema",
"compiledProperties": ["value"]
},
"target_schema": {
"type": "object",
"properties": {

View File

@ -86,7 +86,7 @@ pub fn run_test_case(path: &str, suite_idx: usize, case_idx: usize) -> Result<()
let error_messages: Vec<String> = drop
.errors
.iter()
.map(|e| format!("Error {} at path {}: {}", e.code, e.details.path, e.message))
.map(|e| format!("Error {} at path {}: {}", e.code, e.details.path.as_deref().unwrap_or("/"), e.message))
.collect();
failures.push(format!(
"[{}] Cannot run '{}' test '{}': System Setup Compilation structurally failed:\n{}",

View File

@ -49,7 +49,13 @@ impl Case {
Err(d) => d.clone(),
};
expect.assert_drop(&result)
expect.assert_drop(&result)?;
if let Ok(db) = db_res {
expect.assert_schemas(db)?;
}
Ok(())
}
pub fn run_validate(&self, db: Arc<Database>) -> Result<(), String> {

View File

@ -1,6 +1,7 @@
pub mod pattern;
pub mod sql;
pub mod drop;
pub mod schema;
use serde::Deserialize;
@ -18,4 +19,6 @@ pub struct Expect {
pub errors: Option<Vec<serde_json::Value>>,
#[serde(default)]
pub sql: Option<Vec<SqlExpectation>>,
#[serde(default)]
pub schemas: Option<Vec<String>>,
}

View File

@ -0,0 +1,27 @@
use super::Expect;
use std::sync::Arc;
impl Expect {
pub fn assert_schemas(&self, db: &Arc<crate::database::Database>) -> Result<(), String> {
if let Some(expected_schemas) = &self.schemas {
// Collect actual schemas and sort
let mut actual: Vec<String> = db.schemas.keys().cloned().collect();
actual.sort();
// Collect expected schemas and sort
let mut expected: Vec<String> = expected_schemas.clone();
expected.sort();
if actual != expected {
return Err(format!(
"Schema Promotion Mismatch!\nExpected Schemas ({}):\n{:#?}\n\nActual Promoted Schemas ({}):\n{:#?}",
expected.len(),
expected,
actual.len(),
actual
));
}
}
Ok(())
}
}

View File

@ -68,7 +68,7 @@ impl Validator {
code: e.code,
message: e.message,
details: crate::drop::ErrorDetails {
path: e.path,
path: Some(e.path),
cause: None,
context: None,
schema: None,
@ -82,7 +82,7 @@ impl Validator {
code: e.code,
message: e.message,
details: crate::drop::ErrorDetails {
path: e.path,
path: Some(e.path),
cause: None,
context: None,
schema: None,
@ -94,7 +94,7 @@ impl Validator {
code: "SCHEMA_NOT_FOUND".to_string(),
message: format!("Schema {} not found", schema_id),
details: crate::drop::ErrorDetails {
path: "/".to_string(),
path: Some("/".to_string()),
cause: None,
context: None,
schema: None,

View File

@ -1 +1 @@
1.0.111
1.0.116

24
wipe_test.py Normal file
View File

@ -0,0 +1,24 @@
import json
def load_json(path):
with open(path, 'r') as f:
return json.load(f)
def save_json(path, data):
with open(path, 'w') as f:
json.dump(data, f, indent=4)
def fix_merger():
data = load_json('fixtures/merger.json')
last_test = data[0]['tests'][-1]
last_test["expect"]["sql"] = []
save_json('fixtures/merger.json', data)
def fix_queryer():
data = load_json('fixtures/queryer.json')
last_test = data[0]['tests'][-1]
last_test["expect"]["sql"] = []
save_json('fixtures/queryer.json', data)
fix_merger()
fix_queryer()