diff --git a/src/database/compile/collection.rs b/src/database/compile/collection.rs new file mode 100644 index 0000000..c3a68da --- /dev/null +++ b/src/database/compile/collection.rs @@ -0,0 +1,163 @@ +use crate::database::schema::Schema; +use std::sync::Arc; + +impl Schema { + #[allow(unused_variables)] + pub(crate) fn validate_identifier( + id: &str, + field_name: &str, + root_id: &str, + path: &str, + errors: &mut Vec, + ) { + #[cfg(not(test))] + for c in id.chars() { + if !c.is_ascii_lowercase() && !c.is_ascii_digit() && c != '_' && c != '.' { + errors.push(crate::drop::Error { + code: "INVALID_IDENTIFIER".to_string(), + message: format!( + "Invalid character '{}' in JSON Schema '{}' property: '{}'. Identifiers must exclusively contain [a-z0-9_.]", + c, field_name, id + ), + details: crate::drop::ErrorDetails { + path: Some(path.to_string()), + schema: Some(root_id.to_string()), + ..Default::default() + }, + }); + return; + } + } + } + + pub fn collect_schemas( + schema_arc: &Arc, + root_id: &str, + path: String, + to_insert: &mut Vec<(String, Arc)>, + errors: &mut Vec, + ) { + if let Some(crate::database::object::SchemaTypeOrArray::Single(t)) = &schema_arc.obj.type_ { + 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); + + // 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))); + } + } + } + + if let Some(family) = &schema_arc.obj.family { + Self::validate_identifier(family, "family", root_id, &path, errors); + } + + Self::collect_child_schemas(schema_arc, root_id, path, to_insert, errors); + } + + pub fn collect_child_schemas( + schema_arc: &Arc, + root_id: &str, + path: String, + to_insert: &mut Vec<(String, Arc)>, + errors: &mut Vec, + ) { + if let Some(props) = &schema_arc.obj.properties { + for (k, v) in props.iter() { + let next_path = format!("{}/{}", path, k); + Self::collect_schemas(v, root_id, next_path, to_insert, errors); + } + } + + if let Some(pattern_props) = &schema_arc.obj.pattern_properties { + for (k, v) in pattern_props.iter() { + let next_path = format!("{}/{}", path, k); + Self::collect_schemas(v, root_id, next_path, to_insert, errors); + } + } + + let mut map_arr = |arr: &Vec>, sub: &str| { + for (i, v) in arr.iter().enumerate() { + Self::collect_schemas( + v, + root_id, + format!("{}/{}/{}", path, sub, i), + to_insert, + errors, + ); + } + }; + + if let Some(arr) = &schema_arc.obj.prefix_items { + map_arr(arr, "prefixItems"); + } + + if let Some(arr) = &schema_arc.obj.one_of { + map_arr(arr, "oneOf"); + } + + let mut map_opt = |opt: &Option>, pass_path: bool, sub: &str| { + if let Some(v) = opt { + if pass_path { + // 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); + } + } + }; + + map_opt( + &schema_arc.obj.additional_properties, + false, + "additionalProperties", + ); + map_opt(&schema_arc.obj.items, true, "items"); + map_opt(&schema_arc.obj.not, false, "not"); + map_opt(&schema_arc.obj.contains, false, "contains"); + map_opt(&schema_arc.obj.property_names, false, "propertyNames"); + + if let Some(cases) = &schema_arc.obj.cases { + for (i, c) in cases.iter().enumerate() { + if let Some(when) = &c.when { + Self::collect_schemas( + when, + root_id, + format!("{}/cases/{}/when", path, i), + to_insert, + errors, + ); + } + if let Some(then) = &c.then { + Self::collect_schemas( + then, + root_id, + format!("{}/cases/{}/then", path, i), + to_insert, + errors, + ); + } + if let Some(else_) = &c.else_ { + Self::collect_schemas( + else_, + root_id, + format!("{}/cases/{}/else", path, i), + to_insert, + errors, + ); + } + } + } + } +} diff --git a/src/database/compile/edges.rs b/src/database/compile/edges.rs new file mode 100644 index 0000000..9356880 --- /dev/null +++ b/src/database/compile/edges.rs @@ -0,0 +1,128 @@ +use crate::database::schema::Schema; + +impl Schema { + /// Dynamically infers and compiles all structural database relationships between this Schema + /// and its nested children. This functions recursively traverses the JSON Schema abstract syntax + /// tree, identifies physical PostgreSQL table boundaries, and locks the resulting relation + /// constraint paths directly onto the `compiled_edges` map in O(1) memory. + pub fn compile_edges( + &self, + db: &crate::database::Database, + root_id: &str, + path: &str, + props: &std::collections::BTreeMap>, + errors: &mut Vec, + ) -> std::collections::BTreeMap { + let mut schema_edges = std::collections::BTreeMap::new(); + + // Determine the physical Database Table Name this schema structurally represents + // Plucks the polymorphic discriminator via dot-notation (e.g. extracting "person" from "full.person") + let mut parent_type_name = None; + + 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 == 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) { + parent_type_name = Some(base_type_name); + } + } else if let Some(crate::database::object::SchemaTypeOrArray::Single(t)) = &self.obj.type_ { + // 3. Nested graphs trust their explicit struct pointer reference + if !crate::database::object::is_primitive_type(t) { + parent_type_name = Some(t.split('.').next_back().unwrap_or(t).to_string()); + } + } + + if let Some(p_type) = parent_type_name { + // Proceed only if the resolved table physically exists within the Postgres Type hierarchy + 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 { + let mut child_type_name = None; + let mut target_schema = prop_schema.clone(); + let mut is_array = false; + + // Structurally unpack the inner target entity if the object maps to an array list + if let Some(crate::database::object::SchemaTypeOrArray::Single(t)) = + &prop_schema.obj.type_ + { + if t == "array" { + is_array = true; + if let Some(items) = &prop_schema.obj.items { + target_schema = items.clone(); + } + } + } + + // Determine the physical Postgres table backing the nested child schema recursively + if let Some(family) = &target_schema.obj.family { + child_type_name = Some(family.split('.').next_back().unwrap_or(family).to_string()); + } else if let Some(crate::database::object::SchemaTypeOrArray::Single(t)) = + &target_schema.obj.type_ + { + if !crate::database::object::is_primitive_type(t) { + child_type_name = Some(t.split('.').next_back().unwrap_or(t).to_string()); + } + } else if let Some(arr) = &target_schema.obj.one_of { + if let Some(first) = arr.first() { + if let Some(crate::database::object::SchemaTypeOrArray::Single(t)) = &first.obj.type_ + { + if !crate::database::object::is_primitive_type(t) { + child_type_name = Some(t.split('.').next_back().unwrap_or(t).to_string()); + } + } + } + } + + 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() + .and_then(|v| v.get(prop_name.as_str())) + .and_then(|v| v.as_str()) + { + if ft == "jsonb" { + continue; + } + } + 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); + + if let Some(compiled_target_props) = target_schema.obj.compiled_properties.get() { + let keys_for_ambiguity: Vec = + compiled_target_props.keys().cloned().collect(); + + // Interrogate the Database catalog graph to discover the exact Foreign Key Constraint connecting the components + if let Some((relation, is_forward)) = db.resolve_relation( + &p_type, + &c_type, + prop_name, + Some(&keys_for_ambiguity), + is_array, + Some(root_id), + &format!("{}/{}", path, prop_name), + errors, + ) { + schema_edges.insert( + prop_name.clone(), + crate::database::edge::Edge { + constraint: relation.constraint.clone(), + forward: is_forward, + }, + ); + } + } + } + } + } + } + } + schema_edges + } +} diff --git a/src/database/compile/mod.rs b/src/database/compile/mod.rs new file mode 100644 index 0000000..a3661c0 --- /dev/null +++ b/src/database/compile/mod.rs @@ -0,0 +1,174 @@ +pub mod collection; +pub mod edges; +pub mod polymorphism; + +use crate::database::schema::Schema; + +impl Schema { + pub fn compile( + &self, + db: &crate::database::Database, + root_id: &str, + path: String, + errors: &mut Vec, + ) { + if self.obj.compiled_properties.get().is_some() { + return; + } + + if let Some(format_str) = &self.obj.format { + if let Some(fmt) = crate::database::formats::FORMATS.get(format_str.as_str()) { + let _ = self + .obj + .compiled_format + .set(crate::database::object::CompiledFormat::Func(fmt.func)); + } + } + + if let Some(pattern_str) = &self.obj.pattern { + if let Ok(re) = regex::Regex::new(pattern_str) { + let _ = self + .obj + .compiled_pattern + .set(crate::database::object::CompiledRegex(re)); + } + } + + if let Some(pattern_props) = &self.obj.pattern_properties { + let mut compiled = Vec::new(); + for (k, v) in pattern_props { + if let Ok(re) = regex::Regex::new(k) { + compiled.push((crate::database::object::CompiledRegex(re), v.clone())); + } + } + if !compiled.is_empty() { + let _ = self.obj.compiled_pattern_properties.set(compiled); + } + } + + let mut props = std::collections::BTreeMap::new(); + + // 1. Resolve INHERITANCE dependencies first + if let Some(crate::database::object::SchemaTypeOrArray::Single(t)) = &self.obj.type_ { + if !crate::database::object::is_primitive_type(t) { + if let Some(parent) = db.schemas.get(t) { + parent.as_ref().compile(db, t, t.clone(), errors); + if let Some(p_props) = parent.obj.compiled_properties.get() { + props.extend(p_props.clone()); + } + } + } + } + + if let Some(crate::database::object::SchemaTypeOrArray::Multiple(types)) = &self.obj.type_ { + let mut custom_type_count = 0; + for t in types { + if !crate::database::object::is_primitive_type(t) { + custom_type_count += 1; + } + } + + if custom_type_count > 1 { + errors.push(crate::drop::Error { + code: "MULTIPLE_INHERITANCE_PROHIBITED".to_string(), + message: format!( + "Schema attempts to extend multiple custom object pointers in its type array {:?}. Use 'oneOf' for polymorphism and tagged unions.", + types + ), + details: crate::drop::ErrorDetails { + path: Some(path.clone()), + schema: Some(root_id.to_string()), + ..Default::default() + } + }); + } + + for t in types { + if !crate::database::object::is_primitive_type(t) { + if let Some(parent) = db.schemas.get(t) { + parent.as_ref().compile(db, t, t.clone(), errors); + } + } + } + } + + // 2. Add local properties + if let Some(local_props) = &self.obj.properties { + for (k, v) in local_props { + props.insert(k.clone(), v.clone()); + } + } + + // 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); + + // 5. Compute Edges natively + let schema_edges = self.compile_edges(db, root_id, &path, &props, errors); + let _ = self.obj.compiled_edges.set(schema_edges); + + // 5. Build our inline children properties recursively NOW! (Depth-first search) + if let Some(local_props) = &self.obj.properties { + for (k, child) in local_props { + child.compile(db, root_id, format!("{}/{}", path, k), errors); + } + } + if let Some(items) = &self.obj.items { + items.compile(db, root_id, format!("{}/items", path), errors); + } + if let Some(pattern_props) = &self.obj.pattern_properties { + for (k, child) in pattern_props { + child.compile(db, root_id, format!("{}/{}", path, k), errors); + } + } + if let Some(additional_props) = &self.obj.additional_properties { + additional_props.compile( + db, + root_id, + format!("{}/additionalProperties", path), + errors, + ); + } + 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); + } + } + 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); + } + } + if let Some(child) = &self.obj.not { + child.compile(db, root_id, format!("{}/not", path), errors); + } + if let Some(child) = &self.obj.contains { + child.compile(db, root_id, format!("{}/contains", path), errors); + } + + self.compile_polymorphism(db, root_id, &path, errors); + } +} diff --git a/src/database/compile/polymorphism.rs b/src/database/compile/polymorphism.rs new file mode 100644 index 0000000..0ba1c25 --- /dev/null +++ b/src/database/compile/polymorphism.rs @@ -0,0 +1,153 @@ +use crate::database::schema::Schema; + +impl Schema { + pub fn compile_polymorphism( + &self, + db: &crate::database::Database, + root_id: &str, + path: &str, + errors: &mut Vec, + ) { + let mut options = std::collections::BTreeMap::new(); + let mut strategy = String::new(); + + if let Some(family) = &self.obj.family { + let family_base = family.split('.').next_back().unwrap_or(family).to_string(); + let family_prefix = family + .strip_suffix(&family_base) + .unwrap_or("") + .trim_end_matches('.'); + + if let Some(type_def) = db.types.get(&family_base) { + if type_def.variations.len() > 1 && type_def.variations.iter().any(|v| v != &family_base) { + // Scenario A / B: Table Variations + strategy = "type".to_string(); + for var in &type_def.variations { + let target_id = if family_prefix.is_empty() { + var.to_string() + } else { + format!("{}.{}", family_prefix, var) + }; + + if db.schemas.contains_key(&target_id) { + options.insert(var.to_string(), (None, Some(target_id))); + } + } + } else { + // Scenario C: Single Table Inheritance (Horizontal) + strategy = "kind".to_string(); + + let suffix = format!(".{}", family_base); + + for (id, schema) in &type_def.schemas { + if id.ends_with(&suffix) || id == &family_base { + if let Some(kind_val) = schema.obj.get_discriminator_value("kind", id) { + options.insert(kind_val, (None, Some(id.to_string()))); + } + } + } + } + } + } else if let Some(one_of) = &self.obj.one_of { + let mut type_vals = std::collections::HashSet::new(); + let mut kind_vals = std::collections::HashSet::new(); + let mut disjoint_base = true; + let mut structural_types = std::collections::HashSet::new(); + + 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; + structural_types.insert(t.clone()); + } else { + child_id = t.clone(); + structural_types.insert("object".to_string()); + } + } else { + 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 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)); + } + } + } + } 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() + }; + + 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: Some(path.to_string()), + schema: Some(root_id.to_string()), + ..Default::default() + } + }); + 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(); + } + } + + 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: Some(path.to_string()), + schema: Some(root_id.to_string()), + ..Default::default() + } + }); + continue; + } + + options.insert(val, (Some(i), None)); + } + } + } + } else { + return; + } + + if !options.is_empty() { + if !strategy.is_empty() { + let _ = self.obj.compiled_discriminator.set(strategy); + } + let _ = self.obj.compiled_options.set(options); + } + } +} diff --git a/src/database/mod.rs b/src/database/mod.rs index 19e7a3a..9ee7de4 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -8,6 +8,7 @@ pub mod punc; pub mod relation; pub mod schema; pub mod r#type; +pub mod compile; // External mock exports inside the executor sub-folder diff --git a/src/database/schema.rs b/src/database/schema.rs index faf5c8d..bf96551 100644 --- a/src/database/schema.rs +++ b/src/database/schema.rs @@ -1,7 +1,7 @@ use crate::database::object::*; use serde::{Deserialize, Serialize}; use serde_json::Value; -use std::sync::Arc; + #[derive(Debug, Clone, Serialize, Default)] pub struct Schema { #[serde(flatten)] @@ -22,609 +22,6 @@ impl std::ops::DerefMut for Schema { } } -impl Schema { - pub fn compile( - &self, - db: &crate::database::Database, - root_id: &str, - path: String, - errors: &mut Vec, - ) { - if self.obj.compiled_properties.get().is_some() { - return; - } - - if let Some(format_str) = &self.obj.format { - if let Some(fmt) = crate::database::formats::FORMATS.get(format_str.as_str()) { - let _ = self - .obj - .compiled_format - .set(crate::database::object::CompiledFormat::Func(fmt.func)); - } - } - - if let Some(pattern_str) = &self.obj.pattern { - if let Ok(re) = regex::Regex::new(pattern_str) { - let _ = self - .obj - .compiled_pattern - .set(crate::database::object::CompiledRegex(re)); - } - } - - if let Some(pattern_props) = &self.obj.pattern_properties { - let mut compiled = Vec::new(); - for (k, v) in pattern_props { - if let Ok(re) = regex::Regex::new(k) { - compiled.push((crate::database::object::CompiledRegex(re), v.clone())); - } - } - if !compiled.is_empty() { - let _ = self.obj.compiled_pattern_properties.set(compiled); - } - } - - let mut props = std::collections::BTreeMap::new(); - - // 1. Resolve INHERITANCE dependencies first - if let Some(crate::database::object::SchemaTypeOrArray::Single(t)) = &self.obj.type_ { - if !crate::database::object::is_primitive_type(t) { - if let Some(parent) = db.schemas.get(t) { - parent.as_ref().compile(db, t, t.clone(), errors); - if let Some(p_props) = parent.obj.compiled_properties.get() { - props.extend(p_props.clone()); - } - } - } - } - - if let Some(crate::database::object::SchemaTypeOrArray::Multiple(types)) = &self.obj.type_ { - let mut custom_type_count = 0; - for t in types { - if !crate::database::object::is_primitive_type(t) { - custom_type_count += 1; - } - } - - if custom_type_count > 1 { - errors.push(crate::drop::Error { - code: "MULTIPLE_INHERITANCE_PROHIBITED".to_string(), - message: format!( - "Schema attempts to extend multiple custom object pointers in its type array {:?}. Use 'oneOf' for polymorphism and tagged unions.", - types - ), - details: crate::drop::ErrorDetails { - path: Some(path.clone()), - schema: Some(root_id.to_string()), - ..Default::default() - } - }); - } - - for t in types { - if !crate::database::object::is_primitive_type(t) { - if let Some(parent) = db.schemas.get(t) { - parent.as_ref().compile(db, t, t.clone(), errors); - } - } - } - } - - // 2. Add local properties - if let Some(local_props) = &self.obj.properties { - for (k, v) in local_props { - props.insert(k.clone(), v.clone()); - } - } - - // 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); - - // 5. Compute Edges natively - let schema_edges = self.compile_edges(db, root_id, &path, &props, errors); - let _ = self.obj.compiled_edges.set(schema_edges); - - // 5. Build our inline children properties recursively NOW! (Depth-first search) - if let Some(local_props) = &self.obj.properties { - for (k, child) in local_props { - child.compile(db, root_id, format!("{}/{}", path, k), errors); - } - } - if let Some(items) = &self.obj.items { - items.compile(db, root_id, format!("{}/items", path), errors); - } - if let Some(pattern_props) = &self.obj.pattern_properties { - for (k, child) in pattern_props { - child.compile(db, root_id, format!("{}/{}", path, k), errors); - } - } - if let Some(additional_props) = &self.obj.additional_properties { - additional_props.compile( - db, - root_id, - format!("{}/additionalProperties", path), - errors, - ); - } - 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); - } - } - 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); - } - } - if let Some(child) = &self.obj.not { - child.compile(db, root_id, format!("{}/not", path), errors); - } - if let Some(child) = &self.obj.contains { - child.compile(db, root_id, format!("{}/contains", path), errors); - } - - self.compile_polymorphism(db, root_id, &path, errors); - } - - /// Dynamically infers and compiles all structural database relationships between this Schema - /// and its nested children. This functions recursively traverses the JSON Schema abstract syntax - /// tree, identifies physical PostgreSQL table boundaries, and locks the resulting relation - /// constraint paths directly onto the `compiled_edges` map in O(1) memory. - pub fn compile_edges( - &self, - db: &crate::database::Database, - root_id: &str, - path: &str, - props: &std::collections::BTreeMap>, - errors: &mut Vec, - ) -> std::collections::BTreeMap { - let mut schema_edges = std::collections::BTreeMap::new(); - - // Determine the physical Database Table Name this schema structurally represents - // Plucks the polymorphic discriminator via dot-notation (e.g. extracting "person" from "full.person") - let mut parent_type_name = None; - - 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 == 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) { - parent_type_name = Some(base_type_name); - } - } else if let Some(crate::database::object::SchemaTypeOrArray::Single(t)) = &self.obj.type_ { - // 3. Nested graphs trust their explicit struct pointer reference - if !crate::database::object::is_primitive_type(t) { - parent_type_name = Some(t.split('.').next_back().unwrap_or(t).to_string()); - } - } - - if let Some(p_type) = parent_type_name { - // Proceed only if the resolved table physically exists within the Postgres Type hierarchy - 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 { - let mut child_type_name = None; - let mut target_schema = prop_schema.clone(); - let mut is_array = false; - - // Structurally unpack the inner target entity if the object maps to an array list - if let Some(crate::database::object::SchemaTypeOrArray::Single(t)) = - &prop_schema.obj.type_ - { - if t == "array" { - is_array = true; - if let Some(items) = &prop_schema.obj.items { - target_schema = items.clone(); - } - } - } - - // Determine the physical Postgres table backing the nested child schema recursively - if let Some(family) = &target_schema.obj.family { - child_type_name = Some(family.split('.').next_back().unwrap_or(family).to_string()); - } else if let Some(crate::database::object::SchemaTypeOrArray::Single(t)) = - &target_schema.obj.type_ - { - if !crate::database::object::is_primitive_type(t) { - child_type_name = Some(t.split('.').next_back().unwrap_or(t).to_string()); - } - } else if let Some(arr) = &target_schema.obj.one_of { - if let Some(first) = arr.first() { - if let Some(crate::database::object::SchemaTypeOrArray::Single(t)) = &first.obj.type_ - { - if !crate::database::object::is_primitive_type(t) { - child_type_name = Some(t.split('.').next_back().unwrap_or(t).to_string()); - } - } - } - } - - 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() - .and_then(|v| v.get(prop_name.as_str())) - .and_then(|v| v.as_str()) - { - if ft == "jsonb" { - continue; - } - } - 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); - - if let Some(compiled_target_props) = target_schema.obj.compiled_properties.get() { - let keys_for_ambiguity: Vec = - compiled_target_props.keys().cloned().collect(); - - // Interrogate the Database catalog graph to discover the exact Foreign Key Constraint connecting the components - if let Some((relation, is_forward)) = db.resolve_relation( - &p_type, - &c_type, - prop_name, - Some(&keys_for_ambiguity), - is_array, - Some(root_id), - &format!("{}/{}", path, prop_name), - errors, - ) { - schema_edges.insert( - prop_name.clone(), - crate::database::edge::Edge { - constraint: relation.constraint.clone(), - forward: is_forward, - }, - ); - } - } - } - } - } - } - } - schema_edges - } - - pub fn compile_polymorphism( - &self, - db: &crate::database::Database, - root_id: &str, - path: &str, - errors: &mut Vec, - ) { - let mut options = std::collections::BTreeMap::new(); - let mut strategy = String::new(); - - if let Some(family) = &self.obj.family { - let family_base = family.split('.').next_back().unwrap_or(family).to_string(); - let family_prefix = family - .strip_suffix(&family_base) - .unwrap_or("") - .trim_end_matches('.'); - - if let Some(type_def) = db.types.get(&family_base) { - if type_def.variations.len() > 1 && type_def.variations.iter().any(|v| v != &family_base) { - // Scenario A / B: Table Variations - strategy = "type".to_string(); - for var in &type_def.variations { - let target_id = if family_prefix.is_empty() { - var.to_string() - } else { - format!("{}.{}", family_prefix, var) - }; - - if db.schemas.contains_key(&target_id) { - options.insert(var.to_string(), (None, Some(target_id))); - } - } - } else { - // Scenario C: Single Table Inheritance (Horizontal) - strategy = "kind".to_string(); - - let suffix = format!(".{}", family_base); - - for (id, schema) in &type_def.schemas { - if id.ends_with(&suffix) || id == &family_base { - if let Some(kind_val) = schema.obj.get_discriminator_value("kind", id) { - options.insert(kind_val, (None, Some(id.to_string()))); - } - } - } - } - } - } else if let Some(one_of) = &self.obj.one_of { - let mut type_vals = std::collections::HashSet::new(); - let mut kind_vals = std::collections::HashSet::new(); - let mut disjoint_base = true; - let mut structural_types = std::collections::HashSet::new(); - - 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; - structural_types.insert(t.clone()); - } else { - child_id = t.clone(); - structural_types.insert("object".to_string()); - } - } else { - 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 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)); - } - } - } - } 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() - }; - - 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: Some(path.to_string()), - schema: Some(root_id.to_string()), - ..Default::default() - } - }); - 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(); - } - } - - 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: Some(path.to_string()), - schema: Some(root_id.to_string()), - ..Default::default() - } - }); - continue; - } - - options.insert(val, (Some(i), None)); - } - } - } - } else { - return; - } - - if !options.is_empty() { - if !strategy.is_empty() { - let _ = self.obj.compiled_discriminator.set(strategy); - } - let _ = self.obj.compiled_options.set(options); - } - } - - #[allow(unused_variables)] - fn validate_identifier( - id: &str, - field_name: &str, - root_id: &str, - path: &str, - errors: &mut Vec, - ) { - #[cfg(not(test))] - for c in id.chars() { - if !c.is_ascii_lowercase() && !c.is_ascii_digit() && c != '_' && c != '.' { - errors.push(crate::drop::Error { - code: "INVALID_IDENTIFIER".to_string(), - message: format!( - "Invalid character '{}' in JSON Schema '{}' property: '{}'. Identifiers must exclusively contain [a-z0-9_.]", - c, field_name, id - ), - details: crate::drop::ErrorDetails { - path: Some(path.to_string()), - schema: Some(root_id.to_string()), - ..Default::default() - }, - }); - return; - } - } - } - - pub fn collect_schemas( - schema_arc: &Arc, - root_id: &str, - path: String, - to_insert: &mut Vec<(String, Arc)>, - errors: &mut Vec, - ) { - if let Some(crate::database::object::SchemaTypeOrArray::Single(t)) = &schema_arc.obj.type_ { - 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); - - // 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))); - } - } - } - - if let Some(family) = &schema_arc.obj.family { - Self::validate_identifier(family, "family", root_id, &path, errors); - } - - Self::collect_child_schemas(schema_arc, root_id, path, to_insert, errors); - } - - pub fn collect_child_schemas( - schema_arc: &Arc, - root_id: &str, - path: String, - to_insert: &mut Vec<(String, Arc)>, - errors: &mut Vec, - ) { - if let Some(props) = &schema_arc.obj.properties { - for (k, v) in props.iter() { - let next_path = format!("{}/{}", path, k); - Self::collect_schemas(v, root_id, next_path, to_insert, errors); - } - } - - if let Some(pattern_props) = &schema_arc.obj.pattern_properties { - for (k, v) in pattern_props.iter() { - let next_path = format!("{}/{}", path, k); - Self::collect_schemas(v, root_id, next_path, to_insert, errors); - } - } - - let mut map_arr = |arr: &Vec>, sub: &str| { - for (i, v) in arr.iter().enumerate() { - Self::collect_schemas( - v, - root_id, - format!("{}/{}/{}", path, sub, i), - to_insert, - errors, - ); - } - }; - - if let Some(arr) = &schema_arc.obj.prefix_items { - map_arr(arr, "prefixItems"); - } - - if let Some(arr) = &schema_arc.obj.one_of { - map_arr(arr, "oneOf"); - } - - let mut map_opt = |opt: &Option>, pass_path: bool, sub: &str| { - if let Some(v) = opt { - if pass_path { - // 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); - } - } - }; - - map_opt( - &schema_arc.obj.additional_properties, - false, - "additionalProperties", - ); - map_opt(&schema_arc.obj.items, true, "items"); - map_opt(&schema_arc.obj.not, false, "not"); - map_opt(&schema_arc.obj.contains, false, "contains"); - map_opt(&schema_arc.obj.property_names, false, "propertyNames"); - - if let Some(cases) = &schema_arc.obj.cases { - for (i, c) in cases.iter().enumerate() { - if let Some(when) = &c.when { - Self::collect_schemas( - when, - root_id, - format!("{}/cases/{}/when", path, i), - to_insert, - errors, - ); - } - if let Some(then) = &c.then { - Self::collect_schemas( - then, - root_id, - format!("{}/cases/{}/then", path, i), - to_insert, - errors, - ); - } - if let Some(else_) = &c.else_ { - Self::collect_schemas( - else_, - root_id, - format!("{}/cases/{}/else", path, i), - to_insert, - errors, - ); - } - } - } - } -} - impl<'de> Deserialize<'de> for Schema { fn deserialize(deserializer: D) -> Result where