This commit is contained in:
2026-04-02 21:55:57 -04:00
parent 29d8dfb608
commit 06f6a587de
21 changed files with 673 additions and 457 deletions

View File

@ -255,6 +255,7 @@ impl Schema {
&self,
db: &crate::database::Database,
visited: &mut std::collections::HashSet<String>,
errors: &mut Vec<crate::drop::Error>,
) {
if self.obj.compiled_properties.get().is_some() {
return;
@ -301,7 +302,7 @@ impl Schema {
// 1. Resolve INHERITANCE dependencies first
if let Some(ref_id) = &self.obj.r#ref {
if let Some(parent) = db.schemas.get(ref_id) {
parent.compile(db, visited);
parent.compile(db, visited, errors);
if let Some(p_props) = parent.obj.compiled_properties.get() {
props.extend(p_props.clone());
}
@ -310,7 +311,7 @@ impl Schema {
if let Some(all_of) = &self.obj.all_of {
for ao in all_of {
ao.compile(db, visited);
ao.compile(db, visited, errors);
if let Some(ao_props) = ao.obj.compiled_properties.get() {
props.extend(ao_props.clone());
}
@ -318,14 +319,14 @@ impl Schema {
}
if let Some(then_schema) = &self.obj.then_ {
then_schema.compile(db, visited);
then_schema.compile(db, visited, errors);
if let Some(t_props) = then_schema.obj.compiled_properties.get() {
props.extend(t_props.clone());
}
}
if let Some(else_schema) = &self.obj.else_ {
else_schema.compile(db, visited);
else_schema.compile(db, visited, errors);
if let Some(e_props) = else_schema.obj.compiled_properties.get() {
props.extend(e_props.clone());
}
@ -345,47 +346,47 @@ impl Schema {
let _ = self.obj.compiled_property_names.set(names);
// 4. Compute Edges natively
let schema_edges = self.compile_edges(db, visited, &props);
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);
child.compile(db, visited, errors);
}
}
if let Some(items) = &self.obj.items {
items.compile(db, visited);
items.compile(db, visited, errors);
}
if let Some(pattern_props) = &self.obj.pattern_properties {
for child in pattern_props.values() {
child.compile(db, visited);
child.compile(db, visited, errors);
}
}
if let Some(additional_props) = &self.obj.additional_properties {
additional_props.compile(db, visited);
additional_props.compile(db, visited, errors);
}
if let Some(one_of) = &self.obj.one_of {
for child in one_of {
child.compile(db, visited);
child.compile(db, visited, errors);
}
}
if let Some(arr) = &self.obj.prefix_items {
for child in arr {
child.compile(db, visited);
child.compile(db, visited, errors);
}
}
if let Some(child) = &self.obj.not {
child.compile(db, visited);
child.compile(db, visited, errors);
}
if let Some(child) = &self.obj.contains {
child.compile(db, visited);
child.compile(db, visited, errors);
}
if let Some(child) = &self.obj.property_names {
child.compile(db, visited);
child.compile(db, visited, errors);
}
if let Some(child) = &self.obj.if_ {
child.compile(db, visited);
child.compile(db, visited, errors);
}
if let Some(id) = &self.obj.id {
@ -394,30 +395,38 @@ impl Schema {
}
#[allow(unused_variables)]
fn validate_identifier(id: &str, field_name: &str) -> Result<(), String> {
fn validate_identifier(id: &str, field_name: &str, errors: &mut Vec<crate::drop::Error>) {
#[cfg(not(test))]
for c in id.chars() {
if !c.is_ascii_lowercase() && !c.is_ascii_digit() && c != '_' && c != '.' {
return Err(format!("Invalid character '{}' in JSON Schema '{}' property: '{}'. Identifiers must exclusively contain [a-z0-9_.]", c, field_name, id));
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;
}
}
Ok(())
}
pub fn collect_schemas(
&mut self,
tracking_path: Option<String>,
to_insert: &mut Vec<(String, Schema)>,
) -> Result<(), String> {
errors: &mut Vec<crate::drop::Error>,
) {
if let Some(id) = &self.obj.id {
Self::validate_identifier(id, "$id")?;
Self::validate_identifier(id, "$id", errors);
to_insert.push((id.clone(), self.clone()));
}
if let Some(r#ref) = &self.obj.r#ref {
Self::validate_identifier(r#ref, "$ref")?;
Self::validate_identifier(r#ref, "$ref", errors);
}
if let Some(family) = &self.obj.family {
Self::validate_identifier(family, "$family")?;
Self::validate_identifier(family, "$family", errors);
}
// Is this schema an inline ad-hoc composition?
@ -431,20 +440,20 @@ impl Schema {
// 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)?;
Ok(())
self.collect_child_schemas(origin_path, to_insert, errors);
}
pub fn collect_child_schemas(
&mut self,
origin_path: Option<String>,
to_insert: &mut Vec<(String, Schema)>,
) -> Result<(), String> {
errors: &mut Vec<crate::drop::Error>,
) {
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)?;
inner.collect_schemas(next_path, to_insert, errors);
*v = Arc::new(inner);
}
}
@ -453,48 +462,50 @@ impl Schema {
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)?;
inner.collect_schemas(next_path, to_insert, errors);
*v = Arc::new(inner);
}
}
let mut map_arr = |arr: &mut Vec<Arc<Schema>>| -> Result<(), String> {
let mut map_arr = |arr: &mut Vec<Arc<Schema>>| {
for v in arr.iter_mut() {
let mut inner = (**v).clone();
inner.collect_schemas(origin_path.clone(), to_insert)?;
inner.collect_schemas(origin_path.clone(), to_insert, errors);
*v = Arc::new(inner);
}
Ok(())
};
if let Some(arr) = &mut self.obj.prefix_items { map_arr(arr)?; }
if let Some(arr) = &mut self.obj.all_of { map_arr(arr)?; }
if let Some(arr) = &mut self.obj.one_of { map_arr(arr)?; }
if let Some(arr) = &mut self.obj.prefix_items {
map_arr(arr);
}
if let Some(arr) = &mut self.obj.all_of {
map_arr(arr);
}
if let Some(arr) = &mut self.obj.one_of {
map_arr(arr);
}
let mut map_opt = |opt: &mut Option<Arc<Schema>>, pass_path: bool| -> Result<(), String> {
let mut map_opt = |opt: &mut Option<Arc<Schema>>, 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)?;
inner.collect_schemas(next, to_insert, errors);
*v = Arc::new(inner);
}
Ok(())
};
map_opt(&mut self.obj.additional_properties, false)?;
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)?;
map_opt(&mut self.obj.if_, false)?;
map_opt(&mut self.obj.then_, false)?;
map_opt(&mut self.obj.else_, false)?;
map_opt(&mut self.obj.items, true);
Ok(())
map_opt(&mut self.obj.not, false);
map_opt(&mut self.obj.contains, false);
map_opt(&mut self.obj.property_names, false);
map_opt(&mut self.obj.if_, false);
map_opt(&mut self.obj.then_, false);
map_opt(&mut self.obj.else_, false);
}
/// Dynamically infers and compiles all structural database relationships between this Schema
@ -506,16 +517,23 @@ impl Schema {
db: &crate::database::Database,
visited: &mut std::collections::HashSet<String>,
props: &std::collections::BTreeMap<String, std::sync::Arc<Schema>>,
errors: &mut Vec<crate::drop::Error>,
) -> std::collections::BTreeMap<String, crate::database::edge::Edge> {
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());
parent_type_name = Some(
identifier
.split('.')
.next_back()
.unwrap_or(&identifier)
.to_string(),
);
}
if let Some(p_type) = parent_type_name {
@ -525,12 +543,14 @@ impl Schema {
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();
}
@ -545,24 +565,31 @@ impl Schema {
} 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());
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
// 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);
target_schema.compile(db, visited, errors);
if let Some(compiled_target_props) = target_schema.obj.compiled_properties.get() {
let keys_for_ambiguity: Vec<String> =
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))
{
if let Some((relation, is_forward)) = resolve_relation(
db,
&p_type,
&c_type,
prop_name,
Some(&keys_for_ambiguity),
is_array,
errors,
) {
schema_edges.insert(
prop_name.clone(),
crate::database::edge::Edge {
@ -589,11 +616,12 @@ pub(crate) fn resolve_relation<'a>(
child_type: &str,
prop_name: &str,
relative_keys: Option<&Vec<String>>,
is_array: bool,
errors: &mut Vec<crate::drop::Error>,
) -> 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;
return None;
}
let p_def = db.types.get(parent_type)?;
@ -605,11 +633,22 @@ pub(crate) fn resolve_relation<'a>(
// 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).
for rel in db.relations.values() {
let 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);
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);
@ -622,6 +661,14 @@ pub(crate) fn resolve_relation<'a>(
// Abort relation discovery early if no hierarchical inheritance match was found
if matching_rels.is_empty() {
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: crate::drop::ErrorDetails::default(),
});
return None;
}
@ -648,10 +695,10 @@ pub(crate) fn resolve_relation<'a>(
}
}
// Complex Subgraph Resolution: The database contains multiple equally explicit foreign key constraints
// 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
// 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;
@ -664,7 +711,7 @@ pub(crate) fn resolve_relation<'a>(
}
}
// Twin Deduction Pass 2: Knowing which arrow points outbound, we can mathematically deduce its twin
// 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];
@ -697,9 +744,25 @@ pub(crate) fn resolve_relation<'a>(
}
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 {
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: crate::drop::ErrorDetails::default(),
});
return None;
}
Some((matching_rels[chosen_idx], directions[chosen_idx]))
}