1mod 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#[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 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 pub const fn resolved_schema(&self) -> &'a ResolvedSchema {
46 self.resolved_schema
47 }
48
49 pub const fn managed_state(&self) -> &'a ManagedSchemaState {
51 self.managed_state
52 }
53}
54
55#[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 pub const fn type_ids(&self) -> &BTreeSet<TypeId> {
72 &self.type_ids
73 }
74
75 pub const fn value_type(&self) -> Option<ValueTypeTag> {
77 self.value_type
78 }
79}
80
81#[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 pub const fn binding(&self) -> BindingId {
107 self.binding
108 }
109
110 pub const fn optional(&self) -> bool {
112 self.optional
113 }
114
115 pub const fn variable(&self) -> &QueryVariable {
117 &self.variable
118 }
119
120 pub const fn domain(&self) -> &BindingDomain {
122 &self.domain
123 }
124}
125
126#[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 pub fn columns(&self) -> &[RowColumn] {
139 &self.columns
140 }
141}
142
143#[derive(Clone, Debug, Eq, PartialEq)]
145pub enum DocumentColumnShape {
146 Scalar {
148 value_type: ValueTypeTag,
150 optional: bool,
152 },
153 List {
155 attribute: type_bridge_contract::id::AttributeId,
157 element_type: ValueTypeTag,
159 },
160}
161
162#[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 pub const fn key(&self) -> &QueryVariable {
176 &self.key
177 }
178
179 pub const fn shape(&self) -> &DocumentColumnShape {
181 &self.shape
182 }
183}
184
185#[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 pub fn columns(&self) -> &[DocumentColumn] {
198 &self.columns
199 }
200}
201
202#[derive(Clone, Debug, Eq, PartialEq)]
204pub enum OutputSchema {
205 Rows(RowSchema),
207 Documents(DocumentSchema),
209}
210
211impl OutputSchema {
212 pub const fn rows(&self) -> Option<&RowSchema> {
214 match self {
215 Self::Rows(schema) => Some(schema),
216 Self::Documents(_) => None,
217 }
218 }
219
220 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#[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 pub const fn plan(&self) -> &MigrationAssertionPlan {
243 &self.plan
244 }
245
246 pub fn binding_domain(&self, id: &BindingId) -> Option<&BindingDomain> {
248 self.binding_domains.get(id)
249 }
250
251 pub const fn row_schema(&self) -> &RowSchema {
253 &self.row_schema
254 }
255
256 pub const fn source_state(&self) -> &ManagedSchemaState {
258 &self.source_state
259 }
260
261 pub const fn structural_limits(&self) -> StructuralLimits {
263 self.structural_limits
264 }
265
266 pub const fn witnesses(&self) -> &BTreeSet<BindingId> {
268 &self.witnesses
269 }
270}
271
272pub 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
455const 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 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}