more tests
This commit is contained in:
@ -4,6 +4,7 @@ pub mod executors;
|
||||
pub mod formats;
|
||||
pub mod page;
|
||||
pub mod punc;
|
||||
pub mod object;
|
||||
pub mod relation;
|
||||
pub mod schema;
|
||||
pub mod r#type;
|
||||
@ -23,7 +24,8 @@ use punc::Punc;
|
||||
use relation::Relation;
|
||||
use schema::Schema;
|
||||
use serde_json::Value;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use r#type::Type;
|
||||
|
||||
pub struct Database {
|
||||
@ -31,8 +33,7 @@ pub struct Database {
|
||||
pub types: HashMap<String, Type>,
|
||||
pub puncs: HashMap<String, Punc>,
|
||||
pub relations: HashMap<String, Relation>,
|
||||
pub schemas: HashMap<String, Schema>,
|
||||
pub depths: HashMap<String, usize>,
|
||||
pub schemas: HashMap<String, Arc<Schema>>,
|
||||
pub executor: Box<dyn DatabaseExecutor + Send + Sync>,
|
||||
}
|
||||
|
||||
@ -44,7 +45,6 @@ impl Database {
|
||||
relations: HashMap::new(),
|
||||
puncs: HashMap::new(),
|
||||
schemas: HashMap::new(),
|
||||
depths: HashMap::new(),
|
||||
#[cfg(not(test))]
|
||||
executor: Box::new(SpiExecutor::new()),
|
||||
#[cfg(test)]
|
||||
@ -135,7 +135,7 @@ impl Database {
|
||||
.clone()
|
||||
.unwrap_or_else(|| format!("schema_{}", i));
|
||||
schema.obj.id = Some(id.clone());
|
||||
db.schemas.insert(id, schema);
|
||||
db.schemas.insert(id, Arc::new(schema));
|
||||
}
|
||||
Err(e) => {
|
||||
errors.push(crate::drop::Error {
|
||||
@ -185,18 +185,21 @@ impl Database {
|
||||
|
||||
pub fn compile(&mut self, errors: &mut Vec<crate::drop::Error>) {
|
||||
let mut harvested = Vec::new();
|
||||
for schema in self.schemas.values_mut() {
|
||||
schema.collect_schemas(None, &mut harvested, errors);
|
||||
for schema_arc in self.schemas.values_mut() {
|
||||
if let Some(s) = std::sync::Arc::get_mut(schema_arc) {
|
||||
s.collect_schemas(None, &mut harvested, errors);
|
||||
}
|
||||
}
|
||||
for (id, schema) in harvested {
|
||||
self.schemas.insert(id, Arc::new(schema));
|
||||
}
|
||||
self.schemas.extend(harvested);
|
||||
|
||||
self.collect_schemas(errors);
|
||||
self.collect_depths();
|
||||
|
||||
// Mathematically evaluate all property inheritances, formats, schemas, and foreign key edges topographically over OnceLocks
|
||||
let mut visited = std::collections::HashSet::new();
|
||||
for schema in self.schemas.values() {
|
||||
schema.compile(self, &mut visited, errors);
|
||||
for schema_arc in self.schemas.values() {
|
||||
schema_arc.as_ref().compile(self, &mut visited, errors);
|
||||
}
|
||||
}
|
||||
|
||||
@ -222,35 +225,185 @@ impl Database {
|
||||
}
|
||||
|
||||
for (id, schema) in to_insert {
|
||||
self.schemas.insert(id, schema);
|
||||
self.schemas.insert(id, Arc::new(schema));
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_depths(&mut self) {
|
||||
let mut depths: HashMap<String, usize> = HashMap::new();
|
||||
let schema_ids: Vec<String> = self.schemas.keys().cloned().collect();
|
||||
/// Inspects the Postgres pg_constraint relations catalog to securely identify
|
||||
/// the precise Foreign Key connecting a parent and child hierarchy path.
|
||||
pub fn resolve_relation<'a>(
|
||||
&'a self,
|
||||
parent_type: &str,
|
||||
child_type: &str,
|
||||
prop_name: &str,
|
||||
relative_keys: Option<&Vec<String>>,
|
||||
is_array: bool,
|
||||
schema_id: Option<&str>,
|
||||
path: &str,
|
||||
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;
|
||||
}
|
||||
|
||||
for id in schema_ids {
|
||||
let mut current_id = id.clone();
|
||||
let mut depth = 0;
|
||||
let mut visited = HashSet::new();
|
||||
let p_def = self.types.get(parent_type)?;
|
||||
let c_def = self.types.get(child_type)?;
|
||||
|
||||
while let Some(schema) = self.schemas.get(¤t_id) {
|
||||
if !visited.insert(current_id.clone()) {
|
||||
break; // Cycle detected
|
||||
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> = self.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;
|
||||
}
|
||||
if let Some(crate::database::schema::SchemaTypeOrArray::Single(t)) = &schema.obj.type_ {
|
||||
if !crate::database::schema::is_primitive_type(t) {
|
||||
current_id = t.clone();
|
||||
depth += 1;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
depths.insert(id, depth);
|
||||
}
|
||||
self.depths = depths;
|
||||
}
|
||||
|
||||
// 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]))
|
||||
}
|
||||
}
|
||||
|
||||
367
src/database/object.rs
Normal file
367
src/database/object.rs
Normal file
@ -0,0 +1,367 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use std::collections::BTreeMap;
|
||||
use std::sync::Arc;
|
||||
use std::sync::OnceLock;
|
||||
use crate::database::schema::Schema;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Case {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub when: Option<Arc<Schema>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub then: Option<Arc<Schema>>,
|
||||
#[serde(rename = "else")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub else_: Option<Arc<Schema>>,
|
||||
}
|
||||
|
||||
#[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<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub description: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub title: Option<String>,
|
||||
#[serde(default)] // Allow missing type
|
||||
#[serde(rename = "type")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub type_: Option<SchemaTypeOrArray>, // Handles string or array of strings
|
||||
|
||||
// Object Keywords
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub properties: Option<BTreeMap<String, Arc<Schema>>>,
|
||||
#[serde(rename = "patternProperties")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub pattern_properties: Option<BTreeMap<String, Arc<Schema>>>,
|
||||
#[serde(rename = "additionalProperties")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub additional_properties: Option<Arc<Schema>>,
|
||||
#[serde(rename = "$family")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub family: Option<String>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub required: Option<Vec<String>>,
|
||||
|
||||
// dependencies can be schema dependencies or property dependencies
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub dependencies: Option<BTreeMap<String, Dependency>>,
|
||||
|
||||
// Array Keywords
|
||||
#[serde(rename = "items")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub items: Option<Arc<Schema>>,
|
||||
#[serde(rename = "prefixItems")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub prefix_items: Option<Vec<Arc<Schema>>>,
|
||||
|
||||
// String Validation
|
||||
#[serde(rename = "minLength")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub min_length: Option<f64>,
|
||||
#[serde(rename = "maxLength")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub max_length: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub pattern: Option<String>,
|
||||
|
||||
// Array Validation
|
||||
#[serde(rename = "minItems")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub min_items: Option<f64>,
|
||||
#[serde(rename = "maxItems")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub max_items: Option<f64>,
|
||||
#[serde(rename = "uniqueItems")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub unique_items: Option<bool>,
|
||||
#[serde(rename = "contains")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub contains: Option<Arc<Schema>>,
|
||||
#[serde(rename = "minContains")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub min_contains: Option<f64>,
|
||||
#[serde(rename = "maxContains")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub max_contains: Option<f64>,
|
||||
|
||||
// Object Validation
|
||||
#[serde(rename = "minProperties")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub min_properties: Option<f64>,
|
||||
#[serde(rename = "maxProperties")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub max_properties: Option<f64>,
|
||||
#[serde(rename = "propertyNames")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub property_names: Option<Arc<Schema>>,
|
||||
|
||||
// Numeric Validation
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub format: Option<String>,
|
||||
#[serde(rename = "enum")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub enum_: Option<Vec<Value>>, // `enum` is a reserved keyword in Rust
|
||||
#[serde(
|
||||
default,
|
||||
rename = "const",
|
||||
deserialize_with = "crate::database::object::deserialize_some"
|
||||
)]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub const_: Option<Value>,
|
||||
|
||||
// Numeric Validation
|
||||
#[serde(rename = "multipleOf")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub multiple_of: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub minimum: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub maximum: Option<f64>,
|
||||
#[serde(rename = "exclusiveMinimum")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub exclusive_minimum: Option<f64>,
|
||||
#[serde(rename = "exclusiveMaximum")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub exclusive_maximum: Option<f64>,
|
||||
|
||||
// Combining Keywords
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub cases: Option<Vec<Case>>,
|
||||
#[serde(rename = "oneOf")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub one_of: Option<Vec<Arc<Schema>>>,
|
||||
#[serde(rename = "not")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub not: Option<Arc<Schema>>,
|
||||
|
||||
// Custom Vocabularies
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub form: Option<Vec<String>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub display: Option<Vec<String>>,
|
||||
#[serde(rename = "enumNames")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub enum_names: Option<Vec<String>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub control: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub actions: Option<BTreeMap<String, Action>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub computer: Option<String>,
|
||||
#[serde(default)]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub extensible: Option<bool>,
|
||||
|
||||
#[serde(rename = "compiledProperties")]
|
||||
#[serde(skip_deserializing)]
|
||||
#[serde(skip_serializing_if = "crate::database::object::is_once_lock_vec_empty")]
|
||||
#[serde(serialize_with = "crate::database::object::serialize_once_lock")]
|
||||
pub compiled_property_names: OnceLock<Vec<String>>,
|
||||
|
||||
#[serde(skip)]
|
||||
pub compiled_properties: OnceLock<BTreeMap<String, Arc<Schema>>>,
|
||||
|
||||
#[serde(rename = "compiledDiscriminator")]
|
||||
#[serde(skip_deserializing)]
|
||||
#[serde(skip_serializing_if = "crate::database::object::is_once_lock_string_empty")]
|
||||
#[serde(serialize_with = "crate::database::object::serialize_once_lock")]
|
||||
pub compiled_discriminator: OnceLock<String>,
|
||||
|
||||
#[serde(rename = "compiledOptions")]
|
||||
#[serde(skip_deserializing)]
|
||||
#[serde(skip_serializing_if = "crate::database::object::is_once_lock_map_empty")]
|
||||
#[serde(serialize_with = "crate::database::object::serialize_once_lock")]
|
||||
pub compiled_options: OnceLock<BTreeMap<String, String>>,
|
||||
|
||||
#[serde(rename = "compiledEdges")]
|
||||
#[serde(skip_deserializing)]
|
||||
#[serde(skip_serializing_if = "crate::database::object::is_once_lock_map_empty")]
|
||||
#[serde(serialize_with = "crate::database::object::serialize_once_lock")]
|
||||
pub compiled_edges: OnceLock<BTreeMap<String, crate::database::edge::Edge>>,
|
||||
|
||||
#[serde(skip)]
|
||||
pub compiled_format: OnceLock<CompiledFormat>,
|
||||
#[serde(skip)]
|
||||
pub compiled_pattern: OnceLock<CompiledRegex>,
|
||||
#[serde(skip)]
|
||||
pub compiled_pattern_properties: OnceLock<Vec<(CompiledRegex, Arc<Schema>)>>,
|
||||
}
|
||||
|
||||
/// Represents a compiled format validator
|
||||
#[derive(Clone)]
|
||||
pub enum CompiledFormat {
|
||||
Func(fn(&serde_json::Value) -> Result<(), Box<dyn std::error::Error + Send + Sync>>),
|
||||
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, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum SchemaTypeOrArray {
|
||||
Single(String),
|
||||
Multiple(Vec<String>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Action {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub navigate: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub punc: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum Dependency {
|
||||
Props(Vec<String>),
|
||||
Schema(Arc<Schema>),
|
||||
}
|
||||
|
||||
pub fn serialize_once_lock<T: serde::Serialize, S: serde::Serializer>(
|
||||
lock: &OnceLock<T>,
|
||||
serializer: S,
|
||||
) -> Result<S::Ok, S::Error> {
|
||||
if let Some(val) = lock.get() {
|
||||
val.serialize(serializer)
|
||||
} else {
|
||||
serializer.serialize_none()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_once_lock_map_empty<K, V>(lock: &OnceLock<std::collections::BTreeMap<K, V>>) -> bool {
|
||||
lock.get().map_or(true, |m| m.is_empty())
|
||||
}
|
||||
|
||||
pub fn is_once_lock_vec_empty<T>(lock: &OnceLock<Vec<T>>) -> bool {
|
||||
lock.get().map_or(true, |v| v.is_empty())
|
||||
}
|
||||
|
||||
pub fn is_once_lock_string_empty(lock: &OnceLock<String>) -> 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<Option<Value>, 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"
|
||||
)
|
||||
}
|
||||
|
||||
impl SchemaObject {
|
||||
pub fn identifier(&self) -> Option<String> {
|
||||
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<String> {
|
||||
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::<Vec<_>>())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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(SchemaTypeOrArray::Single(t)) = &self.type_ {
|
||||
if !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(SchemaTypeOrArray::Single(t)) = &self.type_ {
|
||||
if !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
|
||||
}
|
||||
}
|
||||
@ -1,256 +1,7 @@
|
||||
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<T: serde::Serialize, S: serde::Serializer>(
|
||||
lock: &OnceLock<T>,
|
||||
serializer: S,
|
||||
) -> Result<S::Ok, S::Error> {
|
||||
if let Some(val) = lock.get() {
|
||||
val.serialize(serializer)
|
||||
} else {
|
||||
serializer.serialize_none()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_once_lock_map_empty<K, V>(lock: &OnceLock<std::collections::BTreeMap<K, V>>) -> bool {
|
||||
lock.get().map_or(true, |m| m.is_empty())
|
||||
}
|
||||
|
||||
pub fn is_once_lock_vec_empty<T>(lock: &OnceLock<Vec<T>>) -> bool {
|
||||
lock.get().map_or(true, |v| v.is_empty())
|
||||
}
|
||||
|
||||
pub fn is_once_lock_string_empty(lock: &OnceLock<String>) -> 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<Option<Value>, 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<Arc<Schema>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub then: Option<Arc<Schema>>,
|
||||
#[serde(rename = "else")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub else_: Option<Arc<Schema>>,
|
||||
}
|
||||
|
||||
#[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<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub description: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub title: Option<String>,
|
||||
#[serde(default)] // Allow missing type
|
||||
#[serde(rename = "type")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub type_: Option<SchemaTypeOrArray>, // Handles string or array of strings
|
||||
|
||||
// Object Keywords
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub properties: Option<BTreeMap<String, Arc<Schema>>>,
|
||||
#[serde(rename = "patternProperties")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub pattern_properties: Option<BTreeMap<String, Arc<Schema>>>,
|
||||
#[serde(rename = "additionalProperties")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub additional_properties: Option<Arc<Schema>>,
|
||||
#[serde(rename = "$family")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub family: Option<String>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub required: Option<Vec<String>>,
|
||||
|
||||
// dependencies can be schema dependencies or property dependencies
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub dependencies: Option<BTreeMap<String, Dependency>>,
|
||||
|
||||
// Array Keywords
|
||||
#[serde(rename = "items")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub items: Option<Arc<Schema>>,
|
||||
#[serde(rename = "prefixItems")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub prefix_items: Option<Vec<Arc<Schema>>>,
|
||||
|
||||
// String Validation
|
||||
#[serde(rename = "minLength")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub min_length: Option<f64>,
|
||||
#[serde(rename = "maxLength")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub max_length: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub pattern: Option<String>,
|
||||
|
||||
// Array Validation
|
||||
#[serde(rename = "minItems")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub min_items: Option<f64>,
|
||||
#[serde(rename = "maxItems")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub max_items: Option<f64>,
|
||||
#[serde(rename = "uniqueItems")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub unique_items: Option<bool>,
|
||||
#[serde(rename = "contains")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub contains: Option<Arc<Schema>>,
|
||||
#[serde(rename = "minContains")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub min_contains: Option<f64>,
|
||||
#[serde(rename = "maxContains")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub max_contains: Option<f64>,
|
||||
|
||||
// Object Validation
|
||||
#[serde(rename = "minProperties")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub min_properties: Option<f64>,
|
||||
#[serde(rename = "maxProperties")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub max_properties: Option<f64>,
|
||||
#[serde(rename = "propertyNames")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub property_names: Option<Arc<Schema>>,
|
||||
|
||||
// Numeric Validation
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub format: Option<String>,
|
||||
#[serde(rename = "enum")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub enum_: Option<Vec<Value>>, // `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<Value>,
|
||||
|
||||
// Numeric Validation
|
||||
#[serde(rename = "multipleOf")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub multiple_of: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub minimum: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub maximum: Option<f64>,
|
||||
#[serde(rename = "exclusiveMinimum")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub exclusive_minimum: Option<f64>,
|
||||
#[serde(rename = "exclusiveMaximum")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub exclusive_maximum: Option<f64>,
|
||||
|
||||
// Combining Keywords
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub cases: Option<Vec<Case>>,
|
||||
#[serde(rename = "oneOf")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub one_of: Option<Vec<Arc<Schema>>>,
|
||||
#[serde(rename = "not")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub not: Option<Arc<Schema>>,
|
||||
|
||||
// Custom Vocabularies
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub form: Option<Vec<String>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub display: Option<Vec<String>>,
|
||||
#[serde(rename = "enumNames")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub enum_names: Option<Vec<String>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub control: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub actions: Option<BTreeMap<String, Action>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub computer: Option<String>,
|
||||
#[serde(default)]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub extensible: Option<bool>,
|
||||
|
||||
#[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<Vec<String>>,
|
||||
|
||||
#[serde(skip)]
|
||||
pub compiled_properties: OnceLock<BTreeMap<String, Arc<Schema>>>,
|
||||
|
||||
#[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<String>,
|
||||
|
||||
#[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<BTreeMap<String, String>>,
|
||||
|
||||
#[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<BTreeMap<String, crate::database::edge::Edge>>,
|
||||
|
||||
#[serde(skip)]
|
||||
pub compiled_format: OnceLock<CompiledFormat>,
|
||||
#[serde(skip)]
|
||||
pub compiled_pattern: OnceLock<CompiledRegex>,
|
||||
#[serde(skip)]
|
||||
pub compiled_pattern_properties: OnceLock<Vec<(CompiledRegex, Arc<Schema>)>>,
|
||||
}
|
||||
|
||||
/// Represents a compiled format validator
|
||||
#[derive(Clone)]
|
||||
pub enum CompiledFormat {
|
||||
Func(fn(&serde_json::Value) -> Result<(), Box<dyn std::error::Error + Send + Sync>>),
|
||||
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);
|
||||
|
||||
use crate::database::object::*;
|
||||
#[derive(Debug, Clone, Serialize, Default)]
|
||||
pub struct Schema {
|
||||
#[serde(flatten)]
|
||||
@ -293,7 +44,7 @@ impl Schema {
|
||||
let _ = self
|
||||
.obj
|
||||
.compiled_format
|
||||
.set(crate::database::schema::CompiledFormat::Func(fmt.func));
|
||||
.set(crate::database::object::CompiledFormat::Func(fmt.func));
|
||||
}
|
||||
}
|
||||
|
||||
@ -302,7 +53,7 @@ impl Schema {
|
||||
let _ = self
|
||||
.obj
|
||||
.compiled_pattern
|
||||
.set(crate::database::schema::CompiledRegex(re));
|
||||
.set(crate::database::object::CompiledRegex(re));
|
||||
}
|
||||
}
|
||||
|
||||
@ -310,7 +61,7 @@ impl Schema {
|
||||
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()));
|
||||
compiled.push((crate::database::object::CompiledRegex(re), v.clone()));
|
||||
}
|
||||
}
|
||||
if !compiled.is_empty() {
|
||||
@ -321,10 +72,10 @@ impl Schema {
|
||||
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(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.compile(db, visited, errors);
|
||||
parent.as_ref().compile(db, visited, errors);
|
||||
if let Some(p_props) = parent.obj.compiled_properties.get() {
|
||||
props.extend(p_props.clone());
|
||||
}
|
||||
@ -332,10 +83,10 @@ impl Schema {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(crate::database::schema::SchemaTypeOrArray::Multiple(types)) = &self.obj.type_ {
|
||||
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::schema::is_primitive_type(t) {
|
||||
if !crate::database::object::is_primitive_type(t) {
|
||||
custom_type_count += 1;
|
||||
}
|
||||
}
|
||||
@ -355,9 +106,9 @@ impl Schema {
|
||||
}
|
||||
|
||||
for t in types {
|
||||
if !crate::database::schema::is_primitive_type(t) {
|
||||
if !crate::database::object::is_primitive_type(t) {
|
||||
if let Some(parent) = db.schemas.get(t) {
|
||||
parent.compile(db, visited, errors);
|
||||
parent.as_ref().compile(db, visited, errors);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -434,6 +185,106 @@ 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,
|
||||
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(),
|
||||
);
|
||||
}
|
||||
|
||||
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::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(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<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)) = db.resolve_relation(
|
||||
&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
|
||||
}
|
||||
|
||||
pub fn compile_polymorphism(
|
||||
&self,
|
||||
db: &crate::database::Database,
|
||||
@ -444,8 +295,11 @@ impl Schema {
|
||||
|
||||
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('.');
|
||||
|
||||
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
|
||||
@ -456,7 +310,7 @@ impl Schema {
|
||||
} else {
|
||||
format!("{}.{}", family_prefix, var)
|
||||
};
|
||||
|
||||
|
||||
if db.schemas.contains_key(&target_id) {
|
||||
options.insert(var.to_string(), target_id);
|
||||
}
|
||||
@ -464,36 +318,17 @@ impl Schema {
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let suffix = format!(".{}", family_base);
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(id) = &schema.obj.id {
|
||||
if id.ends_with(&suffix) || id == &family_base {
|
||||
if let Some(kind_val) = schema.obj.get_discriminator_value("kind") {
|
||||
options.insert(kind_val, id.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -535,8 +370,8 @@ impl Schema {
|
||||
|
||||
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) {
|
||||
if let Some(crate::database::object::SchemaTypeOrArray::Single(t)) = &c.obj.type_ {
|
||||
if !crate::database::object::is_primitive_type(t) {
|
||||
target_id = Some(t.clone());
|
||||
}
|
||||
}
|
||||
@ -552,8 +387,8 @@ impl Schema {
|
||||
}
|
||||
|
||||
if !options.is_empty() {
|
||||
let _ = self.obj.compiled_discriminator.set(strategy);
|
||||
let _ = self.obj.compiled_options.set(options);
|
||||
let _ = self.obj.compiled_discriminator.set(strategy);
|
||||
let _ = self.obj.compiled_options.set(options);
|
||||
}
|
||||
}
|
||||
|
||||
@ -585,8 +420,8 @@ impl Schema {
|
||||
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) {
|
||||
if let Some(crate::database::object::SchemaTypeOrArray::Single(t)) = &self.obj.type_ {
|
||||
if !crate::database::object::is_primitive_type(t) {
|
||||
Self::validate_identifier(t, "type", errors);
|
||||
}
|
||||
}
|
||||
@ -597,8 +432,8 @@ impl Schema {
|
||||
// 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(crate::database::object::SchemaTypeOrArray::Single(t)) = &self.obj.type_ {
|
||||
if !crate::database::object::is_primitive_type(t) {
|
||||
if let Some(ref path) = tracking_path {
|
||||
to_insert.push((path.clone(), self.clone()));
|
||||
}
|
||||
@ -690,285 +525,6 @@ 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,
|
||||
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(),
|
||||
);
|
||||
}
|
||||
|
||||
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<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),
|
||||
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<String>>,
|
||||
is_array: bool,
|
||||
schema_id: Option<&str>,
|
||||
path: &str,
|
||||
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;
|
||||
}
|
||||
|
||||
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 {
|
||||
@ -1021,104 +577,3 @@ impl<'de> Deserialize<'de> for Schema {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl SchemaObject {
|
||||
pub fn identifier(&self) -> Option<String> {
|
||||
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<String> {
|
||||
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::<Vec<_>>()));
|
||||
}
|
||||
}
|
||||
|
||||
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<String>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Action {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub navigate: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub punc: Option<String>,
|
||||
}
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum Dependency {
|
||||
Props(Vec<String>),
|
||||
Schema(Arc<Schema>),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user