Compare commits

...

3 Commits

Author SHA1 Message Date
29d8dfb608 flow update 2026-03-28 16:51:26 -04:00
5b36ecf06c doc update and more code comments 2026-03-27 19:25:15 -04:00
76467a6fed log cleanup 2026-03-27 19:19:27 -04:00
7 changed files with 63 additions and 28 deletions

View File

@ -24,10 +24,10 @@ To support high-throughput operations while allowing for runtime updates (e.g.,
4. **Lock-Free Reads**: Incoming operations acquire a read lock just long enough to clone the `Arc` inside an `RwLock<Option<Arc<Validator>>>`, ensuring zero blocking during schema updates. 4. **Lock-Free Reads**: Incoming operations acquire a read lock just long enough to clone the `Arc` inside an `RwLock<Option<Arc<Validator>>>`, ensuring zero blocking during schema updates.
### Relational Edge Resolution ### Relational Edge Resolution
When compiling nested object graphs or arrays, the JSPG engine must dynamically infer which Postgres Foreign Key constraint correctly bridges the parent to the nested schema. It utilizes a strict 3-step hierarchical resolution: When compiling nested object graphs or arrays, the JSPG engine must dynamically infer which Postgres Foreign Key constraint correctly bridges the parent to the nested schema. It utilizes a strict 3-step hierarchical resolution applied during the `OnceLock` Compilation phase:
1. **Direct Prefix Match**: If an explicitly prefixed Foreign Key (e.g. `fk_invoice_counterparty_entity` -> `prefix: "counterparty"`) matches the exact name of the requested schema property (e.g. `{"counterparty": {...}}`), it is instantly selected. 1. **Exact Prefix Match**: If an explicitly prefixed Foreign Key (e.g. `fk_invoice_counterparty_entity` -> `prefix: "counterparty"`) directly matches the name of the requested schema property (e.g. `{"counterparty": {...}}`), it is instantly selected.
2. **Base Edge Fallback (1:M)**: If no explicit prefix directly matches the property name, the compiler filters for explicitly one remaining relation with a `null` prefix (e.g. `fk_invoice_line_invoice` -> `prefix: null`). A `null` prefix mathematically denotes the standard structural parent-child ownership edge (bypassing any M:M ambiguity) and is safely picked over explicit (but unmatched) property edges. 2. **Ambiguity Elimination (M:M Twin Deduction)**: If multiple explicitly prefixed relations remain (which happens by design in Many-to-Many junction tables like `contact` or `role`), the compiler uses a process of elimination. It inspects the compiled child JSON schema AST to see which of the relational prefixes the child *natively consumes* as an explicit outbound property (e.g. `contact` natively defines `{ "target": ... }`). It considers that prefix arrow "used up" by the child, and mathematically deduces that its exact twin providing reverse ownership (`"source"`) MUST be the inbound link mapping from the parent. This logic relies on `OnceLock` recursive compilation to accurately peek at child structures.
3. **Ambiguity Elimination (M:M)**: If multiple explicitly prefixed relations remain (which happens by design in Many-to-Many junction tables like `contact` utilizing `fk_relationship_source` and `fk_relationship_target`), the compiler uses a process of elimination. It checks which of the prefix names the child schema *natively consumes* as an outbound property (e.g. `contact` defines `{ "target": ... }`). It considers that prefix "used up" and mathematically deduces the *remaining* explicitly prefixed relation (`"source"`) must be the inbound link from the parent. 3. **Implicit Base Fallback (1:M)**: If no explicit prefix matches, and M:M deduction fails, the compiler filters for exactly one remaining relation with a `null` prefix (e.g. `fk_invoice_line_invoice` -> `prefix: null`). A `null` prefix mathematically denotes the core structural parent-child ownership edge and is safely used as a fallback.
### Global API Reference ### Global API Reference
These functions operate on the global `GLOBAL_JSPG` engine instance and provide administrative boundaries: These functions operate on the global `GLOBAL_JSPG` engine instance and provide administrative boundaries:

0
agreego.sql Normal file
View File

2
flows

Submodule flows updated: a7b0f5dc4d...4d61e13e00

View File

@ -45,7 +45,7 @@ impl MockExecutor {
#[cfg(test)] #[cfg(test)]
impl DatabaseExecutor for MockExecutor { impl DatabaseExecutor for MockExecutor {
fn query(&self, sql: &str, _args: Option<&[Value]>) -> Result<Value, String> { fn query(&self, sql: &str, _args: Option<&[Value]>) -> Result<Value, String> {
println!("DEBUG SQL QUERY: {}", sql); println!("JSPG_SQL: {}", sql);
MOCK_STATE.with(|state| { MOCK_STATE.with(|state| {
let mut s = state.borrow_mut(); let mut s = state.borrow_mut();
s.captured_queries.push(sql.to_string()); s.captured_queries.push(sql.to_string());
@ -66,7 +66,7 @@ impl DatabaseExecutor for MockExecutor {
} }
fn execute(&self, sql: &str, _args: Option<&[Value]>) -> Result<(), String> { fn execute(&self, sql: &str, _args: Option<&[Value]>) -> Result<(), String> {
println!("DEBUG SQL EXECUTE: {}", sql); println!("JSPG_SQL: {}", sql);
MOCK_STATE.with(|state| { MOCK_STATE.with(|state| {
let mut s = state.borrow_mut(); let mut s = state.borrow_mut();
s.captured_queries.push(sql.to_string()); s.captured_queries.push(sql.to_string());
@ -170,7 +170,7 @@ fn parse_and_match_mocks(sql: &str, mocks: &[Value]) -> Option<Vec<Value>> {
.unwrap_or("") .unwrap_or("")
.trim_matches('"'); .trim_matches('"');
let right = part[eq_idx + 1..].trim().trim_matches('\''); let right = part[eq_idx + 1..].trim().trim_matches('\'');
let mock_val_str = match mock_obj.get(left) { let mock_val_str = match mock_obj.get(left) {
Some(Value::String(s)) => s.clone(), Some(Value::String(s)) => s.clone(),
Some(Value::Number(n)) => n.to_string(), Some(Value::Number(n)) => n.to_string(),
@ -189,12 +189,12 @@ fn parse_and_match_mocks(sql: &str, mocks: &[Value]) -> Option<Vec<Value>> {
.last() .last()
.unwrap_or("") .unwrap_or("")
.trim_matches('"'); .trim_matches('"');
let mock_val_str = match mock_obj.get(left) { let mock_val_str = match mock_obj.get(left) {
Some(Value::Null) => "null".to_string(), Some(Value::Null) => "null".to_string(),
_ => "".to_string(), _ => "".to_string(),
}; };
if mock_val_str != "null" { if mock_val_str != "null" {
branch_matches = false; branch_matches = false;
break; break;

View File

@ -497,6 +497,10 @@ impl Schema {
Ok(()) Ok(())
} }
/// 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( pub fn compile_edges(
&self, &self,
db: &crate::database::Database, db: &crate::database::Database,
@ -504,6 +508,9 @@ impl Schema {
props: &std::collections::BTreeMap<String, std::sync::Arc<Schema>>, props: &std::collections::BTreeMap<String, std::sync::Arc<Schema>>,
) -> std::collections::BTreeMap<String, crate::database::edge::Edge> { ) -> std::collections::BTreeMap<String, crate::database::edge::Edge> {
let mut schema_edges = std::collections::BTreeMap::new(); 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; let mut parent_type_name = None;
if let Some(family) = &self.obj.family { if let Some(family) = &self.obj.family {
parent_type_name = Some(family.split('.').next_back().unwrap_or(family).to_string()); parent_type_name = Some(family.split('.').next_back().unwrap_or(family).to_string());
@ -512,11 +519,14 @@ impl Schema {
} }
if let Some(p_type) = parent_type_name { 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) { if db.types.contains_key(&p_type) {
// Iterate over all discovered schema boundaries mapped inside the object
for (prop_name, prop_schema) in props { for (prop_name, prop_schema) in props {
let mut child_type_name = None; let mut child_type_name = None;
let mut target_schema = prop_schema.clone(); let mut target_schema = prop_schema.clone();
// Structurally unpack the inner target entity if the object maps to an array list
if let Some(crate::database::schema::SchemaTypeOrArray::Single(t)) = if let Some(crate::database::schema::SchemaTypeOrArray::Single(t)) =
&prop_schema.obj.type_ &prop_schema.obj.type_
{ {
@ -527,6 +537,7 @@ impl Schema {
} }
} }
// Determine the physical Postgres table backing the nested child schema recursively
if let Some(family) = &target_schema.obj.family { if let Some(family) = &target_schema.obj.family {
child_type_name = Some(family.split('.').next_back().unwrap_or(family).to_string()); child_type_name = Some(family.split('.').next_back().unwrap_or(family).to_string());
} else if let Some(ref_id) = target_schema.obj.identifier() { } else if let Some(ref_id) = target_schema.obj.identifier() {
@ -541,10 +552,14 @@ impl Schema {
if let Some(c_type) = child_type_name { if let Some(c_type) = child_type_name {
if db.types.contains_key(&c_type) { 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); target_schema.compile(db, visited);
if let Some(compiled_target_props) = target_schema.obj.compiled_properties.get() { if let Some(compiled_target_props) = target_schema.obj.compiled_properties.get() {
let keys_for_ambiguity: Vec<String> = let keys_for_ambiguity: Vec<String> =
compiled_target_props.keys().cloned().collect(); 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)) = if let Some((relation, is_forward)) =
resolve_relation(db, &p_type, &c_type, prop_name, Some(&keys_for_ambiguity)) resolve_relation(db, &p_type, &c_type, prop_name, Some(&keys_for_ambiguity))
{ {
@ -566,6 +581,8 @@ impl Schema {
} }
} }
/// 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>( pub(crate) fn resolve_relation<'a>(
db: &'a crate::database::Database, db: &'a crate::database::Database,
parent_type: &str, parent_type: &str,
@ -573,6 +590,8 @@ pub(crate) fn resolve_relation<'a>(
prop_name: &str, prop_name: &str,
relative_keys: Option<&Vec<String>>, relative_keys: Option<&Vec<String>>,
) -> Option<(&'a crate::database::relation::Relation, bool)> { ) -> 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" { if parent_type == "entity" && child_type == "entity" {
return None; return None;
} }
@ -583,6 +602,9 @@ pub(crate) fn resolve_relation<'a>(
let mut matching_rels = Vec::new(); let mut matching_rels = Vec::new();
let mut directions = 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).
for rel in db.relations.values() { for rel in db.relations.values() {
let is_forward = p_def.hierarchy.contains(&rel.source_type) let is_forward = p_def.hierarchy.contains(&rel.source_type)
&& c_def.hierarchy.contains(&rel.destination_type); && c_def.hierarchy.contains(&rel.destination_type);
@ -598,10 +620,12 @@ pub(crate) fn resolve_relation<'a>(
} }
} }
// Abort relation discovery early if no hierarchical inheritance match was found
if matching_rels.is_empty() { if matching_rels.is_empty() {
return None; return None;
} }
// Ideal State: The objects only share a solitary structural relation, resolving ambiguity instantly.
if matching_rels.len() == 1 { if matching_rels.len() == 1 {
return Some((matching_rels[0], directions[0])); return Some((matching_rels[0], directions[0]));
} }
@ -609,6 +633,8 @@ pub(crate) fn resolve_relation<'a>(
let mut chosen_idx = 0; let mut chosen_idx = 0;
let mut resolved = false; 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() { for (i, rel) in matching_rels.iter().enumerate() {
if let Some(prefix) = &rel.prefix { if let Some(prefix) = &rel.prefix {
if prop_name.starts_with(prefix) if prop_name.starts_with(prefix)
@ -622,9 +648,11 @@ pub(crate) fn resolve_relation<'a>(
} }
} }
// 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() { if !resolved && relative_keys.is_some() {
// 1. M:M Disambiguation: The child schema explicitly defines an outbound property // Twin Deduction Pass 1: We inspect the exact properties structurally defined inside the compiled payload
// matching one of the relational prefixes (e.g. "target"). We first identify that consumed relation. // to observe which explicit relation arrow the child payload natively consumes.
let keys = relative_keys.unwrap(); let keys = relative_keys.unwrap();
let mut consumed_rel_idx = None; let mut consumed_rel_idx = None;
for (i, rel) in matching_rels.iter().enumerate() { for (i, rel) in matching_rels.iter().enumerate() {
@ -636,7 +664,8 @@ pub(crate) fn resolve_relation<'a>(
} }
} }
// Then, we find its exact Twin on the same junction boundary that provides the reverse ownership. // 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 { if let Some(used_idx) = consumed_rel_idx {
let used_rel = matching_rels[used_idx]; let used_rel = matching_rels[used_idx];
let mut twin_ids = Vec::new(); let mut twin_ids = Vec::new();
@ -657,8 +686,9 @@ pub(crate) fn resolve_relation<'a>(
} }
} }
// 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 { if !resolved {
// 2. Base 1:M Fallback. If there's EXACTLY ONE relation with a null prefix, it's the base structural edge.
let mut null_prefix_ids = Vec::new(); let mut null_prefix_ids = Vec::new();
for (i, rel) in matching_rels.iter().enumerate() { for (i, rel) in matching_rels.iter().enumerate() {
if rel.prefix.is_none() { if rel.prefix.is_none() {
@ -667,7 +697,6 @@ pub(crate) fn resolve_relation<'a>(
} }
if null_prefix_ids.len() == 1 { if null_prefix_ids.len() == 1 {
chosen_idx = null_prefix_ids[0]; chosen_idx = null_prefix_ids[0];
// resolved = true;
} }
} }

View File

@ -259,13 +259,7 @@ impl Merger {
}; };
if let Some(compiled_edges) = schema.obj.compiled_edges.get() { if let Some(compiled_edges) = schema.obj.compiled_edges.get() {
println!(
"Compiled Edges keys for relation {}: {:?}",
relation_name,
compiled_edges.keys().collect::<Vec<_>>()
);
if let Some(edge) = compiled_edges.get(&relation_name) { if let Some(edge) = compiled_edges.get(&relation_name) {
println!("FOUND EDGE {} -> {:?}", relation_name, edge.constraint);
if let Some(relation) = self.db.relations.get(&edge.constraint) { if let Some(relation) = self.db.relations.get(&edge.constraint) {
let parent_is_source = edge.forward; let parent_is_source = edge.forward;

View File

@ -67,7 +67,10 @@ impl<'a> Compiler<'a> {
if let Some(items) = &node.schema.obj.items { if let Some(items) = &node.schema.obj.items {
let mut resolved_type = None; let mut resolved_type = None;
if let Some(family_target) = items.obj.family.as_ref() { if let Some(family_target) = items.obj.family.as_ref() {
let base_type_name = family_target.split('.').next_back().unwrap_or(family_target); let base_type_name = family_target
.split('.')
.next_back()
.unwrap_or(family_target);
resolved_type = self.db.types.get(base_type_name); resolved_type = self.db.types.get(base_type_name);
} else if let Some(base_type_name) = items.obj.identifier() { } else if let Some(base_type_name) = items.obj.identifier() {
resolved_type = self.db.types.get(&base_type_name); resolved_type = self.db.types.get(&base_type_name);
@ -89,7 +92,10 @@ impl<'a> Compiler<'a> {
} }
// 3. Fallback for root execution of standalone non-entity arrays // 3. Fallback for root execution of standalone non-entity arrays
Err("Cannot compile a root array without a valid entity reference or table mapped via `items`.".to_string()) Err(
"Cannot compile a root array without a valid entity reference or table mapped via `items`."
.to_string(),
)
} }
fn compile_reference(&mut self, node: Node<'a>) -> Result<(String, String), String> { fn compile_reference(&mut self, node: Node<'a>) -> Result<(String, String), String> {
@ -452,7 +458,6 @@ impl<'a> Compiler<'a> {
}, },
}; };
let (val_sql, val_type) = self.compile_node(child_node)?; let (val_sql, val_type) = self.compile_node(child_node)?;
if val_type != "abort" { if val_type != "abort" {
@ -515,7 +520,13 @@ impl<'a> Compiler<'a> {
// Determine if the property schema resolves to a physical Database Entity // Determine if the property schema resolves to a physical Database Entity
let mut bound_type_name = None; let mut bound_type_name = None;
if let Some(family_target) = prop_schema.obj.family.as_ref() { if let Some(family_target) = prop_schema.obj.family.as_ref() {
bound_type_name = Some(family_target.split('.').next_back().unwrap_or(family_target).to_string()); bound_type_name = Some(
family_target
.split('.')
.next_back()
.unwrap_or(family_target)
.to_string(),
);
} else if let Some(lookup_key) = prop_schema.obj.identifier() { } else if let Some(lookup_key) = prop_schema.obj.identifier() {
bound_type_name = Some(lookup_key); bound_type_name = Some(lookup_key);
} }
@ -536,7 +547,10 @@ impl<'a> Compiler<'a> {
} }
if let Some(col) = poly_col { if let Some(col) = poly_col {
if let Some(alias) = type_aliases.get(table_to_alias).or_else(|| type_aliases.get(&node.parent_alias)) { if let Some(alias) = type_aliases
.get(table_to_alias)
.or_else(|| type_aliases.get(&node.parent_alias))
{
where_clauses.push(format!("{}.{} = '{}'", alias, col, type_name)); where_clauses.push(format!("{}.{} = '{}'", alias, col, type_name));
} }
} }
@ -710,8 +724,6 @@ impl<'a> Compiler<'a> {
) -> Result<(), String> { ) -> Result<(), String> {
if let Some(prop_ref) = &node.property_name { if let Some(prop_ref) = &node.property_name {
let prop = prop_ref.as_str(); let prop = prop_ref.as_str();
println!("DEBUG: Eval prop: {}", prop);
let mut parent_relation_alias = node.parent_alias.clone(); let mut parent_relation_alias = node.parent_alias.clone();
let mut child_relation_alias = base_alias.to_string(); let mut child_relation_alias = base_alias.to_string();