filter synthesis: compile named non-table value types structurally

A property typed as a named value type (a schema-only config object like
an operating-hours schedule) previously got a dangling {type}.filter
reference — no filter is ever synthesized for a non-table-backed schema,
so the whole parent filter failed downstream (PROXY_TYPE_RESOLUTION_FAILED;
the punc generator emitted an empty filter type).

Naming a value type is a reuse choice, not a semantics choice: it now
compiles structurally into the parent filter, exactly like an inline
object, recursively (including array items). Table-backed boundaries keep
the lazy {type}.filter reference. A named type with no compilable
structure is omitted instead of dangling.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-03 19:22:57 -04:00
parent 581fc8e0c0
commit 0d282cc930
3 changed files with 116 additions and 5 deletions

View File

@ -87,6 +87,34 @@
"type": "string"
}
}
},
"schedule": {
"type": [
"opening_hours",
"null"
]
}
}
},
"season": {
"type": "object",
"properties": {
"label": {
"type": "string"
}
}
},
"opening_hours": {
"type": "object",
"properties": {
"open": {
"type": "string"
},
"seasons": {
"type": "array",
"items": {
"type": "season"
}
}
}
}
@ -262,6 +290,7 @@
"uuid_field",
"tags",
"ad_hoc",
"schedule",
"$and",
"$or"
],
@ -277,6 +306,7 @@
"uuid_field",
"tags",
"ad_hoc",
"schedule",
"$and",
"$or"
],
@ -298,6 +328,7 @@
"uuid_field",
"tags",
"ad_hoc",
"schedule",
"$and",
"$or"
],
@ -366,6 +397,41 @@
"string.condition",
"null"
]
},
"schedule": {
"type": [
"filter",
"null"
],
"compiledPropertyNames": [
"open",
"seasons"
],
"properties": {
"open": {
"type": [
"string.condition",
"null"
]
},
"seasons": {
"type": [
"filter",
"null"
],
"compiledPropertyNames": [
"label"
],
"properties": {
"label": {
"type": [
"string.condition",
"null"
]
}
}
}
}
}
},
"type": "filter"
@ -483,7 +549,9 @@
]
}
}
}
},
"opening_hours": {},
"season": {}
}
}
}

2
flows

Submodule flows updated: 89748a246e...0d9bd8644e

View File

@ -26,10 +26,14 @@ impl Schema {
if let Some(items) = &child.obj.items {
if !items.is_proxy() {
structural_filter = items.compile_filter(_db, "", _errors);
} else if let Some(target) = Self::value_type_target(items, _db) {
structural_filter = target.compile_filter(_db, "", _errors);
}
}
} else if !child.is_proxy() {
structural_filter = child.compile_filter(_db, "", _errors);
} else if let Some(target) = Self::value_type_target(child, _db) {
structural_filter = target.compile_filter(_db, "", _errors);
}
if let Some(mut inline_schema) = structural_filter {
@ -117,6 +121,37 @@ impl Schema {
None
}
/// Resolves a pure type-pointer schema to a named non-table value type's own schema —
/// a reusable, schema-only object (e.g. an operating-hours config). Naming a value type
/// is a reuse choice, not a semantics choice: it filters structurally, exactly like an
/// inline object. Table-backed boundaries keep the lazy {type}.filter reference instead.
fn value_type_target<'a>(schema: &Arc<Schema>, db: &'a Database) -> Option<&'a Arc<Schema>> {
let t = match &schema.obj.type_ {
Some(SchemaTypeOrArray::Single(t)) => Some(t.as_str()),
Some(SchemaTypeOrArray::Multiple(types)) => {
types.iter().find(|t| *t != "null").map(|s| s.as_str())
}
None => None,
}?;
if matches!(
t,
"string" | "integer" | "number" | "boolean" | "object" | "array" | "null"
) {
return None;
}
if t.ends_with(".condition") || t.ends_with(".filter") {
return None;
}
if db.enums.contains_key(t) {
return None;
}
let base = t.split('.').next_back().unwrap_or(t);
if db.types.contains_key(base) {
return None;
}
db.schemas.get(t)
}
fn resolve_filter_type(schema: &Arc<Schema>, db: &Database) -> Option<Vec<String>> {
if let Some(type_) = &schema.obj.type_ {
match type_ {
@ -165,8 +200,16 @@ impl Schema {
} else if db.enums.contains_key(custom) {
Some(vec![format!("{}.condition", custom)])
} else {
// Assume anything else is a Relational cross-boundary that already has its own .filter dynamically built
// A Relational cross-boundary gets a reference to the target's dynamically built
// .filter — but only a table-backed boundary has one. A named non-table value type
// compiles structurally upstream (value_type_target); reaching here means it had
// no compilable structure — omit it rather than emit a dangling .filter reference.
let base = custom.split('.').next_back().unwrap_or(custom);
if db.types.contains_key(base) {
Some(vec![format!("{}.filter", custom)])
} else {
None
}
}
}
}