Skip to main content

type_bridge_query/
lib.rs

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