Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 29d8dfb608 | |||
| 5b36ecf06c | |||
| 76467a6fed |
@ -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.
|
||||
|
||||
### 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:
|
||||
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.
|
||||
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.
|
||||
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.
|
||||
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. **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. **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. **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
|
||||
These functions operate on the global `GLOBAL_JSPG` engine instance and provide administrative boundaries:
|
||||
|
||||
0
agreego.sql
Normal file
0
agreego.sql
Normal file
2
flows
2
flows
Submodule flows updated: a7b0f5dc4d...4d61e13e00
@ -45,7 +45,7 @@ impl MockExecutor {
|
||||
#[cfg(test)]
|
||||
impl DatabaseExecutor for MockExecutor {
|
||||
fn query(&self, sql: &str, _args: Option<&[Value]>) -> Result<Value, String> {
|
||||
println!("DEBUG SQL QUERY: {}", sql);
|
||||
println!("JSPG_SQL: {}", sql);
|
||||
MOCK_STATE.with(|state| {
|
||||
let mut s = state.borrow_mut();
|
||||
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> {
|
||||
println!("DEBUG SQL EXECUTE: {}", sql);
|
||||
println!("JSPG_SQL: {}", sql);
|
||||
MOCK_STATE.with(|state| {
|
||||
let mut s = state.borrow_mut();
|
||||
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("")
|
||||
.trim_matches('"');
|
||||
let right = part[eq_idx + 1..].trim().trim_matches('\'');
|
||||
|
||||
|
||||
let mock_val_str = match mock_obj.get(left) {
|
||||
Some(Value::String(s)) => s.clone(),
|
||||
Some(Value::Number(n)) => n.to_string(),
|
||||
@ -189,12 +189,12 @@ fn parse_and_match_mocks(sql: &str, mocks: &[Value]) -> Option<Vec<Value>> {
|
||||
.last()
|
||||
.unwrap_or("")
|
||||
.trim_matches('"');
|
||||
|
||||
|
||||
let mock_val_str = match mock_obj.get(left) {
|
||||
Some(Value::Null) => "null".to_string(),
|
||||
_ => "".to_string(),
|
||||
};
|
||||
|
||||
|
||||
if mock_val_str != "null" {
|
||||
branch_matches = false;
|
||||
break;
|
||||
|
||||
@ -497,6 +497,10 @@ impl Schema {
|
||||
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(
|
||||
&self,
|
||||
db: &crate::database::Database,
|
||||
@ -504,6 +508,9 @@ impl Schema {
|
||||
props: &std::collections::BTreeMap<String, std::sync::Arc<Schema>>,
|
||||
) -> 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());
|
||||
@ -512,11 +519,14 @@ impl Schema {
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
// 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_
|
||||
{
|
||||
@ -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 {
|
||||
child_type_name = Some(family.split('.').next_back().unwrap_or(family).to_string());
|
||||
} 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 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);
|
||||
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))
|
||||
{
|
||||
@ -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>(
|
||||
db: &'a crate::database::Database,
|
||||
parent_type: &str,
|
||||
@ -573,6 +590,8 @@ pub(crate) fn resolve_relation<'a>(
|
||||
prop_name: &str,
|
||||
relative_keys: Option<&Vec<String>>,
|
||||
) -> 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;
|
||||
}
|
||||
@ -583,6 +602,9 @@ pub(crate) fn resolve_relation<'a>(
|
||||
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).
|
||||
for rel in db.relations.values() {
|
||||
let is_forward = p_def.hierarchy.contains(&rel.source_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() {
|
||||
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]));
|
||||
}
|
||||
@ -609,6 +633,8 @@ pub(crate) fn resolve_relation<'a>(
|
||||
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)
|
||||
@ -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() {
|
||||
// 1. M:M Disambiguation: The child schema explicitly defines an outbound property
|
||||
// matching one of the relational prefixes (e.g. "target"). We first identify that consumed relation.
|
||||
// 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() {
|
||||
@ -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 {
|
||||
let used_rel = matching_rels[used_idx];
|
||||
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 {
|
||||
// 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();
|
||||
for (i, rel) in matching_rels.iter().enumerate() {
|
||||
if rel.prefix.is_none() {
|
||||
@ -667,7 +697,6 @@ pub(crate) fn resolve_relation<'a>(
|
||||
}
|
||||
if null_prefix_ids.len() == 1 {
|
||||
chosen_idx = null_prefix_ids[0];
|
||||
// resolved = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -259,13 +259,7 @@ impl Merger {
|
||||
};
|
||||
|
||||
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) {
|
||||
println!("FOUND EDGE {} -> {:?}", relation_name, edge.constraint);
|
||||
if let Some(relation) = self.db.relations.get(&edge.constraint) {
|
||||
let parent_is_source = edge.forward;
|
||||
|
||||
|
||||
@ -67,7 +67,10 @@ impl<'a> Compiler<'a> {
|
||||
if let Some(items) = &node.schema.obj.items {
|
||||
let mut resolved_type = None;
|
||||
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);
|
||||
} else if let Some(base_type_name) = items.obj.identifier() {
|
||||
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
|
||||
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> {
|
||||
@ -452,7 +458,6 @@ impl<'a> Compiler<'a> {
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
let (val_sql, val_type) = self.compile_node(child_node)?;
|
||||
|
||||
if val_type != "abort" {
|
||||
@ -515,7 +520,13 @@ impl<'a> Compiler<'a> {
|
||||
// Determine if the property schema resolves to a physical Database Entity
|
||||
let mut bound_type_name = None;
|
||||
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() {
|
||||
bound_type_name = Some(lookup_key);
|
||||
}
|
||||
@ -536,7 +547,10 @@ impl<'a> Compiler<'a> {
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
@ -710,8 +724,6 @@ impl<'a> Compiler<'a> {
|
||||
) -> Result<(), String> {
|
||||
if let Some(prop_ref) = &node.property_name {
|
||||
let prop = prop_ref.as_str();
|
||||
println!("DEBUG: Eval prop: {}", prop);
|
||||
|
||||
let mut parent_relation_alias = node.parent_alias.clone();
|
||||
let mut child_relation_alias = base_alias.to_string();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user