Compare commits

..

5 Commits

10 changed files with 488 additions and 65 deletions

View File

@ -869,6 +869,106 @@
"notify": true,
"relationship": false
},
{
"name": "account",
"hierarchy": [
"account",
"entity"
],
"fields": [
"id",
"type",
"name",
"archived",
"created_at",
"created_by",
"modified_at",
"modified_by",
"kind",
"routing_number",
"card_number"
],
"grouped_fields": {
"entity": [
"id",
"type",
"name",
"archived",
"created_at",
"created_by",
"modified_at",
"modified_by"
],
"account": [
"id",
"type",
"kind",
"routing_number",
"card_number"
]
},
"field_types": {
"id": "uuid",
"type": "text",
"archived": "boolean",
"name": "text",
"created_at": "timestamptz",
"created_by": "uuid",
"modified_at": "timestamptz",
"modified_by": "uuid",
"kind": "text",
"routing_number": "text",
"card_number": "text"
},
"schemas": {
"account": {
"type": "entity",
"properties": {
"kind": {
"type": "string"
}
},
"cases": [
{
"when": {
"properties": {
"kind": {
"const": "checking"
}
}
},
"then": {
"properties": {
"routing_number": {
"type": "string"
}
}
}
},
{
"when": {
"properties": {
"kind": {
"const": "credit"
}
}
},
"then": {
"properties": {
"card_number": {
"type": "string"
}
}
}
}
]
}
},
"lookup_fields": [],
"historical": true,
"notify": true,
"relationship": false
},
{
"name": "invoice",
"schemas": {
@ -3107,6 +3207,103 @@
]
]
}
},
{
"description": "Insert account with checking kind and routing number",
"action": "merge",
"schema_id": "account",
"data": {
"id": "11111111-2222-3333-4444-555555555555",
"type": "account",
"kind": "checking",
"routing_number": "123456789"
},
"expect": {
"success": true,
"sql": [
[
"SELECT to_jsonb(t1.*) || to_jsonb(t2.*)",
"FROM agreego.\"account\" t1",
"LEFT JOIN agreego.\"entity\" t2 ON t2.id = t1.id",
"WHERE t1.id = '11111111-2222-3333-4444-555555555555'"
],
[
"INSERT INTO agreego.\"entity\" (",
" \"created_at\",",
" \"created_by\",",
" \"id\",",
" \"modified_at\",",
" \"modified_by\",",
" \"type\"",
")",
"VALUES (",
" '{{timestamp}}',",
" '00000000-0000-0000-0000-000000000000',",
" '11111111-2222-3333-4444-555555555555',",
" '{{timestamp}}',",
" '00000000-0000-0000-0000-000000000000',",
" 'account'",
")"
],
[
"INSERT INTO agreego.\"account\" (",
" \"id\",",
" \"kind\",",
" \"routing_number\",",
" \"type\"",
")",
"VALUES (",
" '11111111-2222-3333-4444-555555555555',",
" 'checking',",
" '123456789',",
" 'account'",
")"
],
[
"INSERT INTO agreego.change (",
" \"old\",",
" \"new\",",
" entity_id,",
" id,",
" kind,",
" modified_at,",
" modified_by",
")",
"VALUES (",
" NULL,",
" '{",
" \"kind\":\"checking\",",
" \"routing_number\":\"123456789\",",
" \"type\":\"account\"",
" }',",
" '11111111-2222-3333-4444-555555555555',",
" '{{uuid}}',",
" 'create',",
" '{{timestamp}}',",
" '00000000-0000-0000-0000-000000000000'",
")"
],
[
"SELECT pg_notify('entity', '{",
" \"complete\":{",
" \"created_at\":\"{{timestamp}}\",",
" \"created_by\":\"00000000-0000-0000-0000-000000000000\",",
" \"id\":\"11111111-2222-3333-4444-555555555555\",",
" \"kind\":\"checking\",",
" \"modified_at\":\"{{timestamp}}\",",
" \"modified_by\":\"00000000-0000-0000-0000-000000000000\",",
" \"routing_number\":\"123456789\",",
" \"type\":\"account\"",
" },",
" \"new\":{",
" \"kind\":\"checking\",",
" \"routing_number\":\"123456789\",",
" \"type\":\"account\"",
" }",
" }')"
]
]
}
}
]
}

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"
}
}
]
}
}
]
}
]

View File

@ -924,6 +924,91 @@
}
}
},
{
"name": "account",
"hierarchy": [
"account",
"entity"
],
"fields": [
"id",
"type",
"kind",
"archived",
"created_at",
"routing_number",
"card_number"
],
"grouped_fields": {
"entity": [
"id",
"type",
"archived",
"created_at"
],
"account": [
"kind",
"routing_number",
"card_number"
]
},
"field_types": {
"id": "uuid",
"type": "text",
"kind": "text",
"archived": "boolean",
"created_at": "timestamptz",
"routing_number": "text",
"card_number": "text"
},
"variations": [
"account"
],
"schemas": {
"account": {
"type": "entity",
"properties": {
"kind": {
"type": "string"
}
},
"cases": [
{
"when": {
"properties": {
"kind": {
"const": "checking"
}
}
},
"then": {
"properties": {
"routing_number": {
"type": "string"
}
}
}
},
{
"when": {
"properties": {
"kind": {
"const": "credit"
}
}
},
"then": {
"properties": {
"card_number": {
"type": "string"
}
}
}
}
]
}
}
},
{
"name": "invoice",
"schemas": {
@ -2149,6 +2234,30 @@
]
]
}
},
{
"description": "Account select on full schema with cases fields",
"action": "query",
"schema_id": "account",
"expect": {
"success": true,
"sql": [
[
"(SELECT jsonb_strip_nulls((SELECT jsonb_build_object(",
" 'archived', entity_2.archived,",
" 'card_number', account_1.card_number,",
" 'created_at', entity_2.created_at,",
" 'id', entity_2.id,",
" 'kind', account_1.kind,",
" 'routing_number', account_1.routing_number,",
" 'type', entity_2.type",
")",
"FROM agreego.account account_1",
"JOIN agreego.entity entity_2 ON entity_2.id = account_1.id",
"WHERE NOT entity_2.archived)))"
]
]
}
}
]
}

View File

@ -154,7 +154,7 @@ pub struct SchemaObject {
#[serde(skip_serializing_if = "Option::is_none")]
pub extensible: Option<bool>,
#[serde(rename = "compiledProperties")]
#[serde(rename = "compiledPropertyNames")]
#[serde(skip_deserializing)]
#[serde(skip_serializing_if = "crate::database::object::is_once_lock_vec_empty")]
#[serde(serialize_with = "crate::database::object::serialize_once_lock")]

View File

@ -117,13 +117,34 @@ impl Schema {
}
}
// 3. Set the OnceLock!
// 3. Add cases conditionally-defined properties recursively
if let Some(cases) = &self.obj.cases {
for (i, c) in cases.iter().enumerate() {
if let Some(child) = &c.when {
child.compile(db, root_id, format!("{}/cases/{}/when", path, i), errors);
}
if let Some(child) = &c.then {
child.compile(db, root_id, format!("{}/cases/{}/then", path, i), errors);
if let Some(t_props) = child.obj.compiled_properties.get() {
props.extend(t_props.clone());
}
}
if let Some(child) = &c.else_ {
child.compile(db, root_id, format!("{}/cases/{}/else", path, i), errors);
if let Some(e_props) = child.obj.compiled_properties.get() {
props.extend(e_props.clone());
}
}
}
}
// 4. Set the OnceLock!
let _ = self.obj.compiled_properties.set(props.clone());
let mut names: Vec<String> = props.keys().cloned().collect();
names.sort();
let _ = self.obj.compiled_property_names.set(names);
// 4. Compute Edges natively
// 5. Compute Edges natively
let schema_edges = self.compile_edges(db, root_id, &path, &props, errors);
let _ = self.obj.compiled_edges.set(schema_edges);
@ -151,22 +172,12 @@ impl Schema {
}
if let Some(one_of) = &self.obj.one_of {
for (i, child) in one_of.iter().enumerate() {
child.compile(
db,
root_id,
format!("{}/oneOf/{}", path, i),
errors,
);
child.compile(db, root_id, format!("{}/oneOf/{}", path, i), errors);
}
}
if let Some(arr) = &self.obj.prefix_items {
for (i, child) in arr.iter().enumerate() {
child.compile(
db,
root_id,
format!("{}/prefixItems/{}", path, i),
errors,
);
child.compile(db, root_id, format!("{}/prefixItems/{}", path, i), errors);
}
}
if let Some(child) = &self.obj.not {
@ -175,34 +186,6 @@ impl Schema {
if let Some(child) = &self.obj.contains {
child.compile(db, root_id, format!("{}/contains", path), errors);
}
if let Some(cases) = &self.obj.cases {
for (i, c) in cases.iter().enumerate() {
if let Some(child) = &c.when {
child.compile(
db,
root_id,
format!("{}/cases/{}/when", path, i),
errors,
);
}
if let Some(child) = &c.then {
child.compile(
db,
root_id,
format!("{}/cases/{}/then", path, i),
errors,
);
}
if let Some(child) = &c.else_ {
child.compile(
db,
root_id,
format!("{}/cases/{}/else", path, i),
errors,
);
}
}
}
self.compile_polymorphism(db, root_id, &path, errors);
}
@ -285,7 +268,9 @@ 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()
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())
{
@ -296,12 +281,7 @@ impl Schema {
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,
root_id,
format!("{}/{}", path, prop_name),
errors,
);
target_schema.compile(db, root_id, format!("{}/{}", path, prop_name), errors);
if let Some(compiled_target_props) = target_schema.obj.compiled_properties.get() {
let keys_for_ambiguity: Vec<String> =

View File

@ -1457,6 +1457,12 @@ fn test_queryer_0_13() {
crate::tests::runner::run_test_case(&path, 0, 13).unwrap();
}
#[test]
fn test_queryer_0_14() {
let path = format!("{}/fixtures/queryer.json", env!("CARGO_MANIFEST_DIR"));
crate::tests::runner::run_test_case(&path, 0, 14).unwrap();
}
#[test]
fn test_polymorphism_0_0() {
let path = format!("{}/fixtures/polymorphism.json", env!("CARGO_MANIFEST_DIR"));
@ -1559,6 +1565,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"));
@ -8104,3 +8128,9 @@ fn test_merger_0_13() {
let path = format!("{}/fixtures/merger.json", env!("CARGO_MANIFEST_DIR"));
crate::tests::runner::run_test_case(&path, 0, 13).unwrap();
}
#[test]
fn test_merger_0_14() {
let path = format!("{}/fixtures/merger.json", env!("CARGO_MANIFEST_DIR"));
crate::tests::runner::run_test_case(&path, 0, 14).unwrap();
}

View File

@ -95,11 +95,11 @@ fn test_library_api() {
"name": { "type": "string" },
"target": {
"type": "target_schema",
"compiledProperties": ["value"]
"compiledPropertyNames": ["value"]
}
},
"required": ["name"],
"compiledProperties": ["name", "target", "type"],
"compiledPropertyNames": ["name", "target", "type"],
"compiledEdges": {
"target": {
"constraint": "fk_test_target",
@ -112,7 +112,7 @@ fn test_library_api() {
"properties": {
"value": { "type": "number" }
},
"compiledProperties": ["value"]
"compiledPropertyNames": ["value"]
}
}
})

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.118
1.0.121