filters are now entities and auto-generated for all table backed types
This commit is contained in:
@ -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
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 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;
|
||||||
|
|||||||
@ -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>) {
|
||||||
|
|||||||
@ -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"));
|
||||||
|
|||||||
@ -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"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@ -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>>,
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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(())
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user