Compare commits

...

3 Commits

Author SHA1 Message Date
6632570712 merger improvements 2026-03-15 03:24:00 -04:00
d4347072f2 stripping schema nulls from stems 2026-03-15 00:13:33 -04:00
290464adc1 gjson pathing for stem paths 2026-03-13 23:35:37 -04:00
8 changed files with 350 additions and 182 deletions

View File

@ -80,6 +80,7 @@ The Merger provides an automated, high-performance graph synchronization engine
* **Hierarchical Table Inheritance**: The Punc system uses distributed table inheritance (e.g. `person` inherits `user` inherits `organization` inherits `entity`). The Merger splits the incoming JSON payload and performs atomic row updates across *all* relevant tables in the lineage map. * **Hierarchical Table Inheritance**: The Punc system uses distributed table inheritance (e.g. `person` inherits `user` inherits `organization` inherits `entity`). The Merger splits the incoming JSON payload and performs atomic row updates across *all* relevant tables in the lineage map.
* **The Archive Paradigm**: Data is never deleted in the Punc system. The Merger securely enforces referential integrity by toggling the `archived` Boolean flag on the base `entity` table rather than issuing SQL `DELETE` commands. * **The Archive Paradigm**: Data is never deleted in the Punc system. The Merger securely enforces referential integrity by toggling the `archived` Boolean flag on the base `entity` table rather than issuing SQL `DELETE` commands.
* **Change Tracking & Reactivity**: The Merger diffs the incoming JSON against the existing database row (utilizing static, `DashMap`-cached `lk_` SELECT string templates). Every detected change is recorded into the `agreego.change` audit table, tracking the user mapping. It then natively uses `pg_notify` to broadcast a completely flat row-level diff out to the Go WebSocket server for O(1) routing. * **Change Tracking & Reactivity**: The Merger diffs the incoming JSON against the existing database row (utilizing static, `DashMap`-cached `lk_` SELECT string templates). Every detected change is recorded into the `agreego.change` audit table, tracking the user mapping. It then natively uses `pg_notify` to broadcast a completely flat row-level diff out to the Go WebSocket server for O(1) routing.
* **Flat Structural Beats (Unidirectional Flow)**: The Merger purposefully DOES NOT trace or hydrate outbound Foreign Keys or nested parent structures during writes. It emits completely flat, mathematically perfect structural deltas via `pg_notify` representing only the exact Postgres rows that changed. This guarantees the write-path remains O(1) lightning fast. It is the strict responsibility of the upstream Punc Framework (the Go `Speaker`) to intercept these flat beats, evaluate them against active Websocket Schema Topologies, and dynamically issue targeted `jspg_query` reads to hydrate the exact contextual subgraphs required by listening clients.
* **Many-to-Many Graph Edge Management**: Operates seamlessly with the global `agreego.relationship` table, allowing the system to represent and merge arbitrary reified M:M relationships directionally between any two entities. * **Many-to-Many Graph Edge Management**: Operates seamlessly with the global `agreego.relationship` table, allowing the system to represent and merge arbitrary reified M:M relationships directionally between any two entities.
* **Sparse Updates**: Empty JSON strings `""` are directly bound as explicit SQL `NULL` directives to clear data, whilst omitted (missing) properties skip UPDATE execution entirely, ensuring partial UI submissions do not wipe out sibling fields. * **Sparse Updates**: Empty JSON strings `""` are directly bound as explicit SQL `NULL` directives to clear data, whilst omitted (missing) properties skip UPDATE execution entirely, ensuring partial UI submissions do not wipe out sibling fields.
* **Unified Return Structure**: To eliminate UI hydration race conditions and multi-user duplication, `jspg_merge` explicitly strips the response graph and returns only the root `{ "id": "uuid" }` (or an array of IDs for list insertions). External APIs can then explicitly call read APIs to fetch the resulting graph, while the UI relies 100% implicitly on the flat `pg_notify` pipeline for reactive state synchronization. * **Unified Return Structure**: To eliminate UI hydration race conditions and multi-user duplication, `jspg_merge` explicitly strips the response graph and returns only the root `{ "id": "uuid" }` (or an array of IDs for list insertions). External APIs can then explicitly call read APIs to fetch the resulting graph, while the UI relies 100% implicitly on the flat `pg_notify` pipeline for reactive state synchronization.
@ -101,22 +102,24 @@ The Queryer transforms Postgres into a pre-compiled Semantic Query Engine via th
* **Array Inclusion**: `{"$in": [values]}`, `{"$nin": [values]}` use native `jsonb_array_elements_text()` bindings to enforce `IN` and `NOT IN` logic without runtime SQL injection risks. * **Array Inclusion**: `{"$in": [values]}`, `{"$nin": [values]}` use native `jsonb_array_elements_text()` bindings to enforce `IN` and `NOT IN` logic without runtime SQL injection risks.
* **Text Matching (ILIKE)**: Evaluates `$eq` or `$ne` against string fields containing the `%` character natively into Postgres `ILIKE` and `NOT ILIKE` partial substring matches. * **Text Matching (ILIKE)**: Evaluates `$eq` or `$ne` against string fields containing the `%` character natively into Postgres `ILIKE` and `NOT ILIKE` partial substring matches.
* **Type Casting**: Safely resolves dynamic combinations by casting values instantly into the physical database types mapped in the schema (e.g. parsing `uuid` bindings to `::uuid`, formatting DateTimes to `::timestamptz`, and numbers to `::numeric`). * **Type Casting**: Safely resolves dynamic combinations by casting values instantly into the physical database types mapped in the schema (e.g. parsing `uuid` bindings to `::uuid`, formatting DateTimes to `::timestamptz`, and numbers to `::numeric`).
### 4. The Stem Engine
### The Stem Engine
Rather than over-fetching heavy Entity payloads and trimming them, Punc Framework Websockets depend on isolated subgraphs defined as **Stems**. Rather than over-fetching heavy Entity payloads and trimming them, Punc Framework Websockets depend on isolated subgraphs defined as **Stems**.
A `Stem` is **not a JSON Pointer** or a physical path string (like `/properties/contacts/items/phone_number`). It is simply a declaration of an **Entity Type boundary** that exists somewhere within the compiled JSON Schema graph. A `Stem` is a declaration of an **Entity Type boundary** that exists somewhere within the compiled JSON Schema graph, expressed using **`gjson` multipath syntax** (e.g., `contacts.#.phone_numbers.#`).
Because `pg_notify` (Beats) fire rigidly from physical Postgres tables (e.g. `{"type": "phone_number"}`), the Go Framework only ever needs to know: "Does the schema `with_contacts.person` contain the `phone_number` Entity anywhere inside its tree?" Because `pg_notify` (Beats) fire rigidly from physical Postgres tables (e.g. `{"type": "phone_number"}`), the Go Framework only ever needs to know: "Does the schema `with_contacts.person` contain the `phone_number` Entity anywhere inside its tree, and if so, what is the gjson path to iterate its payload?"
* **Initialization:** During startup (`jspg_stems()`), the database crawls all Schemas and maps out every physical Entity Type it references. It builds a flat dictionary of `Schema ID -> [Entity Types]` (e.g. `with_contacts.person -> ["person", "contact", "phone_number", "email_address"]`). * **Initialization:** During startup (`jspg_stems()`), the database crawls all Schemas and maps out every physical Entity Type it references. It builds a highly optimized `HashMap<String, HashMap<String, Arc<Stem>>>` providing strictly `O(1)` memory lookups mapping `Schema ID -> { Stem Path -> Entity Type }`.
* **Identifier Prioritization**: When determining if a nested object boundary is an Entity, JSPG natively prioritizes defined `$id` tags over `$ref` inheritance pointers to prevent polymorphic boundaries from devolving into their generic base classes. * **GJSON Pathing:** Unlike standard JSON Pointers, stems utilize `.#` array iterator syntax. The Go web server consumes this native path (e.g. `lines.#`) across the raw Postgres JSON byte payload, extracting all active UUIDs in one massive sub-millisecond sweep without unmarshaling Go ASTs.
* **Cyclical Deduplication**: Because Punc relationships often reference back on themselves via deeply nested classes, the Stem Engine applies intelligent path deduplication. If the active `current_path` already ends with the target entity string, it traverses the inheritance properties without appending the entity to the stem path again, eliminating infinite powerset loops. * **Polymorphic Condition Selectors:** When trailing paths would otherwise collide because of abstract polymorphic type definitions (e.g., a `target` property bounded by a `oneOf` taking either `phone_number` or `email_address`), JSPG natively appends evaluated `gjson` type conditions into the path (e.g. `contacts.#.target#(type=="phone_number")`). This guarantees `O(1)` key uniqueness in the HashMap while retaining extreme array extraction speeds natively without runtime AST evaluation.
* **Relationship Path Squashing:** When calculating nested string paths structurally to discover these boundaries, JSPG intentionally **omits** properties natively named `target` or `source` if they belong to a native database `relationship` table override. This ensures paths like `phone_numbers/contact/target` correctly register their beat resolution pattern as `phone_numbers/contact/phone_number`. * **Identifier Prioritization:** When determining if a nested object boundary is an Entity, JSPG natively prioritizes defined `$id` tags over `$ref` inheritance pointers to prevent polymorphic boundaries from devolving into their generic base classes.
* **Cyclical Deduplication:** Because Punc relationships often reference back on themselves via deeply nested classes, the Stem Engine applies intelligent path deduplication. If the active `current_path` already ends with the target entity string, it traverses the inheritance properties without appending the entity to the stem path again, eliminating infinite powerset loops.
* **Relationship Path Squashing:** When calculating string paths structurally, JSPG intentionally **omits** properties natively named `target` or `source` if they belong to a native database `relationship` table override.
* **The Go Router**: The Golang Punc framework uses this exact mapping to register WebSocket Beat frequencies exclusively on the Entity types discovered. * **The Go Router**: The Golang Punc framework uses this exact mapping to register WebSocket Beat frequencies exclusively on the Entity types discovered.
* **The Queryer Execution**: When the Go framework asks JSPG to hydrate a partial `phone_number` stem for the `with_contacts.person` schema, instead of jumping through string paths, the SQL Compiler simply reaches into the Schema's AST using the `phone_number` Type string, pulls out exactly that entity's mapping rules, and returns a fully correlated `SELECT` block! This natively handles nested array properties injected via `oneOf` or array references efficiently bypassing runtime powerset expansion. * **The Queryer Execution**: When the Go framework asks JSPG to hydrate a partial `phone_number` stem for the `with_contacts.person` schema, instead of jumping through string paths, the SQL Compiler simply reaches into the Schema's AST using the `phone_number` Type string, pulls out exactly that entity's mapping rules, and returns a fully correlated `SELECT` block! This natively handles nested array properties injected via `oneOf` or array references efficiently bypassing runtime powerset expansion.
* **Performance:** These Stem execution structures are fully statically compiled via SPI and map perfectly to `O(1)` real-time routing logic on the application tier. * **Performance:** These Stem execution structures are fully statically compiled via SPI and map perfectly to `O(1)` real-time routing logic on the application tier.
## 5. Testing & Execution Architecture ## 5. Testing & Execution Architecture
JSPG implements a strict separation of concerns to bypass the need to boot a full PostgreSQL cluster for unit and integration testing. Because `pgrx::spi::Spi` directly links to PostgreSQL C-headers, building the library with `cargo test` on macOS natively normally results in fatal `dyld` crashes. JSPG implements a strict separation of concerns to bypass the need to boot a full PostgreSQL cluster for unit and integration testing. Because `pgrx::spi::Spi` directly links to PostgreSQL C-headers, building the library with `cargo test` on macOS natively normally results in fatal `dyld` crashes.

View File

@ -1148,7 +1148,7 @@
"description": "Full person stem query on phone number contact", "description": "Full person stem query on phone number contact",
"action": "query", "action": "query",
"schema_id": "full.person", "schema_id": "full.person",
"stem": "phone_numbers/contact", "stem": "phone_numbers.#",
"expect": { "expect": {
"success": true, "success": true,
"sql": [ "sql": [
@ -1159,7 +1159,22 @@
" 'id', t1_obj_t3.id,", " 'id', t1_obj_t3.id,",
" 'is_primary', t1_obj_t1.is_primary,", " 'is_primary', t1_obj_t1.is_primary,",
" 'name', t1_obj_t3.name,", " 'name', t1_obj_t3.name,",
" 'type', t1_obj_t3.type)", " 'target',",
" (SELECT jsonb_build_object(",
" 'archived', t1_obj_t3_target_t2.archived,",
" 'created_at', t1_obj_t3_target_t2.created_at,",
" 'id', t1_obj_t3_target_t2.id,",
" 'name', t1_obj_t3_target_t2.name,",
" 'number', t1_obj_t3_target_t1.number,",
" 'type', t1_obj_t3_target_t2.type",
" )",
" FROM agreego.phone_number t1_obj_t3_target_t1",
" JOIN agreego.entity t1_obj_t3_target_t2 ON t1_obj_t3_target_t2.id = t1_obj_t3_target_t1.id",
" WHERE",
" NOT t1_obj_t3_target_t1.archived",
" AND t1_obj_t3_target_t1.parent_id = t1_obj_t3.id),",
" 'type', t1_obj_t3.type",
")",
"FROM agreego.contact t1_obj_t1", "FROM agreego.contact t1_obj_t1",
"JOIN agreego.relationship t1_obj_t2 ON t1_obj_t2.id = t1_obj_t1.id", "JOIN agreego.relationship t1_obj_t2 ON t1_obj_t2.id = t1_obj_t1.id",
"JOIN agreego.entity t1_obj_t3 ON t1_obj_t3.id = t1_obj_t2.id", "JOIN agreego.entity t1_obj_t3 ON t1_obj_t3.id = t1_obj_t2.id",
@ -1172,7 +1187,7 @@
"description": "Full person stem query on phone number contact on phone number", "description": "Full person stem query on phone number contact on phone number",
"action": "query", "action": "query",
"schema_id": "full.person", "schema_id": "full.person",
"stem": "phone_numbers/contact/phone_number", "stem": "phone_numbers.#.target",
"expect": { "expect": {
"success": true, "success": true,
"sql": [ "sql": [
@ -1195,7 +1210,7 @@
"description": "Full person stem query on contact email address", "description": "Full person stem query on contact email address",
"action": "query", "action": "query",
"schema_id": "full.person", "schema_id": "full.person",
"stem": "contacts/contact/email_address", "stem": "contacts.#.target#(type==\"email_address\")",
"expect": { "expect": {
"success": true, "success": true,
"sql": [ "sql": [

View File

@ -10,9 +10,13 @@
"type": "relation", "type": "relation",
"constraint": "fk_contact_entity", "constraint": "fk_contact_entity",
"source_type": "contact", "source_type": "contact",
"source_columns": ["entity_id"], "source_columns": [
"entity_id"
],
"destination_type": "person", "destination_type": "person",
"destination_columns": ["id"], "destination_columns": [
"id"
],
"prefix": null "prefix": null
}, },
{ {
@ -20,88 +24,132 @@
"type": "relation", "type": "relation",
"constraint": "fk_relationship_target", "constraint": "fk_relationship_target",
"source_type": "relationship", "source_type": "relationship",
"source_columns": ["target_id", "target_type"], "source_columns": [
"target_id",
"target_type"
],
"destination_type": "entity", "destination_type": "entity",
"destination_columns": ["id", "type"], "destination_columns": [
"id",
"type"
],
"prefix": "target" "prefix": "target"
} }
], ],
"types": [ "types": [
{ {
"name": "entity", "name": "entity",
"hierarchy": ["entity"], "hierarchy": [
"schemas": [{ "entity"
"$id": "entity", ],
"type": "object", "schemas": [
"properties": {} {
}] "$id": "entity",
"type": "object",
"properties": {}
}
]
}, },
{ {
"name": "person", "name": "person",
"hierarchy": ["person", "entity"], "hierarchy": [
"schemas": [{ "person",
"$id": "person", "entity"
"$ref": "entity", ],
"properties": {} "schemas": [
}] {
"$id": "person",
"$ref": "entity",
"properties": {}
}
]
}, },
{ {
"name": "email_address", "name": "email_address",
"hierarchy": ["email_address", "entity"], "hierarchy": [
"schemas": [{ "email_address",
"$id": "email_address", "entity"
"$ref": "entity", ],
"properties": {} "schemas": [
}] {
"$id": "email_address",
"$ref": "entity",
"properties": {}
}
]
}, },
{ {
"name": "phone_number", "name": "phone_number",
"hierarchy": ["phone_number", "entity"], "hierarchy": [
"schemas": [{ "phone_number",
"$id": "phone_number", "entity"
"$ref": "entity", ],
"properties": {} "schemas": [
}] {
"$id": "phone_number",
"$ref": "entity",
"properties": {}
}
]
}, },
{ {
"name": "relationship", "name": "relationship",
"relationship": true, "relationship": true,
"hierarchy": ["relationship", "entity"], "hierarchy": [
"schemas": [{ "relationship",
"$id": "relationship", "entity"
"$ref": "entity", ],
"properties": {} "schemas": [
}] {
"$id": "relationship",
"$ref": "entity",
"properties": {}
}
]
}, },
{ {
"name": "contact", "name": "contact",
"relationship": true, "relationship": true,
"hierarchy": ["contact", "relationship", "entity"], "hierarchy": [
"schemas": [{ "contact",
"$id": "contact", "relationship",
"$ref": "relationship", "entity"
"properties": { ],
"target": { "schemas": [
"oneOf": [ {
{ "$ref": "phone_number" }, "$id": "contact",
{ "$ref": "email_address" } "$ref": "relationship",
] "properties": {
"target": {
"oneOf": [
{
"$ref": "phone_number"
},
{
"$ref": "email_address"
}
]
}
} }
} }
}] ]
}, },
{ {
"name": "save_person", "name": "save_person",
"schemas": [{ "schemas": [
"$id": "save_person.response", {
"$ref": "person", "$id": "save_person.response",
"properties": { "$ref": "person",
"contacts": { "properties": {
"type": "array", "contacts": {
"items": { "$ref": "contact" } "type": "array",
"items": {
"$ref": "contact"
}
}
} }
} }
}] ]
} }
] ]
}, },
@ -112,59 +160,148 @@
"expect": { "expect": {
"success": true, "success": true,
"stems": { "stems": {
"save_person.response": {
"": {
"type": "person"
},
"contacts/contact": {
"type": "contact",
"relation": "contacts_id"
},
"contacts/contact/email_address": {
"type": "email_address",
"relation": "target_id"
},
"contacts/contact/phone_number": {
"type": "phone_number",
"relation": "target_id"
}
},
"contact": { "contact": {
"": { "": {
"schema": {
"$id": "contact",
"$ref": "relationship",
"properties": {
"target": {
"oneOf": [
{
"$ref": "phone_number"
},
{
"$ref": "email_address"
}
]
}
}
},
"type": "contact" "type": "contact"
}, },
"email_address": { "target#(type==\"email_address\")": {
"type": "email_address", "relation": "target_id",
"relation": "target_id" "schema": {
"$id": "email_address",
"$ref": "entity",
"properties": {}
},
"type": "email_address"
}, },
"phone_number": { "target#(type==\"phone_number\")": {
"type": "phone_number", "relation": "target_id",
"relation": "target_id" "schema": {
} "$id": "phone_number",
}, "$ref": "entity",
"person": { "properties": {}
"": { },
"type": "person" "type": "phone_number"
} }
}, },
"email_address": { "email_address": {
"": { "": {
"schema": {
"$id": "email_address",
"$ref": "entity",
"properties": {}
},
"type": "email_address" "type": "email_address"
} }
}, },
"phone_number": { "entity": {
"": { "": {
"schema": {
"$id": "entity",
"properties": {},
"type": "object"
},
"type": "entity"
}
},
"person": {
"": {
"schema": {
"$id": "person",
"$ref": "entity",
"properties": {}
},
"type": "person"
}
},
"phone_number": {
"": {
"schema": {
"$id": "phone_number",
"$ref": "entity",
"properties": {}
},
"type": "phone_number" "type": "phone_number"
} }
}, },
"relationship": { "relationship": {
"": { "": {
"schema": {
"$id": "relationship",
"$ref": "entity",
"properties": {}
},
"type": "relationship" "type": "relationship"
} }
}, },
"entity": { "save_person.response": {
"": { "": {
"type": "entity" "schema": {
"$id": "save_person.response",
"$ref": "person",
"properties": {
"contacts": {
"items": {
"$ref": "contact"
},
"type": "array"
}
}
},
"type": "person"
},
"contacts.#": {
"relation": "contacts_id",
"schema": {
"$id": "contact",
"$ref": "relationship",
"properties": {
"target": {
"oneOf": [
{
"$ref": "phone_number"
},
{
"$ref": "email_address"
}
]
}
}
},
"type": "contact"
},
"contacts.#.target#(type==\"email_address\")": {
"relation": "target_id",
"schema": {
"$id": "email_address",
"$ref": "entity",
"properties": {}
},
"type": "email_address"
},
"contacts.#.target#(type==\"phone_number\")": {
"relation": "target_id",
"schema": {
"$id": "phone_number",
"$ref": "entity",
"properties": {}
},
"type": "phone_number"
} }
} }
} }

View File

@ -265,12 +265,12 @@ impl Database {
String::from(""), String::from(""),
None, None,
None, None,
true, false,
&mut inner_map, &mut inner_map,
Vec::new(),
&mut errors, &mut errors,
); );
if !inner_map.is_empty() { if !inner_map.is_empty() {
println!("SCHEMA: {} STEMS: {:?}", schema_id, inner_map.keys());
db_stems.insert(schema_id, inner_map); db_stems.insert(schema_id, inner_map);
} }
} }
@ -288,11 +288,12 @@ impl Database {
db: &Database, db: &Database,
root_schema_id: &str, root_schema_id: &str,
schema: &Schema, schema: &Schema,
mut current_path: String, current_path: String,
parent_type: Option<String>, parent_type: Option<String>,
property_name: Option<String>, property_name: Option<String>,
is_root: bool, is_polymorphic: bool,
inner_map: &mut HashMap<String, Arc<Stem>>, inner_map: &mut HashMap<String, Arc<Stem>>,
seen_entities: Vec<String>,
errors: &mut Vec<crate::drop::Error>, errors: &mut Vec<crate::drop::Error>,
) { ) {
let mut is_entity = false; let mut is_entity = false;
@ -323,6 +324,12 @@ impl Database {
} }
} }
if is_entity {
if seen_entities.contains(&entity_type) {
return; // Break cyclical schemas!
}
}
let mut relation_col = None; let mut relation_col = None;
if is_entity { if is_entity {
if let (Some(pt), Some(prop)) = (&parent_type, &property_name) { if let (Some(pt), Some(prop)) = (&parent_type, &property_name) {
@ -344,46 +351,21 @@ impl Database {
} }
} }
let mut final_path = current_path.clone();
if is_polymorphic && !final_path.is_empty() && !final_path.ends_with(&entity_type) {
if final_path.ends_with(".#") {
final_path = format!("{}(type==\"{}\")", final_path, entity_type);
} else {
final_path = format!("{}#(type==\"{}\")", final_path, entity_type);
}
}
let stem = Stem { let stem = Stem {
r#type: entity_type.clone(), r#type: entity_type.clone(),
relation: relation_col, relation: relation_col,
schema: Arc::new(schema.clone()), schema: Arc::new(schema.clone()),
}; };
inner_map.insert(final_path, Arc::new(stem));
let mut branch_path = if is_root {
String::new()
} else if current_path.is_empty() {
entity_type.clone()
} else {
format!("{}/{}", current_path, entity_type)
};
// DEDUPLICATION: If we just recursed into the EXACT same entity type definition,
// do not append again and do not re-register the stem.
let already_registered =
if current_path == entity_type || current_path.ends_with(&format!("/{}", entity_type)) {
branch_path = current_path.clone();
true
} else {
false
};
if !already_registered {
if inner_map.contains_key(&branch_path) {
errors.push(crate::drop::Error {
code: "STEM_COLLISION".to_string(),
message: format!("The stem path `{}` resolves to multiple Entity boundaries. This usually occurs during un-wrapped $family or oneOf polymorphic schemas where multiple Entities are directly assigned to the same property. To fix this, encapsulate the polymorphic branch.", branch_path),
details: crate::drop::ErrorDetails {
path: root_schema_id.to_string(),
},
});
}
inner_map.insert(branch_path.clone(), Arc::new(stem));
}
// Update current_path for structural children
current_path = branch_path;
} }
let next_parent = if is_entity { let next_parent = if is_entity {
@ -392,34 +374,22 @@ impl Database {
parent_type.clone() parent_type.clone()
}; };
let pass_seen = if is_entity {
let mut ns = seen_entities.clone();
ns.push(entity_type.clone());
ns
} else {
seen_entities.clone()
};
// Properties branch // Properties branch
if let Some(props) = &schema.obj.properties { if let Some(props) = &schema.obj.properties {
for (k, v) in props { for (k, v) in props {
// Bypass target and source properties if we are in a relationship
if let Some(parent_str) = &next_parent {
if let Some(pt) = db.types.get(parent_str) {
if pt.relationship && (k == "target" || k == "source") {
Self::discover_stems(
db,
root_schema_id,
v,
current_path.clone(),
next_parent.clone(),
Some(k.clone()),
false,
inner_map,
errors,
);
continue;
}
}
}
// Standard Property Pathing // Standard Property Pathing
let next_path = if current_path.is_empty() { let next_path = if current_path.is_empty() {
k.clone() k.clone()
} else { } else {
format!("{}/{}", current_path, k) format!("{}.{}", current_path, k)
}; };
Self::discover_stems( Self::discover_stems(
@ -431,6 +401,7 @@ impl Database {
Some(k.clone()), Some(k.clone()),
false, false,
inner_map, inner_map,
pass_seen.clone(),
errors, errors,
); );
} }
@ -438,15 +409,22 @@ impl Database {
// Array Item branch // Array Item branch
if let Some(items) = &schema.obj.items { if let Some(items) = &schema.obj.items {
let next_path = if current_path.is_empty() {
String::from("#")
} else {
format!("{}.#", current_path)
};
Self::discover_stems( Self::discover_stems(
db, db,
root_schema_id, root_schema_id,
items, items,
current_path.clone(), next_path,
next_parent.clone(), next_parent.clone(),
property_name.clone(), property_name.clone(),
false, // Arrays themselves aren't polymorphic branches, their items might be false,
inner_map, inner_map,
pass_seen.clone(),
errors, errors,
); );
} }
@ -463,8 +441,9 @@ impl Database {
current_path.clone(), current_path.clone(),
next_parent.clone(), next_parent.clone(),
property_name.clone(), property_name.clone(),
false, is_polymorphic,
inner_map, inner_map,
seen_entities.clone(),
errors, errors,
); );
} }
@ -481,8 +460,9 @@ impl Database {
current_path.clone(), current_path.clone(),
next_parent.clone(), next_parent.clone(),
property_name.clone(), property_name.clone(),
false, true,
inner_map, inner_map,
pass_seen.clone(),
errors, errors,
); );
} }
@ -496,8 +476,9 @@ impl Database {
current_path.clone(), current_path.clone(),
next_parent.clone(), next_parent.clone(),
property_name.clone(), property_name.clone(),
false, is_polymorphic,
inner_map, inner_map,
pass_seen.clone(),
errors, errors,
); );
} }

View File

@ -5,7 +5,6 @@ use std::sync::Arc;
// Schema mirrors the Go Punc Generator's schema struct for consistency. // Schema mirrors the Go Punc Generator's schema struct for consistency.
// It is an order-preserving representation of a JSON Schema. // It is an order-preserving representation of a JSON Schema.
pub fn deserialize_some<'de, D>(deserializer: D) -> Result<Option<Value>, D::Error> pub fn deserialize_some<'de, D>(deserializer: D) -> Result<Option<Value>, D::Error>
where where
D: serde::Deserializer<'de>, D: serde::Deserializer<'de>,
@ -13,117 +12,159 @@ where
let v = Value::deserialize(deserializer)?; let v = Value::deserialize(deserializer)?;
Ok(Some(v)) Ok(Some(v))
} }
#[derive(Debug, Clone, Serialize, Deserialize, Default)] #[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct SchemaObject { pub struct SchemaObject {
// Core Schema Keywords // Core Schema Keywords
#[serde(rename = "$id")] #[serde(rename = "$id")]
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>, pub id: Option<String>,
#[serde(rename = "$ref")] #[serde(rename = "$ref")]
#[serde(skip_serializing_if = "Option::is_none")]
pub r#ref: Option<String>, pub r#ref: Option<String>,
/* #[serde(skip_serializing_if = "Option::is_none")]
Note: The `Ref` field in the Go struct is a pointer populated by the linker.
In Rust, we might handle this differently (e.g., separate lookup or Rc/Arc),
so we omit the direct recursive `Ref` field for now and rely on `ref_string`.
*/
pub description: Option<String>, pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>, pub title: Option<String>,
#[serde(default)] // Allow missing type #[serde(default)] // Allow missing type
#[serde(rename = "type")] #[serde(rename = "type")]
#[serde(skip_serializing_if = "Option::is_none")]
pub type_: Option<SchemaTypeOrArray>, // Handles string or array of strings pub type_: Option<SchemaTypeOrArray>, // Handles string or array of strings
// Object Keywords // Object Keywords
#[serde(skip_serializing_if = "Option::is_none")]
pub properties: Option<BTreeMap<String, Arc<Schema>>>, pub properties: Option<BTreeMap<String, Arc<Schema>>>,
#[serde(rename = "patternProperties")] #[serde(rename = "patternProperties")]
#[serde(skip_serializing_if = "Option::is_none")]
pub pattern_properties: Option<BTreeMap<String, Arc<Schema>>>, pub pattern_properties: Option<BTreeMap<String, Arc<Schema>>>,
#[serde(rename = "additionalProperties")] #[serde(rename = "additionalProperties")]
#[serde(skip_serializing_if = "Option::is_none")]
pub additional_properties: Option<Arc<Schema>>, pub additional_properties: Option<Arc<Schema>>,
#[serde(rename = "$family")] #[serde(rename = "$family")]
#[serde(skip_serializing_if = "Option::is_none")]
pub family: Option<String>, pub family: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub required: Option<Vec<String>>, pub required: Option<Vec<String>>,
// dependencies can be schema dependencies or property dependencies // dependencies can be schema dependencies or property dependencies
#[serde(skip_serializing_if = "Option::is_none")]
pub dependencies: Option<BTreeMap<String, Dependency>>, pub dependencies: Option<BTreeMap<String, Dependency>>,
// Array Keywords // Array Keywords
#[serde(rename = "items")] #[serde(rename = "items")]
#[serde(skip_serializing_if = "Option::is_none")]
pub items: Option<Arc<Schema>>, pub items: Option<Arc<Schema>>,
#[serde(rename = "prefixItems")] #[serde(rename = "prefixItems")]
#[serde(skip_serializing_if = "Option::is_none")]
pub prefix_items: Option<Vec<Arc<Schema>>>, pub prefix_items: Option<Vec<Arc<Schema>>>,
// String Validation // String Validation
#[serde(rename = "minLength")] #[serde(rename = "minLength")]
#[serde(skip_serializing_if = "Option::is_none")]
pub min_length: Option<f64>, pub min_length: Option<f64>,
#[serde(rename = "maxLength")] #[serde(rename = "maxLength")]
#[serde(skip_serializing_if = "Option::is_none")]
pub max_length: Option<f64>, pub max_length: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pattern: Option<String>, pub pattern: Option<String>,
// Array Validation // Array Validation
#[serde(rename = "minItems")] #[serde(rename = "minItems")]
#[serde(skip_serializing_if = "Option::is_none")]
pub min_items: Option<f64>, pub min_items: Option<f64>,
#[serde(rename = "maxItems")] #[serde(rename = "maxItems")]
#[serde(skip_serializing_if = "Option::is_none")]
pub max_items: Option<f64>, pub max_items: Option<f64>,
#[serde(rename = "uniqueItems")] #[serde(rename = "uniqueItems")]
#[serde(skip_serializing_if = "Option::is_none")]
pub unique_items: Option<bool>, pub unique_items: Option<bool>,
#[serde(rename = "contains")] #[serde(rename = "contains")]
#[serde(skip_serializing_if = "Option::is_none")]
pub contains: Option<Arc<Schema>>, pub contains: Option<Arc<Schema>>,
#[serde(rename = "minContains")] #[serde(rename = "minContains")]
#[serde(skip_serializing_if = "Option::is_none")]
pub min_contains: Option<f64>, pub min_contains: Option<f64>,
#[serde(rename = "maxContains")] #[serde(rename = "maxContains")]
#[serde(skip_serializing_if = "Option::is_none")]
pub max_contains: Option<f64>, pub max_contains: Option<f64>,
// Object Validation // Object Validation
#[serde(rename = "minProperties")] #[serde(rename = "minProperties")]
#[serde(skip_serializing_if = "Option::is_none")]
pub min_properties: Option<f64>, pub min_properties: Option<f64>,
#[serde(rename = "maxProperties")] #[serde(rename = "maxProperties")]
#[serde(skip_serializing_if = "Option::is_none")]
pub max_properties: Option<f64>, pub max_properties: Option<f64>,
#[serde(rename = "propertyNames")] #[serde(rename = "propertyNames")]
#[serde(skip_serializing_if = "Option::is_none")]
pub property_names: Option<Arc<Schema>>, pub property_names: Option<Arc<Schema>>,
// Numeric Validation // Numeric Validation
#[serde(skip_serializing_if = "Option::is_none")]
pub format: Option<String>, pub format: Option<String>,
#[serde(rename = "enum")] #[serde(rename = "enum")]
#[serde(skip_serializing_if = "Option::is_none")]
pub enum_: Option<Vec<Value>>, // `enum` is a reserved keyword in Rust pub enum_: Option<Vec<Value>>, // `enum` is a reserved keyword in Rust
#[serde( #[serde(
default, default,
rename = "const", rename = "const",
deserialize_with = "crate::database::schema::deserialize_some" deserialize_with = "crate::database::schema::deserialize_some"
)] )]
#[serde(skip_serializing_if = "Option::is_none")]
pub const_: Option<Value>, pub const_: Option<Value>,
// Numeric Validation // Numeric Validation
#[serde(rename = "multipleOf")] #[serde(rename = "multipleOf")]
#[serde(skip_serializing_if = "Option::is_none")]
pub multiple_of: Option<f64>, pub multiple_of: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub minimum: Option<f64>, pub minimum: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub maximum: Option<f64>, pub maximum: Option<f64>,
#[serde(rename = "exclusiveMinimum")] #[serde(rename = "exclusiveMinimum")]
#[serde(skip_serializing_if = "Option::is_none")]
pub exclusive_minimum: Option<f64>, pub exclusive_minimum: Option<f64>,
#[serde(rename = "exclusiveMaximum")] #[serde(rename = "exclusiveMaximum")]
#[serde(skip_serializing_if = "Option::is_none")]
pub exclusive_maximum: Option<f64>, pub exclusive_maximum: Option<f64>,
// Combining Keywords // Combining Keywords
#[serde(rename = "allOf")] #[serde(rename = "allOf")]
#[serde(skip_serializing_if = "Option::is_none")]
pub all_of: Option<Vec<Arc<Schema>>>, pub all_of: Option<Vec<Arc<Schema>>>,
#[serde(rename = "oneOf")] #[serde(rename = "oneOf")]
#[serde(skip_serializing_if = "Option::is_none")]
pub one_of: Option<Vec<Arc<Schema>>>, pub one_of: Option<Vec<Arc<Schema>>>,
#[serde(rename = "not")] #[serde(rename = "not")]
#[serde(skip_serializing_if = "Option::is_none")]
pub not: Option<Arc<Schema>>, pub not: Option<Arc<Schema>>,
#[serde(rename = "if")] #[serde(rename = "if")]
#[serde(skip_serializing_if = "Option::is_none")]
pub if_: Option<Arc<Schema>>, pub if_: Option<Arc<Schema>>,
#[serde(rename = "then")] #[serde(rename = "then")]
#[serde(skip_serializing_if = "Option::is_none")]
pub then_: Option<Arc<Schema>>, pub then_: Option<Arc<Schema>>,
#[serde(rename = "else")] #[serde(rename = "else")]
#[serde(skip_serializing_if = "Option::is_none")]
pub else_: Option<Arc<Schema>>, pub else_: Option<Arc<Schema>>,
// Custom Vocabularies // Custom Vocabularies
#[serde(skip_serializing_if = "Option::is_none")]
pub form: Option<Vec<String>>, pub form: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub display: Option<Vec<String>>, pub display: Option<Vec<String>>,
#[serde(rename = "enumNames")] #[serde(rename = "enumNames")]
#[serde(skip_serializing_if = "Option::is_none")]
pub enum_names: Option<Vec<String>>, pub enum_names: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub control: Option<String>, pub control: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub actions: Option<BTreeMap<String, Action>>, pub actions: Option<BTreeMap<String, Action>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub computer: Option<String>, pub computer: Option<String>,
#[serde(default)] #[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub extensible: Option<bool>, pub extensible: Option<bool>,
#[serde(skip)] #[serde(skip)]
@ -331,7 +372,9 @@ pub enum SchemaTypeOrArray {
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Action { pub struct Action {
#[serde(skip_serializing_if = "Option::is_none")]
pub navigate: Option<String>, pub navigate: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub punc: Option<String>, pub punc: Option<String>,
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]

View File

@ -8,10 +8,5 @@ pub struct Stem {
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub relation: Option<String>, pub relation: Option<String>,
// The actual database schema node mapping for
// O(1) jump table execution for queryer.
//
// Automatically skipped from `jspg_stems()` JSON payload output.
#[serde(skip)]
pub schema: Arc<Schema>, pub schema: Arc<Schema>,
} }

View File

@ -112,8 +112,6 @@ pub fn jspg_validate(schema_id: &str, instance: JsonB) -> JsonB {
#[cfg_attr(not(test), pg_extern)] #[cfg_attr(not(test), pg_extern)]
pub fn jspg_stems() -> JsonB { pub fn jspg_stems() -> JsonB {
use serde_json::{Map, Value};
let engine_opt = { let engine_opt = {
let lock = GLOBAL_JSPG.read().unwrap(); let lock = GLOBAL_JSPG.read().unwrap();
lock.clone() lock.clone()
@ -121,9 +119,11 @@ pub fn jspg_stems() -> JsonB {
match engine_opt { match engine_opt {
Some(engine) => { Some(engine) => {
JsonB(serde_json::to_value(&engine.database.stems).unwrap_or(Value::Object(Map::new()))) let stems_json = serde_json::to_value(&engine.database.stems)
.unwrap_or(serde_json::Value::Object(serde_json::Map::new()));
JsonB(stems_json)
} }
None => JsonB(Value::Object(Map::new())), None => JsonB(serde_json::Value::Object(serde_json::Map::new())),
} }
} }

View File

@ -69,9 +69,6 @@ impl SqlCompiler {
if let Some(items) = &schema.obj.items { if let Some(items) = &schema.obj.items {
if let Some(ref_id) = &items.obj.r#ref { if let Some(ref_id) = &items.obj.r#ref {
if let Some(type_def) = self.db.types.get(ref_id) { if let Some(type_def) = self.db.types.get(ref_id) {
if is_stem_query && depth > 0 {
return Ok(("".to_string(), "abort".to_string()));
}
return self.compile_entity_node( return self.compile_entity_node(
items, items,
type_def, type_def,
@ -115,9 +112,6 @@ impl SqlCompiler {
} }
if let Some(type_def) = resolved_type { if let Some(type_def) = resolved_type {
if is_stem_query && depth > 0 {
return Ok(("".to_string(), "abort".to_string()));
}
return self.compile_entity_node( return self.compile_entity_node(
schema, schema,
type_def, type_def,