filters are now entities and auto-generated for all table backed types
This commit is contained in:
@ -651,16 +651,21 @@
|
||||
"action": "compile",
|
||||
"expect": {
|
||||
"success": true,
|
||||
"schemas": [
|
||||
"full.contact",
|
||||
"full.person",
|
||||
"full.person/ad_hoc_bubble",
|
||||
"full.person/extended_relations",
|
||||
"full.person/extended_relations/target",
|
||||
"light.email_address",
|
||||
"some_bubble",
|
||||
"student.person"
|
||||
]
|
||||
"schemas": {
|
||||
"full.contact": {},
|
||||
"full.contact.filter": {},
|
||||
"full.person": {},
|
||||
"full.person.filter": {},
|
||||
"full.person/ad_hoc_bubble": {},
|
||||
"full.person/extended_relations": {},
|
||||
"full.person/extended_relations/target": {},
|
||||
"light.email_address": {},
|
||||
"light.email_address.filter": {},
|
||||
"some_bubble": {},
|
||||
"some_bubble.filter": {},
|
||||
"student.person": {},
|
||||
"student.person.filter": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@ -919,11 +924,14 @@
|
||||
"action": "compile",
|
||||
"expect": {
|
||||
"success": true,
|
||||
"schemas": [
|
||||
"entity",
|
||||
"invoice",
|
||||
"invoice_line"
|
||||
]
|
||||
"schemas": {
|
||||
"entity": {},
|
||||
"entity.filter": {},
|
||||
"invoice": {},
|
||||
"invoice.filter": {},
|
||||
"invoice_line": {},
|
||||
"invoice_line.filter": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
222
fixtures/filter.json
Normal file
222
fixtures/filter.json
Normal 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": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
78
src/database/compile/filters.rs
Normal file
78
src/database/compile/filters.rs
Normal 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)])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,6 @@
|
||||
pub mod collection;
|
||||
pub mod edges;
|
||||
pub mod filters;
|
||||
pub mod polymorphism;
|
||||
|
||||
use crate::database::schema::Schema;
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
pub mod compile;
|
||||
pub mod edge;
|
||||
pub mod r#enum;
|
||||
pub mod executors;
|
||||
@ -8,7 +9,6 @@ pub mod punc;
|
||||
pub mod relation;
|
||||
pub mod schema;
|
||||
pub mod r#type;
|
||||
pub mod compile;
|
||||
|
||||
// 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>) {
|
||||
// Collect existing schemas patched in the databse
|
||||
let mut harvested = Vec::new();
|
||||
for (id, schema_arc) in &self.schemas {
|
||||
crate::database::schema::Schema::collect_schemas(
|
||||
@ -234,6 +235,37 @@ impl Database {
|
||||
.as_ref()
|
||||
.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>) {
|
||||
|
||||
@ -533,6 +533,12 @@ fn test_unique_items_6_1() {
|
||||
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]
|
||||
fn test_min_items_0_0() {
|
||||
let path = format!("{}/fixtures/minItems.json", env!("CARGO_MANIFEST_DIR"));
|
||||
|
||||
@ -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": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"value": { "type": "number" }
|
||||
},
|
||||
"compiledPropertyNames": ["value"]
|
||||
},
|
||||
"target_schema.filter": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"value": { "type": ["number.condition", "null"] }
|
||||
},
|
||||
"compiledPropertyNames": ["value"]
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@ -20,5 +20,5 @@ pub struct Expect {
|
||||
#[serde(default)]
|
||||
pub sql: Option<Vec<SqlExpectation>>,
|
||||
#[serde(default)]
|
||||
pub schemas: Option<Vec<String>>,
|
||||
pub schemas: Option<std::collections::HashMap<String, serde_json::Value>>,
|
||||
}
|
||||
|
||||
@ -3,13 +3,13 @@ use std::sync::Arc;
|
||||
|
||||
impl Expect {
|
||||
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
|
||||
let mut actual: Vec<String> = db.schemas.keys().cloned().collect();
|
||||
actual.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();
|
||||
|
||||
if actual != expected {
|
||||
@ -21,6 +21,23 @@ impl Expect {
|
||||
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(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user