1#![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#[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 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 pub const fn resolved_schema(&self) -> &'a ResolvedSchema {
48 self.resolved_schema
49 }
50
51 pub const fn managed_state(&self) -> &'a ManagedSchemaState {
53 self.managed_state
54 }
55}
56
57#[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 pub const fn type_ids(&self) -> &BTreeSet<TypeId> {
74 &self.type_ids
75 }
76
77 pub const fn value_type(&self) -> Option<ValueTypeTag> {
79 self.value_type
80 }
81}
82
83#[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 pub const fn binding(&self) -> BindingId {
109 self.binding
110 }
111
112 pub const fn optional(&self) -> bool {
114 self.optional
115 }
116
117 pub const fn variable(&self) -> &QueryVariable {
119 &self.variable
120 }
121
122 pub const fn domain(&self) -> &BindingDomain {
124 &self.domain
125 }
126}
127
128#[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 pub fn columns(&self) -> &[RowColumn] {
141 &self.columns
142 }
143}
144
145#[derive(Clone, Debug, Eq, PartialEq)]
147pub enum DocumentColumnShape {
148 Scalar {
150 value_type: ValueTypeTag,
152 optional: bool,
154 },
155 List {
157 attribute: type_bridge_contract::id::AttributeId,
159 element_type: ValueTypeTag,
161 },
162}
163
164#[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 pub const fn key(&self) -> &QueryVariable {
178 &self.key
179 }
180
181 pub const fn shape(&self) -> &DocumentColumnShape {
183 &self.shape
184 }
185}
186
187#[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 pub fn columns(&self) -> &[DocumentColumn] {
200 &self.columns
201 }
202}
203
204#[derive(Clone, Debug, Eq, PartialEq)]
206pub enum OutputSchema {
207 Rows(RowSchema),
209 Documents(DocumentSchema),
211}
212
213impl OutputSchema {
214 pub const fn rows(&self) -> Option<&RowSchema> {
216 match self {
217 Self::Rows(schema) => Some(schema),
218 Self::Documents(_) => None,
219 }
220 }
221
222 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#[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 pub const fn plan(&self) -> &MigrationAssertionPlan {
245 &self.plan
246 }
247
248 pub fn binding_domain(&self, id: &BindingId) -> Option<&BindingDomain> {
250 self.binding_domains.get(id)
251 }
252
253 pub const fn row_schema(&self) -> &RowSchema {
255 &self.row_schema
256 }
257
258 pub const fn source_state(&self) -> &ManagedSchemaState {
260 &self.source_state
261 }
262
263 pub const fn structural_limits(&self) -> StructuralLimits {
265 self.structural_limits
266 }
267
268 pub const fn witnesses(&self) -> &BTreeSet<BindingId> {
270 &self.witnesses
271 }
272}
273
274pub 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
457const 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 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}