fixed issue with STI on non-tables
This commit is contained in:
@ -111,6 +111,10 @@ Polymorphism is how an object boundary can dynamically take on entirely differen
|
|||||||
* *Setup*: `{ "family": "widget" }` (Where `widget` is a table type but has no external variations).
|
* *Setup*: `{ "family": "widget" }` (Where `widget` is a table type but has no external variations).
|
||||||
* *Execution*: The engine queries `db.types.get("widget").variations` and finds only `["widget"]`. Since it lacks table inheritance, it is treated as STI. The engine scans the specific, confined `schemas` array directly under `db.types.get("widget")` for any registered key terminating in the base `.widget` (e.g., `stock.widget`). The `family` automatically uses `kind` as the discriminator.
|
* *Execution*: The engine queries `db.types.get("widget").variations` and finds only `["widget"]`. Since it lacks table inheritance, it is treated as STI. The engine scans the specific, confined `schemas` array directly under `db.types.get("widget")` for any registered key terminating in the base `.widget` (e.g., `stock.widget`). The `family` automatically uses `kind` as the discriminator.
|
||||||
* *Options*: `stock` -> `stock.widget`, `tasks` -> `tasks.widget`.
|
* *Options*: `stock` -> `stock.widget`, `tasks` -> `tasks.widget`.
|
||||||
|
* **Scenario D: JSONB Bubble Inheritance (Field-Backed)**
|
||||||
|
* *Setup*: `{ "family": "panel" }` (Where `panel` is NOT a table type, but rather an isolated JSONB boundary defined within another table's `schemas`).
|
||||||
|
* *Execution*: The engine observes `panel` is not in `db.types` (because it has no physical table). It falls back to scanning the global `db.schemas` registry for any registered key terminating in the base `.panel` (e.g., `balance.panel`, `units.panel`). The `family` automatically uses `kind` as the discriminator.
|
||||||
|
* *Options*: `balance` -> `balance.panel`, `units` -> `units.panel`.
|
||||||
|
|
||||||
* **`oneOf` (Strict Tagged Unions)**: A hardcoded list of candidate schemas. Unlike `family` which relies on global DB metadata, `oneOf` forces pure mathematical structural evaluation of the provided candidates. It strictly bans typical JSON Schema "Union of Sets" fallback searches. Every candidate MUST possess a mathematically unique discriminator payload to allow $O(1)$ routing.
|
* **`oneOf` (Strict Tagged Unions)**: A hardcoded list of candidate schemas. Unlike `family` which relies on global DB metadata, `oneOf` forces pure mathematical structural evaluation of the provided candidates. It strictly bans typical JSON Schema "Union of Sets" fallback searches. Every candidate MUST possess a mathematically unique discriminator payload to allow $O(1)$ routing.
|
||||||
* **Disjoint Types**: `oneOf: [{ "type": "person" }, { "type": "widget" }]`. The engine succeeds because the native `type` acts as a unique discriminator (`"person"` vs `"widget"`).
|
* **Disjoint Types**: `oneOf: [{ "type": "person" }, { "type": "widget" }]`. The engine succeeds because the native `type` acts as a unique discriminator (`"person"` vs `"widget"`).
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
"description": "Vertical family Routing (Across Tables)",
|
"description": "Vertical family Routing (Scenario A)",
|
||||||
"database": {
|
"database": {
|
||||||
"types": [
|
"types": [
|
||||||
{
|
{
|
||||||
@ -153,7 +153,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": "Matrix family Routing (Vertical + Horizontal Intersections)",
|
"description": "Matrix family Routing (Scenario B)",
|
||||||
"database": {
|
"database": {
|
||||||
"types": [
|
"types": [
|
||||||
{
|
{
|
||||||
@ -284,7 +284,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": "Horizontal family Routing (Virtual Variations)",
|
"description": "Horizontal family Routing (Scenario C)",
|
||||||
"database": {
|
"database": {
|
||||||
"types": [
|
"types": [
|
||||||
{
|
{
|
||||||
@ -776,5 +776,123 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "JSONB Field Bubble family Routing (Scenario D)",
|
||||||
|
"database": {
|
||||||
|
"types": [
|
||||||
|
{
|
||||||
|
"name": "dashboard",
|
||||||
|
"variations": [
|
||||||
|
"dashboard"
|
||||||
|
],
|
||||||
|
"schemas": {
|
||||||
|
"dashboard": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"type": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"panel": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"id",
|
||||||
|
"kind"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"kind": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"balance.panel": {
|
||||||
|
"type": "panel",
|
||||||
|
"properties": {
|
||||||
|
"amount": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"units.panel": {
|
||||||
|
"type": "panel",
|
||||||
|
"properties": {
|
||||||
|
"count": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "family_panel",
|
||||||
|
"schemas": {
|
||||||
|
"family_panel": {
|
||||||
|
"family": "panel"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"tests": [
|
||||||
|
{
|
||||||
|
"description": "Successfully routes to nested balance panel",
|
||||||
|
"schema_id": "family_panel",
|
||||||
|
"data": {
|
||||||
|
"id": "123",
|
||||||
|
"kind": "balance",
|
||||||
|
"amount": 500
|
||||||
|
},
|
||||||
|
"action": "validate",
|
||||||
|
"expect": {
|
||||||
|
"success": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Fails validation on routed schema due to invalid property type",
|
||||||
|
"schema_id": "family_panel",
|
||||||
|
"data": {
|
||||||
|
"id": "123",
|
||||||
|
"kind": "balance",
|
||||||
|
"amount": "not_an_int"
|
||||||
|
},
|
||||||
|
"action": "validate",
|
||||||
|
"expect": {
|
||||||
|
"success": false,
|
||||||
|
"errors": [
|
||||||
|
{
|
||||||
|
"code": "INVALID_TYPE",
|
||||||
|
"details": {
|
||||||
|
"path": "amount"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Fails when discriminator does not match any bubble schema",
|
||||||
|
"schema_id": "family_panel",
|
||||||
|
"data": {
|
||||||
|
"id": "123",
|
||||||
|
"kind": "unknown_panel"
|
||||||
|
},
|
||||||
|
"action": "validate",
|
||||||
|
"expect": {
|
||||||
|
"success": false,
|
||||||
|
"errors": [
|
||||||
|
{
|
||||||
|
"code": "NO_FAMILY_MATCH",
|
||||||
|
"details": {
|
||||||
|
"path": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@ -9,7 +9,7 @@ impl Schema {
|
|||||||
errors: &mut Vec<crate::drop::Error>,
|
errors: &mut Vec<crate::drop::Error>,
|
||||||
) {
|
) {
|
||||||
let mut options = std::collections::BTreeMap::new();
|
let mut options = std::collections::BTreeMap::new();
|
||||||
let mut strategy = String::new();
|
let strategy: &str;
|
||||||
|
|
||||||
if let Some(family) = &self.obj.family {
|
if let Some(family) = &self.obj.family {
|
||||||
// Formalize the <Variant>.<Base> topology
|
// Formalize the <Variant>.<Base> topology
|
||||||
@ -24,7 +24,7 @@ impl Schema {
|
|||||||
if let Some(type_def) = db.types.get(&family_base) {
|
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) {
|
if type_def.variations.len() > 1 && type_def.variations.iter().any(|v| v != &family_base) {
|
||||||
// Scenario A / B: Table Variations
|
// Scenario A / B: Table Variations
|
||||||
strategy = "type".to_string();
|
strategy = "type";
|
||||||
for var in &type_def.variations {
|
for var in &type_def.variations {
|
||||||
let target_id = if family_prefix.is_empty() {
|
let target_id = if family_prefix.is_empty() {
|
||||||
var.to_string()
|
var.to_string()
|
||||||
@ -38,7 +38,7 @@ impl Schema {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Scenario C: Single Table Inheritance (Horizontal)
|
// Scenario C: Single Table Inheritance (Horizontal)
|
||||||
strategy = "kind".to_string();
|
strategy = "kind";
|
||||||
|
|
||||||
let suffix = format!(".{}", family_base);
|
let suffix = format!(".{}", family_base);
|
||||||
|
|
||||||
@ -50,6 +50,19 @@ impl Schema {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// Scenario D: Field-Backed JSONB Bubble STI (No explicit table representation)
|
||||||
|
strategy = "kind";
|
||||||
|
let suffix = format!(".{}", family_base);
|
||||||
|
|
||||||
|
// Scan the entire database schemas registry for matching suffixes
|
||||||
|
for (id, schema) in &db.schemas {
|
||||||
|
if id.ends_with(&suffix) || id == &family_base {
|
||||||
|
if let Some(kind_val) = schema.obj.get_discriminator_value("kind", id) {
|
||||||
|
options.insert(kind_val, (None, Some(id.to_string())));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if let Some(one_of) = &self.obj.one_of {
|
} else if let Some(one_of) = &self.obj.one_of {
|
||||||
let mut type_vals = std::collections::HashSet::new();
|
let mut type_vals = std::collections::HashSet::new();
|
||||||
@ -84,7 +97,7 @@ impl Schema {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if disjoint_base && structural_types.len() == one_of.len() {
|
if disjoint_base && structural_types.len() == one_of.len() {
|
||||||
strategy = "".to_string();
|
strategy = "";
|
||||||
for (i, c) in one_of.iter().enumerate() {
|
for (i, c) in one_of.iter().enumerate() {
|
||||||
if let Some(crate::database::object::SchemaTypeOrArray::Single(t)) = &c.obj.type_ {
|
if let Some(crate::database::object::SchemaTypeOrArray::Single(t)) = &c.obj.type_ {
|
||||||
if crate::database::object::is_primitive_type(t) {
|
if crate::database::object::is_primitive_type(t) {
|
||||||
@ -96,11 +109,11 @@ impl Schema {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
strategy = if type_vals.len() > 1 && type_vals.len() == one_of.len() {
|
strategy = if type_vals.len() > 1 && type_vals.len() == one_of.len() {
|
||||||
"type".to_string()
|
"type"
|
||||||
} else if kind_vals.len() > 1 && kind_vals.len() == one_of.len() {
|
} else if kind_vals.len() > 1 && kind_vals.len() == one_of.len() {
|
||||||
"kind".to_string()
|
"kind"
|
||||||
} else {
|
} else {
|
||||||
"".to_string()
|
""
|
||||||
};
|
};
|
||||||
|
|
||||||
if strategy.is_empty() {
|
if strategy.is_empty() {
|
||||||
@ -148,7 +161,7 @@ impl Schema {
|
|||||||
|
|
||||||
if !options.is_empty() {
|
if !options.is_empty() {
|
||||||
if !strategy.is_empty() {
|
if !strategy.is_empty() {
|
||||||
let _ = self.obj.compiled_discriminator.set(strategy);
|
let _ = self.obj.compiled_discriminator.set(strategy.to_string());
|
||||||
}
|
}
|
||||||
let _ = self.obj.compiled_options.set(options);
|
let _ = self.obj.compiled_options.set(options);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1619,6 +1619,24 @@ fn test_polymorphism_5_2() {
|
|||||||
crate::tests::runner::run_test_case(&path, 5, 2).unwrap();
|
crate::tests::runner::run_test_case(&path, 5, 2).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_polymorphism_6_0() {
|
||||||
|
let path = format!("{}/fixtures/polymorphism.json", env!("CARGO_MANIFEST_DIR"));
|
||||||
|
crate::tests::runner::run_test_case(&path, 6, 0).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_polymorphism_6_1() {
|
||||||
|
let path = format!("{}/fixtures/polymorphism.json", env!("CARGO_MANIFEST_DIR"));
|
||||||
|
crate::tests::runner::run_test_case(&path, 6, 1).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_polymorphism_6_2() {
|
||||||
|
let path = format!("{}/fixtures/polymorphism.json", env!("CARGO_MANIFEST_DIR"));
|
||||||
|
crate::tests::runner::run_test_case(&path, 6, 2).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_not_0_0() {
|
fn test_not_0_0() {
|
||||||
let path = format!("{}/fixtures/not.json", env!("CARGO_MANIFEST_DIR"));
|
let path = format!("{}/fixtures/not.json", env!("CARGO_MANIFEST_DIR"));
|
||||||
|
|||||||
27
test_out.txt
27
test_out.txt
File diff suppressed because one or more lines are too long
109
test_output.txt
109
test_output.txt
@ -1,109 +0,0 @@
|
|||||||
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.43s
|
|
||||||
Running unittests src/lib.rs (target/debug/deps/jspg-d3f18ff3a7e2b386)
|
|
||||||
|
|
||||||
running 1 test
|
|
||||||
test tests::test_filter_0_0 ... FAILED
|
|
||||||
|
|
||||||
failures:
|
|
||||||
|
|
||||||
---- tests::test_filter_0_0 stdout ----
|
|
||||||
TEST COMPILE ERROR FOR 'Assert filter generation map accurately represents strongly typed conditions natively.': Detailed Schema Match Failure for 'gender.condition'!
|
|
||||||
|
|
||||||
Expected:
|
|
||||||
{
|
|
||||||
"compiledPropertyNames": [
|
|
||||||
"$eq",
|
|
||||||
"$ne",
|
|
||||||
"$nof",
|
|
||||||
"$of"
|
|
||||||
],
|
|
||||||
"properties": {
|
|
||||||
"$eq": {
|
|
||||||
"type": [
|
|
||||||
"gender",
|
|
||||||
"null"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"$ne": {
|
|
||||||
"type": [
|
|
||||||
"gender",
|
|
||||||
"null"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"$nof": {
|
|
||||||
"items": {
|
|
||||||
"type": "gender"
|
|
||||||
},
|
|
||||||
"type": [
|
|
||||||
"array",
|
|
||||||
"null"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"$of": {
|
|
||||||
"items": {
|
|
||||||
"type": "gender"
|
|
||||||
},
|
|
||||||
"type": [
|
|
||||||
"array",
|
|
||||||
"null"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"type": "object"
|
|
||||||
}
|
|
||||||
|
|
||||||
Actual:
|
|
||||||
{
|
|
||||||
"compiledPropertyNames": [
|
|
||||||
"$eq",
|
|
||||||
"$ne",
|
|
||||||
"$nof",
|
|
||||||
"$of",
|
|
||||||
"kind"
|
|
||||||
],
|
|
||||||
"properties": {
|
|
||||||
"$eq": {
|
|
||||||
"type": [
|
|
||||||
"gender",
|
|
||||||
"null"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"$ne": {
|
|
||||||
"type": [
|
|
||||||
"gender",
|
|
||||||
"null"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"$nof": {
|
|
||||||
"items": {
|
|
||||||
"type": "gender"
|
|
||||||
},
|
|
||||||
"type": [
|
|
||||||
"array",
|
|
||||||
"null"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"$of": {
|
|
||||||
"items": {
|
|
||||||
"type": "gender"
|
|
||||||
},
|
|
||||||
"type": [
|
|
||||||
"array",
|
|
||||||
"null"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"type": "condition"
|
|
||||||
}
|
|
||||||
|
|
||||||
thread 'tests::test_filter_0_0' (118346550) panicked at src/tests/fixtures.rs:539:54:
|
|
||||||
called `Result::unwrap()` on an `Err` value: "[Filter Synthesis Object-Oriented Composition] Compile Test 'Assert filter generation map accurately represents strongly typed conditions natively.' failed. Error: Detailed Schema Match Failure for 'gender.condition'!\n\nExpected:\n{\n \"compiledPropertyNames\": [\n \"$eq\",\n \"$ne\",\n \"$nof\",\n \"$of\"\n ],\n \"properties\": {\n \"$eq\": {\n \"type\": [\n \"gender\",\n \"null\"\n ]\n },\n \"$ne\": {\n \"type\": [\n \"gender\",\n \"null\"\n ]\n },\n \"$nof\": {\n \"items\": {\n \"type\": \"gender\"\n },\n \"type\": [\n \"array\",\n \"null\"\n ]\n },\n \"$of\": {\n \"items\": {\n \"type\": \"gender\"\n },\n \"type\": [\n \"array\",\n \"null\"\n ]\n }\n },\n \"type\": \"object\"\n}\n\nActual:\n{\n \"compiledPropertyNames\": [\n \"$eq\",\n \"$ne\",\n \"$nof\",\n \"$of\",\n \"kind\"\n ],\n \"properties\": {\n \"$eq\": {\n \"type\": [\n \"gender\",\n \"null\"\n ]\n },\n \"$ne\": {\n \"type\": [\n \"gender\",\n \"null\"\n ]\n },\n \"$nof\": {\n \"items\": {\n \"type\": \"gender\"\n },\n \"type\": [\n \"array\",\n \"null\"\n ]\n },\n \"$of\": {\n \"items\": {\n \"type\": \"gender\"\n },\n \"type\": [\n \"array\",\n \"null\"\n ]\n }\n },\n \"type\": \"condition\"\n}"
|
|
||||||
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
|
|
||||||
|
|
||||||
|
|
||||||
failures:
|
|
||||||
tests::test_filter_0_0
|
|
||||||
|
|
||||||
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 1362 filtered out; finished in 0.00s
|
|
||||||
|
|
||||||
error: test failed, to rerun pass `--lib`
|
|
||||||
Reference in New Issue
Block a user