Skip to main content

type_bridge_query/
lib.rs

1//! Schema-aware validation for typed query foundations.
2
3#![deny(missing_docs)]
4
5mod engine;
6mod query_v2_claims;
7mod query_validation;
8mod safety_condition;
9
10pub use query_validation::{ValidatedQuery, validate_query_local_function, validate_query_plan};
11
12pub use safety_condition::{lower_condition_to_plan, safety_condition_to_assertion_plan};
13
14use std::collections::{BTreeMap, BTreeSet};
15
16use type_bridge_contract::diagnostic::{Diagnostic, DiagnosticCategory, DiagnosticCode};
17use type_bridge_contract::id::TypeId;
18use type_bridge_contract::limits::StructuralLimits;
19use type_bridge_contract::migration_assertion::{
20    AssertionPattern, BindingId, MigrationAssertionPlan, QueryVariable, ValueOperand,
21};
22use type_bridge_contract::query_plan::{QueryOperand, QueryPattern};
23use type_bridge_contract::schema_delta::ManagedSchemaState;
24use type_bridge_contract::value::ValueTypeTag;
25use type_bridge_schema::ResolvedSchema;
26
27/// Trusted schema inputs which bind a plan to one exact managed selection.
28#[derive(Clone, Copy, Debug)]
29pub struct MigrationAssertionValidationContext<'a> {
30    resolved_schema: &'a ResolvedSchema,
31    managed_state: &'a ManagedSchemaState,
32}
33
34impl<'a> MigrationAssertionValidationContext<'a> {
35    /// Bind the resolved schema view to its trusted managed state.
36    pub const fn new(
37        resolved_schema: &'a ResolvedSchema,
38        managed_state: &'a ManagedSchemaState,
39    ) -> Self {
40        Self {
41            resolved_schema,
42            managed_state,
43        }
44    }
45
46    /// Return the resolved schema used for schema-derived domains.
47    pub const fn resolved_schema(&self) -> &'a ResolvedSchema {
48        self.resolved_schema
49    }
50
51    /// Return the exact selected managed state.
52    pub const fn managed_state(&self) -> &'a ManagedSchemaState {
53        self.managed_state
54    }
55}
56
57/// Schema-derived possible runtime types and optional scalar value domain.
58#[derive(Clone, Debug, Eq, PartialEq)]
59pub struct BindingDomain {
60    type_ids: BTreeSet<TypeId>,
61    value_type: Option<ValueTypeTag>,
62}
63
64impl BindingDomain {
65    pub(crate) const fn new(type_ids: BTreeSet<TypeId>, value_type: Option<ValueTypeTag>) -> Self {
66        Self {
67            type_ids,
68            value_type,
69        }
70    }
71
72    /// Return possible concrete runtime types.
73    pub const fn type_ids(&self) -> &BTreeSet<TypeId> {
74        &self.type_ids
75    }
76
77    /// Return the uniform scalar domain for attribute bindings.
78    pub const fn value_type(&self) -> Option<ValueTypeTag> {
79        self.value_type
80    }
81}
82
83/// One validator-derived output column.
84#[derive(Clone, Debug, Eq, PartialEq)]
85pub struct RowColumn {
86    binding: BindingId,
87    domain: BindingDomain,
88    optional: bool,
89    variable: QueryVariable,
90}
91
92impl RowColumn {
93    pub(crate) const fn new(
94        binding: BindingId,
95        domain: BindingDomain,
96        variable: QueryVariable,
97        optional: bool,
98    ) -> Self {
99        Self {
100            binding,
101            domain,
102            optional,
103            variable,
104        }
105    }
106
107    /// Return the output binding.
108    pub const fn binding(&self) -> BindingId {
109        self.binding
110    }
111
112    /// Whether rows may carry an explicit absence in this column.
113    pub const fn optional(&self) -> bool {
114        self.optional
115    }
116
117    /// Return the canonical query variable.
118    pub const fn variable(&self) -> &QueryVariable {
119        &self.variable
120    }
121
122    /// Return the schema-derived binding domain.
123    pub const fn domain(&self) -> &BindingDomain {
124        &self.domain
125    }
126}
127
128/// Ordered output row schema derived solely by validation.
129#[derive(Clone, Debug, Eq, PartialEq)]
130pub struct RowSchema {
131    columns: Vec<RowColumn>,
132}
133
134impl RowSchema {
135    pub(crate) const fn new(columns: Vec<RowColumn>) -> Self {
136        Self { columns }
137    }
138
139    /// Return ordered output columns.
140    pub fn columns(&self) -> &[RowColumn] {
141        &self.columns
142    }
143}
144
145/// The typed shape of one fetched document field.
146#[derive(Clone, Debug, Eq, PartialEq)]
147pub enum DocumentColumnShape {
148    /// One scalar value; optional sources fetch as explicit absence.
149    Scalar {
150        /// The exact scalar type.
151        value_type: ValueTypeTag,
152        /// Whether documents may carry an explicit absence here.
153        optional: bool,
154    },
155    /// A typed list of every owned value of one attribute.
156    List {
157        /// The listed attribute.
158        attribute: type_bridge_contract::id::AttributeId,
159        /// The exact scalar type of every list element.
160        element_type: ValueTypeTag,
161    },
162}
163
164/// One typed fetched-document column derived solely by validation.
165#[derive(Clone, Debug, Eq, PartialEq)]
166pub struct DocumentColumn {
167    key: QueryVariable,
168    shape: DocumentColumnShape,
169}
170
171impl DocumentColumn {
172    pub(crate) const fn new(key: QueryVariable, shape: DocumentColumnShape) -> Self {
173        Self { key, shape }
174    }
175
176    /// Return the document key.
177    pub const fn key(&self) -> &QueryVariable {
178        &self.key
179    }
180
181    /// Return the typed field shape.
182    pub const fn shape(&self) -> &DocumentColumnShape {
183        &self.shape
184    }
185}
186
187/// Ordered fetched-document schema derived solely by validation.
188#[derive(Clone, Debug, Eq, PartialEq)]
189pub struct DocumentSchema {
190    columns: Vec<DocumentColumn>,
191}
192
193impl DocumentSchema {
194    pub(crate) const fn new(columns: Vec<DocumentColumn>) -> Self {
195        Self { columns }
196    }
197
198    /// Return ordered document columns.
199    pub fn columns(&self) -> &[DocumentColumn] {
200        &self.columns
201    }
202}
203
204/// The validator-derived output shape of one query plan.
205#[derive(Clone, Debug, Eq, PartialEq)]
206pub enum OutputSchema {
207    /// Projected typed row columns.
208    Rows(RowSchema),
209    /// Fetched flat typed documents.
210    Documents(DocumentSchema),
211}
212
213impl OutputSchema {
214    /// Return the row schema of a row-output plan.
215    pub const fn rows(&self) -> Option<&RowSchema> {
216        match self {
217            Self::Rows(schema) => Some(schema),
218            Self::Documents(_) => None,
219        }
220    }
221
222    /// Return the document schema of a document-output plan.
223    pub const fn documents(&self) -> Option<&DocumentSchema> {
224        match self {
225            Self::Documents(schema) => Some(schema),
226            Self::Rows(_) => None,
227        }
228    }
229}
230
231/// Opaque, non-serializable result of schema-aware assertion validation.
232#[derive(Clone, Debug, Eq, PartialEq)]
233pub struct ValidatedMigrationAssertionPlan {
234    binding_domains: BTreeMap<BindingId, BindingDomain>,
235    plan: MigrationAssertionPlan,
236    row_schema: RowSchema,
237    source_state: ManagedSchemaState,
238    structural_limits: StructuralLimits,
239    witnesses: BTreeSet<BindingId>,
240}
241
242impl ValidatedMigrationAssertionPlan {
243    /// Return the context-free trusted plan.
244    pub const fn plan(&self) -> &MigrationAssertionPlan {
245        &self.plan
246    }
247
248    /// Return one schema-derived binding domain.
249    pub fn binding_domain(&self, id: &BindingId) -> Option<&BindingDomain> {
250        self.binding_domains.get(id)
251    }
252
253    /// Return validator-derived output shape.
254    pub const fn row_schema(&self) -> &RowSchema {
255        &self.row_schema
256    }
257
258    /// Return the exact managed and full declared schema identity validated here.
259    pub const fn source_state(&self) -> &ManagedSchemaState {
260        &self.source_state
261    }
262
263    /// Return the exact structural policy used during validation.
264    pub const fn structural_limits(&self) -> StructuralLimits {
265        self.structural_limits
266    }
267
268    /// Return hidden witness bindings.
269    pub const fn witnesses(&self) -> &BTreeSet<BindingId> {
270        &self.witnesses
271    }
272}
273
274/// Validate topology and exact effective schema domains without provider I/O.
275pub fn validate_migration_assertion_plan(
276    plan: &MigrationAssertionPlan,
277    context: &MigrationAssertionValidationContext<'_>,
278    limits: StructuralLimits,
279) -> Result<ValidatedMigrationAssertionPlan, Diagnostic> {
280    if plan.managed_semantics() != context.managed_state().managed_semantic_schema() {
281        return Err(Diagnostic::new(
282            DiagnosticCategory::Integrity,
283            DiagnosticCode::new("migration_assertion_managed_semantic_mismatch")
284                .expect("static query diagnostic code is canonical"),
285            "assertion managed semantic fingerprint does not match validation state",
286        ));
287    }
288    if context.resolved_schema().declared_identity_fingerprint()
289        != context.managed_state().declared_identity()
290    {
291        return Err(Diagnostic::new(
292            DiagnosticCategory::Integrity,
293            DiagnosticCode::new("migration_assertion_declared_identity_mismatch")
294                .expect("static query diagnostic code is canonical"),
295            "resolved schema declaration identity does not match validation state",
296        ));
297    }
298    validate_limits(plan, limits)?;
299    let schema = context.resolved_schema();
300    let converted = convert_assertion_patterns(plan.patterns());
301    let engine::PatternAnalysis {
302        domains,
303        optional_positive: _,
304        positive,
305        scoped_positive,
306        used,
307        value_bindings: _,
308    } = engine::analyze_patterns(
309        &converted,
310        plan.bindings().len(),
311        &[],
312        schema,
313        &BTreeMap::new(),
314        &BTreeSet::new(),
315        &ASSERTION_ENGINE_CODES,
316    )?;
317
318    for binding in plan.bindings() {
319        let id = binding.id();
320        let is_output = plan.outputs().binary_search(&id).is_ok();
321        let is_witness = plan.witnesses().binary_search(&id).is_ok();
322        if is_output && !positive.contains(&id) {
323            return Err(query_failure(
324                "migration_assertion_binding_not_positive",
325                "an output binding must be positively established at the root",
326            ));
327        }
328        if is_witness && !positive.contains(&id) && !scoped_positive.contains(&id) {
329            return Err(query_failure(
330                "migration_assertion_invalid_witness",
331                "witness must be positively established in its lexical scope",
332            ));
333        }
334        if !used.contains(&id) {
335            return Err(query_failure(
336                "migration_assertion_binding_not_used",
337                "every declared binding must be referenced by an assertion pattern",
338            ));
339        }
340        if positive.contains(&id) && domains[&id].is_empty() {
341            return Err(query_failure(
342                "migration_assertion_empty_domain",
343                "schema validation reduced a binding to an empty runtime domain",
344            ));
345        }
346        if !is_output && !is_witness {
347            return Err(query_failure(
348                "migration_assertion_unclassified_binding",
349                "every binding must be an output or a hidden witness",
350            ));
351        }
352    }
353    for witness in plan.witnesses() {
354        if !used.contains(witness)
355            || (!positive.contains(witness) && !scoped_positive.contains(witness))
356        {
357            return Err(query_failure(
358                "migration_assertion_invalid_witness",
359                "witness must be positively bound and used",
360            ));
361        }
362    }
363
364    let binding_domains = domains
365        .into_iter()
366        .filter(|(id, _)| positive.contains(id))
367        .map(|(id, type_ids)| {
368            let value_type =
369                engine::uniform_value_type(&type_ids, schema, &ASSERTION_ENGINE_CODES)?;
370            Ok((
371                id,
372                BindingDomain {
373                    type_ids,
374                    value_type,
375                },
376            ))
377        })
378        .collect::<Result<BTreeMap<_, _>, Diagnostic>>()?;
379    let columns = plan
380        .outputs()
381        .iter()
382        .map(|id| {
383            let binding = plan.binding(*id).expect("validated output binding exists");
384            RowColumn {
385                binding: *id,
386                domain: binding_domains[id].clone(),
387                optional: false,
388                variable: binding.variable().clone(),
389            }
390        })
391        .collect();
392    Ok(ValidatedMigrationAssertionPlan {
393        binding_domains,
394        plan: plan.clone(),
395        row_schema: RowSchema { columns },
396        source_state: context.managed_state().clone(),
397        structural_limits: limits,
398        witnesses: plan.witnesses().iter().copied().collect(),
399    })
400}
401
402fn validate_limits(
403    plan: &MigrationAssertionPlan,
404    limits: StructuralLimits,
405) -> Result<(), Diagnostic> {
406    if !limits.allows_bindings(plan.bindings().len())
407        || !limits.allows_selected_slots(plan.outputs().len())
408        || plan
409            .bindings()
410            .iter()
411            .any(|binding| binding.variable().as_str().len() > limits.output_name_bytes)
412    {
413        return Err(query_failure(
414            "migration_assertion_validation_limit",
415            "assertion exceeds caller structural limits",
416        ));
417    }
418    let mut nodes = 0;
419    inspect_limits(plan.patterns(), 1, limits, &mut nodes)
420}
421
422fn inspect_limits(
423    patterns: &[AssertionPattern],
424    depth: usize,
425    limits: StructuralLimits,
426    nodes: &mut usize,
427) -> Result<(), Diagnostic> {
428    if patterns.len() > limits.boolean_terms {
429        return Err(query_failure(
430            "migration_assertion_validation_limit",
431            "assertion boolean term count exceeds caller limits",
432        ));
433    }
434    for pattern in patterns {
435        *nodes += 1;
436        if !limits.allows_predicate_nodes(*nodes) || !limits.allows_predicate_depth(depth) {
437            return Err(query_failure(
438                "migration_assertion_validation_limit",
439                "assertion pattern size exceeds caller limits",
440            ));
441        }
442        if let AssertionPattern::Not { patterns } = pattern {
443            inspect_limits(patterns, depth + 1, limits, nodes)?;
444        }
445    }
446    Ok(())
447}
448
449fn query_failure(code: &'static str, message: &'static str) -> Diagnostic {
450    Diagnostic::new(
451        DiagnosticCategory::InvalidContract,
452        DiagnosticCode::new(code).expect("static query diagnostic code is canonical"),
453        message,
454    )
455}
456
457/// The released stable diagnostic vocabulary of assertion validation.
458///
459/// Assertion validation delegates to the shared pattern engine; these exact
460/// codes and messages predate that engine and must never drift.
461const ASSERTION_ENGINE_CODES: engine::EngineCodes = engine::EngineCodes {
462    unknown_type: engine::EngineCode {
463        code: "migration_assertion_unknown_type",
464        message: "isa pattern references a type outside the resolved schema",
465    },
466    unknown_attribute: engine::EngineCode {
467        code: "migration_assertion_unknown_attribute",
468        message: "has pattern references an attribute outside the resolved schema",
469    },
470    unknown_relation: engine::EngineCode {
471        code: "migration_assertion_unknown_relation",
472        message: "links pattern relation is absent or not relation-kind",
473    },
474    unknown_role: engine::EngineCode {
475        code: "migration_assertion_unknown_role",
476        message: "links pattern references a role outside the resolved schema",
477    },
478    role_relation_mismatch: engine::EngineCode {
479        code: "migration_assertion_role_relation_mismatch",
480        message: "links role is not effective on the declared relation",
481    },
482    root_reference_not_positive: engine::EngineCode {
483        code: "migration_assertion_binding_not_positive",
484        message: "a root-scope reference is not positively established at the root",
485    },
486    negation_unbound: engine::EngineCode {
487        code: "migration_assertion_negation_unbound_binding",
488        message: "negation-local reference is not positively established in its body",
489    },
490    empty_negated_domain: engine::EngineCode {
491        code: "migration_assertion_empty_negated_domain",
492        message: "negated pattern has an impossible schema domain",
493    },
494    value_domain_mismatch: engine::EngineCode {
495        code: "migration_assertion_value_domain_mismatch",
496        message: "value comparison operands have different scalar domains",
497    },
498    value_comparator_unsupported: engine::EngineCode {
499        code: "migration_assertion_value_comparator_unsupported",
500        message: "ordered comparisons require a provider-orderable scalar domain",
501    },
502    binding_not_scalar: engine::EngineCode {
503        code: "migration_assertion_binding_not_scalar",
504        message: "value operand binding has no uniform attribute scalar domain",
505    },
506    nonuniform_value_domain: engine::EngineCode {
507        code: "migration_assertion_nonuniform_value_domain",
508        message: "binding domain mixes incompatible scalar domains",
509    },
510    disconnected_topology: engine::EngineCode {
511        code: "migration_assertion_disconnected_topology",
512        message: "positive assertion bindings form a disconnected cross join",
513    },
514    // Assertion plans cannot express input operands or function calls;
515    // these entries never fire.
516    unknown_input: engine::EngineCode {
517        code: "migration_assertion_unknown_binding",
518        message: "assertion patterns cannot reference invocation inputs",
519    },
520    unknown_function: engine::EngineCode {
521        code: "migration_assertion_unknown_binding",
522        message: "assertion patterns cannot call schema functions",
523    },
524    function_return_unsupported: engine::EngineCode {
525        code: "migration_assertion_unknown_binding",
526        message: "assertion patterns cannot call schema functions",
527    },
528    function_arity_mismatch: engine::EngineCode {
529        code: "migration_assertion_unknown_binding",
530        message: "assertion patterns cannot call schema functions",
531    },
532    function_argument_type: engine::EngineCode {
533        code: "migration_assertion_unknown_binding",
534        message: "assertion patterns cannot call schema functions",
535    },
536    function_dependency_cycle: engine::EngineCode {
537        code: "migration_assertion_unknown_binding",
538        message: "assertion patterns cannot call schema functions",
539    },
540    value_binding_misuse: engine::EngineCode {
541        code: "migration_assertion_unknown_binding",
542        message: "assertion patterns cannot call schema functions",
543    },
544    try_unbound: engine::EngineCode {
545        code: "migration_assertion_unknown_binding",
546        message: "assertion patterns cannot express optional blocks",
547    },
548    try_uncorrelated: engine::EngineCode {
549        code: "migration_assertion_unknown_binding",
550        message: "assertion patterns cannot express optional blocks",
551    },
552    empty_try_domain: engine::EngineCode {
553        code: "migration_assertion_unknown_binding",
554        message: "assertion patterns cannot express optional blocks",
555    },
556    try_binding_shared: engine::EngineCode {
557        code: "migration_assertion_unknown_binding",
558        message: "assertion patterns cannot express optional blocks",
559    },
560    local_unbound: engine::EngineCode {
561        code: "migration_assertion_unknown_binding",
562        message: "assertion patterns cannot define local functions",
563    },
564    local_uncorrelated: engine::EngineCode {
565        code: "migration_assertion_unknown_binding",
566        message: "assertion patterns cannot define local functions",
567    },
568    empty_local_domain: engine::EngineCode {
569        code: "migration_assertion_unknown_binding",
570        message: "assertion patterns cannot define local functions",
571    },
572    local_return_domain: engine::EngineCode {
573        code: "migration_assertion_unknown_binding",
574        message: "assertion patterns cannot define local functions",
575    },
576};
577
578fn convert_assertion_patterns(patterns: &[AssertionPattern]) -> Vec<QueryPattern> {
579    patterns.iter().map(convert_assertion_pattern).collect()
580}
581
582fn convert_assertion_pattern(pattern: &AssertionPattern) -> QueryPattern {
583    match pattern {
584        AssertionPattern::Isa {
585            binding,
586            include_subtypes,
587            type_id,
588        } => QueryPattern::Isa {
589            binding: *binding,
590            include_subtypes: *include_subtypes,
591            type_id: type_id.clone(),
592        },
593        AssertionPattern::Has {
594            attribute,
595            attribute_id,
596            owner,
597        } => QueryPattern::Has {
598            attribute: *attribute,
599            attribute_id: attribute_id.clone(),
600            owner: *owner,
601        },
602        AssertionPattern::Links {
603            players,
604            relation,
605            relation_id,
606        } => QueryPattern::Links {
607            players: players.clone(),
608            relation: *relation,
609            relation_id: relation_id.clone(),
610        },
611        AssertionPattern::Value {
612            comparator,
613            left,
614            right,
615        } => QueryPattern::Value {
616            comparator: *comparator,
617            left: convert_operand(left),
618            right: convert_operand(right),
619        },
620        AssertionPattern::Not { patterns } => QueryPattern::Not {
621            patterns: convert_assertion_patterns(patterns),
622        },
623    }
624}
625
626fn convert_operand(operand: &ValueOperand) -> QueryOperand {
627    match operand {
628        ValueOperand::Binding { binding } => QueryOperand::Binding { binding: *binding },
629        ValueOperand::Literal { value } => QueryOperand::Literal {
630            value: value.clone(),
631        },
632    }
633}