Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bfe3dd2e8c | |||
| 609371c03c | |||
| 93d1315f0b | |||
| e407adf6a1 | |||
| 0b4607b7d4 | |||
| a53e89df52 |
152
append_test.py
Normal file
152
append_test.py
Normal 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
34
fix_everything.py
Normal 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
22
fix_expect.py
Normal 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)
|
||||||
|
|
||||||
63
fix_test.py
Normal file
63
fix_test.py
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import json
|
||||||
|
|
||||||
|
path = "fixtures/database.json"
|
||||||
|
|
||||||
|
with open(path, "r") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
|
||||||
|
test_case = data[-1]
|
||||||
|
|
||||||
|
test_case["database"]["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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
with open(path, "w") as f:
|
||||||
|
json.dump(data, f, indent=2)
|
||||||
|
|
||||||
@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
19
scratch.rs
Normal file
19
scratch.rs
Normal 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!");
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -60,10 +60,14 @@ impl Database {
|
|||||||
db.enums.insert(def.name.clone(), def);
|
db.enums.insert(def.name.clone(), def);
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
let name = item.get("name").and_then(|v| v.as_str()).unwrap_or("unknown");
|
||||||
errors.push(crate::drop::Error {
|
errors.push(crate::drop::Error {
|
||||||
code: "DATABASE_ENUM_PARSE_FAILED".to_string(),
|
code: "DATABASE_ENUM_PARSE_FAILED".to_string(),
|
||||||
message: format!("Failed to parse database enum: {}", e),
|
message: format!("Failed to parse database enum '{}': {}", name, e),
|
||||||
details: crate::drop::ErrorDetails::default(),
|
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);
|
db.types.insert(def.name.clone(), def);
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
let name = item.get("name").and_then(|v| v.as_str()).unwrap_or("unknown");
|
||||||
errors.push(crate::drop::Error {
|
errors.push(crate::drop::Error {
|
||||||
code: "DATABASE_TYPE_PARSE_FAILED".to_string(),
|
code: "DATABASE_TYPE_PARSE_FAILED".to_string(),
|
||||||
message: format!("Failed to parse database type: {}", e),
|
message: format!("Failed to parse database type '{}': {}", name, e),
|
||||||
details: crate::drop::ErrorDetails::default(),
|
details: crate::drop::ErrorDetails {
|
||||||
|
context: Some(serde_json::json!(name)),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -98,10 +106,14 @@ impl Database {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
let constraint = item.get("constraint").and_then(|v| v.as_str()).unwrap_or("unknown");
|
||||||
errors.push(crate::drop::Error {
|
errors.push(crate::drop::Error {
|
||||||
code: "DATABASE_RELATION_PARSE_FAILED".to_string(),
|
code: "DATABASE_RELATION_PARSE_FAILED".to_string(),
|
||||||
message: format!("Failed to parse database relation: {}", e),
|
message: format!("Failed to parse database relation '{}': {}", constraint, e),
|
||||||
details: crate::drop::ErrorDetails::default(),
|
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);
|
db.puncs.insert(def.name.clone(), def);
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
let name = item.get("name").and_then(|v| v.as_str()).unwrap_or("unknown");
|
||||||
errors.push(crate::drop::Error {
|
errors.push(crate::drop::Error {
|
||||||
code: "DATABASE_PUNC_PARSE_FAILED".to_string(),
|
code: "DATABASE_PUNC_PARSE_FAILED".to_string(),
|
||||||
message: format!("Failed to parse database punc: {}", e),
|
message: format!("Failed to parse database punc '{}': {}", name, e),
|
||||||
details: crate::drop::ErrorDetails::default(),
|
details: crate::drop::ErrorDetails {
|
||||||
|
context: Some(serde_json::json!(name)),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -135,7 +151,10 @@ impl Database {
|
|||||||
errors.push(crate::drop::Error {
|
errors.push(crate::drop::Error {
|
||||||
code: "DATABASE_SCHEMA_PARSE_FAILED".to_string(),
|
code: "DATABASE_SCHEMA_PARSE_FAILED".to_string(),
|
||||||
message: format!("Failed to parse database schema key '{}': {}", key, e),
|
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
|
// Abort relation discovery early if no hierarchical inheritance match was found
|
||||||
if matching_rels.is_empty() {
|
if matching_rels.is_empty() {
|
||||||
let mut details = crate::drop::ErrorDetails {
|
let mut details = crate::drop::ErrorDetails {
|
||||||
path: path.to_string(),
|
path: Some(path.to_string()),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
if let Some(sid) = schema_id {
|
if let Some(sid) = schema_id {
|
||||||
@ -381,7 +400,7 @@ impl Database {
|
|||||||
// and forces a clean structural error for the architect.
|
// and forces a clean structural error for the architect.
|
||||||
if !resolved {
|
if !resolved {
|
||||||
let mut details = crate::drop::ErrorDetails {
|
let mut details = crate::drop::ErrorDetails {
|
||||||
path: path.to_string(),
|
path: Some(path.to_string()),
|
||||||
context: serde_json::to_value(&matching_rels).ok(),
|
context: serde_json::to_value(&matching_rels).ok(),
|
||||||
cause: Some("Multiple conflicting constraints found matching prefixes".to_string()),
|
cause: Some("Multiple conflicting constraints found matching prefixes".to_string()),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
|
|||||||
@ -103,7 +103,7 @@ impl Schema {
|
|||||||
types
|
types
|
||||||
),
|
),
|
||||||
details: crate::drop::ErrorDetails {
|
details: crate::drop::ErrorDetails {
|
||||||
path: path.clone(),
|
path: Some(path.clone()),
|
||||||
schema: Some(root_id.to_string()),
|
schema: Some(root_id.to_string()),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
@ -427,7 +427,7 @@ impl Schema {
|
|||||||
code: "AMBIGUOUS_POLYMORPHISM".to_string(),
|
code: "AMBIGUOUS_POLYMORPHISM".to_string(),
|
||||||
message: format!("oneOf boundaries must map mathematically unique 'type' or 'kind' discriminators, or strictly contain disjoint primitive types."),
|
message: format!("oneOf boundaries must map mathematically unique 'type' or 'kind' discriminators, or strictly contain disjoint primitive types."),
|
||||||
details: crate::drop::ErrorDetails {
|
details: crate::drop::ErrorDetails {
|
||||||
path: path.to_string(),
|
path: Some(path.to_string()),
|
||||||
schema: Some(root_id.to_string()),
|
schema: Some(root_id.to_string()),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
@ -449,7 +449,7 @@ impl Schema {
|
|||||||
code: "POLYMORPHIC_COLLISION".to_string(),
|
code: "POLYMORPHIC_COLLISION".to_string(),
|
||||||
message: format!("Polymorphic boundary defines multiple candidates mapped to the identical discriminator value '{}'.", val),
|
message: format!("Polymorphic boundary defines multiple candidates mapped to the identical discriminator value '{}'.", val),
|
||||||
details: crate::drop::ErrorDetails {
|
details: crate::drop::ErrorDetails {
|
||||||
path: path.to_string(),
|
path: Some(path.to_string()),
|
||||||
schema: Some(root_id.to_string()),
|
schema: Some(root_id.to_string()),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
@ -491,7 +491,7 @@ impl Schema {
|
|||||||
c, field_name, id
|
c, field_name, id
|
||||||
),
|
),
|
||||||
details: crate::drop::ErrorDetails {
|
details: crate::drop::ErrorDetails {
|
||||||
path: path.to_string(),
|
path: Some(path.to_string()),
|
||||||
schema: Some(root_id.to_string()),
|
schema: Some(root_id.to_string()),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
@ -508,21 +508,24 @@ impl Schema {
|
|||||||
to_insert: &mut Vec<(String, Arc<Schema>)>,
|
to_insert: &mut Vec<(String, Arc<Schema>)>,
|
||||||
errors: &mut Vec<crate::drop::Error>,
|
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 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);
|
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 +533,6 @@ impl Schema {
|
|||||||
Self::validate_identifier(family, "$family", root_id, &path, errors);
|
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);
|
Self::collect_child_schemas(schema_arc, root_id, path, to_insert, errors);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -575,7 +574,9 @@ impl Schema {
|
|||||||
let mut map_opt = |opt: &Option<Arc<Schema>>, pass_path: bool, sub: &str| {
|
let mut map_opt = |opt: &Option<Arc<Schema>>, pass_path: bool, sub: &str| {
|
||||||
if let Some(v) = opt {
|
if let Some(v) = opt {
|
||||||
if pass_path {
|
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 {
|
} else {
|
||||||
Self::collect_child_schemas(v, root_id, format!("{}/{}", path, sub), to_insert, errors);
|
Self::collect_child_schemas(v, root_id, format!("{}/{}", path, sub), to_insert, errors);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -66,7 +66,8 @@ pub struct Error {
|
|||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
|
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
|
||||||
pub struct ErrorDetails {
|
pub struct ErrorDetails {
|
||||||
pub path: String,
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub path: Option<String>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub cause: Option<String>,
|
pub cause: Option<String>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
|||||||
@ -30,7 +30,7 @@ fn jspg_failure() -> JsonB {
|
|||||||
code: "ENGINE_NOT_INITIALIZED".to_string(),
|
code: "ENGINE_NOT_INITIALIZED".to_string(),
|
||||||
message: "JSPG extension has not been initialized via jspg_setup".to_string(),
|
message: "JSPG extension has not been initialized via jspg_setup".to_string(),
|
||||||
details: crate::drop::ErrorDetails {
|
details: crate::drop::ErrorDetails {
|
||||||
path: "".to_string(),
|
path: None,
|
||||||
cause: None,
|
cause: None,
|
||||||
context: None,
|
context: None,
|
||||||
schema: None,
|
schema: None,
|
||||||
|
|||||||
@ -31,7 +31,7 @@ impl Merger {
|
|||||||
code: "MERGE_FAILED".to_string(),
|
code: "MERGE_FAILED".to_string(),
|
||||||
message: format!("Unknown schema_id: {}", schema_id),
|
message: format!("Unknown schema_id: {}", schema_id),
|
||||||
details: crate::drop::ErrorDetails {
|
details: crate::drop::ErrorDetails {
|
||||||
path: "".to_string(),
|
path: None,
|
||||||
cause: None,
|
cause: None,
|
||||||
context: Some(data),
|
context: Some(data),
|
||||||
schema: None,
|
schema: None,
|
||||||
@ -76,7 +76,7 @@ impl Merger {
|
|||||||
code: final_code,
|
code: final_code,
|
||||||
message: final_message,
|
message: final_message,
|
||||||
details: crate::drop::ErrorDetails {
|
details: crate::drop::ErrorDetails {
|
||||||
path: "".to_string(),
|
path: None,
|
||||||
cause: final_cause,
|
cause: final_cause,
|
||||||
context: None,
|
context: None,
|
||||||
schema: None,
|
schema: None,
|
||||||
@ -92,7 +92,7 @@ impl Merger {
|
|||||||
code: "MERGE_FAILED".to_string(),
|
code: "MERGE_FAILED".to_string(),
|
||||||
message: format!("Executor Error in pre-ordered notify: {:?}", e),
|
message: format!("Executor Error in pre-ordered notify: {:?}", e),
|
||||||
details: crate::drop::ErrorDetails {
|
details: crate::drop::ErrorDetails {
|
||||||
path: "".to_string(),
|
path: None,
|
||||||
cause: None,
|
cause: None,
|
||||||
context: None,
|
context: None,
|
||||||
schema: None,
|
schema: None,
|
||||||
|
|||||||
@ -33,7 +33,7 @@ impl Queryer {
|
|||||||
code: "FILTER_PARSE_FAILED".to_string(),
|
code: "FILTER_PARSE_FAILED".to_string(),
|
||||||
message: msg.clone(),
|
message: msg.clone(),
|
||||||
details: crate::drop::ErrorDetails {
|
details: crate::drop::ErrorDetails {
|
||||||
path: "".to_string(), // filters apply to the root query
|
path: None, // filters apply to the root query
|
||||||
cause: Some(msg),
|
cause: Some(msg),
|
||||||
context: filters.cloned(),
|
context: filters.cloned(),
|
||||||
schema: Some(schema_id.to_string()),
|
schema: Some(schema_id.to_string()),
|
||||||
@ -138,7 +138,7 @@ impl Queryer {
|
|||||||
code: "QUERY_COMPILATION_FAILED".to_string(),
|
code: "QUERY_COMPILATION_FAILED".to_string(),
|
||||||
message: e.clone(),
|
message: e.clone(),
|
||||||
details: crate::drop::ErrorDetails {
|
details: crate::drop::ErrorDetails {
|
||||||
path: "".to_string(),
|
path: None,
|
||||||
cause: Some(e),
|
cause: Some(e),
|
||||||
context: None,
|
context: None,
|
||||||
schema: Some(schema_id.to_string()),
|
schema: Some(schema_id.to_string()),
|
||||||
@ -165,7 +165,7 @@ impl Queryer {
|
|||||||
code: "QUERY_FAILED".to_string(),
|
code: "QUERY_FAILED".to_string(),
|
||||||
message: format!("Expected array from generic query, got: {:?}", other),
|
message: format!("Expected array from generic query, got: {:?}", other),
|
||||||
details: crate::drop::ErrorDetails {
|
details: crate::drop::ErrorDetails {
|
||||||
path: "".to_string(),
|
path: None,
|
||||||
cause: Some(format!("Expected array, got {}", other)),
|
cause: Some(format!("Expected array, got {}", other)),
|
||||||
context: Some(serde_json::json!([sql])),
|
context: Some(serde_json::json!([sql])),
|
||||||
schema: Some(schema_id.to_string()),
|
schema: Some(schema_id.to_string()),
|
||||||
@ -175,7 +175,7 @@ impl Queryer {
|
|||||||
code: "QUERY_FAILED".to_string(),
|
code: "QUERY_FAILED".to_string(),
|
||||||
message: format!("SPI error in queryer: {}", e),
|
message: format!("SPI error in queryer: {}", e),
|
||||||
details: crate::drop::ErrorDetails {
|
details: crate::drop::ErrorDetails {
|
||||||
path: "".to_string(),
|
path: None,
|
||||||
cause: Some(format!("SPI error in queryer: {}", e)),
|
cause: Some(format!("SPI error in queryer: {}", e)),
|
||||||
context: Some(serde_json::json!([sql])),
|
context: Some(serde_json::json!([sql])),
|
||||||
schema: Some(schema_id.to_string()),
|
schema: Some(schema_id.to_string()),
|
||||||
|
|||||||
@ -3683,6 +3683,12 @@ fn test_database_4_0() {
|
|||||||
crate::tests::runner::run_test_case(&path, 4, 0).unwrap();
|
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]
|
#[test]
|
||||||
fn test_cases_0_0() {
|
fn test_cases_0_0() {
|
||||||
let path = format!("{}/fixtures/cases.json", env!("CARGO_MANIFEST_DIR"));
|
let path = format!("{}/fixtures/cases.json", env!("CARGO_MANIFEST_DIR"));
|
||||||
|
|||||||
@ -18,7 +18,7 @@ fn test_library_api() {
|
|||||||
"errors": [{
|
"errors": [{
|
||||||
"code": "ENGINE_NOT_INITIALIZED",
|
"code": "ENGINE_NOT_INITIALIZED",
|
||||||
"message": "JSPG extension has not been initialized via jspg_setup",
|
"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": {
|
"target_schema": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|||||||
@ -86,7 +86,7 @@ pub fn run_test_case(path: &str, suite_idx: usize, case_idx: usize) -> Result<()
|
|||||||
let error_messages: Vec<String> = drop
|
let error_messages: Vec<String> = drop
|
||||||
.errors
|
.errors
|
||||||
.iter()
|
.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();
|
.collect();
|
||||||
failures.push(format!(
|
failures.push(format!(
|
||||||
"[{}] Cannot run '{}' test '{}': System Setup Compilation structurally failed:\n{}",
|
"[{}] Cannot run '{}' test '{}': System Setup Compilation structurally failed:\n{}",
|
||||||
|
|||||||
@ -49,7 +49,13 @@ impl Case {
|
|||||||
Err(d) => d.clone(),
|
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> {
|
pub fn run_validate(&self, db: Arc<Database>) -> Result<(), String> {
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
pub mod pattern;
|
pub mod pattern;
|
||||||
pub mod sql;
|
pub mod sql;
|
||||||
pub mod drop;
|
pub mod drop;
|
||||||
|
pub mod schema;
|
||||||
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
@ -18,4 +19,6 @@ pub struct Expect {
|
|||||||
pub errors: Option<Vec<serde_json::Value>>,
|
pub errors: Option<Vec<serde_json::Value>>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub sql: Option<Vec<SqlExpectation>>,
|
pub sql: Option<Vec<SqlExpectation>>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub schemas: Option<Vec<String>>,
|
||||||
}
|
}
|
||||||
|
|||||||
27
src/tests/types/expect/schema.rs
Normal file
27
src/tests/types/expect/schema.rs
Normal 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(())
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -68,7 +68,7 @@ impl Validator {
|
|||||||
code: e.code,
|
code: e.code,
|
||||||
message: e.message,
|
message: e.message,
|
||||||
details: crate::drop::ErrorDetails {
|
details: crate::drop::ErrorDetails {
|
||||||
path: e.path,
|
path: Some(e.path),
|
||||||
cause: None,
|
cause: None,
|
||||||
context: None,
|
context: None,
|
||||||
schema: None,
|
schema: None,
|
||||||
@ -82,7 +82,7 @@ impl Validator {
|
|||||||
code: e.code,
|
code: e.code,
|
||||||
message: e.message,
|
message: e.message,
|
||||||
details: crate::drop::ErrorDetails {
|
details: crate::drop::ErrorDetails {
|
||||||
path: e.path,
|
path: Some(e.path),
|
||||||
cause: None,
|
cause: None,
|
||||||
context: None,
|
context: None,
|
||||||
schema: None,
|
schema: None,
|
||||||
@ -94,7 +94,7 @@ impl Validator {
|
|||||||
code: "SCHEMA_NOT_FOUND".to_string(),
|
code: "SCHEMA_NOT_FOUND".to_string(),
|
||||||
message: format!("Schema {} not found", schema_id),
|
message: format!("Schema {} not found", schema_id),
|
||||||
details: crate::drop::ErrorDetails {
|
details: crate::drop::ErrorDetails {
|
||||||
path: "/".to_string(),
|
path: Some("/".to_string()),
|
||||||
cause: None,
|
cause: None,
|
||||||
context: None,
|
context: None,
|
||||||
schema: None,
|
schema: None,
|
||||||
|
|||||||
Reference in New Issue
Block a user