diff --git a/fixtures/merger.json b/fixtures/merger.json index 25ed43e..f9898df 100644 --- a/fixtures/merger.json +++ b/fixtures/merger.json @@ -869,6 +869,106 @@ "notify": true, "relationship": false }, + { + "name": "account", + "hierarchy": [ + "account", + "entity" + ], + "fields": [ + "id", + "type", + "name", + "archived", + "created_at", + "created_by", + "modified_at", + "modified_by", + "kind", + "routing_number", + "card_number" + ], + "grouped_fields": { + "entity": [ + "id", + "type", + "name", + "archived", + "created_at", + "created_by", + "modified_at", + "modified_by" + ], + "account": [ + "id", + "type", + "kind", + "routing_number", + "card_number" + ] + }, + "field_types": { + "id": "uuid", + "type": "text", + "archived": "boolean", + "name": "text", + "created_at": "timestamptz", + "created_by": "uuid", + "modified_at": "timestamptz", + "modified_by": "uuid", + "kind": "text", + "routing_number": "text", + "card_number": "text" + }, + "schemas": { + "account": { + "type": "entity", + "properties": { + "kind": { + "type": "string" + } + }, + "cases": [ + { + "when": { + "properties": { + "kind": { + "const": "checking" + } + } + }, + "then": { + "properties": { + "routing_number": { + "type": "string" + } + } + } + }, + { + "when": { + "properties": { + "kind": { + "const": "credit" + } + } + }, + "then": { + "properties": { + "card_number": { + "type": "string" + } + } + } + } + ] + } + }, + "lookup_fields": [], + "historical": true, + "notify": true, + "relationship": false + }, { "name": "invoice", "schemas": { @@ -3107,6 +3207,103 @@ ] ] } + }, + { + "description": "Insert account with checking kind and routing number", + "action": "merge", + "schema_id": "account", + "data": { + "id": "11111111-2222-3333-4444-555555555555", + "type": "account", + "kind": "checking", + "routing_number": "123456789" + }, + "expect": { + "success": true, + "sql": [ + [ + "SELECT to_jsonb(t1.*) || to_jsonb(t2.*)", + "FROM agreego.\"account\" t1", + "LEFT JOIN agreego.\"entity\" t2 ON t2.id = t1.id", + "WHERE t1.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',", + " 'account'", + ")" + ], + [ + "INSERT INTO agreego.\"account\" (", + " \"id\",", + " \"kind\",", + " \"routing_number\",", + " \"type\"", + ")", + "VALUES (", + " '11111111-2222-3333-4444-555555555555',", + " 'checking',", + " '123456789',", + " 'account'", + ")" + ], + [ + "INSERT INTO agreego.change (", + " \"old\",", + " \"new\",", + " entity_id,", + " id,", + " kind,", + " modified_at,", + " modified_by", + ")", + "VALUES (", + " NULL,", + " '{", + " \"kind\":\"checking\",", + " \"routing_number\":\"123456789\",", + " \"type\":\"account\"", + " }',", + " '11111111-2222-3333-4444-555555555555',", + " '{{uuid}}',", + " 'create',", + " '{{timestamp}}',", + " '00000000-0000-0000-0000-000000000000'", + ")" + ], + [ + "SELECT pg_notify('entity', '{", + " \"complete\":{", + " \"created_at\":\"{{timestamp}}\",", + " \"created_by\":\"00000000-0000-0000-0000-000000000000\",", + " \"id\":\"11111111-2222-3333-4444-555555555555\",", + " \"kind\":\"checking\",", + " \"modified_at\":\"{{timestamp}}\",", + " \"modified_by\":\"00000000-0000-0000-0000-000000000000\",", + " \"routing_number\":\"123456789\",", + " \"type\":\"account\"", + " },", + " \"new\":{", + " \"kind\":\"checking\",", + " \"routing_number\":\"123456789\",", + " \"type\":\"account\"", + " }", + " }')" + ] + ] + } } ] } diff --git a/fixtures/queryer.json b/fixtures/queryer.json index 338eaf1..fb671bd 100644 --- a/fixtures/queryer.json +++ b/fixtures/queryer.json @@ -924,6 +924,91 @@ } } }, + { + "name": "account", + "hierarchy": [ + "account", + "entity" + ], + "fields": [ + "id", + "type", + "kind", + "archived", + "created_at", + "routing_number", + "card_number" + ], + "grouped_fields": { + "entity": [ + "id", + "type", + "archived", + "created_at" + ], + "account": [ + "kind", + "routing_number", + "card_number" + ] + }, + "field_types": { + "id": "uuid", + "type": "text", + "kind": "text", + "archived": "boolean", + "created_at": "timestamptz", + "routing_number": "text", + "card_number": "text" + }, + "variations": [ + "account" + ], + "schemas": { + "account": { + "type": "entity", + "properties": { + "kind": { + "type": "string" + } + }, + "cases": [ + { + "when": { + "properties": { + "kind": { + "const": "checking" + } + } + }, + "then": { + "properties": { + "routing_number": { + "type": "string" + } + } + } + }, + { + "when": { + "properties": { + "kind": { + "const": "credit" + } + } + }, + "then": { + "properties": { + "card_number": { + "type": "string" + } + } + } + } + ] + } + } + }, { "name": "invoice", "schemas": { @@ -2149,6 +2234,30 @@ ] ] } + }, + { + "description": "Account select on full schema with cases fields", + "action": "query", + "schema_id": "account", + "expect": { + "success": true, + "sql": [ + [ + "(SELECT jsonb_strip_nulls((SELECT jsonb_build_object(", + " 'archived', entity_2.archived,", + " 'card_number', account_1.card_number,", + " 'created_at', entity_2.created_at,", + " 'id', entity_2.id,", + " 'kind', account_1.kind,", + " 'routing_number', account_1.routing_number,", + " 'type', entity_2.type", + ")", + "FROM agreego.account account_1", + "JOIN agreego.entity entity_2 ON entity_2.id = account_1.id", + "WHERE NOT entity_2.archived)))" + ] + ] + } } ] } diff --git a/src/database/object.rs b/src/database/object.rs index 479250a..fdaf74f 100644 --- a/src/database/object.rs +++ b/src/database/object.rs @@ -154,7 +154,7 @@ pub struct SchemaObject { #[serde(skip_serializing_if = "Option::is_none")] pub extensible: Option, - #[serde(rename = "compiledProperties")] + #[serde(rename = "compiledPropertyNames")] #[serde(skip_deserializing)] #[serde(skip_serializing_if = "crate::database::object::is_once_lock_vec_empty")] #[serde(serialize_with = "crate::database::object::serialize_once_lock")] diff --git a/src/database/schema.rs b/src/database/schema.rs index 93cd33e..dc2e226 100644 --- a/src/database/schema.rs +++ b/src/database/schema.rs @@ -117,13 +117,34 @@ impl Schema { } } - // 3. Set the OnceLock! + // 3. Add cases conditionally-defined properties recursively + 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), errors); + } + if let Some(child) = &c.then { + child.compile(db, root_id, format!("{}/cases/{}/then", path, i), errors); + if let Some(t_props) = child.obj.compiled_properties.get() { + props.extend(t_props.clone()); + } + } + if let Some(child) = &c.else_ { + child.compile(db, root_id, format!("{}/cases/{}/else", path, i), errors); + if let Some(e_props) = child.obj.compiled_properties.get() { + props.extend(e_props.clone()); + } + } + } + } + + // 4. Set the OnceLock! let _ = self.obj.compiled_properties.set(props.clone()); let mut names: Vec = props.keys().cloned().collect(); names.sort(); let _ = self.obj.compiled_property_names.set(names); - // 4. Compute Edges natively + // 5. Compute Edges natively let schema_edges = self.compile_edges(db, root_id, &path, &props, errors); let _ = self.obj.compiled_edges.set(schema_edges); @@ -151,22 +172,12 @@ 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), - errors, - ); + child.compile(db, root_id, format!("{}/oneOf/{}", path, i), 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), - errors, - ); + child.compile(db, root_id, format!("{}/prefixItems/{}", path, i), errors); } } if let Some(child) = &self.obj.not { @@ -175,34 +186,6 @@ impl Schema { if let Some(child) = &self.obj.contains { child.compile(db, root_id, format!("{}/contains", path), errors); } - 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), - errors, - ); - } - if let Some(child) = &c.then { - child.compile( - db, - root_id, - format!("{}/cases/{}/then", path, i), - errors, - ); - } - if let Some(child) = &c.else_ { - child.compile( - db, - root_id, - format!("{}/cases/{}/else", path, i), - errors, - ); - } - } - } self.compile_polymorphism(db, root_id, &path, errors); } @@ -285,7 +268,9 @@ impl Schema { if let Some(c_type) = child_type_name { // Skip edge compilation for JSONB columns — they store data inline, not relationally. // The physical column type from field_types is the single source of truth. - if let Some(ft) = type_def.field_types.as_ref() + if let Some(ft) = type_def + .field_types + .as_ref() .and_then(|v| v.get(prop_name.as_str())) .and_then(|v| v.as_str()) { @@ -296,12 +281,7 @@ 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), - errors, - ); + target_schema.compile(db, root_id, format!("{}/{}", path, prop_name), errors); if let Some(compiled_target_props) = target_schema.obj.compiled_properties.get() { let keys_for_ambiguity: Vec = diff --git a/src/tests/fixtures.rs b/src/tests/fixtures.rs index 3d06ef4..727f2c6 100644 --- a/src/tests/fixtures.rs +++ b/src/tests/fixtures.rs @@ -1457,6 +1457,12 @@ fn test_queryer_0_13() { crate::tests::runner::run_test_case(&path, 0, 13).unwrap(); } +#[test] +fn test_queryer_0_14() { + let path = format!("{}/fixtures/queryer.json", env!("CARGO_MANIFEST_DIR")); + crate::tests::runner::run_test_case(&path, 0, 14).unwrap(); +} + #[test] fn test_polymorphism_0_0() { let path = format!("{}/fixtures/polymorphism.json", env!("CARGO_MANIFEST_DIR")); @@ -8122,3 +8128,9 @@ 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(); } + +#[test] +fn test_merger_0_14() { + let path = format!("{}/fixtures/merger.json", env!("CARGO_MANIFEST_DIR")); + crate::tests::runner::run_test_case(&path, 0, 14).unwrap(); +} diff --git a/src/tests/mod.rs b/src/tests/mod.rs index dfc959b..58499cb 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -95,11 +95,11 @@ fn test_library_api() { "name": { "type": "string" }, "target": { "type": "target_schema", - "compiledProperties": ["value"] + "compiledPropertyNames": ["value"] } }, "required": ["name"], - "compiledProperties": ["name", "target", "type"], + "compiledPropertyNames": ["name", "target", "type"], "compiledEdges": { "target": { "constraint": "fk_test_target", @@ -112,7 +112,7 @@ fn test_library_api() { "properties": { "value": { "type": "number" } }, - "compiledProperties": ["value"] + "compiledPropertyNames": ["value"] } } })