filters are now entities and auto-generated for all table backed types

This commit is contained in:
2026-04-17 01:46:02 -04:00
parent 8175b10a97
commit 87a845e85a
9 changed files with 399 additions and 19 deletions

View File

@ -651,16 +651,21 @@
"action": "compile", "action": "compile",
"expect": { "expect": {
"success": true, "success": true,
"schemas": [ "schemas": {
"full.contact", "full.contact": {},
"full.person", "full.contact.filter": {},
"full.person/ad_hoc_bubble", "full.person": {},
"full.person/extended_relations", "full.person.filter": {},
"full.person/extended_relations/target", "full.person/ad_hoc_bubble": {},
"light.email_address", "full.person/extended_relations": {},
"some_bubble", "full.person/extended_relations/target": {},
"student.person" "light.email_address": {},
] "light.email_address.filter": {},
"some_bubble": {},
"some_bubble.filter": {},
"student.person": {},
"student.person.filter": {}
}
} }
} }
] ]
@ -919,11 +924,14 @@
"action": "compile", "action": "compile",
"expect": { "expect": {
"success": true, "success": true,
"schemas": [ "schemas": {
"entity", "entity": {},
"invoice", "entity.filter": {},
"invoice_line" "invoice": {},
] "invoice.filter": {},
"invoice_line": {},
"invoice_line.filter": {}
}
} }
} }
] ]

222
fixtures/filter.json Normal file
View File

@ -0,0 +1,222 @@
[
{
"description": "Filter Synthesis Object-Oriented Composition",
"database": {
"puncs": [],
"enums": [],
"relations": [
{
"id": "rel1",
"type": "relation",
"constraint": "fk_person_billing_address",
"source_type": "person",
"source_columns": [
"billing_address_id"
],
"destination_type": "address",
"destination_columns": [
"id"
],
"prefix": "billing_address"
}
],
"types": [
{
"id": "type1",
"type": "type",
"name": "person",
"module": "core",
"source": "person",
"hierarchy": [
"person"
],
"variations": [
"person"
],
"schemas": {
"person": {
"type": "object",
"properties": {
"first_name": {
"type": "string"
},
"age": {
"type": "integer"
},
"billing_address": {
"type": "address"
},
"birth_date": {
"type": "string",
"format": "date-time"
},
"tags": {
"type": "array",
"items": {
"type": "string"
}
},
"ad_hoc": {
"type": "object",
"properties": {
"foo": {
"type": "string"
}
}
}
}
}
}
},
{
"id": "type2",
"type": "type",
"name": "address",
"module": "core",
"source": "address",
"hierarchy": [
"address"
],
"variations": [
"address"
],
"schemas": {
"address": {
"type": "object",
"properties": {
"city": {
"type": "string"
}
}
}
}
},
{
"id": "type3",
"type": "type",
"name": "filter",
"module": "core",
"source": "filter",
"hierarchy": [
"filter"
],
"variations": [
"filter",
"string.condition",
"integer.condition",
"date.condition"
],
"schemas": {
"condition": {
"type": "object",
"properties": {
"kind": {
"type": "string"
}
}
},
"string.condition": {
"type": "condition",
"properties": {
"$eq": {
"type": [
"string",
"null"
]
}
}
},
"integer.condition": {
"type": "condition",
"properties": {
"$eq": {
"type": [
"integer",
"null"
]
}
}
},
"date.condition": {
"type": "condition",
"properties": {
"$eq": {
"type": [
"string",
"null"
]
}
}
}
}
}
]
},
"tests": [
{
"description": "Assert filter generation map accurately represents strongly typed conditions natively.",
"action": "compile",
"expect": {
"success": true,
"schemas": {
"person": {},
"person.filter": {
"type": "object",
"compiledPropertyNames": [
"age",
"billing_address",
"birth_date",
"first_name"
],
"properties": {
"first_name": {
"type": [
"string.condition",
"null"
]
},
"age": {
"type": [
"integer.condition",
"null"
]
},
"billing_address": {
"type": [
"address.filter",
"null"
]
},
"birth_date": {
"type": [
"date.condition",
"null"
]
}
}
},
"address": {},
"address.filter": {
"type": "object",
"compiledPropertyNames": [
"city"
],
"properties": {
"city": {
"type": [
"string.condition",
"null"
]
}
}
},
"condition": {},
"string.condition": {},
"integer.condition": {},
"date.condition": {}
}
}
}
]
}
]

View File

@ -0,0 +1,78 @@
use crate::database::object::{SchemaObject, SchemaTypeOrArray};
use crate::database::schema::Schema;
use crate::database::Database;
use std::collections::BTreeMap;
use std::sync::Arc;
impl Schema {
pub fn compile_filter(
&self,
_db: &Database,
_root_id: &str,
_errors: &mut Vec<crate::drop::Error>,
) -> Option<Schema> {
if let Some(props) = self.obj.compiled_properties.get() {
let mut filter_props = BTreeMap::new();
for (key, child) in props {
if let Some(mut filter_type) = Self::resolve_filter_type(child) {
filter_type.push("null".to_string());
let mut child_obj = SchemaObject::default();
child_obj.type_ = Some(SchemaTypeOrArray::Multiple(filter_type));
filter_props.insert(key.clone(), Arc::new(Schema { obj: child_obj, always_fail: false }));
}
}
if !filter_props.is_empty() {
let mut wrapper_obj = SchemaObject::default();
wrapper_obj.type_ = Some(SchemaTypeOrArray::Single("object".to_string()));
wrapper_obj.properties = Some(filter_props);
return Some(Schema { obj: wrapper_obj, always_fail: false });
}
}
None
}
fn resolve_filter_type(schema: &Arc<Schema>) -> Option<Vec<String>> {
if let Some(type_) = &schema.obj.type_ {
match type_ {
SchemaTypeOrArray::Single(t) => {
return Self::map_filter_string(t, schema);
}
SchemaTypeOrArray::Multiple(types) => {
for t in types {
if t != "null" {
return Self::map_filter_string(t, schema);
}
}
}
}
}
None
}
fn map_filter_string(t: &str, schema: &Arc<Schema>) -> Option<Vec<String>> {
match t {
"string" => {
if let Some(fmt) = &schema.obj.format {
if fmt == "date-time" {
return Some(vec!["date.condition".to_string()]);
}
}
Some(vec!["string.condition".to_string()])
}
"integer" => Some(vec!["integer.condition".to_string()]),
"number" => Some(vec!["number.condition".to_string()]),
"boolean" => Some(vec!["boolean.condition".to_string()]),
"object" => None, // Inline structures are ignored in Composed References
"array" => None, // We don't filter primitive arrays or map complex arrays yet
"null" => None,
custom => {
// Assume anything else is a Relational cross-boundary that already has its own .filter dynamically built
Some(vec![format!("{}.filter", custom)])
}
}
}
}

View File

@ -1,5 +1,6 @@
pub mod collection; pub mod collection;
pub mod edges; pub mod edges;
pub mod filters;
pub mod polymorphism; pub mod polymorphism;
use crate::database::schema::Schema; use crate::database::schema::Schema;

View File

@ -1,3 +1,4 @@
pub mod compile;
pub mod edge; pub mod edge;
pub mod r#enum; pub mod r#enum;
pub mod executors; pub mod executors;
@ -8,7 +9,6 @@ pub mod punc;
pub mod relation; pub mod relation;
pub mod schema; pub mod schema;
pub mod r#type; pub mod r#type;
pub mod compile;
// External mock exports inside the executor sub-folder // External mock exports inside the executor sub-folder
@ -210,6 +210,7 @@ impl Database {
} }
pub fn compile(&mut self, errors: &mut Vec<crate::drop::Error>) { pub fn compile(&mut self, errors: &mut Vec<crate::drop::Error>) {
// Collect existing schemas patched in the databse
let mut harvested = Vec::new(); let mut harvested = Vec::new();
for (id, schema_arc) in &self.schemas { for (id, schema_arc) in &self.schemas {
crate::database::schema::Schema::collect_schemas( crate::database::schema::Schema::collect_schemas(
@ -234,6 +235,37 @@ impl Database {
.as_ref() .as_ref()
.compile(self, root_id, id.clone(), errors); .compile(self, root_id, id.clone(), errors);
} }
// Phase 2: Synthesize Composed Filter References
let mut filter_schemas = Vec::new();
for type_def in self.types.values() {
for (id, schema_arc) in &type_def.schemas {
// Only run synthesis on actual structured, table-backed boundaries. Exclude subschemas!
let base_name = id.split('.').last().unwrap_or(id);
let is_table_backed = base_name == type_def.name;
if is_table_backed && !id.contains('/') {
if let Some(filter_schema) = schema_arc.compile_filter(self, id, errors) {
filter_schemas.push((format!("{}.filter", id), Arc::new(filter_schema)));
}
}
}
}
let mut filter_ids = Vec::new();
for (id, filter_arc) in filter_schemas {
filter_ids.push(id.clone());
self.schemas.insert(id, filter_arc);
}
// Now actively compile the newly injected filters to lock all nested compose references natively
for id in filter_ids {
if let Some(filter_arc) = self.schemas.get(&id).cloned() {
let root_id = id.split('/').next().unwrap_or(&id);
filter_arc
.as_ref()
.compile(self, root_id, id.clone(), errors);
}
}
} }
fn collect_schemas(&mut self, errors: &mut Vec<crate::drop::Error>) { fn collect_schemas(&mut self, errors: &mut Vec<crate::drop::Error>) {

View File

@ -533,6 +533,12 @@ fn test_unique_items_6_1() {
crate::tests::runner::run_test_case(&path, 6, 1).unwrap(); crate::tests::runner::run_test_case(&path, 6, 1).unwrap();
} }
#[test]
fn test_filter_0_0() {
let path = format!("{}/fixtures/filter.json", env!("CARGO_MANIFEST_DIR"));
crate::tests::runner::run_test_case(&path, 0, 0).unwrap();
}
#[test] #[test]
fn test_min_items_0_0() { fn test_min_items_0_0() {
let path = format!("{}/fixtures/minItems.json", env!("CARGO_MANIFEST_DIR")); let path = format!("{}/fixtures/minItems.json", env!("CARGO_MANIFEST_DIR"));

View File

@ -107,12 +107,28 @@ fn test_library_api() {
} }
} }
}, },
"source_schema.filter": {
"type": "object",
"properties": {
"type": { "type": ["string.condition", "null"] },
"name": { "type": ["string.condition", "null"] },
"target": { "type": ["target_schema.filter", "null"] }
},
"compiledPropertyNames": ["name", "target", "type"]
},
"target_schema": { "target_schema": {
"type": "object", "type": "object",
"properties": { "properties": {
"value": { "type": "number" } "value": { "type": "number" }
}, },
"compiledPropertyNames": ["value"] "compiledPropertyNames": ["value"]
},
"target_schema.filter": {
"type": "object",
"properties": {
"value": { "type": ["number.condition", "null"] }
},
"compiledPropertyNames": ["value"]
} }
} }
}) })

View File

@ -20,5 +20,5 @@ pub struct Expect {
#[serde(default)] #[serde(default)]
pub sql: Option<Vec<SqlExpectation>>, pub sql: Option<Vec<SqlExpectation>>,
#[serde(default)] #[serde(default)]
pub schemas: Option<Vec<String>>, pub schemas: Option<std::collections::HashMap<String, serde_json::Value>>,
} }

View File

@ -3,13 +3,13 @@ use std::sync::Arc;
impl Expect { impl Expect {
pub fn assert_schemas(&self, db: &Arc<crate::database::Database>) -> Result<(), String> { pub fn assert_schemas(&self, db: &Arc<crate::database::Database>) -> Result<(), String> {
if let Some(expected_schemas) = &self.schemas { if let Some(expected_map) = &self.schemas {
// Collect actual schemas and sort // Collect actual schemas and sort
let mut actual: Vec<String> = db.schemas.keys().cloned().collect(); let mut actual: Vec<String> = db.schemas.keys().cloned().collect();
actual.sort(); actual.sort();
// Collect expected schemas and sort // Collect expected schemas and sort
let mut expected: Vec<String> = expected_schemas.clone(); let mut expected: Vec<String> = expected_map.keys().cloned().collect();
expected.sort(); expected.sort();
if actual != expected { if actual != expected {
@ -21,6 +21,23 @@ impl Expect {
actual actual
)); ));
} }
for (key, expected_val) in expected_map {
if expected_val.is_object() && expected_val.as_object().unwrap().is_empty() {
continue; // A `{}` means we just wanted to test it was collected/promoted, skip deep match
}
let actual_ast = db.schemas.get(key).unwrap();
let actual_val = serde_json::to_value(actual_ast).unwrap();
if actual_val != *expected_val {
return Err(format!(
"Detailed Schema Match Failure for '{}'!\n\nExpected:\n{}\n\nActual:\n{}",
key,
serde_json::to_string_pretty(expected_val).unwrap(),
serde_json::to_string_pretty(&actual_val).unwrap()
));
}
}
} }
Ok(()) Ok(())
} }