Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 61a8c5eed7 | |||
| 77af67aef5 | |||
| cd85a8a2c3 | |||
| d3cb72a5e2 | |||
| 57baa389b6 |
@ -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\"",
|
||||
" }",
|
||||
" }')"
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@ -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)))"
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@ -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")]
|
||||
|
||||
@ -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> =
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
@ -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"]
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@ -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(),
|
||||
|
||||
@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user