410 lines
14 KiB
Rust
410 lines
14 KiB
Rust
pub mod edge;
|
|
pub mod r#enum;
|
|
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;
|
|
|
|
// External mock exports inside the executor sub-folder
|
|
|
|
use r#enum::Enum;
|
|
use executors::DatabaseExecutor;
|
|
|
|
#[cfg(not(test))]
|
|
use executors::pgrx::SpiExecutor;
|
|
|
|
#[cfg(test)]
|
|
use executors::mock::MockExecutor;
|
|
|
|
use punc::Punc;
|
|
use relation::Relation;
|
|
use schema::Schema;
|
|
use serde_json::Value;
|
|
use std::collections::HashMap;
|
|
use std::sync::Arc;
|
|
use r#type::Type;
|
|
|
|
pub struct Database {
|
|
pub enums: HashMap<String, Enum>,
|
|
pub types: HashMap<String, Type>,
|
|
pub puncs: HashMap<String, Punc>,
|
|
pub relations: HashMap<String, Relation>,
|
|
pub schemas: HashMap<String, Arc<Schema>>,
|
|
pub executor: Box<dyn DatabaseExecutor + Send + Sync>,
|
|
}
|
|
|
|
impl Database {
|
|
pub fn new(val: &serde_json::Value) -> (Self, crate::drop::Drop) {
|
|
let mut db = Self {
|
|
enums: HashMap::new(),
|
|
types: HashMap::new(),
|
|
relations: HashMap::new(),
|
|
puncs: HashMap::new(),
|
|
schemas: HashMap::new(),
|
|
#[cfg(not(test))]
|
|
executor: Box::new(SpiExecutor::new()),
|
|
#[cfg(test)]
|
|
executor: Box::new(MockExecutor::new()),
|
|
};
|
|
|
|
let mut errors = Vec::new();
|
|
|
|
if let Some(arr) = val.get("enums").and_then(|v| v.as_array()) {
|
|
for item in arr {
|
|
match serde_json::from_value::<Enum>(item.clone()) {
|
|
Ok(def) => {
|
|
db.enums.insert(def.name.clone(), def);
|
|
}
|
|
Err(e) => {
|
|
errors.push(crate::drop::Error {
|
|
code: "DATABASE_ENUM_PARSE_FAILED".to_string(),
|
|
message: format!("Failed to parse database enum: {}", e),
|
|
details: crate::drop::ErrorDetails::default(),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if let Some(arr) = val.get("types").and_then(|v| v.as_array()) {
|
|
for item in arr {
|
|
match serde_json::from_value::<Type>(item.clone()) {
|
|
Ok(def) => {
|
|
db.types.insert(def.name.clone(), def);
|
|
}
|
|
Err(e) => {
|
|
errors.push(crate::drop::Error {
|
|
code: "DATABASE_TYPE_PARSE_FAILED".to_string(),
|
|
message: format!("Failed to parse database type: {}", e),
|
|
details: crate::drop::ErrorDetails::default(),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if let Some(arr) = val.get("relations").and_then(|v| v.as_array()) {
|
|
for item in arr {
|
|
match serde_json::from_value::<Relation>(item.clone()) {
|
|
Ok(def) => {
|
|
if db.types.contains_key(&def.source_type)
|
|
&& db.types.contains_key(&def.destination_type)
|
|
{
|
|
db.relations.insert(def.constraint.clone(), def);
|
|
}
|
|
}
|
|
Err(e) => {
|
|
errors.push(crate::drop::Error {
|
|
code: "DATABASE_RELATION_PARSE_FAILED".to_string(),
|
|
message: format!("Failed to parse database relation: {}", e),
|
|
details: crate::drop::ErrorDetails::default(),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if let Some(arr) = val.get("puncs").and_then(|v| v.as_array()) {
|
|
for item in arr {
|
|
match serde_json::from_value::<Punc>(item.clone()) {
|
|
Ok(def) => {
|
|
db.puncs.insert(def.name.clone(), def);
|
|
}
|
|
Err(e) => {
|
|
errors.push(crate::drop::Error {
|
|
code: "DATABASE_PUNC_PARSE_FAILED".to_string(),
|
|
message: format!("Failed to parse database punc: {}", e),
|
|
details: crate::drop::ErrorDetails::default(),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if let Some(arr) = val.get("schemas").and_then(|v| v.as_array()) {
|
|
for (i, item) in arr.iter().enumerate() {
|
|
match serde_json::from_value::<Schema>(item.clone()) {
|
|
Ok(mut schema) => {
|
|
let id = schema
|
|
.obj
|
|
.id
|
|
.clone()
|
|
.unwrap_or_else(|| format!("schema_{}", i));
|
|
schema.obj.id = Some(id.clone());
|
|
db.schemas.insert(id, Arc::new(schema));
|
|
}
|
|
Err(e) => {
|
|
errors.push(crate::drop::Error {
|
|
code: "DATABASE_SCHEMA_PARSE_FAILED".to_string(),
|
|
message: format!("Failed to parse database schema: {}", e),
|
|
details: crate::drop::ErrorDetails::default(),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
db.compile(&mut errors);
|
|
let drop = if errors.is_empty() {
|
|
crate::drop::Drop::success()
|
|
} else {
|
|
crate::drop::Drop::with_errors(errors)
|
|
};
|
|
(db, drop)
|
|
}
|
|
|
|
/// Override the default executor for unit testing
|
|
pub fn with_executor(mut self, executor: Box<dyn DatabaseExecutor + Send + Sync>) -> Self {
|
|
self.executor = executor;
|
|
self
|
|
}
|
|
|
|
/// Executes a query expecting a single JSONB array return, representing rows.
|
|
pub fn query(&self, sql: &str, args: Option<Vec<Value>>) -> Result<Value, String> {
|
|
self.executor.query(sql, args)
|
|
}
|
|
|
|
/// Executes an operation (INSERT, UPDATE, DELETE, or pg_notify) that does not return rows.
|
|
pub fn execute(&self, sql: &str, args: Option<Vec<Value>>) -> Result<(), String> {
|
|
self.executor.execute(sql, args)
|
|
}
|
|
|
|
/// Returns the current authenticated user's ID
|
|
pub fn auth_user_id(&self) -> Result<String, String> {
|
|
self.executor.auth_user_id()
|
|
}
|
|
|
|
/// Returns the current transaction timestamp
|
|
pub fn timestamp(&self) -> Result<String, String> {
|
|
self.executor.timestamp()
|
|
}
|
|
|
|
pub fn compile(&mut self, errors: &mut Vec<crate::drop::Error>) {
|
|
let mut harvested = Vec::new();
|
|
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.collect_schemas(errors);
|
|
|
|
// Mathematically evaluate all property inheritances, formats, schemas, and foreign key edges topographically over OnceLocks
|
|
let mut visited = std::collections::HashSet::new();
|
|
for schema_arc in self.schemas.values() {
|
|
schema_arc.as_ref().compile(self, &mut visited, errors);
|
|
}
|
|
}
|
|
|
|
fn collect_schemas(&mut self, errors: &mut Vec<crate::drop::Error>) {
|
|
let mut to_insert = Vec::new();
|
|
|
|
// Pass 1: Extract all Schemas structurally off top level definitions into the master registry.
|
|
// Validate every node recursively via string filters natively!
|
|
for type_def in self.types.values() {
|
|
for mut schema in type_def.schemas.clone() {
|
|
schema.collect_schemas(None, &mut to_insert, errors);
|
|
}
|
|
}
|
|
for punc_def in self.puncs.values() {
|
|
for mut schema in punc_def.schemas.clone() {
|
|
schema.collect_schemas(None, &mut to_insert, errors);
|
|
}
|
|
}
|
|
for enum_def in self.enums.values() {
|
|
for mut schema in enum_def.schemas.clone() {
|
|
schema.collect_schemas(None, &mut to_insert, errors);
|
|
}
|
|
}
|
|
|
|
for (id, schema) in to_insert {
|
|
self.schemas.insert(id, Arc::new(schema));
|
|
}
|
|
}
|
|
|
|
/// 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;
|
|
}
|
|
|
|
let p_def = self.types.get(parent_type)?;
|
|
let c_def = self.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> = 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;
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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]))
|
|
}
|
|
}
|