Compare commits

..

5 Commits

Author SHA1 Message Date
c893e29c59 version: 1.0.99 2026-03-27 18:02:29 -04:00
7523431007 test pgrx no fixes 2026-03-27 18:02:24 -04:00
dd98bfac9e version: 1.0.98 2026-03-27 16:51:05 -04:00
2f3a1d16b7 version: 1.0.97 2026-03-27 16:35:31 -04:00
e86fe5cc4e fixed relationship resolution in merger and queryer 2026-03-27 16:35:23 -04:00
7 changed files with 180 additions and 100 deletions

View File

@ -23,6 +23,12 @@ To support high-throughput operations while allowing for runtime updates (e.g.,
3. **Immutable AST Caching**: The `Validator` struct immutably owns the `Database` registry. Schemas themselves are frozen structurally, but utilize `OnceLock` interior mutability during the Compilation Phase to permanently cache resolved `$ref` inheritances, properties, and `compiled_edges` directly onto their AST nodes. This guarantees strict `O(1)` relationship and property validation execution at runtime without locking or recursive DB polling. 3. **Immutable AST Caching**: The `Validator` struct immutably owns the `Database` registry. Schemas themselves are frozen structurally, but utilize `OnceLock` interior mutability during the Compilation Phase to permanently cache resolved `$ref` inheritances, properties, and `compiled_edges` directly onto their AST nodes. This guarantees strict `O(1)` relationship and property validation execution at runtime without locking or recursive DB polling.
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
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.
### 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

View File

@ -19,7 +19,7 @@
{ {
"id": "22222222-2222-2222-2222-222222222222", "id": "22222222-2222-2222-2222-222222222222",
"type": "relation", "type": "relation",
"constraint": "fk_order_customer", "constraint": "fk_order_customer_person",
"source_type": "order", "source_type": "order",
"source_columns": [ "source_columns": [
"customer_id" "customer_id"
@ -41,8 +41,7 @@
"destination_type": "order", "destination_type": "order",
"destination_columns": [ "destination_columns": [
"id" "id"
], ]
"prefix": "lines"
}, },
{ {
"id": "44444444-4444-4444-4444-444444444444", "id": "44444444-4444-4444-4444-444444444444",

View File

@ -27,7 +27,9 @@
{ {
"$id": "get_orders.response", "$id": "get_orders.response",
"type": "array", "type": "array",
"items": { "$ref": "light.order" } "items": {
"$ref": "light.order"
}
} }
] ]
} }
@ -69,7 +71,7 @@
{ {
"id": "22222222-2222-2222-2222-222222222222", "id": "22222222-2222-2222-2222-222222222222",
"type": "relation", "type": "relation",
"constraint": "fk_order_customer", "constraint": "fk_order_customer_person",
"source_type": "order", "source_type": "order",
"source_columns": [ "source_columns": [
"customer_id" "customer_id"
@ -80,6 +82,22 @@
], ],
"prefix": "customer" "prefix": "customer"
}, },
{
"id": "22222222-2222-2222-2222-222222222227",
"type": "relation",
"constraint": "fk_order_counterparty_entity",
"source_type": "order",
"source_columns": [
"counterparty_id",
"counterparty_type"
],
"destination_type": "entity",
"destination_columns": [
"id",
"type"
],
"prefix": "counterparty"
},
{ {
"id": "33333333-3333-3333-3333-333333333333", "id": "33333333-3333-3333-3333-333333333333",
"type": "relation", "type": "relation",
@ -91,8 +109,7 @@
"destination_type": "order", "destination_type": "order",
"destination_columns": [ "destination_columns": [
"id" "id"
], ]
"prefix": "lines"
} }
], ],
"types": [ "types": [
@ -713,14 +730,18 @@
"created_by", "created_by",
"modified_at", "modified_at",
"modified_by", "modified_by",
"archived" "archived",
"counterparty_id",
"counterparty_type"
], ],
"grouped_fields": { "grouped_fields": {
"order": [ "order": [
"id", "id",
"type", "type",
"total", "total",
"customer_id" "customer_id",
"counterparty_id",
"counterparty_type"
], ],
"entity": [ "entity": [
"id", "id",
@ -748,7 +769,9 @@
"created_at": "timestamptz", "created_at": "timestamptz",
"created_by": "uuid", "created_by": "uuid",
"modified_at": "timestamptz", "modified_at": "timestamptz",
"modified_by": "uuid" "modified_by": "uuid",
"counterparty_id": "uuid",
"counterparty_type": "text"
}, },
"variations": [ "variations": [
"order" "order"

View File

@ -622,7 +622,23 @@ pub(crate) fn resolve_relation<'a>(
} }
} }
if !resolved {
// 1. If there's EXACTLY ONE relation with a null prefix, it's the base structural edge. Pick it.
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 !resolved && relative_keys.is_some() { if !resolved && relative_keys.is_some() {
// 2. M:M Disambiguation: The child schema will explicitly define an outbound property
// matching one of the relational prefixes (e.g. "target"). We use the OTHER one (e.g. "source").
let keys = relative_keys.unwrap(); let keys = relative_keys.unwrap();
let mut missing_prefix_ids = Vec::new(); let mut missing_prefix_ids = Vec::new();
for (i, rel) in matching_rels.iter().enumerate() { for (i, rel) in matching_rels.iter().enumerate() {
@ -634,6 +650,7 @@ pub(crate) fn resolve_relation<'a>(
} }
if missing_prefix_ids.len() == 1 { if missing_prefix_ids.len() == 1 {
chosen_idx = missing_prefix_ids[0]; chosen_idx = missing_prefix_ids[0];
// resolved = true;
} }
} }

View File

@ -3,8 +3,8 @@
pub mod cache; pub mod cache;
use crate::database::r#type::Type;
use crate::database::Database; use crate::database::Database;
use crate::database::r#type::Type;
use serde_json::Value; use serde_json::Value;
use std::sync::Arc; use std::sync::Arc;
@ -50,15 +50,21 @@ impl Merger {
let mut final_cause = None; let mut final_cause = None;
if let Ok(Value::Object(map)) = serde_json::from_str::<Value>(&msg) { if let Ok(Value::Object(map)) = serde_json::from_str::<Value>(&msg) {
if let (Some(Value::String(e_msg)), Some(Value::String(e_code))) = (map.get("error"), map.get("code")) { if let (Some(Value::String(e_msg)), Some(Value::String(e_code))) =
(map.get("error"), map.get("code"))
{
final_message = e_msg.clone(); final_message = e_msg.clone();
final_code = e_code.clone(); final_code = e_code.clone();
let mut cause_parts = Vec::new(); let mut cause_parts = Vec::new();
if let Some(Value::String(d)) = map.get("detail") { if let Some(Value::String(d)) = map.get("detail") {
if !d.is_empty() { cause_parts.push(d.clone()); } if !d.is_empty() {
cause_parts.push(d.clone());
}
} }
if let Some(Value::String(h)) = map.get("hint") { if let Some(Value::String(h)) = map.get("hint") {
if !h.is_empty() { cause_parts.push(h.clone()); } if !h.is_empty() {
cause_parts.push(h.clone());
}
} }
if !cause_parts.is_empty() { if !cause_parts.is_empty() {
final_cause = Some(cause_parts.join("\n")); final_cause = Some(cause_parts.join("\n"));
@ -186,7 +192,7 @@ impl Merger {
let mut entity_objects = std::collections::BTreeMap::new(); let mut entity_objects = std::collections::BTreeMap::new();
let mut entity_arrays = std::collections::BTreeMap::new(); let mut entity_arrays = std::collections::BTreeMap::new();
for (k, v) in obj { for (k, v) in obj.clone() {
// Always retain system and unmapped core fields natively implicitly mapped to the Postgres tables // Always retain system and unmapped core fields natively implicitly mapped to the Postgres tables
if k == "id" || k == "type" || k == "created" { if k == "id" || k == "type" || k == "created" {
entity_fields.insert(k.clone(), v.clone()); entity_fields.insert(k.clone(), v.clone());
@ -253,7 +259,11 @@ 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<_>>()); 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); 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) {
@ -266,15 +276,16 @@ impl Merger {
} }
} }
let mut merged_relative = match self.merge_internal(rel_schema.clone(), Value::Object(relative), notifications)? { let mut merged_relative = match self.merge_internal(
rel_schema.clone(),
Value::Object(relative),
notifications,
)? {
Value::Object(m) => m, Value::Object(m) => m,
_ => continue, _ => continue,
}; };
merged_relative.insert( merged_relative.insert("type".to_string(), Value::String(relative_type_name));
"type".to_string(),
Value::String(relative_type_name),
);
Self::apply_entity_relation( Self::apply_entity_relation(
&mut entity_fields, &mut entity_fields,
@ -297,7 +308,11 @@ impl Merger {
&entity_fields, &entity_fields,
); );
let merged_relative = match self.merge_internal(rel_schema.clone(), Value::Object(relative), notifications)? { let merged_relative = match self.merge_internal(
rel_schema.clone(),
Value::Object(relative),
notifications,
)? {
Value::Object(m) => m, Value::Object(m) => m,
_ => continue, _ => continue,
}; };
@ -318,6 +333,20 @@ impl Merger {
entity_replaces = replaces; entity_replaces = replaces;
} }
#[cfg(not(test))]
if type_name == "contact" || type_name == "person" {
pgrx::notice!("=== DEBUG {} PAYLOAD ===", type_name);
pgrx::notice!("1. Incoming obj iteration: {:?}", obj);
pgrx::notice!("2. Final mapped entity_fields: {:?}", entity_fields);
pgrx::notice!(
"3. TypeDef fields check: {:?}",
type_def.fields.contains(&"source_id".to_string())
);
if !entity_fields.contains_key("source_id") {
pgrx::notice!("CRITICAL ERROR: source_id was dropped during mapping loop!");
}
}
self.merge_entity_fields( self.merge_entity_fields(
entity_change_kind.as_deref().unwrap_or(""), entity_change_kind.as_deref().unwrap_or(""),
&type_name, &type_name,
@ -360,7 +389,9 @@ impl Merger {
); );
let mut item_schema = rel_schema.clone(); let mut item_schema = rel_schema.clone();
if let Some(crate::database::schema::SchemaTypeOrArray::Single(t)) = &rel_schema.obj.type_ { if let Some(crate::database::schema::SchemaTypeOrArray::Single(t)) =
&rel_schema.obj.type_
{
if t == "array" { if t == "array" {
if let Some(items_def) = &rel_schema.obj.items { if let Some(items_def) = &rel_schema.obj.items {
item_schema = items_def.clone(); item_schema = items_def.clone();
@ -368,8 +399,11 @@ impl Merger {
} }
} }
let merged_relative = let merged_relative = match self.merge_internal(
match self.merge_internal(item_schema, Value::Object(relative_item), notifications)? { item_schema,
Value::Object(relative_item),
notifications,
)? {
Value::Object(m) => m, Value::Object(m) => m,
_ => continue, _ => continue,
}; };
@ -548,7 +582,12 @@ impl Merger {
entity_fields = new_fields; entity_fields = new_fields;
} }
Ok((entity_fields, entity_change_kind, entity_fetched, replaces_id)) Ok((
entity_fields,
entity_change_kind,
entity_fetched,
replaces_id,
))
} }
fn fetch_entity( fn fetch_entity(
@ -735,9 +774,7 @@ impl Merger {
columns.join(", "), columns.join(", "),
values.join(", ") values.join(", ")
); );
self self.db.execute(&sql, None)?;
.db
.execute(&sql, None)?;
} else if change_kind == "update" || change_kind == "delete" { } else if change_kind == "update" || change_kind == "delete" {
entity_pairs.remove("id"); entity_pairs.remove("id");
entity_pairs.remove("type"); entity_pairs.remove("type");
@ -769,9 +806,7 @@ impl Merger {
set_clauses.join(", "), set_clauses.join(", "),
Self::quote_literal(&Value::String(id_str.to_string())) Self::quote_literal(&Value::String(id_str.to_string()))
); );
self self.db.execute(&sql, None)?;
.db
.execute(&sql, None)?;
} }
} }

View File

@ -1 +1 @@
1.0.96 1.0.99