diff --git a/append_test.py b/append_test.py new file mode 100644 index 0000000..9c6e357 --- /dev/null +++ b/append_test.py @@ -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) + diff --git a/fix_everything.py b/fix_everything.py new file mode 100644 index 0000000..3b86a03 --- /dev/null +++ b/fix_everything.py @@ -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) + diff --git a/fix_expect.py b/fix_expect.py new file mode 100644 index 0000000..2cfa967 --- /dev/null +++ b/fix_expect.py @@ -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) + diff --git a/fix_test.py b/fix_test.py new file mode 100644 index 0000000..9a82e32 --- /dev/null +++ b/fix_test.py @@ -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) + diff --git a/fixtures/database.json b/fixtures/database.json index 51dd349..6a07651 100644 --- a/fixtures/database.json +++ b/fixtures/database.json @@ -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" + ] + } + } + ] } ] \ No newline at end of file diff --git a/scratch.rs b/scratch.rs new file mode 100644 index 0000000..2d7bb72 --- /dev/null +++ b/scratch.rs @@ -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!"); + } +} diff --git a/src/database/schema.rs b/src/database/schema.rs index 1de96a6..3d55224 100644 --- a/src/database/schema.rs +++ b/src/database/schema.rs @@ -508,21 +508,24 @@ impl Schema { to_insert: &mut Vec<(String, Arc)>, errors: &mut Vec, ) { - 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 +533,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); } @@ -575,7 +574,9 @@ impl Schema { let mut map_opt = |opt: &Option>, 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); } diff --git a/src/tests/fixtures.rs b/src/tests/fixtures.rs index 51c80da..5901b93 100644 --- a/src/tests/fixtures.rs +++ b/src/tests/fixtures.rs @@ -3683,6 +3683,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")); diff --git a/src/tests/mod.rs b/src/tests/mod.rs index d9ee539..16a7fc3 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -107,10 +107,6 @@ fn test_library_api() { } } }, - "source_schema/target": { - "type": "target_schema", - "compiledProperties": ["value"] - }, "target_schema": { "type": "object", "properties": { diff --git a/src/tests/types/case.rs b/src/tests/types/case.rs index b71dd80..5124b41 100644 --- a/src/tests/types/case.rs +++ b/src/tests/types/case.rs @@ -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) -> Result<(), String> { diff --git a/src/tests/types/expect/mod.rs b/src/tests/types/expect/mod.rs index a08695f..744e43a 100644 --- a/src/tests/types/expect/mod.rs +++ b/src/tests/types/expect/mod.rs @@ -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>, #[serde(default)] pub sql: Option>, + #[serde(default)] + pub schemas: Option>, } diff --git a/src/tests/types/expect/schema.rs b/src/tests/types/expect/schema.rs new file mode 100644 index 0000000..807c17d --- /dev/null +++ b/src/tests/types/expect/schema.rs @@ -0,0 +1,27 @@ +use super::Expect; +use std::sync::Arc; + +impl Expect { + pub fn assert_schemas(&self, db: &Arc) -> Result<(), String> { + if let Some(expected_schemas) = &self.schemas { + // Collect actual schemas and sort + let mut actual: Vec = db.schemas.keys().cloned().collect(); + actual.sort(); + + // Collect expected schemas and sort + let mut expected: Vec = 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(()) + } +}