Compare commits

..

5 Commits

11 changed files with 3989 additions and 3391 deletions

View File

@ -664,5 +664,268 @@
}
}
]
},
{
"description": "JSONB boundaries",
"database": {
"relations": [
{
"id": "33333333-3333-3333-3333-333333333333",
"type": "relation",
"constraint": "fk_invoice_line_invoice",
"source_type": "invoice_line",
"source_columns": [
"invoice_id"
],
"destination_type": "invoice",
"destination_columns": [
"id"
]
}
],
"types": [
{
"name": "entity",
"hierarchy": [
"entity"
],
"grouped_fields": {
"entity": [
"id",
"type",
"archived",
"created_at"
]
},
"field_types": {
"id": "uuid",
"archived": "boolean",
"created_at": "timestamptz",
"type": "text"
},
"schemas": {
"entity": {
"type": "object",
"properties": {
"id": {
"type": "string",
"format": "uuid"
},
"type": {
"type": "string"
},
"archived": {
"type": "boolean"
},
"created_at": {
"type": "string",
"format": "date-time"
},
"created": {
"type": "boolean"
}
}
}
},
"fields": [
"id",
"type",
"archived",
"created_at"
],
"variations": [
"entity",
"invoice",
"invoice_line"
]
},
{
"name": "invoice",
"schemas": {
"invoice": {
"type": "entity",
"properties": {
"total": {
"type": "number"
},
"lines": {
"type": "array",
"items": {
"type": "invoice_line"
}
},
"metadata_line": {
"type": "invoice_line"
},
"metadata_lines": {
"type": "array",
"items": {
"type": "invoice_line"
}
},
"metadata_nested_line": {
"type": "object",
"properties": {
"line": {
"type": "invoice_line"
}
}
},
"metadata_nested_lines": {
"type": "object",
"properties": {
"lines": {
"type": "array",
"items": {
"type": "invoice_line"
}
}
}
}
}
}
},
"hierarchy": [
"invoice",
"entity"
],
"fields": [
"id",
"type",
"total",
"metadata_line",
"metadata_lines",
"metadata_nested_line",
"metadata_nested_lines",
"created_at",
"created_by",
"modified_at",
"modified_by",
"archived"
],
"grouped_fields": {
"invoice": [
"id",
"type",
"total",
"metadata_line",
"metadata_lines",
"metadata_nested_line",
"metadata_nested_lines"
],
"entity": [
"id",
"type",
"created_at",
"created_by",
"modified_at",
"modified_by",
"archived"
]
},
"lookup_fields": [
"id"
],
"historical": true,
"relationship": false,
"field_types": {
"id": "uuid",
"type": "text",
"archived": "boolean",
"total": "numeric",
"metadata_line": "jsonb",
"metadata_lines": "jsonb",
"metadata_nested_line": "jsonb",
"metadata_nested_lines": "jsonb",
"created_at": "timestamptz",
"created_by": "uuid",
"modified_at": "timestamptz",
"modified_by": "uuid"
},
"variations": [
"invoice"
]
},
{
"name": "invoice_line",
"schemas": {
"invoice_line": {
"type": "entity",
"properties": {
"invoice_id": {
"type": "string"
},
"price": {
"type": "number"
}
}
}
},
"hierarchy": [
"invoice_line",
"entity"
],
"fields": [
"id",
"type",
"invoice_id",
"price",
"created_at",
"created_by",
"modified_at",
"modified_by",
"archived"
],
"grouped_fields": {
"invoice_line": [
"id",
"type",
"invoice_id",
"price"
],
"entity": [
"id",
"type",
"created_at",
"created_by",
"modified_at",
"modified_by",
"archived"
]
},
"lookup_fields": [],
"historical": true,
"relationship": false,
"field_types": {
"id": "uuid",
"type": "text",
"archived": "boolean",
"invoice_id": "uuid",
"price": "numeric",
"created_at": "timestamptz",
"created_by": "uuid",
"modified_at": "timestamptz",
"modified_by": "uuid"
},
"variations": [
"invoice_line"
]
}
]
},
"tests": [
{
"description": "Assert no JSONB paths promoted",
"action": "compile",
"expect": {
"success": true,
"schemas": [
"entity",
"invoice",
"invoice_line"
]
}
}
]
}
]

File diff suppressed because it is too large Load Diff

View File

@ -636,5 +636,110 @@
}
}
]
},
{
"description": "STI Projections (Lacking Kind Discriminator Definitions)",
"database": {
"types": [
{
"name": "widget",
"variations": [
"widget"
],
"schemas": {
"widget": {
"type": "object",
"properties": {
"type": {
"type": "string"
}
}
},
"stock.widget": {
"type": "widget",
"properties": {
"kind": {
"type": "string"
},
"amount": {
"type": "integer"
}
}
},
"projected.widget": {
"type": "widget",
"properties": {
"alias": {
"type": "string"
}
}
}
}
}
],
"schemas": {
"stock_widget_validation": {
"type": "stock.widget"
},
"projected_widget_validation": {
"type": "projected.widget"
}
}
},
"tests": [
{
"description": "stock.widget securely expects kind when configured",
"schema_id": "stock_widget_validation",
"data": {
"type": "widget",
"amount": 5
},
"action": "validate",
"expect": {
"success": false,
"errors": [
{
"code": "MISSING_KIND",
"details": {
"path": ""
}
}
]
}
},
{
"description": "projected.widget seamlessly bypasses kind expectation when excluded from schema",
"schema_id": "projected_widget_validation",
"data": {
"type": "widget",
"alias": "Test Projection"
},
"action": "validate",
"expect": {
"success": true
}
},
{
"description": "projected.widget securely fails if user erroneously provides extra kind property",
"schema_id": "projected_widget_validation",
"data": {
"type": "widget",
"alias": "Test Projection",
"kind": "projected"
},
"action": "validate",
"expect": {
"success": false,
"errors": [
{
"code": "STRICT_PROPERTY_VIOLATION",
"details": {
"path": "kind"
}
}
]
}
}
]
}
]

File diff suppressed because it is too large Load Diff

View File

@ -2,9 +2,9 @@ pub mod edge;
pub mod r#enum;
pub mod executors;
pub mod formats;
pub mod object;
pub mod page;
pub mod punc;
pub mod object;
pub mod relation;
pub mod schema;
pub mod r#type;
@ -60,7 +60,10 @@ impl Database {
db.enums.insert(def.name.clone(), def);
}
Err(e) => {
let name = item.get("name").and_then(|v| v.as_str()).unwrap_or("unknown");
let name = item
.get("name")
.and_then(|v| v.as_str())
.unwrap_or("unknown");
errors.push(crate::drop::Error {
code: "DATABASE_ENUM_PARSE_FAILED".to_string(),
message: format!("Failed to parse database enum '{}': {}", name, e),
@ -81,7 +84,10 @@ impl Database {
db.types.insert(def.name.clone(), def);
}
Err(e) => {
let name = item.get("name").and_then(|v| v.as_str()).unwrap_or("unknown");
let name = item
.get("name")
.and_then(|v| v.as_str())
.unwrap_or("unknown");
errors.push(crate::drop::Error {
code: "DATABASE_TYPE_PARSE_FAILED".to_string(),
message: format!("Failed to parse database type '{}': {}", name, e),
@ -106,7 +112,10 @@ impl Database {
}
}
Err(e) => {
let constraint = item.get("constraint").and_then(|v| v.as_str()).unwrap_or("unknown");
let constraint = item
.get("constraint")
.and_then(|v| v.as_str())
.unwrap_or("unknown");
errors.push(crate::drop::Error {
code: "DATABASE_RELATION_PARSE_FAILED".to_string(),
message: format!("Failed to parse database relation '{}': {}", constraint, e),
@ -127,7 +136,10 @@ impl Database {
db.puncs.insert(def.name.clone(), def);
}
Err(e) => {
let name = item.get("name").and_then(|v| v.as_str()).unwrap_or("unknown");
let name = item
.get("name")
.and_then(|v| v.as_str())
.unwrap_or("unknown");
errors.push(crate::drop::Error {
code: "DATABASE_PUNC_PARSE_FAILED".to_string(),
message: format!("Failed to parse database punc '{}': {}", name, e),
@ -199,7 +211,13 @@ impl Database {
pub fn compile(&mut self, errors: &mut Vec<crate::drop::Error>) {
let mut harvested = Vec::new();
for (id, schema_arc) in &self.schemas {
crate::database::schema::Schema::collect_schemas(schema_arc, id, id.clone(), &mut harvested, errors);
crate::database::schema::Schema::collect_schemas(
schema_arc,
id,
id.clone(),
&mut harvested,
errors,
);
}
for (id, schema_arc) in harvested {
self.schemas.insert(id, schema_arc);
@ -208,11 +226,12 @@ impl Database {
self.collect_schemas(errors);
// Mathematically evaluate all property inheritances, formats, schemas, and foreign key edges topographically over OnceLocks
let mut visited = std::collections::HashSet::new();
for (id, schema_arc) in &self.schemas {
// First compile pass initializes exact structural root_id mapping to resolve DB constraints
let root_id = id.split('/').next().unwrap_or(id);
schema_arc.as_ref().compile(self, root_id, id.clone(), &mut visited, errors);
schema_arc
.as_ref()
.compile(self, root_id, id.clone(), errors);
}
}
@ -224,19 +243,37 @@ impl Database {
for type_def in self.types.values() {
for (id, schema_arc) in &type_def.schemas {
to_insert.push((id.clone(), Arc::clone(schema_arc)));
crate::database::schema::Schema::collect_schemas(schema_arc, id, id.clone(), &mut to_insert, errors);
crate::database::schema::Schema::collect_schemas(
schema_arc,
id,
id.clone(),
&mut to_insert,
errors,
);
}
}
for punc_def in self.puncs.values() {
for (id, schema_arc) in &punc_def.schemas {
to_insert.push((id.clone(), Arc::clone(schema_arc)));
crate::database::schema::Schema::collect_schemas(schema_arc, id, id.clone(), &mut to_insert, errors);
crate::database::schema::Schema::collect_schemas(
schema_arc,
id,
id.clone(),
&mut to_insert,
errors,
);
}
}
for enum_def in self.enums.values() {
for (id, schema_arc) in &enum_def.schemas {
to_insert.push((id.clone(), Arc::clone(schema_arc)));
crate::database::schema::Schema::collect_schemas(schema_arc, id, id.clone(), &mut to_insert, errors);
crate::database::schema::Schema::collect_schemas(
schema_arc,
id,
id.clone(),
&mut to_insert,
errors,
);
}
}
@ -276,10 +313,10 @@ impl Database {
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);
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

View File

@ -28,21 +28,12 @@ impl Schema {
db: &crate::database::Database,
root_id: &str,
path: String,
visited: &mut std::collections::HashSet<String>,
errors: &mut Vec<crate::drop::Error>,
) {
if self.obj.compiled_properties.get().is_some() {
return;
}
if let Some(crate::database::object::SchemaTypeOrArray::Single(t)) = &self.obj.type_ {
if !crate::database::object::is_primitive_type(t) {
if !visited.insert(t.clone()) {
return; // Break cyclical resolution
}
}
}
if let Some(format_str) = &self.obj.format {
if let Some(fmt) = crate::database::formats::FORMATS.get(format_str.as_str()) {
let _ = self
@ -79,7 +70,7 @@ impl Schema {
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.as_ref().compile(db, t, t.clone(), visited, errors);
parent.as_ref().compile(db, t, t.clone(), errors);
if let Some(p_props) = parent.obj.compiled_properties.get() {
props.extend(p_props.clone());
}
@ -113,7 +104,7 @@ impl Schema {
for t in types {
if !crate::database::object::is_primitive_type(t) {
if let Some(parent) = db.schemas.get(t) {
parent.as_ref().compile(db, t, t.clone(), visited, errors);
parent.as_ref().compile(db, t, t.clone(), errors);
}
}
}
@ -133,21 +124,21 @@ impl Schema {
let _ = self.obj.compiled_property_names.set(names);
// 4. Compute Edges natively
let schema_edges = self.compile_edges(db, root_id, &path, visited, &props, errors);
let schema_edges = self.compile_edges(db, root_id, &path, &props, errors);
let _ = self.obj.compiled_edges.set(schema_edges);
// 5. Build our inline children properties recursively NOW! (Depth-first search)
if let Some(local_props) = &self.obj.properties {
for (k, child) in local_props {
child.compile(db, root_id, format!("{}/{}", path, k), visited, errors);
child.compile(db, root_id, format!("{}/{}", path, k), errors);
}
}
if let Some(items) = &self.obj.items {
items.compile(db, root_id, format!("{}/items", path), visited, errors);
items.compile(db, root_id, format!("{}/items", path), errors);
}
if let Some(pattern_props) = &self.obj.pattern_properties {
for (k, child) in pattern_props {
child.compile(db, root_id, format!("{}/{}", path, k), visited, errors);
child.compile(db, root_id, format!("{}/{}", path, k), errors);
}
}
if let Some(additional_props) = &self.obj.additional_properties {
@ -155,7 +146,6 @@ impl Schema {
db,
root_id,
format!("{}/additionalProperties", path),
visited,
errors,
);
}
@ -165,7 +155,6 @@ impl Schema {
db,
root_id,
format!("{}/oneOf/{}", path, i),
visited,
errors,
);
}
@ -176,16 +165,15 @@ impl Schema {
db,
root_id,
format!("{}/prefixItems/{}", path, i),
visited,
errors,
);
}
}
if let Some(child) = &self.obj.not {
child.compile(db, root_id, format!("{}/not", path), visited, errors);
child.compile(db, root_id, format!("{}/not", path), errors);
}
if let Some(child) = &self.obj.contains {
child.compile(db, root_id, format!("{}/contains", path), visited, errors);
child.compile(db, root_id, format!("{}/contains", path), errors);
}
if let Some(cases) = &self.obj.cases {
for (i, c) in cases.iter().enumerate() {
@ -194,7 +182,6 @@ impl Schema {
db,
root_id,
format!("{}/cases/{}/when", path, i),
visited,
errors,
);
}
@ -203,7 +190,6 @@ impl Schema {
db,
root_id,
format!("{}/cases/{}/then", path, i),
visited,
errors,
);
}
@ -212,7 +198,6 @@ impl Schema {
db,
root_id,
format!("{}/cases/{}/else", path, i),
visited,
errors,
);
}
@ -220,12 +205,6 @@ impl Schema {
}
self.compile_polymorphism(db, root_id, &path, errors);
if let Some(crate::database::object::SchemaTypeOrArray::Single(t)) = &self.obj.type_ {
if !crate::database::object::is_primitive_type(t) {
visited.remove(t);
}
}
}
/// Dynamically infers and compiles all structural database relationships between this Schema
@ -237,7 +216,6 @@ impl Schema {
db: &crate::database::Database,
root_id: &str,
path: &str,
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> {
@ -263,31 +241,11 @@ impl Schema {
}
}
if parent_type_name.is_none() {
// 3. Absolute fallback for anonymous inline structures
let base_type_name = root_id
.split('.')
.next_back()
.unwrap_or(root_id)
.to_string();
if db.types.contains_key(&base_type_name) {
parent_type_name = Some(base_type_name);
}
}
if let Some(p_type) = parent_type_name {
// Proceed only if the resolved table physically exists within the Postgres Type hierarchy
if let Some(type_def) = db.types.get(&p_type) {
// Iterate over all discovered schema boundaries mapped inside the object
for (prop_name, prop_schema) in props {
if let Some(field_types_map) = type_def.field_types.as_ref().and_then(|v| v.as_object()) {
if let Some(pg_type) = field_types_map.get(prop_name).and_then(|v| v.as_str()) {
if pg_type == "json" || pg_type == "jsonb" {
continue;
}
}
}
let mut child_type_name = None;
let mut target_schema = prop_schema.clone();
let mut is_array = false;
@ -325,6 +283,16 @@ impl Schema {
}
if let Some(c_type) = child_type_name {
// Skip edge compilation for JSONB columns — they store data inline, not relationally.
// The physical column type from field_types is the single source of truth.
if let Some(ft) = type_def.field_types.as_ref()
.and_then(|v| v.get(prop_name.as_str()))
.and_then(|v| v.as_str())
{
if ft == "jsonb" {
continue;
}
}
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.
@ -332,9 +300,9 @@ impl Schema {
db,
root_id,
format!("{}/{}", path, prop_name),
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();

View File

@ -347,22 +347,23 @@ impl<'a> Compiler<'a> {
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 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()
"jsonb_build_object()".to_string()
} else {
format!("jsonb_build_object({})", select_args.join(", "))
format!("jsonb_build_object({})", select_args.join(", "))
}
} else {
} else {
let (sql, _) = self.compile_node(child_node)?;
sql
};
};
case_statements.push(format!(
"WHEN {}.{} = '{}' THEN ({})",

View File

@ -1559,6 +1559,24 @@ fn test_polymorphism_4_1() {
crate::tests::runner::run_test_case(&path, 4, 1).unwrap();
}
#[test]
fn test_polymorphism_5_0() {
let path = format!("{}/fixtures/polymorphism.json", env!("CARGO_MANIFEST_DIR"));
crate::tests::runner::run_test_case(&path, 5, 0).unwrap();
}
#[test]
fn test_polymorphism_5_1() {
let path = format!("{}/fixtures/polymorphism.json", env!("CARGO_MANIFEST_DIR"));
crate::tests::runner::run_test_case(&path, 5, 1).unwrap();
}
#[test]
fn test_polymorphism_5_2() {
let path = format!("{}/fixtures/polymorphism.json", env!("CARGO_MANIFEST_DIR"));
crate::tests::runner::run_test_case(&path, 5, 2).unwrap();
}
#[test]
fn test_not_0_0() {
let path = format!("{}/fixtures/not.json", env!("CARGO_MANIFEST_DIR"));
@ -3695,6 +3713,12 @@ fn test_database_5_0() {
crate::tests::runner::run_test_case(&path, 5, 0).unwrap();
}
#[test]
fn test_database_6_0() {
let path = format!("{}/fixtures/database.json", env!("CARGO_MANIFEST_DIR"));
crate::tests::runner::run_test_case(&path, 6, 0).unwrap();
}
#[test]
fn test_cases_0_0() {
let path = format!("{}/fixtures/cases.json", env!("CARGO_MANIFEST_DIR"));

View File

@ -24,9 +24,6 @@ impl<'a> ValidationContext<'a> {
if let Some(obj) = self.instance.as_object() {
for key in obj.keys() {
if key == "type" || key == "kind" {
continue; // Reserved keywords implicitly allowed
}
if !result.evaluated_keys.contains(key) && !self.overrides.contains(key) {
result.errors.push(ValidationError {
code: "STRICT_PROPERTY_VIOLATION".to_string(),

View File

@ -54,14 +54,19 @@ impl<'a> ValidationContext<'a> {
// If the target mathematically declares a horizontal structural STI variation natively
if schema_identifier_str.contains('.') {
if obj.get("kind").is_none() {
result.errors.push(ValidationError {
code: "MISSING_KIND".to_string(),
message: "Schema mechanically requires horizontal kind discrimination".to_string(),
path: self.path.clone(),
});
} else {
result.evaluated_keys.insert("kind".to_string());
let requires_kind = self.schema.compiled_properties.get()
.map_or(false, |p| p.contains_key("kind"));
if requires_kind {
if obj.get("kind").is_none() {
result.errors.push(ValidationError {
code: "MISSING_KIND".to_string(),
message: "Schema mechanically requires horizontal kind discrimination".to_string(),
path: self.path.clone(),
});
} else {
result.evaluated_keys.insert("kind".to_string());
}
}
}
} else {

View File

@ -1 +1 @@
1.0.117
1.0.120