more tests

This commit is contained in:
2026-04-10 01:06:02 -04:00
parent 3cca5ef2d5
commit be78af1507
11 changed files with 1081 additions and 965 deletions

View File

@ -240,7 +240,7 @@ The Queryer transforms Postgres into a pre-compiled Semantic Query Engine, desig
* **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`).
* **Polymorphic SQL Generation (`$family`)**: Compiles `$family` properties by analyzing the **Physical Database Variations**, *not* the schema descendants.
* **The Dot Convention**: When a schema requests `$family: "target.schema"`, the compiler extracts the base type (e.g. `schema`) and looks up its Physical Table definition.
* **Multi-Table Branching**: If the Physical Table is a parent to other tables (e.g. `organization` has variations `["organization", "bot", "person"]`), the compiler generates a dynamic `CASE WHEN type = '...' THEN ...` query, expanding into `JOIN`s for each variation.
* **Multi-Table Branching**: If the Physical Table is a parent to other tables (e.g. `organization` has variations `["organization", "bot", "person"]`), the compiler generates a dynamic `CASE WHEN type = '...' THEN ...` query, expanding into sub-queries for each variation. To ensure safe resolution, the compiler dynamically evaluates correlation boundaries: it attempts standard Relational Edge discovery first. If no explicit relational edge exists (indicating pure Table Inheritance rather than a standard foreign-key graph relationship), it safely invokes a **Table Parity Fallback**. This generates an explicit ID correlation constraint (`AND inner.id = outer.id`), perfectly binding the structural variations back to the parent row to eliminate Cartesian products.
* **Single-Table Bypass**: If the Physical Table is a leaf node with only one variation (e.g. `person` has variations `["person"]`), the compiler cleanly bypasses `CASE` generation and compiles a simple `SELECT` across the base table, as all schema extensions (e.g. `light.person`, `full.person`) are guaranteed to reside in the exact same physical row.
---

View File

@ -25,19 +25,19 @@
]
},
{
"name": "get_light_organizations",
"name": "get_light_organization",
"schemas": [
{
"$id": "get_light_organizations.response",
"$id": "get_light_organization.response",
"$family": "light.organization"
}
]
},
{
"name": "get_full_organizations",
"name": "get_full_organization",
"schemas": [
{
"$id": "get_full_organizations.response",
"$id": "get_full_organization.response",
"$family": "full.organization"
}
]
@ -897,11 +897,27 @@
},
{
"name": "widget",
"hierarchy": ["widget", "entity"],
"fields": ["id", "type", "kind", "archived", "created_at"],
"hierarchy": [
"widget",
"entity"
],
"fields": [
"id",
"type",
"kind",
"archived",
"created_at"
],
"grouped_fields": {
"entity": ["id", "type", "archived", "created_at"],
"widget": ["kind"]
"entity": [
"id",
"type",
"archived",
"created_at"
],
"widget": [
"kind"
]
},
"field_types": {
"id": "uuid",
@ -910,27 +926,35 @@
"archived": "boolean",
"created_at": "timestamptz"
},
"variations": ["widget"],
"variations": [
"widget"
],
"schemas": [
{
"$id": "widget",
"type": "entity",
"properties": {
"kind": { "type": "string" }
"kind": {
"type": "string"
}
}
},
{
"$id": "stock.widget",
"type": "widget",
"properties": {
"kind": { "const": "stock" }
"kind": {
"const": "stock"
}
}
},
{
"$id": "tasks.widget",
"type": "widget",
"properties": {
"kind": { "const": "tasks" }
"kind": {
"const": "tasks"
}
}
}
]
@ -1597,50 +1621,60 @@
"success": true,
"sql": [
[
"(SELECT jsonb_strip_nulls((SELECT COALESCE(jsonb_agg(jsonb_build_object(",
" 'id', organization_1.id,",
" 'type', CASE",
" WHEN organization_1.type = 'bot' THEN",
" ((SELECT jsonb_build_object(",
" 'archived', entity_5.archived,",
" 'created_at', entity_5.created_at,",
" 'id', entity_5.id,",
" 'name', organization_4.name,",
" 'token', bot_3.token,",
" 'type', entity_5.type",
" )",
" FROM agreego.bot bot_3",
" JOIN agreego.organization organization_4 ON organization_4.id = bot_3.id",
" JOIN agreego.entity entity_5 ON entity_5.id = organization_4.id",
" WHERE NOT entity_5.archived))",
" WHEN organization_1.type = 'organization' THEN",
" ((SELECT jsonb_build_object(",
" 'archived', entity_7.archived,",
" 'created_at', entity_7.created_at,",
" 'id', entity_7.id,",
" 'name', organization_6.name,",
" 'type', entity_7.type",
" )",
" FROM agreego.organization organization_6",
" JOIN agreego.entity entity_7 ON entity_7.id = organization_6.id",
" WHERE NOT entity_7.archived))",
" WHEN organization_1.type = 'person' THEN",
" ((SELECT jsonb_build_object(",
" 'age', person_8.age,",
" 'archived', entity_10.archived,",
" 'created_at', entity_10.created_at,",
" 'first_name', person_8.first_name,",
" 'id', entity_10.id,",
" 'last_name', person_8.last_name,",
" 'name', organization_9.name,",
" 'type', entity_10.type",
" )",
" FROM agreego.person person_8",
" JOIN agreego.organization organization_9 ON organization_9.id = person_8.id",
" JOIN agreego.entity entity_10 ON entity_10.id = organization_9.id",
" WHERE NOT entity_10.archived))",
" ELSE NULL END",
")), '[]'::jsonb)",
"(SELECT jsonb_strip_nulls((SELECT COALESCE(jsonb_agg(",
" CASE",
" WHEN organization_1.type = 'bot' THEN (",
" (SELECT jsonb_build_object(",
" 'archived', entity_5.archived,",
" 'created_at', entity_5.created_at,",
" 'id', entity_5.id,",
" 'name', organization_4.name,",
" 'role', bot_3.role,",
" 'token', bot_3.token,",
" 'type', entity_5.type",
" )",
" FROM agreego.bot bot_3",
" JOIN agreego.organization organization_4 ON organization_4.id = bot_3.id",
" JOIN agreego.entity entity_5 ON entity_5.id = organization_4.id",
" WHERE",
" NOT entity_5.archived",
" AND entity_5.id = entity_2.id)",
" )",
" WHEN organization_1.type = 'organization' THEN (",
" (SELECT jsonb_build_object(",
" 'archived', entity_7.archived,",
" 'created_at', entity_7.created_at,",
" 'id', entity_7.id,",
" 'name', organization_6.name,",
" 'type', entity_7.type",
" )",
" FROM agreego.organization organization_6",
" JOIN agreego.entity entity_7 ON entity_7.id = organization_6.id",
" WHERE",
" NOT entity_7.archived",
" AND entity_7.id = entity_2.id)",
" )",
" WHEN organization_1.type = 'person' THEN (",
" (SELECT jsonb_build_object(",
" 'age', person_8.age,",
" 'archived', entity_10.archived,",
" 'created_at', entity_10.created_at,",
" 'first_name', person_8.first_name,",
" 'id', entity_10.id,",
" 'last_name', person_8.last_name,",
" 'name', organization_9.name,",
" 'type', entity_10.type",
" )",
" FROM agreego.person person_8",
" JOIN agreego.organization organization_9 ON organization_9.id = person_8.id",
" JOIN agreego.entity entity_10 ON entity_10.id = organization_9.id",
" WHERE",
" NOT entity_10.archived",
" AND entity_10.id = entity_2.id)",
" )",
" ELSE NULL",
" END",
"), '[]'::jsonb)",
"FROM agreego.organization organization_1",
"JOIN agreego.entity entity_2 ON entity_2.id = organization_1.id",
"WHERE NOT entity_2.archived)))"
@ -1651,12 +1685,47 @@
{
"description": "Light organizations select via a punc response with family",
"action": "query",
"schema_id": "get_light_organizations.response",
"schema_id": "get_light_organization.response",
"expect": {
"success": true,
"sql": [
[
"FIX ME"
"(SELECT jsonb_strip_nulls((SELECT ",
" CASE",
" WHEN organization_1.type = 'bot' THEN (",
" (SELECT jsonb_build_object(",
" 'archived', entity_5.archived,",
" 'created_at', entity_5.created_at,",
" 'id', entity_5.id,",
" 'name', organization_4.name,",
" 'token', bot_3.token,",
" 'type', entity_5.type",
" )",
" FROM agreego.bot bot_3",
" JOIN agreego.organization organization_4 ON organization_4.id = bot_3.id",
" JOIN agreego.entity entity_5 ON entity_5.id = organization_4.id",
" WHERE NOT entity_5.archived AND entity_5.id = entity_2.id)",
" )",
" WHEN organization_1.type = 'person' THEN (",
" (SELECT jsonb_build_object(",
" 'archived', entity_8.archived,",
" 'created_at', entity_8.created_at,",
" 'first_name', person_6.first_name,",
" 'id', entity_8.id,",
" 'last_name', person_6.last_name,",
" 'name', organization_7.name,",
" 'type', entity_8.type",
" )",
" FROM agreego.person person_6",
" JOIN agreego.organization organization_7 ON organization_7.id = person_6.id",
" JOIN agreego.entity entity_8 ON entity_8.id = organization_7.id",
" WHERE NOT entity_8.archived AND entity_8.id = entity_2.id)",
" )",
" ELSE NULL",
" END",
"FROM agreego.organization organization_1",
"JOIN agreego.entity entity_2 ON entity_2.id = organization_1.id",
"WHERE NOT entity_2.archived)))"
]
]
}
@ -1664,123 +1733,176 @@
{
"description": "Full organizations select via a punc response with family",
"action": "query",
"schema_id": "get_full_organizations.response",
"schema_id": "get_full_organization.response",
"expect": {
"success": true,
"sql": [
[
"(SELECT jsonb_strip_nulls((SELECT jsonb_build_object(",
" 'addresses', (SELECT COALESCE(jsonb_agg(jsonb_build_object(",
"(SELECT jsonb_strip_nulls((SELECT CASE",
" WHEN organization_1.type = 'person' THEN (",
" (SELECT jsonb_build_object(",
" 'addresses',",
" (SELECT COALESCE(jsonb_agg(jsonb_build_object(",
" 'archived', entity_8.archived,",
" 'created_at', entity_8.created_at,",
" 'id', entity_8.id,",
" 'is_primary', contact_6.is_primary,",
" 'target',",
" (SELECT jsonb_build_object(",
" 'archived', entity_10.archived,",
" 'city', address_9.city,",
" 'created_at', entity_10.created_at,",
" 'id', entity_10.id,",
" 'type', entity_10.type",
" )",
" FROM agreego.address address_9",
" JOIN agreego.entity entity_10 ON entity_10.id = address_9.id",
" WHERE",
" NOT entity_10.archived",
" AND relationship_7.target_id = entity_10.id),",
" 'type', entity_8.type",
" )), '[]'::jsonb)",
" FROM agreego.contact contact_6",
" JOIN agreego.relationship relationship_7 ON relationship_7.id = contact_6.id",
" JOIN agreego.entity entity_8 ON entity_8.id = relationship_7.id",
" WHERE",
" NOT entity_8.archived",
" AND relationship_7.target_type = 'address'",
" AND relationship_7.source_id = entity_5.id),",
" 'age', person_3.age,",
" 'archived', entity_5.archived,",
" 'contacts',",
" (SELECT COALESCE(jsonb_agg(jsonb_build_object(",
" 'archived', entity_13.archived,",
" 'created_at', entity_13.created_at,",
" 'id', entity_13.id,",
" 'is_primary', contact_11.is_primary,",
" 'target',",
" CASE",
" WHEN entity_13.target_type = 'address' THEN (",
" (SELECT jsonb_build_object(",
" 'archived', entity_15.archived,",
" 'city', address_14.city,",
" 'created_at', entity_15.created_at,",
" 'id', entity_15.id,",
" 'type', entity_15.type",
" )",
" FROM agreego.address address_14",
" JOIN agreego.entity entity_15 ON entity_15.id = address_14.id",
" WHERE",
" NOT entity_15.archived",
" AND relationship_12.target_id = entity_15.id)",
" )",
" WHEN entity_13.target_type = 'email_address' THEN (",
" (SELECT jsonb_build_object(",
" 'address', email_address_16.address,",
" 'archived', entity_17.archived,",
" 'created_at', entity_17.created_at,",
" 'id', entity_17.id,",
" 'type', entity_17.type",
" )",
" FROM agreego.email_address email_address_16",
" JOIN agreego.entity entity_17 ON entity_17.id = email_address_16.id",
" WHERE",
" NOT entity_17.archived",
" AND relationship_12.target_id = entity_17.id)",
" )",
" WHEN entity_13.target_type = 'phone_number' THEN (",
" (SELECT jsonb_build_object(",
" 'archived', entity_19.archived,",
" 'created_at', entity_19.created_at,",
" 'id', entity_19.id,",
" 'number', phone_number_18.number,",
" 'type', entity_19.type",
" )",
" FROM agreego.phone_number phone_number_18",
" JOIN agreego.entity entity_19 ON entity_19.id = phone_number_18.id",
" WHERE",
" NOT entity_19.archived",
" AND relationship_12.target_id = entity_19.id)",
" )",
" ELSE NULL",
" END,",
" 'type', entity_13.type",
" )), '[]'::jsonb)",
" FROM agreego.contact contact_11",
" JOIN agreego.relationship relationship_12 ON relationship_12.id = contact_11.id",
" JOIN agreego.entity entity_13 ON entity_13.id = relationship_12.id",
" WHERE",
" NOT entity_13.archived",
" AND relationship_12.source_id = entity_5.id),",
" 'created_at', entity_5.created_at,",
" 'email_addresses',",
" (SELECT COALESCE(jsonb_agg(jsonb_build_object(",
" 'archived', entity_22.archived,",
" 'created_at', entity_22.created_at,",
" 'id', entity_22.id,",
" 'is_primary', contact_20.is_primary,",
" 'target',",
" (SELECT jsonb_build_object(",
" 'address', email_address_23.address,",
" 'archived', entity_24.archived,",
" 'created_at', entity_24.created_at,",
" 'id', entity_24.id,",
" 'type', entity_24.type",
" )",
" FROM agreego.email_address email_address_23",
" JOIN agreego.entity entity_24 ON entity_24.id = email_address_23.id",
" WHERE",
" NOT entity_24.archived",
" AND relationship_21.target_id = entity_24.id),",
" 'type', entity_22.type",
" )), '[]'::jsonb)",
" FROM agreego.contact contact_20",
" JOIN agreego.relationship relationship_21 ON relationship_21.id = contact_20.id",
" JOIN agreego.entity entity_22 ON entity_22.id = relationship_21.id",
" WHERE",
" NOT entity_22.archived",
" AND relationship_21.target_type = 'email_address'",
" AND relationship_21.source_id = entity_5.id),",
" 'first_name', person_3.first_name,",
" 'id', entity_5.id,",
" 'is_primary', contact_3.is_primary,",
" 'target', (SELECT jsonb_build_object(",
" 'archived', entity_7.archived,",
" 'city', address_6.city,",
" 'created_at', entity_7.created_at,",
" 'id', entity_7.id,",
" 'type', entity_7.type",
" )",
" FROM agreego.address address_6",
" JOIN agreego.entity entity_7 ON entity_7.id = address_6.id",
" WHERE NOT entity_7.archived AND relationship_4.target_id = entity_7.id),",
" 'last_name', person_3.last_name,",
" 'name', organization_4.name,",
" 'phone_numbers',",
" (SELECT COALESCE(jsonb_agg(jsonb_build_object(",
" 'archived', entity_27.archived,",
" 'created_at', entity_27.created_at,",
" 'id', entity_27.id,",
" 'is_primary', contact_25.is_primary,",
" 'target',",
" (SELECT jsonb_build_object(",
" 'archived', entity_29.archived,",
" 'created_at', entity_29.created_at,",
" 'id', entity_29.id,",
" 'number', phone_number_28.number,",
" 'type', entity_29.type",
" )",
" FROM agreego.phone_number phone_number_28",
" JOIN agreego.entity entity_29 ON entity_29.id = phone_number_28.id",
" WHERE",
" NOT entity_29.archived",
" AND relationship_26.target_id = entity_29.id),",
" 'type', entity_27.type",
" )), '[]'::jsonb)",
" FROM agreego.contact contact_25",
" JOIN agreego.relationship relationship_26 ON relationship_26.id = contact_25.id",
" JOIN agreego.entity entity_27 ON entity_27.id = relationship_26.id",
" WHERE",
" NOT entity_27.archived",
" AND relationship_26.target_type = 'phone_number'",
" AND relationship_26.source_id = entity_5.id),",
" 'type', entity_5.type",
" )), '[]'::jsonb)",
" FROM agreego.contact contact_3",
" JOIN agreego.relationship relationship_4 ON relationship_4.id = contact_3.id",
" JOIN agreego.entity entity_5 ON entity_5.id = relationship_4.id",
" WHERE NOT entity_5.archived AND relationship_4.target_type = 'address' AND relationship_4.source_id = entity_2.id),",
" 'archived', entity_2.archived,",
" 'contacts', (SELECT COALESCE(jsonb_agg(jsonb_build_object(",
" 'archived', entity_10.archived,",
" 'created_at', entity_10.created_at,",
" 'id', entity_10.id,",
" 'is_primary', contact_8.is_primary,",
" 'target', CASE WHEN entity_10.target_type = 'address' THEN ((SELECT jsonb_build_object(",
" 'archived', entity_12.archived,",
" 'city', address_11.city,",
" 'created_at', entity_12.created_at,",
" 'id', entity_12.id,",
" 'type', entity_12.type",
" )",
" FROM agreego.address address_11",
" JOIN agreego.entity entity_12 ON entity_12.id = address_11.id",
" WHERE NOT entity_12.archived AND relationship_9.target_id = entity_12.id)) WHEN entity_10.target_type = 'email_address' THEN ((SELECT jsonb_build_object(",
" 'address', email_address_13.address,",
" 'archived', entity_14.archived,",
" 'created_at', entity_14.created_at,",
" 'id', entity_14.id,",
" 'type', entity_14.type",
" )",
" FROM agreego.email_address email_address_13",
" JOIN agreego.entity entity_14 ON entity_14.id = email_address_13.id",
" WHERE NOT entity_14.archived AND relationship_9.target_id = entity_14.id)) WHEN entity_10.target_type = 'phone_number' THEN ((SELECT jsonb_build_object(",
" 'archived', entity_16.archived,",
" 'created_at', entity_16.created_at,",
" 'id', entity_16.id,",
" 'number', phone_number_15.number,",
" 'type', entity_16.type",
" )",
" FROM agreego.phone_number phone_number_15",
" JOIN agreego.entity entity_16 ON entity_16.id = phone_number_15.id",
" WHERE NOT entity_16.archived AND relationship_9.target_id = entity_16.id)) ELSE NULL END,",
" 'type', entity_10.type",
" )), '[]'::jsonb)",
" FROM agreego.contact contact_8",
" JOIN agreego.relationship relationship_9 ON relationship_9.id = contact_8.id",
" JOIN agreego.entity entity_10 ON entity_10.id = relationship_9.id",
" WHERE NOT entity_10.archived AND relationship_9.source_id = entity_2.id),",
" 'created_at', entity_2.created_at,",
" 'email_addresses', (SELECT COALESCE(jsonb_agg(jsonb_build_object(",
" 'archived', entity_19.archived,",
" 'created_at', entity_19.created_at,",
" 'id', entity_19.id,",
" 'is_primary', contact_17.is_primary,",
" 'target', (SELECT jsonb_build_object(",
" 'address', email_address_20.address,",
" 'archived', entity_21.archived,",
" 'created_at', entity_21.created_at,",
" 'id', entity_21.id,",
" 'type', entity_21.type",
" )",
" FROM agreego.email_address email_address_20",
" JOIN agreego.entity entity_21 ON entity_21.id = email_address_20.id",
" WHERE NOT entity_21.archived AND relationship_18.target_id = entity_21.id),",
" 'type', entity_19.type",
" )), '[]'::jsonb)",
" FROM agreego.contact contact_17",
" JOIN agreego.relationship relationship_18 ON relationship_18.id = contact_17.id",
" JOIN agreego.entity entity_19 ON entity_19.id = relationship_18.id",
" WHERE NOT entity_19.archived AND relationship_18.target_type = 'email_address' AND relationship_18.source_id = entity_2.id),",
" 'id', entity_2.id,",
" 'name', organization_1.name,",
" 'phone_numbers', (SELECT COALESCE(jsonb_agg(jsonb_build_object(",
" 'archived', entity_24.archived,",
" 'created_at', entity_24.created_at,",
" 'id', entity_24.id,",
" 'is_primary', contact_22.is_primary,",
" 'target', (SELECT jsonb_build_object(",
" 'archived', entity_26.archived,",
" 'created_at', entity_26.created_at,",
" 'id', entity_26.id,",
" 'number', phone_number_25.number,",
" 'type', entity_26.type",
" )",
" FROM agreego.phone_number phone_number_25",
" JOIN agreego.entity entity_26 ON entity_26.id = phone_number_25.id",
" WHERE NOT entity_26.archived AND relationship_23.target_id = entity_26.id),",
" 'type', entity_24.type",
" )), '[]'::jsonb)",
" FROM agreego.contact contact_22",
" JOIN agreego.relationship relationship_23 ON relationship_23.id = contact_22.id",
" JOIN agreego.entity entity_24 ON entity_24.id = relationship_23.id",
" WHERE NOT entity_24.archived AND relationship_23.target_type = 'phone_number' AND relationship_23.source_id = entity_2.id),",
" 'type', entity_2.type",
")))",
" )",
" FROM agreego.person person_3",
" JOIN agreego.organization organization_4 ON organization_4.id = person_3.id",
" JOIN agreego.entity entity_5 ON entity_5.id = organization_4.id",
" WHERE NOT entity_5.archived AND entity_5.id = entity_2.id))",
" ELSE NULL",
"END",
"FROM agreego.organization organization_1",
"JOIN agreego.entity entity_2 ON entity_2.id = organization_1.id",
"WHERE NOT entity_2.archived)"
"WHERE NOT entity_2.archived)))"
]
]
}
@ -1833,7 +1955,32 @@
"success": true,
"sql": [
[
"FIX ME"
"(SELECT jsonb_strip_nulls((SELECT COALESCE(jsonb_agg(",
" CASE",
" WHEN widget_1.kind = 'stock' THEN (",
" jsonb_build_object(",
" 'archived', entity_2.archived,",
" 'created_at', entity_2.created_at,",
" 'id', entity_2.id,",
" 'kind', widget_1.kind,",
" 'type', entity_2.type",
" )",
" )",
" WHEN widget_1.kind = 'tasks' THEN (",
" jsonb_build_object(",
" 'archived', entity_2.archived,",
" 'created_at', entity_2.created_at,",
" 'id', entity_2.id,",
" 'kind', widget_1.kind,",
" 'type', entity_2.type",
" )",
" )",
" ELSE NULL",
" END",
"), '[]'::jsonb)",
"FROM agreego.widget widget_1",
"JOIN agreego.entity entity_2 ON entity_2.id = widget_1.id",
"WHERE NOT entity_2.archived)))"
]
]
}

View File

@ -4,6 +4,7 @@ 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;
@ -23,7 +24,8 @@ use punc::Punc;
use relation::Relation;
use schema::Schema;
use serde_json::Value;
use std::collections::{HashMap, HashSet};
use std::collections::HashMap;
use std::sync::Arc;
use r#type::Type;
pub struct Database {
@ -31,8 +33,7 @@ pub struct Database {
pub types: HashMap<String, Type>,
pub puncs: HashMap<String, Punc>,
pub relations: HashMap<String, Relation>,
pub schemas: HashMap<String, Schema>,
pub depths: HashMap<String, usize>,
pub schemas: HashMap<String, Arc<Schema>>,
pub executor: Box<dyn DatabaseExecutor + Send + Sync>,
}
@ -44,7 +45,6 @@ impl Database {
relations: HashMap::new(),
puncs: HashMap::new(),
schemas: HashMap::new(),
depths: HashMap::new(),
#[cfg(not(test))]
executor: Box::new(SpiExecutor::new()),
#[cfg(test)]
@ -135,7 +135,7 @@ impl Database {
.clone()
.unwrap_or_else(|| format!("schema_{}", i));
schema.obj.id = Some(id.clone());
db.schemas.insert(id, schema);
db.schemas.insert(id, Arc::new(schema));
}
Err(e) => {
errors.push(crate::drop::Error {
@ -185,18 +185,21 @@ impl Database {
pub fn compile(&mut self, errors: &mut Vec<crate::drop::Error>) {
let mut harvested = Vec::new();
for schema in self.schemas.values_mut() {
schema.collect_schemas(None, &mut harvested, errors);
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.schemas.extend(harvested);
self.collect_schemas(errors);
self.collect_depths();
// Mathematically evaluate all property inheritances, formats, schemas, and foreign key edges topographically over OnceLocks
let mut visited = std::collections::HashSet::new();
for schema in self.schemas.values() {
schema.compile(self, &mut visited, errors);
for schema_arc in self.schemas.values() {
schema_arc.as_ref().compile(self, &mut visited, errors);
}
}
@ -222,35 +225,185 @@ impl Database {
}
for (id, schema) in to_insert {
self.schemas.insert(id, schema);
self.schemas.insert(id, Arc::new(schema));
}
}
fn collect_depths(&mut self) {
let mut depths: HashMap<String, usize> = HashMap::new();
let schema_ids: Vec<String> = self.schemas.keys().cloned().collect();
/// 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;
}
for id in schema_ids {
let mut current_id = id.clone();
let mut depth = 0;
let mut visited = HashSet::new();
let p_def = self.types.get(parent_type)?;
let c_def = self.types.get(child_type)?;
while let Some(schema) = self.schemas.get(&current_id) {
if !visited.insert(current_id.clone()) {
break; // Cycle detected
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;
}
if let Some(crate::database::schema::SchemaTypeOrArray::Single(t)) = &schema.obj.type_ {
if !crate::database::schema::is_primitive_type(t) {
current_id = t.clone();
depth += 1;
continue;
}
}
// 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
}
}
break;
}
depths.insert(id, depth);
}
self.depths = depths;
}
// 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]))
}
}

367
src/database/object.rs Normal file
View File

@ -0,0 +1,367 @@
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::BTreeMap;
use std::sync::Arc;
use std::sync::OnceLock;
use crate::database::schema::Schema;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Case {
#[serde(skip_serializing_if = "Option::is_none")]
pub when: Option<Arc<Schema>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub then: Option<Arc<Schema>>,
#[serde(rename = "else")]
#[serde(skip_serializing_if = "Option::is_none")]
pub else_: Option<Arc<Schema>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct SchemaObject {
// Core Schema Keywords
#[serde(rename = "$id")]
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(default)] // Allow missing type
#[serde(rename = "type")]
#[serde(skip_serializing_if = "Option::is_none")]
pub type_: Option<SchemaTypeOrArray>, // Handles string or array of strings
// Object Keywords
#[serde(skip_serializing_if = "Option::is_none")]
pub properties: Option<BTreeMap<String, Arc<Schema>>>,
#[serde(rename = "patternProperties")]
#[serde(skip_serializing_if = "Option::is_none")]
pub pattern_properties: Option<BTreeMap<String, Arc<Schema>>>,
#[serde(rename = "additionalProperties")]
#[serde(skip_serializing_if = "Option::is_none")]
pub additional_properties: Option<Arc<Schema>>,
#[serde(rename = "$family")]
#[serde(skip_serializing_if = "Option::is_none")]
pub family: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub required: Option<Vec<String>>,
// dependencies can be schema dependencies or property dependencies
#[serde(skip_serializing_if = "Option::is_none")]
pub dependencies: Option<BTreeMap<String, Dependency>>,
// Array Keywords
#[serde(rename = "items")]
#[serde(skip_serializing_if = "Option::is_none")]
pub items: Option<Arc<Schema>>,
#[serde(rename = "prefixItems")]
#[serde(skip_serializing_if = "Option::is_none")]
pub prefix_items: Option<Vec<Arc<Schema>>>,
// String Validation
#[serde(rename = "minLength")]
#[serde(skip_serializing_if = "Option::is_none")]
pub min_length: Option<f64>,
#[serde(rename = "maxLength")]
#[serde(skip_serializing_if = "Option::is_none")]
pub max_length: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pattern: Option<String>,
// Array Validation
#[serde(rename = "minItems")]
#[serde(skip_serializing_if = "Option::is_none")]
pub min_items: Option<f64>,
#[serde(rename = "maxItems")]
#[serde(skip_serializing_if = "Option::is_none")]
pub max_items: Option<f64>,
#[serde(rename = "uniqueItems")]
#[serde(skip_serializing_if = "Option::is_none")]
pub unique_items: Option<bool>,
#[serde(rename = "contains")]
#[serde(skip_serializing_if = "Option::is_none")]
pub contains: Option<Arc<Schema>>,
#[serde(rename = "minContains")]
#[serde(skip_serializing_if = "Option::is_none")]
pub min_contains: Option<f64>,
#[serde(rename = "maxContains")]
#[serde(skip_serializing_if = "Option::is_none")]
pub max_contains: Option<f64>,
// Object Validation
#[serde(rename = "minProperties")]
#[serde(skip_serializing_if = "Option::is_none")]
pub min_properties: Option<f64>,
#[serde(rename = "maxProperties")]
#[serde(skip_serializing_if = "Option::is_none")]
pub max_properties: Option<f64>,
#[serde(rename = "propertyNames")]
#[serde(skip_serializing_if = "Option::is_none")]
pub property_names: Option<Arc<Schema>>,
// Numeric Validation
#[serde(skip_serializing_if = "Option::is_none")]
pub format: Option<String>,
#[serde(rename = "enum")]
#[serde(skip_serializing_if = "Option::is_none")]
pub enum_: Option<Vec<Value>>, // `enum` is a reserved keyword in Rust
#[serde(
default,
rename = "const",
deserialize_with = "crate::database::object::deserialize_some"
)]
#[serde(skip_serializing_if = "Option::is_none")]
pub const_: Option<Value>,
// Numeric Validation
#[serde(rename = "multipleOf")]
#[serde(skip_serializing_if = "Option::is_none")]
pub multiple_of: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub minimum: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub maximum: Option<f64>,
#[serde(rename = "exclusiveMinimum")]
#[serde(skip_serializing_if = "Option::is_none")]
pub exclusive_minimum: Option<f64>,
#[serde(rename = "exclusiveMaximum")]
#[serde(skip_serializing_if = "Option::is_none")]
pub exclusive_maximum: Option<f64>,
// Combining Keywords
#[serde(skip_serializing_if = "Option::is_none")]
pub cases: Option<Vec<Case>>,
#[serde(rename = "oneOf")]
#[serde(skip_serializing_if = "Option::is_none")]
pub one_of: Option<Vec<Arc<Schema>>>,
#[serde(rename = "not")]
#[serde(skip_serializing_if = "Option::is_none")]
pub not: Option<Arc<Schema>>,
// Custom Vocabularies
#[serde(skip_serializing_if = "Option::is_none")]
pub form: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub display: Option<Vec<String>>,
#[serde(rename = "enumNames")]
#[serde(skip_serializing_if = "Option::is_none")]
pub enum_names: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub control: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub actions: Option<BTreeMap<String, Action>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub computer: Option<String>,
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub extensible: Option<bool>,
#[serde(rename = "compiledProperties")]
#[serde(skip_deserializing)]
#[serde(skip_serializing_if = "crate::database::object::is_once_lock_vec_empty")]
#[serde(serialize_with = "crate::database::object::serialize_once_lock")]
pub compiled_property_names: OnceLock<Vec<String>>,
#[serde(skip)]
pub compiled_properties: OnceLock<BTreeMap<String, Arc<Schema>>>,
#[serde(rename = "compiledDiscriminator")]
#[serde(skip_deserializing)]
#[serde(skip_serializing_if = "crate::database::object::is_once_lock_string_empty")]
#[serde(serialize_with = "crate::database::object::serialize_once_lock")]
pub compiled_discriminator: OnceLock<String>,
#[serde(rename = "compiledOptions")]
#[serde(skip_deserializing)]
#[serde(skip_serializing_if = "crate::database::object::is_once_lock_map_empty")]
#[serde(serialize_with = "crate::database::object::serialize_once_lock")]
pub compiled_options: OnceLock<BTreeMap<String, String>>,
#[serde(rename = "compiledEdges")]
#[serde(skip_deserializing)]
#[serde(skip_serializing_if = "crate::database::object::is_once_lock_map_empty")]
#[serde(serialize_with = "crate::database::object::serialize_once_lock")]
pub compiled_edges: OnceLock<BTreeMap<String, crate::database::edge::Edge>>,
#[serde(skip)]
pub compiled_format: OnceLock<CompiledFormat>,
#[serde(skip)]
pub compiled_pattern: OnceLock<CompiledRegex>,
#[serde(skip)]
pub compiled_pattern_properties: OnceLock<Vec<(CompiledRegex, Arc<Schema>)>>,
}
/// Represents a compiled format validator
#[derive(Clone)]
pub enum CompiledFormat {
Func(fn(&serde_json::Value) -> Result<(), Box<dyn std::error::Error + Send + Sync>>),
Regex(regex::Regex),
}
impl std::fmt::Debug for CompiledFormat {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
CompiledFormat::Func(_) => write!(f, "CompiledFormat::Func(...)"),
CompiledFormat::Regex(r) => write!(f, "CompiledFormat::Regex({:?})", r),
}
}
}
/// A wrapper for compiled regex patterns
#[derive(Debug, Clone)]
pub struct CompiledRegex(pub regex::Regex);
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum SchemaTypeOrArray {
Single(String),
Multiple(Vec<String>),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Action {
#[serde(skip_serializing_if = "Option::is_none")]
pub navigate: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub punc: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum Dependency {
Props(Vec<String>),
Schema(Arc<Schema>),
}
pub fn serialize_once_lock<T: serde::Serialize, S: serde::Serializer>(
lock: &OnceLock<T>,
serializer: S,
) -> Result<S::Ok, S::Error> {
if let Some(val) = lock.get() {
val.serialize(serializer)
} else {
serializer.serialize_none()
}
}
pub fn is_once_lock_map_empty<K, V>(lock: &OnceLock<std::collections::BTreeMap<K, V>>) -> bool {
lock.get().map_or(true, |m| m.is_empty())
}
pub fn is_once_lock_vec_empty<T>(lock: &OnceLock<Vec<T>>) -> bool {
lock.get().map_or(true, |v| v.is_empty())
}
pub fn is_once_lock_string_empty(lock: &OnceLock<String>) -> bool {
lock.get().map_or(true, |s| s.is_empty())
}
// Schema mirrors the Go Punc Generator's schema struct for consistency.
// It is an order-preserving representation of a JSON Schema.
pub fn deserialize_some<'de, D>(deserializer: D) -> Result<Option<Value>, D::Error>
where
D: serde::Deserializer<'de>,
{
let v = Value::deserialize(deserializer)?;
Ok(Some(v))
}
pub fn is_primitive_type(t: &str) -> bool {
matches!(
t,
"string" | "number" | "integer" | "boolean" | "object" | "array" | "null"
)
}
impl SchemaObject {
pub fn identifier(&self) -> Option<String> {
if let Some(id) = &self.id {
return Some(id.split('.').next_back().unwrap_or("").to_string());
}
if let Some(SchemaTypeOrArray::Single(t)) = &self.type_ {
if !is_primitive_type(t) {
return Some(t.split('.').next_back().unwrap_or("").to_string());
}
}
None
}
pub fn get_discriminator_value(&self, dim: &str) -> Option<String> {
let is_split = self
.compiled_properties
.get()
.map_or(false, |p| p.contains_key("kind"));
if let Some(id) = &self.id {
if id.contains("light.person") || id.contains("light.organization") {
println!(
"[DEBUG SPLIT] ID: {}, dim: {}, is_split: {:?}, props: {:?}",
id,
dim,
is_split,
self
.compiled_properties
.get()
.map(|p| p.keys().cloned().collect::<Vec<_>>())
);
}
}
if let Some(props) = self.compiled_properties.get() {
if let Some(prop_schema) = props.get(dim) {
if let Some(c) = &prop_schema.obj.const_ {
if let Some(s) = c.as_str() {
return Some(s.to_string());
}
}
if let Some(e) = &prop_schema.obj.enum_ {
if e.len() == 1 {
if let Some(s) = e[0].as_str() {
return Some(s.to_string());
}
}
}
}
}
if dim == "kind" {
if let Some(id) = &self.id {
let base = id.split('/').last().unwrap_or(id);
if let Some(idx) = base.rfind('.') {
return Some(base[..idx].to_string());
}
}
if let Some(SchemaTypeOrArray::Single(t)) = &self.type_ {
if !is_primitive_type(t) {
let base = t.split('/').last().unwrap_or(t);
if let Some(idx) = base.rfind('.') {
return Some(base[..idx].to_string());
}
}
}
}
if dim == "type" {
if let Some(id) = &self.id {
let base = id.split('/').last().unwrap_or(id);
if is_split {
return Some(base.split('.').next_back().unwrap_or(base).to_string());
} else {
return Some(base.to_string());
}
}
if let Some(SchemaTypeOrArray::Single(t)) = &self.type_ {
if !is_primitive_type(t) {
let base = t.split('/').last().unwrap_or(t);
if is_split {
return Some(base.split('.').next_back().unwrap_or(base).to_string());
} else {
return Some(base.to_string());
}
}
}
}
None
}
}

View File

@ -1,256 +1,7 @@
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::BTreeMap;
use std::sync::Arc;
use std::sync::OnceLock;
pub fn serialize_once_lock<T: serde::Serialize, S: serde::Serializer>(
lock: &OnceLock<T>,
serializer: S,
) -> Result<S::Ok, S::Error> {
if let Some(val) = lock.get() {
val.serialize(serializer)
} else {
serializer.serialize_none()
}
}
pub fn is_once_lock_map_empty<K, V>(lock: &OnceLock<std::collections::BTreeMap<K, V>>) -> bool {
lock.get().map_or(true, |m| m.is_empty())
}
pub fn is_once_lock_vec_empty<T>(lock: &OnceLock<Vec<T>>) -> bool {
lock.get().map_or(true, |v| v.is_empty())
}
pub fn is_once_lock_string_empty(lock: &OnceLock<String>) -> bool {
lock.get().map_or(true, |s| s.is_empty())
}
// Schema mirrors the Go Punc Generator's schema struct for consistency.
// It is an order-preserving representation of a JSON Schema.
pub fn deserialize_some<'de, D>(deserializer: D) -> Result<Option<Value>, D::Error>
where
D: serde::Deserializer<'de>,
{
let v = Value::deserialize(deserializer)?;
Ok(Some(v))
}
pub fn is_primitive_type(t: &str) -> bool {
matches!(
t,
"string" | "number" | "integer" | "boolean" | "object" | "array" | "null"
)
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Case {
#[serde(skip_serializing_if = "Option::is_none")]
pub when: Option<Arc<Schema>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub then: Option<Arc<Schema>>,
#[serde(rename = "else")]
#[serde(skip_serializing_if = "Option::is_none")]
pub else_: Option<Arc<Schema>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct SchemaObject {
// Core Schema Keywords
#[serde(rename = "$id")]
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(default)] // Allow missing type
#[serde(rename = "type")]
#[serde(skip_serializing_if = "Option::is_none")]
pub type_: Option<SchemaTypeOrArray>, // Handles string or array of strings
// Object Keywords
#[serde(skip_serializing_if = "Option::is_none")]
pub properties: Option<BTreeMap<String, Arc<Schema>>>,
#[serde(rename = "patternProperties")]
#[serde(skip_serializing_if = "Option::is_none")]
pub pattern_properties: Option<BTreeMap<String, Arc<Schema>>>,
#[serde(rename = "additionalProperties")]
#[serde(skip_serializing_if = "Option::is_none")]
pub additional_properties: Option<Arc<Schema>>,
#[serde(rename = "$family")]
#[serde(skip_serializing_if = "Option::is_none")]
pub family: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub required: Option<Vec<String>>,
// dependencies can be schema dependencies or property dependencies
#[serde(skip_serializing_if = "Option::is_none")]
pub dependencies: Option<BTreeMap<String, Dependency>>,
// Array Keywords
#[serde(rename = "items")]
#[serde(skip_serializing_if = "Option::is_none")]
pub items: Option<Arc<Schema>>,
#[serde(rename = "prefixItems")]
#[serde(skip_serializing_if = "Option::is_none")]
pub prefix_items: Option<Vec<Arc<Schema>>>,
// String Validation
#[serde(rename = "minLength")]
#[serde(skip_serializing_if = "Option::is_none")]
pub min_length: Option<f64>,
#[serde(rename = "maxLength")]
#[serde(skip_serializing_if = "Option::is_none")]
pub max_length: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pattern: Option<String>,
// Array Validation
#[serde(rename = "minItems")]
#[serde(skip_serializing_if = "Option::is_none")]
pub min_items: Option<f64>,
#[serde(rename = "maxItems")]
#[serde(skip_serializing_if = "Option::is_none")]
pub max_items: Option<f64>,
#[serde(rename = "uniqueItems")]
#[serde(skip_serializing_if = "Option::is_none")]
pub unique_items: Option<bool>,
#[serde(rename = "contains")]
#[serde(skip_serializing_if = "Option::is_none")]
pub contains: Option<Arc<Schema>>,
#[serde(rename = "minContains")]
#[serde(skip_serializing_if = "Option::is_none")]
pub min_contains: Option<f64>,
#[serde(rename = "maxContains")]
#[serde(skip_serializing_if = "Option::is_none")]
pub max_contains: Option<f64>,
// Object Validation
#[serde(rename = "minProperties")]
#[serde(skip_serializing_if = "Option::is_none")]
pub min_properties: Option<f64>,
#[serde(rename = "maxProperties")]
#[serde(skip_serializing_if = "Option::is_none")]
pub max_properties: Option<f64>,
#[serde(rename = "propertyNames")]
#[serde(skip_serializing_if = "Option::is_none")]
pub property_names: Option<Arc<Schema>>,
// Numeric Validation
#[serde(skip_serializing_if = "Option::is_none")]
pub format: Option<String>,
#[serde(rename = "enum")]
#[serde(skip_serializing_if = "Option::is_none")]
pub enum_: Option<Vec<Value>>, // `enum` is a reserved keyword in Rust
#[serde(
default,
rename = "const",
deserialize_with = "crate::database::schema::deserialize_some"
)]
#[serde(skip_serializing_if = "Option::is_none")]
pub const_: Option<Value>,
// Numeric Validation
#[serde(rename = "multipleOf")]
#[serde(skip_serializing_if = "Option::is_none")]
pub multiple_of: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub minimum: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub maximum: Option<f64>,
#[serde(rename = "exclusiveMinimum")]
#[serde(skip_serializing_if = "Option::is_none")]
pub exclusive_minimum: Option<f64>,
#[serde(rename = "exclusiveMaximum")]
#[serde(skip_serializing_if = "Option::is_none")]
pub exclusive_maximum: Option<f64>,
// Combining Keywords
#[serde(skip_serializing_if = "Option::is_none")]
pub cases: Option<Vec<Case>>,
#[serde(rename = "oneOf")]
#[serde(skip_serializing_if = "Option::is_none")]
pub one_of: Option<Vec<Arc<Schema>>>,
#[serde(rename = "not")]
#[serde(skip_serializing_if = "Option::is_none")]
pub not: Option<Arc<Schema>>,
// Custom Vocabularies
#[serde(skip_serializing_if = "Option::is_none")]
pub form: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub display: Option<Vec<String>>,
#[serde(rename = "enumNames")]
#[serde(skip_serializing_if = "Option::is_none")]
pub enum_names: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub control: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub actions: Option<BTreeMap<String, Action>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub computer: Option<String>,
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub extensible: Option<bool>,
#[serde(rename = "compiledProperties")]
#[serde(skip_deserializing)]
#[serde(skip_serializing_if = "crate::database::schema::is_once_lock_vec_empty")]
#[serde(serialize_with = "crate::database::schema::serialize_once_lock")]
pub compiled_property_names: OnceLock<Vec<String>>,
#[serde(skip)]
pub compiled_properties: OnceLock<BTreeMap<String, Arc<Schema>>>,
#[serde(rename = "compiledDiscriminator")]
#[serde(skip_deserializing)]
#[serde(skip_serializing_if = "crate::database::schema::is_once_lock_string_empty")]
#[serde(serialize_with = "crate::database::schema::serialize_once_lock")]
pub compiled_discriminator: OnceLock<String>,
#[serde(rename = "compiledOptions")]
#[serde(skip_deserializing)]
#[serde(skip_serializing_if = "crate::database::schema::is_once_lock_map_empty")]
#[serde(serialize_with = "crate::database::schema::serialize_once_lock")]
pub compiled_options: OnceLock<BTreeMap<String, String>>,
#[serde(rename = "compiledEdges")]
#[serde(skip_deserializing)]
#[serde(skip_serializing_if = "crate::database::schema::is_once_lock_map_empty")]
#[serde(serialize_with = "crate::database::schema::serialize_once_lock")]
pub compiled_edges: OnceLock<BTreeMap<String, crate::database::edge::Edge>>,
#[serde(skip)]
pub compiled_format: OnceLock<CompiledFormat>,
#[serde(skip)]
pub compiled_pattern: OnceLock<CompiledRegex>,
#[serde(skip)]
pub compiled_pattern_properties: OnceLock<Vec<(CompiledRegex, Arc<Schema>)>>,
}
/// Represents a compiled format validator
#[derive(Clone)]
pub enum CompiledFormat {
Func(fn(&serde_json::Value) -> Result<(), Box<dyn std::error::Error + Send + Sync>>),
Regex(regex::Regex),
}
impl std::fmt::Debug for CompiledFormat {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
CompiledFormat::Func(_) => write!(f, "CompiledFormat::Func(...)"),
CompiledFormat::Regex(r) => write!(f, "CompiledFormat::Regex({:?})", r),
}
}
}
/// A wrapper for compiled regex patterns
#[derive(Debug, Clone)]
pub struct CompiledRegex(pub regex::Regex);
use crate::database::object::*;
#[derive(Debug, Clone, Serialize, Default)]
pub struct Schema {
#[serde(flatten)]
@ -293,7 +44,7 @@ impl Schema {
let _ = self
.obj
.compiled_format
.set(crate::database::schema::CompiledFormat::Func(fmt.func));
.set(crate::database::object::CompiledFormat::Func(fmt.func));
}
}
@ -302,7 +53,7 @@ impl Schema {
let _ = self
.obj
.compiled_pattern
.set(crate::database::schema::CompiledRegex(re));
.set(crate::database::object::CompiledRegex(re));
}
}
@ -310,7 +61,7 @@ impl Schema {
let mut compiled = Vec::new();
for (k, v) in pattern_props {
if let Ok(re) = regex::Regex::new(k) {
compiled.push((crate::database::schema::CompiledRegex(re), v.clone()));
compiled.push((crate::database::object::CompiledRegex(re), v.clone()));
}
}
if !compiled.is_empty() {
@ -321,10 +72,10 @@ impl Schema {
let mut props = std::collections::BTreeMap::new();
// 1. Resolve INHERITANCE dependencies first
if let Some(crate::database::schema::SchemaTypeOrArray::Single(t)) = &self.obj.type_ {
if !crate::database::schema::is_primitive_type(t) {
if let Some(crate::database::object::SchemaTypeOrArray::Single(t)) = &self.obj.type_ {
if !crate::database::object::is_primitive_type(t) {
if let Some(parent) = db.schemas.get(t) {
parent.compile(db, visited, errors);
parent.as_ref().compile(db, visited, errors);
if let Some(p_props) = parent.obj.compiled_properties.get() {
props.extend(p_props.clone());
}
@ -332,10 +83,10 @@ impl Schema {
}
}
if let Some(crate::database::schema::SchemaTypeOrArray::Multiple(types)) = &self.obj.type_ {
if let Some(crate::database::object::SchemaTypeOrArray::Multiple(types)) = &self.obj.type_ {
let mut custom_type_count = 0;
for t in types {
if !crate::database::schema::is_primitive_type(t) {
if !crate::database::object::is_primitive_type(t) {
custom_type_count += 1;
}
}
@ -355,9 +106,9 @@ impl Schema {
}
for t in types {
if !crate::database::schema::is_primitive_type(t) {
if !crate::database::object::is_primitive_type(t) {
if let Some(parent) = db.schemas.get(t) {
parent.compile(db, visited, errors);
parent.as_ref().compile(db, visited, errors);
}
}
}
@ -434,6 +185,106 @@ impl Schema {
}
}
/// 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,
visited: &mut std::collections::HashSet<String>,
props: &std::collections::BTreeMap<String, std::sync::Arc<Schema>>,
errors: &mut Vec<crate::drop::Error>,
) -> 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());
} else if let Some(identifier) = self.obj.identifier() {
parent_type_name = Some(
identifier
.split('.')
.next_back()
.unwrap_or(&identifier)
.to_string(),
);
}
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();
let mut is_array = false;
// Structurally unpack the inner target entity if the object maps to an array list
if let Some(crate::database::object::SchemaTypeOrArray::Single(t)) =
&prop_schema.obj.type_
{
if t == "array" {
is_array = true;
if let Some(items) = &prop_schema.obj.items {
target_schema = items.clone();
}
}
}
// 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() {
child_type_name = Some(ref_id.split('.').next_back().unwrap_or(&ref_id).to_string());
} else if let Some(arr) = &target_schema.obj.one_of {
if let Some(first) = arr.first() {
if let Some(ref_id) = first.obj.identifier() {
child_type_name =
Some(ref_id.split('.').next_back().unwrap_or(&ref_id).to_string());
}
}
}
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, errors);
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)) = db.resolve_relation(
&p_type,
&c_type,
prop_name,
Some(&keys_for_ambiguity),
is_array,
self.id.as_deref(),
&format!("/{}", prop_name),
errors,
) {
schema_edges.insert(
prop_name.clone(),
crate::database::edge::Edge {
constraint: relation.constraint.clone(),
forward: is_forward,
},
);
}
}
}
}
}
}
}
schema_edges
}
pub fn compile_polymorphism(
&self,
db: &crate::database::Database,
@ -444,8 +295,11 @@ impl Schema {
if let Some(family) = &self.obj.family {
let family_base = family.split('.').next_back().unwrap_or(family).to_string();
let family_prefix = family.strip_suffix(&family_base).unwrap_or("").trim_end_matches('.');
let family_prefix = family
.strip_suffix(&family_base)
.unwrap_or("")
.trim_end_matches('.');
if let Some(type_def) = db.types.get(&family_base) {
if type_def.variations.len() > 1 && type_def.variations.iter().any(|v| v != &family_base) {
// Scenario A / B: Table Variations
@ -456,7 +310,7 @@ impl Schema {
} else {
format!("{}.{}", family_prefix, var)
};
if db.schemas.contains_key(&target_id) {
options.insert(var.to_string(), target_id);
}
@ -464,36 +318,17 @@ impl Schema {
} else {
// Scenario C: Single Table Inheritance (Horizontal)
strategy = "kind".to_string();
let mut target_family_ids = std::collections::HashSet::new();
target_family_ids.insert(family.clone());
// Iteratively build local descendants since db.descendants is removed natively
let mut added = true;
while added {
added = false;
for schema in &type_def.schemas {
if let Some(id) = &schema.obj.id {
if !target_family_ids.contains(id) {
if let Some(crate::database::schema::SchemaTypeOrArray::Single(t)) = &schema.obj.type_ {
if target_family_ids.contains(t) {
target_family_ids.insert(id.clone());
added = true;
}
}
}
}
}
}
let suffix = format!(".{}", family_base);
for schema in &type_def.schemas {
if let Some(id) = &schema.obj.id {
if target_family_ids.contains(id) {
if let Some(kind_val) = schema.obj.get_discriminator_value("kind") {
options.insert(kind_val, id.to_string());
}
}
}
if let Some(id) = &schema.obj.id {
if id.ends_with(&suffix) || id == &family_base {
if let Some(kind_val) = schema.obj.get_discriminator_value("kind") {
options.insert(kind_val, id.to_string());
}
}
}
}
}
}
@ -535,8 +370,8 @@ impl Schema {
let mut target_id = c.obj.id.clone();
if target_id.is_none() {
if let Some(crate::database::schema::SchemaTypeOrArray::Single(t)) = &c.obj.type_ {
if !crate::database::schema::is_primitive_type(t) {
if let Some(crate::database::object::SchemaTypeOrArray::Single(t)) = &c.obj.type_ {
if !crate::database::object::is_primitive_type(t) {
target_id = Some(t.clone());
}
}
@ -552,8 +387,8 @@ impl Schema {
}
if !options.is_empty() {
let _ = self.obj.compiled_discriminator.set(strategy);
let _ = self.obj.compiled_options.set(options);
let _ = self.obj.compiled_discriminator.set(strategy);
let _ = self.obj.compiled_options.set(options);
}
}
@ -585,8 +420,8 @@ impl Schema {
Self::validate_identifier(id, "$id", errors);
to_insert.push((id.clone(), self.clone()));
}
if let Some(crate::database::schema::SchemaTypeOrArray::Single(t)) = &self.obj.type_ {
if !crate::database::schema::is_primitive_type(t) {
if let Some(crate::database::object::SchemaTypeOrArray::Single(t)) = &self.obj.type_ {
if !crate::database::object::is_primitive_type(t) {
Self::validate_identifier(t, "type", errors);
}
}
@ -597,8 +432,8 @@ impl Schema {
// Is this schema an inline ad-hoc composition?
// Meaning it has a tracking context, lacks an explicit $id, but extends an Entity ref with explicit properties!
if self.obj.id.is_none() && self.obj.properties.is_some() {
if let Some(crate::database::schema::SchemaTypeOrArray::Single(t)) = &self.obj.type_ {
if !crate::database::schema::is_primitive_type(t) {
if let Some(crate::database::object::SchemaTypeOrArray::Single(t)) = &self.obj.type_ {
if !crate::database::object::is_primitive_type(t) {
if let Some(ref path) = tracking_path {
to_insert.push((path.clone(), self.clone()));
}
@ -690,285 +525,6 @@ impl Schema {
}
}
}
/// 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,
visited: &mut std::collections::HashSet<String>,
props: &std::collections::BTreeMap<String, std::sync::Arc<Schema>>,
errors: &mut Vec<crate::drop::Error>,
) -> 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());
} else if let Some(identifier) = self.obj.identifier() {
parent_type_name = Some(
identifier
.split('.')
.next_back()
.unwrap_or(&identifier)
.to_string(),
);
}
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();
let mut is_array = false;
// 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_
{
if t == "array" {
is_array = true;
if let Some(items) = &prop_schema.obj.items {
target_schema = items.clone();
}
}
}
// 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() {
child_type_name = Some(ref_id.split('.').next_back().unwrap_or(&ref_id).to_string());
} else if let Some(arr) = &target_schema.obj.one_of {
if let Some(first) = arr.first() {
if let Some(ref_id) = first.obj.identifier() {
child_type_name =
Some(ref_id.split('.').next_back().unwrap_or(&ref_id).to_string());
}
}
}
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, errors);
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),
is_array,
self.id.as_deref(),
&format!("/{}", prop_name),
errors,
) {
schema_edges.insert(
prop_name.clone(),
crate::database::edge::Edge {
constraint: relation.constraint.clone(),
forward: is_forward,
},
);
}
}
}
}
}
}
}
schema_edges
}
}
/// 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,
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 = db.types.get(parent_type)?;
let c_def = db.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> = db.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]))
}
impl<'de> Deserialize<'de> for Schema {
@ -1021,104 +577,3 @@ impl<'de> Deserialize<'de> for Schema {
})
}
}
impl SchemaObject {
pub fn identifier(&self) -> Option<String> {
if let Some(id) = &self.id {
return Some(id.split('.').next_back().unwrap_or("").to_string());
}
if let Some(SchemaTypeOrArray::Single(t)) = &self.type_ {
if !is_primitive_type(t) {
return Some(t.split('.').next_back().unwrap_or("").to_string());
}
}
None
}
pub fn get_discriminator_value(&self, dim: &str) -> Option<String> {
let is_split = self.compiled_properties.get().map_or(false, |p| p.contains_key("kind"));
if let Some(id) = &self.id {
if id.contains("light.person") || id.contains("light.organization") {
println!("[DEBUG SPLIT] ID: {}, dim: {}, is_split: {:?}, props: {:?}", id, dim, is_split, self.compiled_properties.get().map(|p| p.keys().cloned().collect::<Vec<_>>()));
}
}
if let Some(props) = self.compiled_properties.get() {
if let Some(prop_schema) = props.get(dim) {
if let Some(c) = &prop_schema.obj.const_ {
if let Some(s) = c.as_str() {
return Some(s.to_string());
}
}
if let Some(e) = &prop_schema.obj.enum_ {
if e.len() == 1 {
if let Some(s) = e[0].as_str() {
return Some(s.to_string());
}
}
}
}
}
if dim == "kind" {
if let Some(id) = &self.id {
let base = id.split('/').last().unwrap_or(id);
if let Some(idx) = base.rfind('.') {
return Some(base[..idx].to_string());
}
}
if let Some(crate::database::schema::SchemaTypeOrArray::Single(t)) = &self.type_ {
if !crate::database::schema::is_primitive_type(t) {
let base = t.split('/').last().unwrap_or(t);
if let Some(idx) = base.rfind('.') {
return Some(base[..idx].to_string());
}
}
}
}
if dim == "type" {
if let Some(id) = &self.id {
let base = id.split('/').last().unwrap_or(id);
if is_split {
return Some(base.split('.').next_back().unwrap_or(base).to_string());
} else {
return Some(base.to_string());
}
}
if let Some(crate::database::schema::SchemaTypeOrArray::Single(t)) = &self.type_ {
if !crate::database::schema::is_primitive_type(t) {
let base = t.split('/').last().unwrap_or(t);
if is_split {
return Some(base.split('.').next_back().unwrap_or(base).to_string());
} else {
return Some(base.to_string());
}
}
}
}
None
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum SchemaTypeOrArray {
Single(String),
Multiple(Vec<String>),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Action {
#[serde(skip_serializing_if = "Option::is_none")]
pub navigate: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub punc: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum Dependency {
Props(Vec<String>),
Schema(Arc<Schema>),
}

View File

@ -25,7 +25,7 @@ impl Merger {
let mut notifications_queue = Vec::new();
let target_schema = match self.db.schemas.get(schema_id) {
Some(s) => Arc::new(s.clone()),
Some(s) => Arc::clone(s),
None => {
return crate::drop::Drop::with_errors(vec![crate::drop::Error {
code: "MERGE_FAILED".to_string(),
@ -144,7 +144,7 @@ impl Merger {
if let Some(v) = val {
if let Some(target_id) = options.get(v) {
if let Some(target_schema) = self.db.schemas.get(target_id) {
schema = Arc::new(target_schema.clone());
schema = Arc::clone(target_schema);
} else {
return Err(format!("Polymorphic mapped target '{}' not found in database registry", target_id));
}
@ -169,7 +169,7 @@ impl Merger {
notifications: &mut Vec<String>,
) -> Result<Value, String> {
let mut item_schema = schema.clone();
if let Some(crate::database::schema::SchemaTypeOrArray::Single(t)) = &schema.obj.type_ {
if let Some(crate::database::object::SchemaTypeOrArray::Single(t)) = &schema.obj.type_ {
if t == "array" {
if let Some(items_def) = &schema.obj.items {
item_schema = items_def.clone();
@ -389,7 +389,7 @@ impl Merger {
);
let mut item_schema = rel_schema.clone();
if let Some(crate::database::schema::SchemaTypeOrArray::Single(t)) =
if let Some(crate::database::object::SchemaTypeOrArray::Single(t)) =
&rel_schema.obj.type_
{
if t == "array" {

View File

@ -1,4 +1,6 @@
use crate::database::Database;
use std::sync::Arc;
pub struct Compiler<'a> {
pub db: &'a Database,
pub filter_keys: &'a [String],
@ -15,6 +17,7 @@ pub struct Node<'a> {
pub property_name: Option<String>,
pub depth: usize,
pub ast_path: String,
pub is_polymorphic_branch: bool,
}
impl<'a> Compiler<'a> {
@ -26,7 +29,7 @@ impl<'a> Compiler<'a> {
.get(schema_id)
.ok_or_else(|| format!("Schema not found: {}", schema_id))?;
let target_schema = std::sync::Arc::new(schema.clone());
let target_schema = std::sync::Arc::clone(schema);
let mut compiler = Compiler {
db: &self.db,
@ -43,6 +46,7 @@ impl<'a> Compiler<'a> {
property_name: None,
depth: 0,
ast_path: String::new(),
is_polymorphic_branch: false,
};
let (sql, _) = compiler.compile_node(node)?;
@ -54,7 +58,7 @@ impl<'a> Compiler<'a> {
fn compile_node(&mut self, node: Node<'a>) -> Result<(String, String), String> {
// Determine the base schema type (could be an array, object, or literal)
match &node.schema.obj.type_ {
Some(crate::database::schema::SchemaTypeOrArray::Single(t)) if t == "array" => {
Some(crate::database::object::SchemaTypeOrArray::Single(t)) if t == "array" => {
self.compile_array(node)
}
_ => self.compile_reference(node),
@ -116,12 +120,12 @@ impl<'a> Compiler<'a> {
}
// Handle Direct Refs via type pointer
if let Some(crate::database::schema::SchemaTypeOrArray::Single(t)) = &node.schema.obj.type_ {
if !crate::database::schema::is_primitive_type(t) {
if let Some(crate::database::object::SchemaTypeOrArray::Single(t)) = &node.schema.obj.type_ {
if !crate::database::object::is_primitive_type(t) {
// If it's just an ad-hoc struct ref, we should resolve it
if let Some(target_schema) = self.db.schemas.get(t) {
let mut ref_node = node.clone();
ref_node.schema = std::sync::Arc::new(target_schema.clone());
ref_node.schema = Arc::clone(target_schema);
return self.compile_node(ref_node);
}
return Err(format!("Unresolved schema type pointer: {}", t));
@ -133,7 +137,7 @@ impl<'a> Compiler<'a> {
if options.len() == 1 {
let target_id = options.values().next().unwrap();
let mut bypass_schema = crate::database::schema::Schema::default();
bypass_schema.obj.type_ = Some(crate::database::schema::SchemaTypeOrArray::Single(target_id.clone()));
bypass_schema.obj.type_ = Some(crate::database::object::SchemaTypeOrArray::Single(target_id.clone()));
let mut bypass_node = node.clone();
bypass_node.schema = std::sync::Arc::new(bypass_schema);
return self.compile_node(bypass_node);
@ -166,17 +170,28 @@ impl<'a> Compiler<'a> {
) -> Result<(String, String), String> {
let (table_aliases, from_clauses) = self.compile_from_clause(r#type);
// 2. Map properties and build jsonb_build_object args
let mut select_args = self.compile_select_clause(r#type, &table_aliases, node.clone())?;
let jsonb_obj_sql = if node.schema.obj.family.is_some() || node.schema.obj.one_of.is_some() {
let base_alias = table_aliases
.get(&r#type.name)
.cloned()
.unwrap_or_else(|| node.parent_alias.to_string());
let mut case_node = node.clone();
case_node.parent_alias = base_alias.clone();
let arc_aliases = std::sync::Arc::new(table_aliases.clone());
case_node.parent_type_aliases = Some(arc_aliases);
case_node.parent_type = Some(r#type);
// 2.5 Inject polymorphism directly into the query object
let mut poly_args = self.compile_polymorphism_select(r#type, &table_aliases, node.clone())?;
select_args.append(&mut poly_args);
let jsonb_obj_sql = if select_args.is_empty() {
"jsonb_build_object()".to_string()
let (case_sql, _) = self.compile_one_of(case_node)?;
case_sql
} else {
format!("jsonb_build_object({})", select_args.join(", "))
let select_args = self.compile_select_clause(r#type, &table_aliases, node.clone())?;
if select_args.is_empty() {
"jsonb_build_object()".to_string()
} else {
format!("jsonb_build_object({})", select_args.join(", "))
}
};
// 3. Build WHERE clauses
@ -205,55 +220,6 @@ impl<'a> Compiler<'a> {
))
}
fn compile_polymorphism_select(
&mut self,
r#type: &'a crate::database::r#type::Type,
table_aliases: &std::collections::HashMap<String, String>,
node: Node<'a>,
) -> Result<Vec<String>, String> {
let mut select_args = Vec::new();
if node.schema.obj.family.is_some() || node.schema.obj.one_of.is_some() {
let base_alias = table_aliases
.get(&r#type.name)
.cloned()
.unwrap_or_else(|| node.parent_alias.to_string());
let disc = node.schema.obj.compiled_discriminator.get();
if disc.is_none() {
return Ok(select_args);
}
let options = node.schema.obj.compiled_options.get();
if options.is_none() {
return Ok(select_args);
}
let options = options.unwrap();
if options.len() == 1 {
let target_id = options.values().next().unwrap();
if let Some(target_schema) = self.db.schemas.get(target_id) {
let mut bypass_node = node.clone();
bypass_node.schema = std::sync::Arc::new(target_schema.clone());
let mut bypassed_args = self.compile_select_clause(r#type, table_aliases, bypass_node)?;
select_args.append(&mut bypassed_args);
return Ok(select_args);
}
}
select_args.push(format!("'id', {}.id", base_alias));
let mut case_node = node.clone();
case_node.parent_alias = base_alias.clone();
let arc_aliases = std::sync::Arc::new(table_aliases.clone());
case_node.parent_type_aliases = Some(arc_aliases);
let (case_sql, _) = self.compile_one_of(case_node)?;
select_args.push(format!("'{}', {}", disc.unwrap(), case_sql));
}
Ok(select_args)
}
fn compile_object(
&mut self,
props: &std::collections::BTreeMap<String, std::sync::Arc<crate::database::schema::Schema>>,
@ -301,8 +267,25 @@ impl<'a> Compiler<'a> {
for (disc_val, target_id) in options {
if let Some(target_schema) = self.db.schemas.get(target_id) {
let mut child_node = node.clone();
child_node.schema = std::sync::Arc::new(target_schema.clone());
let (val_sql, _) = self.compile_node(child_node)?;
child_node.schema = Arc::clone(target_schema);
child_node.is_polymorphic_branch = true;
let val_sql = if disc == "kind" && node.parent_type.is_some() && node.parent_type_aliases.is_some() {
let aliases_arc = node.parent_type_aliases.as_ref().unwrap();
let aliases = aliases_arc.as_ref();
let p_type = node.parent_type.unwrap();
let select_args = self.compile_select_clause(p_type, aliases, child_node.clone())?;
if select_args.is_empty() {
"jsonb_build_object()".to_string()
} else {
format!("jsonb_build_object({})", select_args.join(", "))
}
} else {
let (sql, _) = self.compile_node(child_node)?;
sql
};
case_statements.push(format!(
"WHEN {}.{} = '{}' THEN ({})",
@ -365,18 +348,18 @@ impl<'a> Compiler<'a> {
let prop_schema = &merged_props[prop_key];
let is_object_or_array = match &prop_schema.obj.type_ {
Some(crate::database::schema::SchemaTypeOrArray::Single(s)) => {
Some(crate::database::object::SchemaTypeOrArray::Single(s)) => {
s == "object" || s == "array"
}
Some(crate::database::schema::SchemaTypeOrArray::Multiple(v)) => {
Some(crate::database::object::SchemaTypeOrArray::Multiple(v)) => {
v.contains(&"object".to_string()) || v.contains(&"array".to_string())
}
_ => false,
};
let is_custom_object_pointer = match &prop_schema.obj.type_ {
Some(crate::database::schema::SchemaTypeOrArray::Single(s)) => {
!crate::database::schema::is_primitive_type(s)
Some(crate::database::object::SchemaTypeOrArray::Single(s)) => {
!crate::database::object::is_primitive_type(s)
}
_ => false,
};
@ -426,6 +409,7 @@ impl<'a> Compiler<'a> {
} else {
format!("{}/{}", node.ast_path, prop_key)
},
is_polymorphic_branch: false,
};
let (val_sql, val_type) = self.compile_node(child_node)?;
@ -465,6 +449,8 @@ impl<'a> Compiler<'a> {
self.compile_filter_conditions(r#type, type_aliases, &node, &base_alias, &mut where_clauses);
self.compile_polymorphic_bounds(r#type, type_aliases, &node, &mut where_clauses);
let start_len = where_clauses.len();
self.compile_relation_conditions(
r#type,
type_aliases,
@ -473,6 +459,14 @@ impl<'a> Compiler<'a> {
&mut where_clauses,
)?;
if node.is_polymorphic_branch && where_clauses.len() == start_len {
if let Some(parent_aliases) = &node.parent_type_aliases {
if let Some(outer_entity_alias) = parent_aliases.get("entity") {
where_clauses.push(format!("{}.id = {}.id", entity_alias, outer_entity_alias));
}
}
}
Ok(where_clauses)
}

View File

@ -13,7 +13,7 @@ impl<'a> ValidationContext<'a> {
if let Some(ref type_) = self.schema.type_ {
match type_ {
crate::database::schema::SchemaTypeOrArray::Single(t) => {
crate::database::object::SchemaTypeOrArray::Single(t) => {
if !Validator::check_type(t, current) {
result.errors.push(ValidationError {
code: "INVALID_TYPE".to_string(),
@ -22,7 +22,7 @@ impl<'a> ValidationContext<'a> {
});
}
}
crate::database::schema::SchemaTypeOrArray::Multiple(types) => {
crate::database::object::SchemaTypeOrArray::Multiple(types) => {
let mut valid = false;
for t in types {
if Validator::check_type(t, current) {

View File

@ -10,7 +10,7 @@ impl<'a> ValidationContext<'a> {
let current = self.instance;
if let Some(compiled_fmt) = self.schema.compiled_format.get() {
match compiled_fmt {
crate::database::schema::CompiledFormat::Func(f) => {
crate::database::object::CompiledFormat::Func(f) => {
let should = if let Some(s) = current.as_str() {
!s.is_empty()
} else {
@ -24,7 +24,7 @@ impl<'a> ValidationContext<'a> {
});
}
}
crate::database::schema::CompiledFormat::Regex(re) => {
crate::database::object::CompiledFormat::Regex(re) => {
if let Some(s) = current.as_str()
&& !re.is_match(s)
{

View File

@ -124,7 +124,7 @@ impl<'a> ValidationContext<'a> {
for (prop, dep) in deps {
if obj.contains_key(prop) {
match dep {
crate::database::schema::Dependency::Props(required_props) => {
crate::database::object::Dependency::Props(required_props) => {
for req_prop in required_props {
if !obj.contains_key(req_prop) {
result.errors.push(ValidationError {
@ -135,7 +135,7 @@ impl<'a> ValidationContext<'a> {
}
}
}
crate::database::schema::Dependency::Schema(dep_schema) => {
crate::database::object::Dependency::Schema(dep_schema) => {
let derived = self.derive_for_schema(dep_schema, false);
let dep_res = derived.validate()?;
result.evaluated_keys.extend(dep_res.evaluated_keys.clone());
@ -155,7 +155,7 @@ impl<'a> ValidationContext<'a> {
if let Some(child_instance) = obj.get(key) {
let new_path = self.join_path(key);
let is_ref = match &sub_schema.type_ {
Some(crate::database::schema::SchemaTypeOrArray::Single(t)) => !crate::database::schema::is_primitive_type(t),
Some(crate::database::object::SchemaTypeOrArray::Single(t)) => !crate::database::object::is_primitive_type(t),
_ => false,
};
let next_extensible = if is_ref { false } else { self.extensible };
@ -184,7 +184,7 @@ impl<'a> ValidationContext<'a> {
if compiled_re.0.is_match(key) {
let new_path = self.join_path(key);
let is_ref = match &sub_schema.type_ {
Some(crate::database::schema::SchemaTypeOrArray::Single(t)) => !crate::database::schema::is_primitive_type(t),
Some(crate::database::object::SchemaTypeOrArray::Single(t)) => !crate::database::object::is_primitive_type(t),
_ => false,
};
let next_extensible = if is_ref { false } else { self.extensible };
@ -226,7 +226,7 @@ impl<'a> ValidationContext<'a> {
if !locally_matched {
let new_path = self.join_path(key);
let is_ref = match &additional_schema.type_ {
Some(crate::database::schema::SchemaTypeOrArray::Single(t)) => !crate::database::schema::is_primitive_type(t),
Some(crate::database::object::SchemaTypeOrArray::Single(t)) => !crate::database::object::is_primitive_type(t),
_ => false,
};
let next_extensible = if is_ref { false } else { self.extensible };

View File

@ -120,7 +120,7 @@ impl<'a> ValidationContext<'a> {
if let Some(target_id) = options.get(val) {
if let Some(target_schema) = self.db.schemas.get(target_id) {
let derived = self.derive_for_schema(target_schema, false);
let derived = self.derive_for_schema(target_schema.as_ref(), false);
let sub_res = derived.validate()?;
let is_valid = sub_res.is_valid();
result.merge(sub_res);
@ -173,17 +173,17 @@ impl<'a> ValidationContext<'a> {
let mut custom_types = Vec::new();
match &self.schema.type_ {
Some(crate::database::schema::SchemaTypeOrArray::Single(t)) => {
if !crate::database::schema::is_primitive_type(t) {
Some(crate::database::object::SchemaTypeOrArray::Single(t)) => {
if !crate::database::object::is_primitive_type(t) {
custom_types.push(t.clone());
}
}
Some(crate::database::schema::SchemaTypeOrArray::Multiple(arr)) => {
Some(crate::database::object::SchemaTypeOrArray::Multiple(arr)) => {
if arr.contains(&payload_primitive.to_string()) || (payload_primitive == "integer" && arr.contains(&"number".to_string())) {
// It natively matched a primitive in the array options, skip forcing custom proxy fallback
} else {
for t in arr {
if !crate::database::schema::is_primitive_type(t) {
if !crate::database::object::is_primitive_type(t) {
custom_types.push(t.clone());
}
}