use serde::{Deserialize, Serialize}; use serde_json::Value; use std::collections::BTreeMap; use std::sync::Arc; use std::sync::OnceLock; pub fn serialize_once_lock( lock: &OnceLock, serializer: S, ) -> Result { if let Some(val) = lock.get() { val.serialize(serializer) } else { serializer.serialize_none() } } pub fn is_once_lock_map_empty(lock: &OnceLock>) -> bool { lock.get().map_or(true, |m| m.is_empty()) } pub fn is_once_lock_vec_empty(lock: &OnceLock>) -> bool { lock.get().map_or(true, |v| v.is_empty()) } pub fn is_once_lock_string_empty(lock: &OnceLock) -> bool { lock.get().map_or(true, |s| s.is_empty()) } // Schema mirrors the Go Punc Generator's schema struct for consistency. // It is an order-preserving representation of a JSON Schema. pub fn deserialize_some<'de, D>(deserializer: D) -> Result, D::Error> where D: serde::Deserializer<'de>, { let v = Value::deserialize(deserializer)?; Ok(Some(v)) } pub fn is_primitive_type(t: &str) -> bool { matches!( t, "string" | "number" | "integer" | "boolean" | "object" | "array" | "null" ) } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Case { #[serde(skip_serializing_if = "Option::is_none")] pub when: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub then: Option>, #[serde(rename = "else")] #[serde(skip_serializing_if = "Option::is_none")] pub else_: Option>, } #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct SchemaObject { // Core Schema Keywords #[serde(rename = "$id")] #[serde(skip_serializing_if = "Option::is_none")] pub id: Option, #[serde(skip_serializing_if = "Option::is_none")] pub description: Option, #[serde(skip_serializing_if = "Option::is_none")] pub title: Option, #[serde(default)] // Allow missing type #[serde(rename = "type")] #[serde(skip_serializing_if = "Option::is_none")] pub type_: Option, // Handles string or array of strings // Object Keywords #[serde(skip_serializing_if = "Option::is_none")] pub properties: Option>>, #[serde(rename = "patternProperties")] #[serde(skip_serializing_if = "Option::is_none")] pub pattern_properties: Option>>, #[serde(rename = "additionalProperties")] #[serde(skip_serializing_if = "Option::is_none")] pub additional_properties: Option>, #[serde(rename = "$family")] #[serde(skip_serializing_if = "Option::is_none")] pub family: Option, #[serde(skip_serializing_if = "Option::is_none")] pub required: Option>, // dependencies can be schema dependencies or property dependencies #[serde(skip_serializing_if = "Option::is_none")] pub dependencies: Option>, // Array Keywords #[serde(rename = "items")] #[serde(skip_serializing_if = "Option::is_none")] pub items: Option>, #[serde(rename = "prefixItems")] #[serde(skip_serializing_if = "Option::is_none")] pub prefix_items: Option>>, // String Validation #[serde(rename = "minLength")] #[serde(skip_serializing_if = "Option::is_none")] pub min_length: Option, #[serde(rename = "maxLength")] #[serde(skip_serializing_if = "Option::is_none")] pub max_length: Option, #[serde(skip_serializing_if = "Option::is_none")] pub pattern: Option, // Array Validation #[serde(rename = "minItems")] #[serde(skip_serializing_if = "Option::is_none")] pub min_items: Option, #[serde(rename = "maxItems")] #[serde(skip_serializing_if = "Option::is_none")] pub max_items: Option, #[serde(rename = "uniqueItems")] #[serde(skip_serializing_if = "Option::is_none")] pub unique_items: Option, #[serde(rename = "contains")] #[serde(skip_serializing_if = "Option::is_none")] pub contains: Option>, #[serde(rename = "minContains")] #[serde(skip_serializing_if = "Option::is_none")] pub min_contains: Option, #[serde(rename = "maxContains")] #[serde(skip_serializing_if = "Option::is_none")] pub max_contains: Option, // Object Validation #[serde(rename = "minProperties")] #[serde(skip_serializing_if = "Option::is_none")] pub min_properties: Option, #[serde(rename = "maxProperties")] #[serde(skip_serializing_if = "Option::is_none")] pub max_properties: Option, #[serde(rename = "propertyNames")] #[serde(skip_serializing_if = "Option::is_none")] pub property_names: Option>, // Numeric Validation #[serde(skip_serializing_if = "Option::is_none")] pub format: Option, #[serde(rename = "enum")] #[serde(skip_serializing_if = "Option::is_none")] pub enum_: Option>, // `enum` is a reserved keyword in Rust #[serde( default, rename = "const", deserialize_with = "crate::database::schema::deserialize_some" )] #[serde(skip_serializing_if = "Option::is_none")] pub const_: Option, // Numeric Validation #[serde(rename = "multipleOf")] #[serde(skip_serializing_if = "Option::is_none")] pub multiple_of: Option, #[serde(skip_serializing_if = "Option::is_none")] pub minimum: Option, #[serde(skip_serializing_if = "Option::is_none")] pub maximum: Option, #[serde(rename = "exclusiveMinimum")] #[serde(skip_serializing_if = "Option::is_none")] pub exclusive_minimum: Option, #[serde(rename = "exclusiveMaximum")] #[serde(skip_serializing_if = "Option::is_none")] pub exclusive_maximum: Option, // Combining Keywords #[serde(skip_serializing_if = "Option::is_none")] pub cases: Option>, #[serde(rename = "oneOf")] #[serde(skip_serializing_if = "Option::is_none")] pub one_of: Option>>, #[serde(rename = "not")] #[serde(skip_serializing_if = "Option::is_none")] pub not: Option>, // Custom Vocabularies #[serde(skip_serializing_if = "Option::is_none")] pub form: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub display: Option>, #[serde(rename = "enumNames")] #[serde(skip_serializing_if = "Option::is_none")] pub enum_names: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub control: Option, #[serde(skip_serializing_if = "Option::is_none")] pub actions: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub computer: Option, #[serde(default)] #[serde(skip_serializing_if = "Option::is_none")] pub extensible: Option, #[serde(rename = "compiledProperties")] #[serde(skip_deserializing)] #[serde(skip_serializing_if = "crate::database::schema::is_once_lock_vec_empty")] #[serde(serialize_with = "crate::database::schema::serialize_once_lock")] pub compiled_property_names: OnceLock>, #[serde(skip)] pub compiled_properties: OnceLock>>, #[serde(rename = "compiledDiscriminator")] #[serde(skip_deserializing)] #[serde(skip_serializing_if = "crate::database::schema::is_once_lock_string_empty")] #[serde(serialize_with = "crate::database::schema::serialize_once_lock")] pub compiled_discriminator: OnceLock, #[serde(rename = "compiledOptions")] #[serde(skip_deserializing)] #[serde(skip_serializing_if = "crate::database::schema::is_once_lock_map_empty")] #[serde(serialize_with = "crate::database::schema::serialize_once_lock")] pub compiled_options: OnceLock>, #[serde(rename = "compiledEdges")] #[serde(skip_deserializing)] #[serde(skip_serializing_if = "crate::database::schema::is_once_lock_map_empty")] #[serde(serialize_with = "crate::database::schema::serialize_once_lock")] pub compiled_edges: OnceLock>, #[serde(skip)] pub compiled_format: OnceLock, #[serde(skip)] pub compiled_pattern: OnceLock, #[serde(skip)] pub compiled_pattern_properties: OnceLock)>>, } /// Represents a compiled format validator #[derive(Clone)] pub enum CompiledFormat { Func(fn(&serde_json::Value) -> Result<(), Box>), Regex(regex::Regex), } impl std::fmt::Debug for CompiledFormat { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { CompiledFormat::Func(_) => write!(f, "CompiledFormat::Func(...)"), CompiledFormat::Regex(r) => write!(f, "CompiledFormat::Regex({:?})", r), } } } /// A wrapper for compiled regex patterns #[derive(Debug, Clone)] pub struct CompiledRegex(pub regex::Regex); #[derive(Debug, Clone, Serialize, Default)] pub struct Schema { #[serde(flatten)] pub obj: SchemaObject, #[serde(skip)] pub always_fail: bool, } impl std::ops::Deref for Schema { type Target = SchemaObject; fn deref(&self) -> &Self::Target { &self.obj } } impl std::ops::DerefMut for Schema { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.obj } } impl Schema { pub fn compile( &self, db: &crate::database::Database, visited: &mut std::collections::HashSet, errors: &mut Vec, ) { if self.obj.compiled_properties.get().is_some() { return; } if let Some(id) = &self.obj.id { if !visited.insert(id.clone()) { return; // Break cyclical resolution } } 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::schema::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::schema::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::schema::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::schema::SchemaTypeOrArray::Single(t)) = &self.obj.type_ { if !crate::database::schema::is_primitive_type(t) { if let Some(parent) = db.schemas.get(t) { parent.compile(db, visited, errors); if let Some(p_props) = parent.obj.compiled_properties.get() { props.extend(p_props.clone()); } } } } if let Some(crate::database::schema::SchemaTypeOrArray::Multiple(types)) = &self.obj.type_ { let mut custom_type_count = 0; for t in types { if !crate::database::schema::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.", self.obj.identifier().unwrap_or("unknown".to_string()) ), details: crate::drop::ErrorDetails { path: self.obj.identifier().unwrap_or("unknown".to_string()), ..Default::default() } }); } for t in types { if !crate::database::schema::is_primitive_type(t) { if let Some(parent) = db.schemas.get(t) { parent.compile(db, visited, 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. 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 let schema_edges = self.compile_edges(db, visited, &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 child in local_props.values() { child.compile(db, visited, errors); } } if let Some(items) = &self.obj.items { items.compile(db, visited, errors); } if let Some(pattern_props) = &self.obj.pattern_properties { for child in pattern_props.values() { child.compile(db, visited, errors); } } if let Some(additional_props) = &self.obj.additional_properties { additional_props.compile(db, visited, errors); } if let Some(one_of) = &self.obj.one_of { for child in one_of { child.compile(db, visited, errors); } } if let Some(arr) = &self.obj.prefix_items { for child in arr { child.compile(db, visited, errors); } } if let Some(child) = &self.obj.not { child.compile(db, visited, errors); } if let Some(child) = &self.obj.contains { child.compile(db, visited, errors); } if let Some(cases) = &self.obj.cases { for c in cases { if let Some(child) = &c.when { child.compile(db, visited, errors); } if let Some(child) = &c.then { child.compile(db, visited, errors); } if let Some(child) = &c.else_ { child.compile(db, visited, errors); } } } self.compile_polymorphism(db, errors); if let Some(id) = &self.obj.id { visited.remove(id); } } pub fn compile_polymorphism( &self, db: &crate::database::Database, 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(), target_id); } } } else { // Scenario C: Single Table Inheritance (Horizontal) strategy = "kind".to_string(); let mut target_family_ids = std::collections::HashSet::new(); target_family_ids.insert(family.clone()); // Iteratively build local descendants since db.descendants is removed natively let mut added = true; while added { added = false; for schema in &type_def.schemas { if let Some(id) = &schema.obj.id { if !target_family_ids.contains(id) { if let Some(crate::database::schema::SchemaTypeOrArray::Single(t)) = &schema.obj.type_ { if target_family_ids.contains(t) { target_family_ids.insert(id.clone()); added = true; } } } } } } for schema in &type_def.schemas { if let Some(id) = &schema.obj.id { if target_family_ids.contains(id) { if let Some(kind_val) = schema.obj.get_discriminator_value("kind") { options.insert(kind_val, 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(); for c in one_of { if let Some(t_val) = c.obj.get_discriminator_value("type") { type_vals.insert(t_val); } if let Some(k_val) = c.obj.get_discriminator_value("kind") { kind_vals.insert(k_val); } } 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() { return; } for c in one_of { if let Some(val) = c.obj.get_discriminator_value(&strategy) { 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::default() }); continue; } let mut target_id = c.obj.id.clone(); if target_id.is_none() { if let Some(crate::database::schema::SchemaTypeOrArray::Single(t)) = &c.obj.type_ { if !crate::database::schema::is_primitive_type(t) { target_id = Some(t.clone()); } } } if let Some(tid) = target_id { options.insert(val, tid); } } } } else { return; } if !options.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, 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::default(), }); return; } } } pub fn collect_schemas( &mut self, tracking_path: Option, to_insert: &mut Vec<(String, Schema)>, errors: &mut Vec, ) { if let Some(id) = &self.obj.id { Self::validate_identifier(id, "$id", errors); to_insert.push((id.clone(), self.clone())); } if let Some(crate::database::schema::SchemaTypeOrArray::Single(t)) = &self.obj.type_ { if !crate::database::schema::is_primitive_type(t) { Self::validate_identifier(t, "type", errors); } } if let Some(family) = &self.obj.family { Self::validate_identifier(family, "$family", errors); } // Is this schema an inline ad-hoc composition? // Meaning it has a tracking context, lacks an explicit $id, but extends an Entity ref with explicit properties! if self.obj.id.is_none() && self.obj.properties.is_some() { if let Some(crate::database::schema::SchemaTypeOrArray::Single(t)) = &self.obj.type_ { if !crate::database::schema::is_primitive_type(t) { if let Some(ref path) = tracking_path { to_insert.push((path.clone(), self.clone())); } } } } // Provide the path origin to children natively, prioritizing the explicit `$id` boundary if one exists let origin_path = self.obj.id.clone().or(tracking_path); self.collect_child_schemas(origin_path, to_insert, errors); } pub fn collect_child_schemas( &mut self, origin_path: Option, to_insert: &mut Vec<(String, Schema)>, errors: &mut Vec, ) { if let Some(props) = &mut self.obj.properties { for (k, v) in props.iter_mut() { let mut inner = (**v).clone(); let next_path = origin_path.as_ref().map(|o| format!("{}/{}", o, k)); inner.collect_schemas(next_path, to_insert, errors); *v = Arc::new(inner); } } if let Some(pattern_props) = &mut self.obj.pattern_properties { for (k, v) in pattern_props.iter_mut() { let mut inner = (**v).clone(); let next_path = origin_path.as_ref().map(|o| format!("{}/{}", o, k)); inner.collect_schemas(next_path, to_insert, errors); *v = Arc::new(inner); } } let mut map_arr = |arr: &mut Vec>| { for v in arr.iter_mut() { let mut inner = (**v).clone(); inner.collect_schemas(origin_path.clone(), to_insert, errors); *v = Arc::new(inner); } }; if let Some(arr) = &mut self.obj.prefix_items { map_arr(arr); } if let Some(arr) = &mut self.obj.one_of { map_arr(arr); } let mut map_opt = |opt: &mut Option>, pass_path: bool| { if let Some(v) = opt { let mut inner = (**v).clone(); let next = if pass_path { origin_path.clone() } else { None }; inner.collect_schemas(next, to_insert, errors); *v = Arc::new(inner); } }; map_opt(&mut self.obj.additional_properties, false); // `items` absolutely must inherit the EXACT property path assigned to the Array wrapper! // This allows nested Arrays enclosing bare Entity structs to correctly register as the boundary mapping. map_opt(&mut self.obj.items, true); map_opt(&mut self.obj.not, false); map_opt(&mut self.obj.contains, false); map_opt(&mut self.obj.property_names, false); if let Some(cases) = &mut self.obj.cases { for c in cases.iter_mut() { if let Some(when) = &mut c.when { let mut inner = (**when).clone(); inner.collect_schemas(origin_path.clone(), to_insert, errors); *when = Arc::new(inner); } if let Some(then) = &mut c.then { let mut inner = (**then).clone(); inner.collect_schemas(origin_path.clone(), to_insert, errors); *then = Arc::new(inner); } if let Some(else_) = &mut c.else_ { let mut inner = (**else_).clone(); inner.collect_schemas(origin_path.clone(), to_insert, errors); *else_ = Arc::new(inner); } } } } /// 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, visited: &mut std::collections::HashSet, 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 { parent_type_name = Some(family.split('.').next_back().unwrap_or(family).to_string()); } else if let Some(identifier) = self.obj.identifier() { parent_type_name = Some( identifier .split('.') .next_back() .unwrap_or(&identifier) .to_string(), ); } if let Some(p_type) = parent_type_name { // Proceed only if the resolved table physically exists within the Postgres Type hierarchy if db.types.contains_key(&p_type) { // 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::schema::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(ref_id) = target_schema.obj.identifier() { child_type_name = Some(ref_id.split('.').next_back().unwrap_or(&ref_id).to_string()); } else if let Some(arr) = &target_schema.obj.one_of { if let Some(first) = arr.first() { if let Some(ref_id) = first.obj.identifier() { child_type_name = Some(ref_id.split('.').next_back().unwrap_or(&ref_id).to_string()); } } } if let Some(c_type) = child_type_name { 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, visited, 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)) = resolve_relation( db, &p_type, &c_type, prop_name, Some(&keys_for_ambiguity), is_array, self.id.as_deref(), &format!("/{}", prop_name), errors, ) { schema_edges.insert( prop_name.clone(), crate::database::edge::Edge { constraint: relation.constraint.clone(), forward: is_forward, }, ); } } } } } } } schema_edges } } /// Inspects the Postgres pg_constraint relations catalog to securely identify /// the precise Foreign Key connecting a parent and child hierarchy path. pub(crate) fn resolve_relation<'a>( db: &'a crate::database::Database, parent_type: &str, child_type: &str, prop_name: &str, relative_keys: Option<&Vec>, is_array: bool, schema_id: Option<&str>, path: &str, errors: &mut Vec, ) -> Option<(&'a crate::database::relation::Relation, bool)> { // Enforce graph locality by ensuring we don't accidentally crawl to pure structural entity boundaries if parent_type == "entity" && child_type == "entity" { return None; } let p_def = db.types.get(parent_type)?; let c_def = db.types.get(child_type)?; let mut matching_rels = Vec::new(); let mut directions = Vec::new(); // Scour the complete catalog for any Edge matching the inheritance scope of the two objects // This automatically binds polymorphic structures (e.g. recognizing a relationship targeting User // also natively binds instances specifically typed as Person). let mut all_rels: Vec<&crate::database::relation::Relation> = db.relations.values().collect(); all_rels.sort_by(|a, b| a.constraint.cmp(&b.constraint)); for rel in all_rels { let mut is_forward = p_def.hierarchy.contains(&rel.source_type) && c_def.hierarchy.contains(&rel.destination_type); let is_reverse = p_def.hierarchy.contains(&rel.destination_type) && c_def.hierarchy.contains(&rel.source_type); // Structural Cardinality Filtration: // If the schema requires a collection (Array), it is mathematically impossible for a pure // Forward scalar edge (where the parent holds exactly one UUID pointer) to fulfill a One-to-Many request. // Thus, if it's an array, we fully reject pure Forward edges and only accept Reverse edges (or Junction edges). if is_array && is_forward && !is_reverse { is_forward = false; } if is_forward { matching_rels.push(rel); directions.push(true); } else if is_reverse { matching_rels.push(rel); directions.push(false); } } // Abort relation discovery early if no hierarchical inheritance match was found if matching_rels.is_empty() { let mut details = crate::drop::ErrorDetails { path: path.to_string(), ..Default::default() }; if let Some(sid) = schema_id { details.schema = Some(sid.to_string()); } errors.push(crate::drop::Error { code: "EDGE_MISSING".to_string(), message: format!( "No database relation exists between '{}' and '{}' for property '{}'", parent_type, child_type, prop_name ), details, }); return None; } // Ideal State: The objects only share a solitary structural relation, resolving ambiguity instantly. if matching_rels.len() == 1 { return Some((matching_rels[0], directions[0])); } let mut chosen_idx = 0; let mut resolved = false; // Exact Prefix Disambiguation: Determine if the database specifically names this constraint // directly mapping to the JSON Schema property name (e.g., `fk_{child}_{property_name}`) for (i, rel) in matching_rels.iter().enumerate() { if let Some(prefix) = &rel.prefix { if prop_name.starts_with(prefix) || prefix.starts_with(prop_name) || prefix.replace("_", "") == prop_name.replace("_", "") { chosen_idx = i; resolved = true; break; } } } // Complex Subgraph Resolution: The database contains multiple equally explicit foreign key constraints // linking these objects (such as pointing to `source` and `target` in Many-to-Many junction models). if !resolved && relative_keys.is_some() { // Twin Deduction Pass 1: We inspect the exact properties structurally defined inside the compiled payload // to observe which explicit relation arrow the child payload natively consumes. let keys = relative_keys.unwrap(); let mut consumed_rel_idx = None; for (i, rel) in matching_rels.iter().enumerate() { if let Some(prefix) = &rel.prefix { if keys.contains(prefix) { consumed_rel_idx = Some(i); break; // Found the routing edge explicitly consumed by the schema payload } } } // Twin Deduction Pass 2: Knowing which arrow points outbound, we can mathematically deduce its twin // providing the reverse ownership on the same junction boundary must be the incoming Edge to the parent. if let Some(used_idx) = consumed_rel_idx { let used_rel = matching_rels[used_idx]; let mut twin_ids = Vec::new(); for (i, rel) in matching_rels.iter().enumerate() { if i != used_idx && rel.source_type == used_rel.source_type && rel.destination_type == used_rel.destination_type && rel.prefix.is_some() { twin_ids.push(i); } } if twin_ids.len() == 1 { chosen_idx = twin_ids[0]; resolved = true; } } } // Implicit Base Fallback: If no complex explicit paths resolve, but exactly one relation // sits entirely naked (without a constraint prefix), it must be the core structural parent ownership. if !resolved { let mut null_prefix_ids = Vec::new(); for (i, rel) in matching_rels.iter().enumerate() { if rel.prefix.is_none() { null_prefix_ids.push(i); } } if null_prefix_ids.len() == 1 { chosen_idx = null_prefix_ids[0]; resolved = true; } } // If we exhausted all mathematical deduction pathways and STILL cannot isolate a single edge, // we must abort rather than silently guessing. Returning None prevents arbitrary SQL generation // and forces a clean structural error for the architect. if !resolved { let mut details = crate::drop::ErrorDetails { path: path.to_string(), context: serde_json::to_value(&matching_rels).ok(), cause: Some("Multiple conflicting constraints found matching prefixes".to_string()), ..Default::default() }; if let Some(sid) = schema_id { details.schema = Some(sid.to_string()); } errors.push(crate::drop::Error { code: "AMBIGUOUS_TYPE_RELATIONS".to_string(), message: format!( "Ambiguous database relation between '{}' and '{}' for property '{}'", parent_type, child_type, prop_name ), details, }); return None; } Some((matching_rels[chosen_idx], directions[chosen_idx])) } impl<'de> Deserialize<'de> for Schema { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { let v: Value = Deserialize::deserialize(deserializer)?; if let Some(b) = v.as_bool() { let mut obj = SchemaObject::default(); if b { obj.extensible = Some(true); } return Ok(Schema { obj, always_fail: !b, }); } let mut obj: SchemaObject = serde_json::from_value(v.clone()).map_err(serde::de::Error::custom)?; // If a schema is effectively empty (except for potentially carrying an ID), // it functions as a boolean `true` schema in Draft2020 which means it should not // restrict additional properties natively let is_empty = obj.type_.is_none() && obj.properties.is_none() && obj.pattern_properties.is_none() && obj.additional_properties.is_none() && obj.required.is_none() && obj.dependencies.is_none() && obj.items.is_none() && obj.prefix_items.is_none() && obj.contains.is_none() && obj.format.is_none() && obj.enum_.is_none() && obj.const_.is_none() && obj.cases.is_none() && obj.one_of.is_none() && obj.not.is_none() && obj.family.is_none(); if is_empty && obj.extensible.is_none() { obj.extensible = Some(true); } Ok(Schema { obj, always_fail: false, }) } } impl SchemaObject { pub fn identifier(&self) -> Option { if let Some(id) = &self.id { return Some(id.split('.').next_back().unwrap_or("").to_string()); } if let Some(SchemaTypeOrArray::Single(t)) = &self.type_ { if !is_primitive_type(t) { return Some(t.split('.').next_back().unwrap_or("").to_string()); } } None } pub fn get_discriminator_value(&self, dim: &str) -> Option { let is_split = self.compiled_properties.get().map_or(false, |p| p.contains_key("kind")); if let Some(id) = &self.id { if id.contains("light.person") || id.contains("light.organization") { println!("[DEBUG SPLIT] ID: {}, dim: {}, is_split: {:?}, props: {:?}", id, dim, is_split, self.compiled_properties.get().map(|p| p.keys().cloned().collect::>())); } } if let Some(props) = self.compiled_properties.get() { if let Some(prop_schema) = props.get(dim) { if let Some(c) = &prop_schema.obj.const_ { if let Some(s) = c.as_str() { return Some(s.to_string()); } } if let Some(e) = &prop_schema.obj.enum_ { if e.len() == 1 { if let Some(s) = e[0].as_str() { return Some(s.to_string()); } } } } } if dim == "kind" { if let Some(id) = &self.id { let base = id.split('/').last().unwrap_or(id); if let Some(idx) = base.rfind('.') { return Some(base[..idx].to_string()); } } if let Some(crate::database::schema::SchemaTypeOrArray::Single(t)) = &self.type_ { if !crate::database::schema::is_primitive_type(t) { let base = t.split('/').last().unwrap_or(t); if let Some(idx) = base.rfind('.') { return Some(base[..idx].to_string()); } } } } if dim == "type" { if let Some(id) = &self.id { let base = id.split('/').last().unwrap_or(id); if is_split { return Some(base.split('.').next_back().unwrap_or(base).to_string()); } else { return Some(base.to_string()); } } if let Some(crate::database::schema::SchemaTypeOrArray::Single(t)) = &self.type_ { if !crate::database::schema::is_primitive_type(t) { let base = t.split('/').last().unwrap_or(t); if is_split { return Some(base.split('.').next_back().unwrap_or(base).to_string()); } else { return Some(base.to_string()); } } } } None } } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(untagged)] pub enum SchemaTypeOrArray { Single(String), Multiple(Vec), } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Action { #[serde(skip_serializing_if = "Option::is_none")] pub navigate: Option, #[serde(skip_serializing_if = "Option::is_none")] pub punc: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(untagged)] pub enum Dependency { Props(Vec), Schema(Arc), }