1use std::collections::BTreeSet;
16use std::fmt;
17
18use serde::Serialize;
19
20use crate::capability::{CapabilityId, CapabilitySet};
21use crate::codec::to_canonical_json;
22use crate::diagnostic::{Diagnostic, DiagnosticCategory, DiagnosticCode};
23use crate::fingerprint::{CanonicalizationVersion, Fingerprint, FingerprintDomain};
24use crate::id::{AttributeId, FunctionId, Label, RoleId, TypeId};
25use crate::limits::StructuralLimits;
26use crate::migration_assertion::{
27 AssertionBinding, AssertionRolePlayer, BindingId, QueryVariable, ValueComparator,
28};
29use crate::schema_fingerprint::ManagedSemanticSchemaFingerprint;
30use crate::value::{CanonicalValue, ValueTypeTag};
31
32#[path = "query_plan_v2.rs"]
33mod v2;
34pub use v2::{
35 CompatibilityValueV2, HydrationBindingV2, HydrationDescriptorV2, HydrationFieldV2,
36 HydrationPlayerV2, HydrationProjectionV2, HydrationRoleV2, ModelOutputV2, ModelQueryV2,
37 QueryBindingPairV2, QueryComparatorV2, QueryFieldV2, QueryMissingOrderV2,
38 QueryModelOutputSlotV2, QueryModelOutputV2, QueryNamedOutputSlotV2, QueryOrderDirectionV2,
39 QueryOrderTermV2, QueryPatternV2, QueryPlanV2Compatibility, QueryRowCardinalityV2,
40 QueryStableOrderV2, QueryWindowV2, ReleasedValueKindV2,
41};
42
43pub const QUERY_PLAN_FORMAT_V1: &str = "typebridge.query-plan/v1";
45pub const QUERY_PLAN_FORMAT_V2: &str = "typebridge.query-plan/v2";
47pub const QUERY_PLAN_FINGERPRINT_DOMAIN: &str = "typebridge.query.plan";
49pub const QUERY_PLAN_CANONICALIZATION: &str = "typebridge.query-plan-c14n/v1";
51pub const QUERY_PLAN_CANONICALIZATION_V1: &str = QUERY_PLAN_CANONICALIZATION;
53pub const QUERY_PLAN_CANONICALIZATION_V2: &str = "typebridge.query-plan-c14n/v2";
55
56const CAP_PLAN: &str = "query.plan";
57const CAP_ISA: &str = "query.pattern.isa";
58const CAP_ISA_SUBTYPES: &str = "query.pattern.isa-subtypes";
59const CAP_HAS: &str = "query.pattern.has";
60const CAP_LINKS: &str = "query.pattern.links";
61const CAP_VALUE: &str = "query.pattern.value";
62const CAP_NEGATION: &str = "query.pattern.negation";
63const CAP_DISJUNCTION: &str = "query.pattern.disjunction";
64const CAP_INPUT_COLUMNS: &str = "query.input.columns";
65const CAP_STAGE_SELECT: &str = "query.stage.select";
66const CAP_STAGE_REQUIRE: &str = "query.stage.require";
67const CAP_STAGE_DISTINCT: &str = "query.stage.distinct";
68const CAP_STAGE_SORT: &str = "query.stage.sort";
69const CAP_STAGE_OFFSET: &str = "query.stage.offset";
70const CAP_STAGE_LIMIT: &str = "query.stage.limit";
71const CAP_OUTPUT_ROWS: &str = "query.output.rows";
72const CAP_FUNCTION_CALL: &str = "query.pattern.function-call";
73const CAP_STAGE_REDUCE: &str = "query.stage.reduce";
74const CAP_TRY: &str = "query.pattern.try";
75const CAP_OUTPUT_DOCUMENTS: &str = "query.output.documents";
76const CAP_LOCAL_FUNCTIONS: &str = "query.function.local";
77const CAP_REACHABLE: &str = "query.pattern.reachable";
78const CAP_INPUT_GIVEN_ROWS: &str = "query.input.given-rows";
79
80#[must_use]
82pub fn query_plan_capability_vocabulary() -> CapabilitySet {
83 [
84 CAP_PLAN,
85 CAP_ISA,
86 CAP_ISA_SUBTYPES,
87 CAP_HAS,
88 CAP_LINKS,
89 CAP_VALUE,
90 CAP_NEGATION,
91 CAP_INPUT_COLUMNS,
92 CAP_STAGE_SELECT,
93 CAP_STAGE_REQUIRE,
94 CAP_STAGE_DISTINCT,
95 CAP_STAGE_SORT,
96 CAP_STAGE_OFFSET,
97 CAP_STAGE_LIMIT,
98 CAP_OUTPUT_ROWS,
99 CAP_FUNCTION_CALL,
100 CAP_STAGE_REDUCE,
101 CAP_TRY,
102 CAP_OUTPUT_DOCUMENTS,
103 CAP_LOCAL_FUNCTIONS,
104 CAP_REACHABLE,
105 ]
106 .into_iter()
107 .map(|value| CapabilityId::new(value).expect("static capability id is canonical"))
108 .collect()
109}
110
111#[must_use]
118pub fn query_plan_authoring_capability_vocabulary() -> CapabilitySet {
119 query_plan_capability_vocabulary()
120 .into_iter()
121 .chain([v2::CAP_PLAN_V2, v2::CAP_DISJUNCTION].map(|value| {
122 CapabilityId::new(value).expect("static V2 authoring capability is canonical")
123 }))
124 .collect()
125}
126
127#[must_use]
133pub fn query_plan_v2_capability_vocabulary() -> CapabilitySet {
134 query_plan_capability_vocabulary()
135 .into_iter()
136 .chain(
137 [
138 v2::CAP_PLAN_V2,
139 v2::CAP_DISJUNCTION,
140 v2::CAP_STRING_OPERATORS,
141 v2::CAP_LINKS_SUBTYPES,
142 v2::CAP_CROSS_JOIN,
143 v2::CAP_OUTPUT_NAMED,
144 v2::CAP_OUTPUT_COLLECT,
145 v2::CAP_OUTPUT_COLLECT_DISTINCT,
146 v2::CAP_OUTPUT_HYDRATED,
147 v2::CAP_EXACTLY_ONE,
148 v2::CAP_PAGE,
149 v2::CAP_DISTINCT_COUNT,
150 v2::CAP_DISTINCT_EXISTS,
151 v2::CAP_STABLE_SELECTED,
152 v2::CAP_STABLE_ROOT,
153 v2::CAP_STABLE_COLLECTION,
154 v2::CAP_SAME_SNAPSHOT_HYDRATION,
155 v2::CAP_BATCH_IDENTITY_REBIND,
156 ]
157 .into_iter()
158 .map(|value| {
159 CapabilityId::new(value).expect("static V2 query capability is canonical")
160 }),
161 )
162 .collect()
163}
164
165#[must_use]
175pub fn query_given_rows_capability() -> CapabilityId {
176 CapabilityId::new(CAP_INPUT_GIVEN_ROWS).expect("static capability id is canonical")
177}
178
179#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
181#[serde(transparent)]
182pub struct InputColumnId(u16);
183
184impl InputColumnId {
185 #[must_use]
187 pub const fn new(value: u16) -> Self {
188 Self(value)
189 }
190
191 #[must_use]
193 pub const fn get(self) -> u16 {
194 self.0
195 }
196}
197
198impl fmt::Display for InputColumnId {
199 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
200 write!(formatter, "{}", self.0)
201 }
202}
203
204#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
209pub struct InputColumn {
210 id: InputColumnId,
211 optional: bool,
212 public_name: QueryVariable,
213 value_type: ValueTypeTag,
214}
215
216impl InputColumn {
217 #[must_use]
219 pub const fn new(
220 id: InputColumnId,
221 public_name: QueryVariable,
222 value_type: ValueTypeTag,
223 optional: bool,
224 ) -> Self {
225 Self {
226 id,
227 optional,
228 public_name,
229 value_type,
230 }
231 }
232
233 #[must_use]
235 pub const fn id(&self) -> InputColumnId {
236 self.id
237 }
238
239 #[must_use]
241 pub const fn public_name(&self) -> &QueryVariable {
242 &self.public_name
243 }
244
245 #[must_use]
247 pub const fn value_type(&self) -> ValueTypeTag {
248 self.value_type
249 }
250
251 #[must_use]
253 pub const fn optional(&self) -> bool {
254 self.optional
255 }
256}
257
258#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
260#[serde(tag = "kind", rename_all = "snake_case")]
261pub enum QueryOperand {
262 Binding {
264 binding: BindingId,
266 },
267 Literal {
269 value: CanonicalValue,
271 },
272 Input {
274 column: InputColumnId,
276 },
277}
278
279#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
286#[serde(tag = "kind", rename_all = "snake_case")]
287pub enum QueryPattern {
288 Isa {
290 binding: BindingId,
292 include_subtypes: bool,
294 type_id: TypeId,
296 },
297 Has {
299 attribute: BindingId,
301 attribute_id: AttributeId,
303 owner: BindingId,
305 },
306 Links {
308 players: Vec<AssertionRolePlayer>,
310 relation: BindingId,
312 relation_id: TypeId,
314 },
315 Value {
317 comparator: ValueComparator,
319 left: QueryOperand,
321 right: QueryOperand,
323 },
324 Or {
330 branches: Vec<Vec<QueryPattern>>,
332 },
333 Not {
335 patterns: Vec<QueryPattern>,
337 },
338 Try {
345 patterns: Vec<QueryPattern>,
347 },
348 Reachable {
359 min_depth: u8,
361 max_depth: u8,
363 relation: TypeId,
365 role_from: RoleId,
367 role_to: RoleId,
369 source: BindingId,
371 target: BindingId,
373 },
374 FunctionCall {
380 arguments: Vec<QueryOperand>,
382 assigned: BindingId,
384 function: FunctionId,
386 },
387}
388
389#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
391#[serde(rename_all = "snake_case")]
392pub enum OrderDirection {
393 Ascending,
395 Descending,
397}
398
399#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
401pub struct OrderTerm {
402 binding: BindingId,
403 direction: OrderDirection,
404}
405
406impl OrderTerm {
407 #[must_use]
409 pub const fn new(binding: BindingId, direction: OrderDirection) -> Self {
410 Self { binding, direction }
411 }
412
413 #[must_use]
415 pub const fn binding(&self) -> BindingId {
416 self.binding
417 }
418
419 #[must_use]
421 pub const fn direction(&self) -> OrderDirection {
422 self.direction
423 }
424}
425
426#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
433#[serde(rename_all = "snake_case")]
434pub enum Reducer {
435 Count,
437 Max,
439 Mean,
441 Median,
443 Min,
445 Std,
447 Sum,
449}
450
451impl Reducer {
452 #[must_use]
454 pub const fn total_without_groups(self) -> bool {
455 matches!(self, Self::Count | Self::Sum)
456 }
457
458 #[must_use]
460 pub const fn requires_input(self) -> bool {
461 !matches!(self, Self::Count)
462 }
463}
464
465#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
467pub struct ReduceAssignment {
468 assigned: BindingId,
469 input: Option<BindingId>,
470 reducer: Reducer,
471}
472
473impl ReduceAssignment {
474 #[must_use]
476 pub const fn new(assigned: BindingId, reducer: Reducer, input: Option<BindingId>) -> Self {
477 Self {
478 assigned,
479 input,
480 reducer,
481 }
482 }
483
484 #[must_use]
486 pub const fn assigned(&self) -> BindingId {
487 self.assigned
488 }
489
490 #[must_use]
492 pub const fn input(&self) -> Option<BindingId> {
493 self.input
494 }
495
496 #[must_use]
498 pub const fn reducer(&self) -> Reducer {
499 self.reducer
500 }
501}
502
503#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
509#[serde(tag = "kind", rename_all = "snake_case")]
510pub enum ReadStage {
511 Match {
513 patterns: Vec<QueryPattern>,
515 },
516 Select {
518 bindings: Vec<BindingId>,
520 },
521 Require {
523 bindings: Vec<BindingId>,
525 },
526 Distinct,
528 Reduce {
533 assignments: Vec<ReduceAssignment>,
535 groups: Vec<BindingId>,
537 },
538 Sort {
540 terms: Vec<OrderTerm>,
542 },
543 Offset {
545 rows: u64,
547 },
548 Limit {
550 rows: u64,
552 },
553}
554
555impl ReadStage {
556 const fn ordinal(&self) -> u8 {
557 match self {
558 Self::Match { .. } => 0,
559 Self::Select { .. } => 1,
560 Self::Require { .. } => 2,
561 Self::Distinct => 3,
562 Self::Reduce { .. } => 4,
563 Self::Sort { .. } => 5,
564 Self::Offset { .. } => 6,
565 Self::Limit { .. } => 7,
566 }
567 }
568}
569
570#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
575pub struct LocalReturn {
576 input: BindingId,
577 reducer: Reducer,
578 value_type: ValueTypeTag,
579}
580
581impl LocalReturn {
582 #[must_use]
584 pub const fn new(reducer: Reducer, input: BindingId, value_type: ValueTypeTag) -> Self {
585 Self {
586 input,
587 reducer,
588 value_type,
589 }
590 }
591
592 #[must_use]
594 pub const fn input(&self) -> BindingId {
595 self.input
596 }
597
598 #[must_use]
600 pub const fn reducer(&self) -> Reducer {
601 self.reducer
602 }
603
604 #[must_use]
606 pub const fn value_type(&self) -> ValueTypeTag {
607 self.value_type
608 }
609}
610
611#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
618pub struct LocalFunction {
619 bindings: Vec<AssertionBinding>,
620 body: Vec<QueryPattern>,
621 name: FunctionId,
622 parameters: Vec<Label>,
623 returns: LocalReturn,
624}
625
626impl LocalFunction {
627 #[must_use]
629 pub const fn new(
630 name: FunctionId,
631 bindings: Vec<AssertionBinding>,
632 parameters: Vec<Label>,
633 body: Vec<QueryPattern>,
634 returns: LocalReturn,
635 ) -> Self {
636 Self {
637 bindings,
638 body,
639 name,
640 parameters,
641 returns,
642 }
643 }
644
645 #[must_use]
647 pub const fn name(&self) -> &FunctionId {
648 &self.name
649 }
650
651 #[must_use]
653 pub fn bindings(&self) -> &[AssertionBinding] {
654 &self.bindings
655 }
656
657 #[must_use]
659 pub fn parameters(&self) -> &[Label] {
660 &self.parameters
661 }
662
663 #[must_use]
665 pub fn body(&self) -> &[QueryPattern] {
666 &self.body
667 }
668
669 #[must_use]
671 pub const fn returns(&self) -> &LocalReturn {
672 &self.returns
673 }
674}
675
676#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
678#[serde(tag = "kind", rename_all = "snake_case")]
679pub enum DocumentSource {
680 Binding {
683 binding: BindingId,
685 },
686 AttributeList {
689 attribute: AttributeId,
691 owner: BindingId,
693 },
694}
695
696#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
698pub struct DocumentField {
699 key: QueryVariable,
700 source: DocumentSource,
701}
702
703impl DocumentField {
704 #[must_use]
706 pub const fn new(key: QueryVariable, source: DocumentSource) -> Self {
707 Self { key, source }
708 }
709
710 #[must_use]
712 pub const fn key(&self) -> &QueryVariable {
713 &self.key
714 }
715
716 #[must_use]
718 pub const fn source(&self) -> &DocumentSource {
719 &self.source
720 }
721}
722
723#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
725#[serde(tag = "kind", rename_all = "snake_case")]
726pub enum QueryOutput {
727 Rows {
729 columns: Vec<BindingId>,
731 },
732 Documents {
734 fields: Vec<DocumentField>,
736 },
737}
738
739#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
741pub struct QueryPlan {
742 bindings: Vec<AssertionBinding>,
743 #[serde(skip_serializing_if = "Option::is_none")]
744 compatibility: Option<QueryPlanV2Compatibility>,
745 format: String,
746 functions: Vec<LocalFunction>,
747 inputs: Vec<InputColumn>,
748 managed_semantics: ManagedSemanticSchemaFingerprint,
749 output: QueryOutput,
750 pipeline: Vec<ReadStage>,
751 required_capabilities: CapabilitySet,
752}
753
754impl QueryPlan {
755 pub fn new(
757 bindings: Vec<AssertionBinding>,
758 inputs: Vec<InputColumn>,
759 pipeline: Vec<ReadStage>,
760 output: QueryOutput,
761 managed_semantics: ManagedSemanticSchemaFingerprint,
762 ) -> Result<Self, Diagnostic> {
763 Self::new_with_limits(
764 bindings,
765 Vec::new(),
766 inputs,
767 pipeline,
768 output,
769 None,
770 managed_semantics,
771 StructuralLimits::CANONICAL,
772 )
773 }
774
775 pub fn new_with_functions(
777 bindings: Vec<AssertionBinding>,
778 functions: Vec<LocalFunction>,
779 inputs: Vec<InputColumn>,
780 pipeline: Vec<ReadStage>,
781 output: QueryOutput,
782 managed_semantics: ManagedSemanticSchemaFingerprint,
783 ) -> Result<Self, Diagnostic> {
784 Self::new_with_limits(
785 bindings,
786 functions,
787 inputs,
788 pipeline,
789 output,
790 None,
791 managed_semantics,
792 StructuralLimits::CANONICAL,
793 )
794 }
795
796 pub fn new_v2(
801 bindings: Vec<AssertionBinding>,
802 inputs: Vec<InputColumn>,
803 pipeline: Vec<ReadStage>,
804 output: QueryOutput,
805 managed_semantics: ManagedSemanticSchemaFingerprint,
806 ) -> Result<Self, Diagnostic> {
807 Self::new_v2_with_functions(
808 bindings,
809 Vec::new(),
810 inputs,
811 pipeline,
812 output,
813 QueryPlanV2Compatibility::native(),
814 managed_semantics,
815 )
816 }
817
818 pub fn new_v2_with_functions(
821 bindings: Vec<AssertionBinding>,
822 functions: Vec<LocalFunction>,
823 inputs: Vec<InputColumn>,
824 pipeline: Vec<ReadStage>,
825 output: QueryOutput,
826 compatibility: QueryPlanV2Compatibility,
827 managed_semantics: ManagedSemanticSchemaFingerprint,
828 ) -> Result<Self, Diagnostic> {
829 Self::new_with_limits(
830 bindings,
831 functions,
832 inputs,
833 pipeline,
834 output,
835 Some(compatibility),
836 managed_semantics,
837 StructuralLimits::CANONICAL,
838 )
839 }
840
841 #[expect(
842 clippy::too_many_arguments,
843 reason = "the shared constructor receives every independently versioned plan component"
844 )]
845 fn new_with_limits(
846 bindings: Vec<AssertionBinding>,
847 functions: Vec<LocalFunction>,
848 inputs: Vec<InputColumn>,
849 pipeline: Vec<ReadStage>,
850 output: QueryOutput,
851 compatibility: Option<QueryPlanV2Compatibility>,
852 managed_semantics: ManagedSemanticSchemaFingerprint,
853 limits: StructuralLimits,
854 ) -> Result<Self, Diagnostic> {
855 if compatibility.is_none() {
856 validate_v1_reachability(&pipeline)?;
857 }
858 Self::validate_plan_structure(
859 &bindings,
860 &functions,
861 &inputs,
862 &pipeline,
863 &output,
864 compatibility.as_ref(),
865 limits,
866 )?;
867 let required_capabilities = derive_capabilities(
868 &pipeline,
869 &functions,
870 &inputs,
871 &output,
872 compatibility.as_ref(),
873 )?;
874 let format = if compatibility.is_some() {
875 QUERY_PLAN_FORMAT_V2
876 } else {
877 QUERY_PLAN_FORMAT_V1
878 };
879 Ok(Self {
880 bindings,
881 compatibility,
882 format: format.to_owned(),
883 functions,
884 inputs,
885 managed_semantics,
886 output,
887 pipeline,
888 required_capabilities,
889 })
890 }
891
892 pub fn check_structural_limits(&self, limits: StructuralLimits) -> Result<(), Diagnostic> {
899 Self::validate_plan_structure(
900 &self.bindings,
901 &self.functions,
902 &self.inputs,
903 &self.pipeline,
904 &self.output,
905 self.compatibility.as_ref(),
906 limits,
907 )
908 }
909
910 fn validate_plan_structure(
914 bindings: &[AssertionBinding],
915 functions: &[LocalFunction],
916 inputs: &[InputColumn],
917 pipeline: &[ReadStage],
918 output: &QueryOutput,
919 compatibility: Option<&QueryPlanV2Compatibility>,
920 limits: StructuralLimits,
921 ) -> Result<(), Diagnostic> {
922 if bindings.is_empty() || !limits.allows_bindings(bindings.len()) {
923 return Err(failure(
924 DiagnosticCategory::ResourceLimit,
925 "query_plan_binding_limit",
926 "plan binding count is empty or exceeds the structural ceiling",
927 ));
928 }
929 let mut names = BTreeSet::new();
930 for (index, binding) in bindings.iter().enumerate() {
931 if usize::from(binding.id().get()) != index {
932 return Err(failure(
933 DiagnosticCategory::InvalidContract,
934 "query_plan_bindings_not_dense",
935 "plan binding IDs must be ordered dense zero-based ordinals",
936 ));
937 }
938 validate_query_name_limit(
939 binding.variable(),
940 limits,
941 "a query variable name exceeds the structural ceiling",
942 )?;
943 if !names.insert(binding.variable().clone()) {
944 return Err(failure(
945 DiagnosticCategory::InvalidContract,
946 "query_plan_duplicate_variable",
947 "plan query variables must be unique",
948 ));
949 }
950 }
951 if !limits.allows_bindings(inputs.len().max(1)) {
952 return Err(failure(
953 DiagnosticCategory::ResourceLimit,
954 "query_plan_input_limit",
955 "plan input column count exceeds the structural ceiling",
956 ));
957 }
958 for (index, column) in inputs.iter().enumerate() {
959 if usize::from(column.id().get()) != index {
960 return Err(failure(
961 DiagnosticCategory::InvalidContract,
962 "query_plan_inputs_not_dense",
963 "input column IDs must be ordered dense zero-based ordinals",
964 ));
965 }
966 validate_query_name_limit(
967 column.public_name(),
968 limits,
969 "an input column name exceeds the structural ceiling",
970 )?;
971 if !names.insert(column.public_name().clone()) {
972 return Err(failure(
973 DiagnosticCategory::InvalidContract,
974 "query_plan_duplicate_variable",
975 "input column names must not collide with query variables",
976 ));
977 }
978 }
979
980 let mut nodes = 0usize;
981 validate_local_functions(functions, limits, &mut nodes)?;
982
983 let (mandatory, optional, has_sort) =
984 validate_pipeline(pipeline, bindings.len(), inputs.len(), limits, &mut nodes)?;
985 let visible: BTreeSet<BindingId> = mandatory.union(&optional).copied().collect();
986
987 match output {
988 QueryOutput::Rows { columns } => {
989 if columns.is_empty() || !limits.allows_selected_slots(columns.len()) {
990 return Err(failure(
991 DiagnosticCategory::ResourceLimit,
992 "query_plan_output_limit",
993 "output column count is empty or exceeds the structural ceiling",
994 ));
995 }
996 let mut seen = BTreeSet::new();
997 for column in columns {
998 if !visible.contains(column) {
999 return Err(failure(
1000 DiagnosticCategory::InvalidContract,
1001 "query_plan_output_not_visible",
1002 "output projects a binding outside the visible row environment",
1003 ));
1004 }
1005 if !seen.insert(*column) {
1006 return Err(failure(
1007 DiagnosticCategory::InvalidContract,
1008 "query_plan_duplicate_output_column",
1009 "output projects one binding twice",
1010 ));
1011 }
1012 }
1013 }
1014 QueryOutput::Documents { fields } => {
1015 if fields.is_empty() || !limits.allows_selected_slots(fields.len()) {
1016 return Err(failure(
1017 DiagnosticCategory::ResourceLimit,
1018 "query_plan_output_limit",
1019 "output column count is empty or exceeds the structural ceiling",
1020 ));
1021 }
1022 let mut keys = BTreeSet::new();
1023 for field in fields {
1024 validate_query_name_limit(
1025 field.key(),
1026 limits,
1027 "a document output key exceeds the structural ceiling",
1028 )?;
1029 if !keys.insert(field.key().clone()) {
1030 return Err(failure(
1031 DiagnosticCategory::InvalidContract,
1032 "query_plan_duplicate_output_column",
1033 "documents fetch one key twice",
1034 ));
1035 }
1036 match field.source() {
1037 DocumentSource::Binding { binding } => {
1038 if !visible.contains(binding) {
1039 return Err(failure(
1040 DiagnosticCategory::InvalidContract,
1041 "query_plan_output_not_visible",
1042 "output projects a binding outside the visible row environment",
1043 ));
1044 }
1045 }
1046 DocumentSource::AttributeList { owner, .. } => {
1047 if !mandatory.contains(owner) {
1050 return Err(failure(
1051 DiagnosticCategory::InvalidContract,
1052 "query_plan_output_not_visible",
1053 "attribute lists require a mandatory owner binding",
1054 ));
1055 }
1056 }
1057 }
1058 }
1059 }
1060 }
1061 if !has_sort
1065 && pipeline
1066 .iter()
1067 .any(|stage| matches!(stage, ReadStage::Offset { .. } | ReadStage::Limit { .. }))
1068 {
1069 return Err(failure(
1070 DiagnosticCategory::InvalidContract,
1071 "query_plan_unordered_truncation",
1072 "offset and limit require an explicit total sort order",
1073 ));
1074 }
1075 if let Some(compatibility) = compatibility {
1076 compatibility.validate(bindings.len(), limits)?;
1077 }
1078
1079 Ok(())
1080 }
1081
1082 #[must_use]
1084 pub fn format(&self) -> &str {
1085 &self.format
1086 }
1087
1088 #[must_use]
1090 pub fn bindings(&self) -> &[AssertionBinding] {
1091 &self.bindings
1092 }
1093
1094 #[must_use]
1099 pub const fn v2_compatibility(&self) -> Option<&QueryPlanV2Compatibility> {
1100 self.compatibility.as_ref()
1101 }
1102
1103 #[must_use]
1105 pub fn inputs(&self) -> &[InputColumn] {
1106 &self.inputs
1107 }
1108
1109 #[must_use]
1111 pub fn functions(&self) -> &[LocalFunction] {
1112 &self.functions
1113 }
1114
1115 pub fn pipeline(&self) -> &[ReadStage] {
1117 &self.pipeline
1118 }
1119
1120 #[must_use]
1122 pub const fn output(&self) -> &QueryOutput {
1123 &self.output
1124 }
1125
1126 #[must_use]
1128 pub const fn managed_semantics(&self) -> &ManagedSemanticSchemaFingerprint {
1129 &self.managed_semantics
1130 }
1131
1132 #[must_use]
1134 pub const fn required_capabilities(&self) -> &CapabilitySet {
1135 &self.required_capabilities
1136 }
1137
1138 pub fn canonical_bytes(&self) -> Result<Vec<u8>, Diagnostic> {
1140 match self.format.as_str() {
1141 QUERY_PLAN_FORMAT_V1 => to_canonical_json(&QueryPlanV1Projection::new(self)?),
1142 QUERY_PLAN_FORMAT_V2 => to_canonical_json(self),
1143 _ => Err(failure(
1144 DiagnosticCategory::InvalidContract,
1145 "query_plan_format_unsupported",
1146 "query plan wire format is unsupported",
1147 )),
1148 }
1149 }
1150
1151 pub fn fingerprint(&self) -> Result<QueryPlanFingerprint, Diagnostic> {
1153 QueryPlanFingerprint::compute(self)
1154 }
1155}
1156
1157#[derive(Serialize)]
1163struct QueryPlanV1Projection<'a> {
1164 bindings: &'a [AssertionBinding],
1165 format: &'a str,
1166 functions: Vec<LocalFunctionV1Projection<'a>>,
1167 inputs: &'a [InputColumn],
1168 managed_semantics: &'a ManagedSemanticSchemaFingerprint,
1169 output: &'a QueryOutput,
1170 pipeline: Vec<ReadStageV1Projection<'a>>,
1171 required_capabilities: &'a CapabilitySet,
1172}
1173
1174impl<'a> QueryPlanV1Projection<'a> {
1175 fn new(plan: &'a QueryPlan) -> Result<Self, Diagnostic> {
1176 Ok(Self {
1177 bindings: plan.bindings(),
1178 format: plan.format(),
1179 functions: plan
1180 .functions()
1181 .iter()
1182 .map(LocalFunctionV1Projection::new)
1183 .collect::<Result<Vec<_>, _>>()?,
1184 inputs: plan.inputs(),
1185 managed_semantics: plan.managed_semantics(),
1186 output: plan.output(),
1187 pipeline: plan
1188 .pipeline()
1189 .iter()
1190 .map(ReadStageV1Projection::new)
1191 .collect::<Result<Vec<_>, _>>()?,
1192 required_capabilities: plan.required_capabilities(),
1193 })
1194 }
1195}
1196
1197#[derive(Serialize)]
1198struct LocalFunctionV1Projection<'a> {
1199 bindings: &'a [AssertionBinding],
1200 body: Vec<QueryPatternV1Projection<'a>>,
1201 name: &'a FunctionId,
1202 parameters: &'a [Label],
1203 returns: &'a LocalReturn,
1204}
1205
1206impl<'a> LocalFunctionV1Projection<'a> {
1207 fn new(function: &'a LocalFunction) -> Result<Self, Diagnostic> {
1208 Ok(Self {
1209 bindings: function.bindings(),
1210 body: function
1211 .body()
1212 .iter()
1213 .map(QueryPatternV1Projection::new)
1214 .collect::<Result<Vec<_>, _>>()?,
1215 name: function.name(),
1216 parameters: function.parameters(),
1217 returns: function.returns(),
1218 })
1219 }
1220}
1221
1222#[derive(Serialize)]
1223#[serde(tag = "kind", rename_all = "snake_case")]
1224enum ReadStageV1Projection<'a> {
1225 Match {
1226 patterns: Vec<QueryPatternV1Projection<'a>>,
1227 },
1228 Select {
1229 bindings: &'a [BindingId],
1230 },
1231 Require {
1232 bindings: &'a [BindingId],
1233 },
1234 Distinct,
1235 Reduce {
1236 assignments: &'a [ReduceAssignment],
1237 groups: &'a [BindingId],
1238 },
1239 Sort {
1240 terms: &'a [OrderTerm],
1241 },
1242 Offset {
1243 rows: u64,
1244 },
1245 Limit {
1246 rows: u64,
1247 },
1248}
1249
1250impl<'a> ReadStageV1Projection<'a> {
1251 fn new(stage: &'a ReadStage) -> Result<Self, Diagnostic> {
1252 Ok(match stage {
1253 ReadStage::Match { patterns } => Self::Match {
1254 patterns: patterns
1255 .iter()
1256 .map(QueryPatternV1Projection::new)
1257 .collect::<Result<Vec<_>, _>>()?,
1258 },
1259 ReadStage::Select { bindings } => Self::Select { bindings },
1260 ReadStage::Require { bindings } => Self::Require { bindings },
1261 ReadStage::Distinct => Self::Distinct,
1262 ReadStage::Reduce {
1263 assignments,
1264 groups,
1265 } => Self::Reduce {
1266 assignments,
1267 groups,
1268 },
1269 ReadStage::Sort { terms } => Self::Sort { terms },
1270 ReadStage::Offset { rows } => Self::Offset { rows: *rows },
1271 ReadStage::Limit { rows } => Self::Limit { rows: *rows },
1272 })
1273 }
1274}
1275
1276#[derive(Serialize)]
1277#[serde(tag = "kind", rename_all = "snake_case")]
1278enum QueryPatternV1Projection<'a> {
1279 Isa {
1280 binding: BindingId,
1281 include_subtypes: bool,
1282 type_id: &'a TypeId,
1283 },
1284 Has {
1285 attribute: BindingId,
1286 attribute_id: &'a AttributeId,
1287 owner: BindingId,
1288 },
1289 Links {
1290 players: &'a [AssertionRolePlayer],
1291 relation: BindingId,
1292 relation_id: &'a TypeId,
1293 },
1294 Value {
1295 comparator: ValueComparator,
1296 left: &'a QueryOperand,
1297 right: &'a QueryOperand,
1298 },
1299 Not {
1300 patterns: Vec<QueryPatternV1Projection<'a>>,
1301 },
1302 Try {
1303 patterns: Vec<QueryPatternV1Projection<'a>>,
1304 },
1305 Reachable {
1306 max_depth: u8,
1307 relation: &'a TypeId,
1308 role_from: &'a RoleId,
1309 role_to: &'a RoleId,
1310 source: BindingId,
1311 target: BindingId,
1312 },
1313 FunctionCall {
1314 arguments: &'a [QueryOperand],
1315 assigned: BindingId,
1316 function: &'a FunctionId,
1317 },
1318}
1319
1320impl<'a> QueryPatternV1Projection<'a> {
1321 fn new(pattern: &'a QueryPattern) -> Result<Self, Diagnostic> {
1322 Ok(match pattern {
1323 QueryPattern::Isa {
1324 binding,
1325 include_subtypes,
1326 type_id,
1327 } => Self::Isa {
1328 binding: *binding,
1329 include_subtypes: *include_subtypes,
1330 type_id,
1331 },
1332 QueryPattern::Has {
1333 attribute,
1334 attribute_id,
1335 owner,
1336 } => Self::Has {
1337 attribute: *attribute,
1338 attribute_id,
1339 owner: *owner,
1340 },
1341 QueryPattern::Links {
1342 players,
1343 relation,
1344 relation_id,
1345 } => Self::Links {
1346 players,
1347 relation: *relation,
1348 relation_id,
1349 },
1350 QueryPattern::Value {
1351 comparator,
1352 left,
1353 right,
1354 } => Self::Value {
1355 comparator: *comparator,
1356 left,
1357 right,
1358 },
1359 QueryPattern::Or { .. } => {
1360 return Err(failure(
1361 DiagnosticCategory::InvalidContract,
1362 "query_plan_v1_disjunction_unsupported",
1363 "ordinary disjunction is additive in query-plan V2",
1364 ));
1365 }
1366 QueryPattern::Not { patterns } => Self::Not {
1367 patterns: patterns
1368 .iter()
1369 .map(Self::new)
1370 .collect::<Result<Vec<_>, _>>()?,
1371 },
1372 QueryPattern::Try { patterns } => Self::Try {
1373 patterns: patterns
1374 .iter()
1375 .map(Self::new)
1376 .collect::<Result<Vec<_>, _>>()?,
1377 },
1378 QueryPattern::Reachable {
1379 max_depth,
1380 relation,
1381 role_from,
1382 role_to,
1383 source,
1384 target,
1385 ..
1386 } => Self::Reachable {
1387 max_depth: *max_depth,
1388 relation,
1389 role_from,
1390 role_to,
1391 source: *source,
1392 target: *target,
1393 },
1394 QueryPattern::FunctionCall {
1395 arguments,
1396 assigned,
1397 function,
1398 } => Self::FunctionCall {
1399 arguments,
1400 assigned: *assigned,
1401 function,
1402 },
1403 })
1404 }
1405}
1406
1407fn validate_v1_reachability(pipeline: &[ReadStage]) -> Result<(), Diagnostic> {
1408 fn validate_patterns(patterns: &[QueryPattern]) -> Result<(), Diagnostic> {
1409 for pattern in patterns {
1410 match pattern {
1411 QueryPattern::Or { .. } => {
1412 return Err(failure(
1413 DiagnosticCategory::InvalidContract,
1414 "query_plan_v1_disjunction_unsupported",
1415 "ordinary disjunction is additive in query-plan V2",
1416 ));
1417 }
1418 QueryPattern::Reachable { min_depth, .. } if *min_depth != 1 => {
1419 return Err(failure(
1420 DiagnosticCategory::InvalidContract,
1421 "query_plan_v1_reachable_min_depth",
1422 "V1 reachability always starts at one hop",
1423 ));
1424 }
1425 QueryPattern::Not { patterns } | QueryPattern::Try { patterns } => {
1426 validate_patterns(patterns)?;
1427 }
1428 QueryPattern::Isa { .. }
1429 | QueryPattern::Has { .. }
1430 | QueryPattern::Links { .. }
1431 | QueryPattern::Value { .. }
1432 | QueryPattern::Reachable { .. }
1433 | QueryPattern::FunctionCall { .. } => {}
1434 }
1435 }
1436 Ok(())
1437 }
1438
1439 for stage in pipeline {
1440 if let ReadStage::Match { patterns } = stage {
1441 validate_patterns(patterns)?;
1442 }
1443 }
1444 Ok(())
1445}
1446
1447#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
1453#[serde(transparent)]
1454pub struct InputRow {
1455 values: Vec<Option<CanonicalValue>>,
1456}
1457
1458impl InputRow {
1459 #[must_use]
1461 pub const fn new(values: Vec<Option<CanonicalValue>>) -> Self {
1462 Self { values }
1463 }
1464
1465 #[must_use]
1467 pub fn values(&self) -> &[Option<CanonicalValue>] {
1468 &self.values
1469 }
1470}
1471
1472#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
1474#[serde(rename_all = "snake_case")]
1475pub enum QueryOperation {
1476 Rows,
1478 Count,
1480 Exists,
1482}
1483
1484#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
1490pub struct QueryInvocation {
1491 inputs: Vec<InputRow>,
1492 operation: QueryOperation,
1493 plan_fingerprint: QueryPlanFingerprint,
1494}
1495
1496impl QueryInvocation {
1497 pub fn new(
1499 plan: &QueryPlan,
1500 operation: QueryOperation,
1501 inputs: Vec<InputRow>,
1502 ) -> Result<Self, Diagnostic> {
1503 Self::new_with_limits(plan, operation, inputs, StructuralLimits::CANONICAL)
1504 }
1505
1506 fn new_with_limits(
1507 plan: &QueryPlan,
1508 operation: QueryOperation,
1509 inputs: Vec<InputRow>,
1510 limits: StructuralLimits,
1511 ) -> Result<Self, Diagnostic> {
1512 if plan.inputs().is_empty() {
1513 if !inputs.is_empty() {
1514 return Err(failure(
1515 DiagnosticCategory::InvalidContract,
1516 "query_invocation_unexpected_inputs",
1517 "the plan declares no input columns yet the invocation carries rows",
1518 ));
1519 }
1520 } else {
1521 if inputs.is_empty() {
1522 return Err(failure(
1523 DiagnosticCategory::InvalidContract,
1524 "query_invocation_missing_inputs",
1525 "the plan declares input columns and requires at least one row",
1526 ));
1527 }
1528 if !limits.allows_input_rows(inputs.len()) {
1529 return Err(failure(
1530 DiagnosticCategory::ResourceLimit,
1531 "query_invocation_row_limit",
1532 "invocation input row count exceeds the structural ceiling",
1533 ));
1534 }
1535 let input_bytes = serde_json::to_vec(&inputs).map_err(|_| {
1536 failure(
1537 DiagnosticCategory::InvalidContract,
1538 "query_invocation_inputs_unencodable",
1539 "invocation input rows cannot be encoded",
1540 )
1541 })?;
1542 if !limits.allows_input_bytes(input_bytes.len()) {
1543 return Err(failure(
1544 DiagnosticCategory::ResourceLimit,
1545 "query_invocation_input_byte_limit",
1546 "invocation input rows exceed the structural byte ceiling",
1547 ));
1548 }
1549 for row in &inputs {
1550 if row.values().len() != plan.inputs().len() {
1551 return Err(failure(
1552 DiagnosticCategory::InvalidContract,
1553 "query_invocation_row_arity",
1554 "input row does not carry exactly the declared column set",
1555 ));
1556 }
1557 for (column, value) in plan.inputs().iter().zip(row.values()) {
1558 match value {
1559 None if column.optional() => {}
1560 None => {
1561 return Err(failure(
1562 DiagnosticCategory::InvalidContract,
1563 "query_invocation_missing_value",
1564 "a required input column carries no value",
1565 ));
1566 }
1567 Some(value) if value.value_type() == column.value_type() => {}
1568 Some(_) => {
1569 return Err(failure(
1570 DiagnosticCategory::InvalidContract,
1571 "query_invocation_value_type",
1572 "input value type differs from the declared column type",
1573 ));
1574 }
1575 }
1576 }
1577 }
1578 }
1579 Ok(Self {
1580 inputs,
1581 operation,
1582 plan_fingerprint: plan.fingerprint()?,
1583 })
1584 }
1585
1586 #[must_use]
1588 pub fn inputs(&self) -> &[InputRow] {
1589 &self.inputs
1590 }
1591
1592 #[must_use]
1594 pub const fn operation(&self) -> QueryOperation {
1595 self.operation
1596 }
1597
1598 #[must_use]
1600 pub const fn plan_fingerprint(&self) -> &QueryPlanFingerprint {
1601 &self.plan_fingerprint
1602 }
1603
1604 pub fn binds(&self, plan: &QueryPlan) -> Result<bool, Diagnostic> {
1606 Ok(self.plan_fingerprint == plan.fingerprint()?)
1607 }
1608
1609 #[must_use]
1620 pub fn transport_capabilities(&self) -> CapabilitySet {
1621 let mut capabilities = CapabilitySet::new();
1622 if self.inputs.len() > 1
1623 || self.inputs.first().is_some_and(|row| {
1624 row.values().iter().any(|value| {
1625 value.is_none() || matches!(value, Some(CanonicalValue::DateTimeTz(_)))
1626 })
1627 })
1628 {
1629 capabilities.insert(query_given_rows_capability());
1630 }
1631 capabilities
1632 }
1633}
1634
1635#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
1637#[serde(transparent)]
1638pub struct QueryPlanFingerprint(Fingerprint);
1639
1640impl QueryPlanFingerprint {
1641 pub fn compute(plan: &QueryPlan) -> Result<Self, Diagnostic> {
1643 let canonicalization = match plan.format() {
1644 QUERY_PLAN_FORMAT_V1 => QUERY_PLAN_CANONICALIZATION_V1,
1645 QUERY_PLAN_FORMAT_V2 => QUERY_PLAN_CANONICALIZATION_V2,
1646 _ => {
1647 return Err(failure(
1648 DiagnosticCategory::InvalidContract,
1649 "query_plan_format_unsupported",
1650 "query plan format has no fingerprint canonicalization",
1651 ));
1652 }
1653 };
1654 Ok(Self(Fingerprint::compute(
1655 FingerprintDomain::new(QUERY_PLAN_FINGERPRINT_DOMAIN)?,
1656 CanonicalizationVersion::new(canonicalization)?,
1657 None,
1658 &plan.canonical_bytes()?,
1659 )))
1660 }
1661
1662 #[must_use]
1664 pub const fn as_fingerprint(&self) -> &Fingerprint {
1665 &self.0
1666 }
1667}
1668
1669pub fn decode_query_plan(bytes: &[u8]) -> Result<QueryPlan, Diagnostic> {
1671 crate::query_plan_wire::decode_query_plan(bytes)
1672}
1673
1674pub fn decode_query_invocation(
1680 plan: &QueryPlan,
1681 bytes: &[u8],
1682) -> Result<QueryInvocation, Diagnostic> {
1683 crate::query_invocation_wire::decode_query_invocation(plan, bytes)
1684}
1685
1686fn validate_local_functions(
1687 functions: &[LocalFunction],
1688 limits: StructuralLimits,
1689 nodes: &mut usize,
1690) -> Result<(), Diagnostic> {
1691 if !limits.allows_bindings(functions.len().max(1)) {
1692 return Err(failure(
1693 DiagnosticCategory::ResourceLimit,
1694 "query_plan_local_function_limit",
1695 "plan-local function count exceeds the structural ceiling",
1696 ));
1697 }
1698 let mut names = BTreeSet::new();
1699 for function in functions {
1700 if !names.insert(function.name().clone()) {
1701 return Err(failure(
1702 DiagnosticCategory::InvalidContract,
1703 "query_plan_duplicate_local_function",
1704 "plan-local function names must be unique",
1705 ));
1706 }
1707 let bindings = function.bindings();
1708 if bindings.is_empty() || !limits.allows_bindings(bindings.len()) {
1709 return Err(failure(
1710 DiagnosticCategory::ResourceLimit,
1711 "query_plan_binding_limit",
1712 "plan binding count is empty or exceeds the structural ceiling",
1713 ));
1714 }
1715 let mut local_names = BTreeSet::new();
1716 for (index, binding) in bindings.iter().enumerate() {
1717 if usize::from(binding.id().get()) != index {
1718 return Err(failure(
1719 DiagnosticCategory::InvalidContract,
1720 "query_plan_bindings_not_dense",
1721 "plan binding IDs must be ordered dense zero-based ordinals",
1722 ));
1723 }
1724 validate_query_name_limit(
1725 binding.variable(),
1726 limits,
1727 "a local query variable name exceeds the structural ceiling",
1728 )?;
1729 if !local_names.insert(binding.variable().clone()) {
1730 return Err(failure(
1731 DiagnosticCategory::InvalidContract,
1732 "query_plan_duplicate_variable",
1733 "plan query variables must be unique",
1734 ));
1735 }
1736 }
1737 if function.parameters().is_empty() || function.parameters().len() > bindings.len() {
1738 return Err(failure(
1739 DiagnosticCategory::InvalidContract,
1740 "query_plan_local_function_parameters",
1741 "parameters must be a non-empty prefix of the local bindings",
1742 ));
1743 }
1744 if function.body().is_empty() || function.body().len() > limits.boolean_terms {
1745 return Err(failure(
1746 DiagnosticCategory::ResourceLimit,
1747 "query_plan_pattern_limit",
1748 "plan root conjunction is empty or exceeds the term ceiling",
1749 ));
1750 }
1751 for pattern in function.body() {
1752 if matches!(
1754 pattern,
1755 QueryPattern::Or { .. }
1756 | QueryPattern::Not { .. }
1757 | QueryPattern::Try { .. }
1758 | QueryPattern::Reachable { .. }
1759 | QueryPattern::FunctionCall { .. }
1760 ) {
1761 return Err(failure(
1762 DiagnosticCategory::InvalidContract,
1763 "query_plan_local_function_body_unsupported",
1764 "local bodies admit only isa, has, links, and value patterns",
1765 ));
1766 }
1767 inspect_pattern(pattern, 1, bindings.len(), 0, limits, nodes)?;
1771 }
1772 let returns = function.returns();
1773 check_binding(returns.input(), bindings.len())?;
1774 if !returns.reducer().total_without_groups() {
1775 return Err(failure(
1776 DiagnosticCategory::InvalidContract,
1777 "query_plan_local_function_return_partial",
1778 "local returns admit only reducers total on empty streams",
1779 ));
1780 }
1781 let declared_valid = match returns.reducer() {
1782 Reducer::Count => returns.value_type() == ValueTypeTag::Long,
1783 Reducer::Sum => matches!(
1784 returns.value_type(),
1785 ValueTypeTag::Long | ValueTypeTag::Double
1786 ),
1787 Reducer::Max | Reducer::Min | Reducer::Mean | Reducer::Median | Reducer::Std => false,
1788 };
1789 if !declared_valid {
1790 return Err(failure(
1791 DiagnosticCategory::InvalidContract,
1792 "query_plan_local_function_return_type",
1793 "declared return type does not fit the reducer",
1794 ));
1795 }
1796 }
1797 Ok(())
1798}
1799
1800fn validate_query_name_limit(
1801 name: &QueryVariable,
1802 limits: StructuralLimits,
1803 message: &'static str,
1804) -> Result<(), Diagnostic> {
1805 if name.as_str().len() > limits.output_name_bytes {
1806 return Err(failure(
1807 DiagnosticCategory::ResourceLimit,
1808 "query_plan_name_limit",
1809 message,
1810 ));
1811 }
1812 Ok(())
1813}
1814
1815fn validate_pipeline(
1816 pipeline: &[ReadStage],
1817 binding_count: usize,
1818 input_count: usize,
1819 limits: StructuralLimits,
1820 nodes: &mut usize,
1821) -> Result<(BTreeSet<BindingId>, BTreeSet<BindingId>, bool), Diagnostic> {
1822 let Some((first, rest)) = pipeline.split_first() else {
1823 return Err(failure(
1824 DiagnosticCategory::InvalidContract,
1825 "query_plan_empty_pipeline",
1826 "a read pipeline requires at least its match stage",
1827 ));
1828 };
1829 let ReadStage::Match { patterns } = first else {
1830 return Err(failure(
1831 DiagnosticCategory::InvalidContract,
1832 "query_plan_match_not_first",
1833 "the pattern conjunction must be the first pipeline stage",
1834 ));
1835 };
1836 if patterns.is_empty() || patterns.len() > limits.boolean_terms {
1837 return Err(failure(
1838 DiagnosticCategory::ResourceLimit,
1839 "query_plan_pattern_limit",
1840 "plan root conjunction is empty or exceeds the term ceiling",
1841 ));
1842 }
1843 for pattern in patterns {
1844 inspect_pattern(pattern, 1, binding_count, input_count, limits, nodes)?;
1845 }
1846
1847 let mut pattern_bound = BTreeSet::new();
1852 for pattern in patterns {
1853 collect_pattern_bindings(pattern, &mut pattern_bound);
1854 }
1855 let mut root_mandatory = BTreeSet::new();
1859 let mut scoped_positive = BTreeSet::new();
1860 for pattern in patterns {
1861 collect_direct_positive_bindings(pattern, &mut root_mandatory);
1862 collect_negation_positive_bindings(pattern, &mut scoped_positive);
1863 }
1864 let mut optional = BTreeSet::new();
1865 for pattern in patterns {
1866 let QueryPattern::Try { patterns } = pattern else {
1867 continue;
1868 };
1869 let mut body_positive = BTreeSet::new();
1870 for child in patterns {
1871 collect_direct_positive_bindings(child, &mut body_positive);
1872 }
1873 for local in body_positive.difference(&root_mandatory) {
1874 if !optional.insert(*local) {
1875 return Err(failure(
1876 DiagnosticCategory::InvalidContract,
1877 "query_plan_try_binding_shared",
1878 "an optional binding belongs to exactly one try body",
1879 ));
1880 }
1881 }
1882 }
1883 if !scoped_positive.is_disjoint(&optional) {
1884 return Err(failure(
1885 DiagnosticCategory::InvalidContract,
1886 "query_plan_try_binding_shared",
1887 "an optional binding cannot also be a negation-local witness",
1888 ));
1889 }
1890 let mut mandatory = root_mandatory;
1891 let mut previous_ordinal = 0u8;
1892 let mut has_sort = false;
1893 for stage in rest {
1894 let ordinal = stage.ordinal();
1895 if ordinal <= previous_ordinal {
1896 return Err(failure(
1897 DiagnosticCategory::InvalidContract,
1898 "query_plan_stage_order",
1899 "pipeline stages must follow the canonical order exactly once each",
1900 ));
1901 }
1902 previous_ordinal = ordinal;
1903 match stage {
1904 ReadStage::Match { .. } => unreachable!("ordinal zero cannot follow"),
1905 ReadStage::Select { bindings } => {
1906 let union: BTreeSet<BindingId> = mandatory.union(&optional).copied().collect();
1907 let selected = canonical_stage_set(bindings, &union, "select")?;
1908 mandatory.retain(|id| selected.contains(id));
1909 optional.retain(|id| selected.contains(id));
1910 }
1911 ReadStage::Require { bindings } => {
1912 let union: BTreeSet<BindingId> = mandatory.union(&optional).copied().collect();
1916 let required = canonical_stage_set(bindings, &union, "require")?;
1917 if required.iter().any(|id| optional.contains(id)) {
1918 return Err(failure(
1919 DiagnosticCategory::InvalidContract,
1920 "query_plan_require_optional_reserved",
1921 "requiring an optional binding is reserved in this vocabulary",
1922 ));
1923 }
1924 }
1925 ReadStage::Distinct => {}
1926 ReadStage::Reduce {
1927 assignments,
1928 groups,
1929 } => {
1930 if assignments.is_empty()
1931 || assignments.len() > limits.boolean_terms
1932 || groups.len() > limits.boolean_terms
1933 {
1934 return Err(failure(
1935 DiagnosticCategory::ResourceLimit,
1936 "query_plan_reduce_term_limit",
1937 "reduce has no assignments or exceeds the term ceiling",
1938 ));
1939 }
1940 let mut previous = None;
1941 let mut next_visible = BTreeSet::new();
1942 for group in groups {
1943 if previous.is_some_and(|previous: BindingId| previous >= *group) {
1944 return Err(failure(
1945 DiagnosticCategory::InvalidContract,
1946 "query_plan_stage_set_not_canonical",
1947 "stage binding sets must be strictly ascending",
1948 ));
1949 }
1950 previous = Some(*group);
1951 if !mandatory.contains(group) {
1954 return Err(failure(
1955 DiagnosticCategory::InvalidContract,
1956 "query_plan_stage_unknown_binding",
1957 "reduce groups a binding outside the mandatory row environment",
1958 ));
1959 }
1960 next_visible.insert(*group);
1961 }
1962 for assignment in assignments {
1963 check_binding(assignment.assigned(), binding_count)?;
1964 if pattern_bound.contains(&assignment.assigned())
1965 || !next_visible.insert(assignment.assigned())
1966 {
1967 return Err(failure(
1968 DiagnosticCategory::InvalidContract,
1969 "query_plan_reduce_assigned_bound",
1970 "reduce must assign a fresh binding free of patterns and groups",
1971 ));
1972 }
1973 match assignment.input() {
1974 Some(input) => {
1975 if !mandatory.contains(&input) && !optional.contains(&input) {
1976 return Err(failure(
1977 DiagnosticCategory::InvalidContract,
1978 "query_plan_stage_unknown_binding",
1979 "reduce consumes a binding outside the visible row environment",
1980 ));
1981 }
1982 if optional.contains(&input)
1986 && !assignment.reducer().total_without_groups()
1987 {
1988 return Err(failure(
1989 DiagnosticCategory::InvalidContract,
1990 "query_plan_reduce_optional_input",
1991 "this reducer is undefined over an optional input",
1992 ));
1993 }
1994 }
1995 None => {
1996 if assignment.reducer().requires_input() {
1997 return Err(failure(
1998 DiagnosticCategory::InvalidContract,
1999 "query_plan_reduce_missing_input",
2000 "this reducer consumes an input binding",
2001 ));
2002 }
2003 }
2004 }
2005 if groups.is_empty() && !assignment.reducer().total_without_groups() {
2006 return Err(failure(
2007 DiagnosticCategory::InvalidContract,
2008 "query_plan_reduce_requires_groups",
2009 "reducers undefined on empty streams require group bindings",
2010 ));
2011 }
2012 }
2013 mandatory = next_visible;
2014 optional.clear();
2015 }
2016 ReadStage::Sort { terms } => {
2017 if terms.is_empty() || !limits.allows_order_terms(terms.len()) {
2018 return Err(failure(
2019 DiagnosticCategory::ResourceLimit,
2020 "query_plan_sort_term_limit",
2021 "sort has no terms or exceeds the term ceiling",
2022 ));
2023 }
2024 let mut sorted = BTreeSet::new();
2025 for term in terms {
2026 if !mandatory.contains(&term.binding()) {
2029 return Err(failure(
2030 DiagnosticCategory::InvalidContract,
2031 "query_plan_stage_unknown_binding",
2032 "sort references a binding outside the mandatory row environment",
2033 ));
2034 }
2035 if !sorted.insert(term.binding()) {
2036 return Err(failure(
2037 DiagnosticCategory::InvalidContract,
2038 "query_plan_duplicate_sort_binding",
2039 "sort references one binding twice",
2040 ));
2041 }
2042 }
2043 has_sort = true;
2044 }
2045 ReadStage::Offset { .. } | ReadStage::Limit { .. } => {}
2046 }
2047 }
2048 Ok((mandatory, optional, has_sort))
2049}
2050
2051fn canonical_stage_set(
2052 bindings: &[BindingId],
2053 visible: &BTreeSet<BindingId>,
2054 stage: &'static str,
2055) -> Result<BTreeSet<BindingId>, Diagnostic> {
2056 if bindings.is_empty() {
2057 return Err(failure(
2058 DiagnosticCategory::InvalidContract,
2059 "query_plan_empty_stage_set",
2060 stage,
2061 ));
2062 }
2063 let mut set = BTreeSet::new();
2064 let mut previous = None;
2065 for binding in bindings {
2066 if previous.is_some_and(|previous: BindingId| previous >= *binding) {
2067 return Err(failure(
2068 DiagnosticCategory::InvalidContract,
2069 "query_plan_stage_set_not_canonical",
2070 "stage binding sets must be strictly ascending",
2071 ));
2072 }
2073 previous = Some(*binding);
2074 if !visible.contains(binding) {
2075 return Err(failure(
2076 DiagnosticCategory::InvalidContract,
2077 "query_plan_stage_unknown_binding",
2078 "stage references a binding outside the visible row environment",
2079 ));
2080 }
2081 set.insert(*binding);
2082 }
2083 Ok(set)
2084}
2085
2086fn inspect_pattern(
2087 pattern: &QueryPattern,
2088 depth: usize,
2089 binding_count: usize,
2090 input_count: usize,
2091 limits: StructuralLimits,
2092 nodes: &mut usize,
2093) -> Result<(), Diagnostic> {
2094 *nodes += 1;
2095 if !limits.allows_predicate_nodes(*nodes) {
2096 return Err(failure(
2097 DiagnosticCategory::ResourceLimit,
2098 "query_plan_pattern_node_limit",
2099 "plan pattern count exceeds the structural ceiling",
2100 ));
2101 }
2102 if !limits.allows_predicate_depth(depth) {
2103 return Err(failure(
2104 DiagnosticCategory::ResourceLimit,
2105 "query_plan_pattern_depth_limit",
2106 "plan pattern depth exceeds the structural ceiling",
2107 ));
2108 }
2109 match pattern {
2110 QueryPattern::Isa { binding, .. } => check_binding(*binding, binding_count),
2111 QueryPattern::Has {
2112 owner, attribute, ..
2113 } => {
2114 check_binding(*owner, binding_count)?;
2115 check_binding(*attribute, binding_count)
2116 }
2117 QueryPattern::Links {
2118 relation, players, ..
2119 } => {
2120 check_binding(*relation, binding_count)?;
2121 if players.is_empty() || players.len() > limits.boolean_terms {
2122 return Err(failure(
2123 DiagnosticCategory::ResourceLimit,
2124 "query_plan_role_player_limit",
2125 "links pattern has no players or exceeds the term ceiling",
2126 ));
2127 }
2128 for player in players {
2129 check_binding(player.player(), binding_count)?;
2130 }
2131 Ok(())
2132 }
2133 QueryPattern::Value { left, right, .. } => {
2134 check_operand(left, binding_count, input_count)?;
2135 check_operand(right, binding_count, input_count)
2136 }
2137 QueryPattern::Or { branches } => {
2138 if branches.is_empty()
2139 || branches.len() > limits.boolean_terms
2140 || branches
2141 .iter()
2142 .any(|branch| branch.is_empty() || branch.len() > limits.boolean_terms)
2143 {
2144 return Err(failure(
2145 DiagnosticCategory::ResourceLimit,
2146 "query_plan_disjunction_term_limit",
2147 "disjunction branches are empty or exceed the boolean-term ceiling",
2148 ));
2149 }
2150 for branch in branches {
2151 for child in branch {
2152 inspect_pattern(child, depth + 1, binding_count, input_count, limits, nodes)?;
2153 }
2154 }
2155 Ok(())
2156 }
2157 QueryPattern::Not { patterns } => {
2158 if patterns.is_empty() || patterns.len() > limits.boolean_terms {
2159 return Err(failure(
2160 DiagnosticCategory::ResourceLimit,
2161 "query_plan_negation_term_limit",
2162 "negation is empty or exceeds the boolean-term ceiling",
2163 ));
2164 }
2165 for child in patterns {
2166 inspect_pattern(child, depth + 1, binding_count, input_count, limits, nodes)?;
2167 }
2168 Ok(())
2169 }
2170 QueryPattern::Try { patterns } => {
2171 if depth > 1 {
2172 return Err(failure(
2173 DiagnosticCategory::InvalidContract,
2174 "query_plan_try_not_root",
2175 "optional blocks are admitted only in the root conjunction",
2176 ));
2177 }
2178 if patterns.is_empty() || patterns.len() > limits.boolean_terms {
2179 return Err(failure(
2180 DiagnosticCategory::ResourceLimit,
2181 "query_plan_try_term_limit",
2182 "optional block is empty or exceeds the boolean-term ceiling",
2183 ));
2184 }
2185 for child in patterns {
2186 if matches!(
2187 child,
2188 QueryPattern::Or { .. }
2189 | QueryPattern::Not { .. }
2190 | QueryPattern::Try { .. }
2191 | QueryPattern::Reachable { .. }
2192 | QueryPattern::FunctionCall { .. }
2193 ) {
2194 return Err(failure(
2195 DiagnosticCategory::InvalidContract,
2196 "query_plan_try_body_unsupported",
2197 "the first optional vocabulary admits only isa, has, links, and value patterns",
2198 ));
2199 }
2200 inspect_pattern(child, depth + 1, binding_count, input_count, limits, nodes)?;
2201 }
2202 Ok(())
2203 }
2204 QueryPattern::Reachable {
2205 min_depth,
2206 max_depth,
2207 source,
2208 target,
2209 ..
2210 } => {
2211 if depth > 1 {
2212 return Err(failure(
2213 DiagnosticCategory::InvalidContract,
2214 "query_plan_reachable_not_root",
2215 "bounded reachability is admitted only in the root conjunction",
2216 ));
2217 }
2218 if *min_depth == 1 && *max_depth == 0 {
2219 return Err(failure(
2220 DiagnosticCategory::ResourceLimit,
2221 "query_plan_reachable_depth",
2222 "reachability requires a finite hop bound within the depth ceiling",
2223 ));
2224 }
2225 if min_depth > max_depth {
2226 return Err(failure(
2227 DiagnosticCategory::InvalidContract,
2228 "query_plan_reachable_bounds",
2229 "reachability minimum depth must not exceed its maximum depth",
2230 ));
2231 }
2232 if !limits.allows_predicate_depth(usize::from(*max_depth)) {
2233 return Err(failure(
2234 DiagnosticCategory::ResourceLimit,
2235 "query_plan_reachable_depth",
2236 "reachability requires a finite hop bound within the depth ceiling",
2237 ));
2238 }
2239 let first_positive = usize::from((*min_depth).max(1));
2244 let bound = usize::from(*max_depth);
2245 let expanded_hops = if first_positive <= bound {
2246 (first_positive..=bound).fold(0usize, usize::saturating_add)
2247 } else {
2248 0
2249 };
2250 let expanded_clauses = expanded_hops.saturating_add(usize::from(*min_depth == 0));
2251 *nodes = nodes.saturating_add(expanded_clauses.saturating_sub(1));
2252 if !limits.allows_predicate_nodes(*nodes) {
2253 return Err(failure(
2254 DiagnosticCategory::ResourceLimit,
2255 "query_plan_reachable_expansion_limit",
2256 "reachability expansion exceeds the plan pattern-node ceiling",
2257 ));
2258 }
2259 check_binding(*source, binding_count)?;
2260 check_binding(*target, binding_count)
2261 }
2262 QueryPattern::FunctionCall {
2263 arguments,
2264 assigned,
2265 ..
2266 } => {
2267 if depth > 1 {
2268 return Err(failure(
2269 DiagnosticCategory::InvalidContract,
2270 "query_plan_function_in_negation",
2271 "function calls are admitted only in the root conjunction",
2272 ));
2273 }
2274 if arguments.len() > limits.boolean_terms {
2275 return Err(failure(
2276 DiagnosticCategory::ResourceLimit,
2277 "query_plan_function_argument_limit",
2278 "function call arguments exceed the term ceiling",
2279 ));
2280 }
2281 for argument in arguments {
2282 check_operand(argument, binding_count, input_count)?;
2283 }
2284 check_binding(*assigned, binding_count)
2285 }
2286 }
2287}
2288
2289fn collect_pattern_bindings(pattern: &QueryPattern, bindings: &mut BTreeSet<BindingId>) {
2290 let mut operand = |operand: &QueryOperand| {
2291 if let QueryOperand::Binding { binding } = operand {
2292 bindings.insert(*binding);
2293 }
2294 };
2295 match pattern {
2296 QueryPattern::Isa { binding, .. } => {
2297 bindings.insert(*binding);
2298 }
2299 QueryPattern::Has {
2300 owner, attribute, ..
2301 } => {
2302 bindings.insert(*owner);
2303 bindings.insert(*attribute);
2304 }
2305 QueryPattern::Links {
2306 relation, players, ..
2307 } => {
2308 bindings.insert(*relation);
2309 for player in players {
2310 bindings.insert(player.player());
2311 }
2312 }
2313 QueryPattern::Value { left, right, .. } => {
2314 operand(left);
2315 operand(right);
2316 }
2317 QueryPattern::Or { branches } => {
2318 for branch in branches {
2319 for child in branch {
2320 collect_pattern_bindings(child, bindings);
2321 }
2322 }
2323 }
2324 QueryPattern::Not { patterns } | QueryPattern::Try { patterns } => {
2325 for child in patterns {
2326 collect_pattern_bindings(child, bindings);
2327 }
2328 }
2329 QueryPattern::Reachable { source, target, .. } => {
2330 bindings.insert(*source);
2331 bindings.insert(*target);
2332 }
2333 QueryPattern::FunctionCall {
2334 arguments,
2335 assigned,
2336 ..
2337 } => {
2338 for argument in arguments {
2339 operand(argument);
2340 }
2341 bindings.insert(*assigned);
2342 }
2343 }
2344}
2345
2346fn collect_direct_positive_bindings(pattern: &QueryPattern, bindings: &mut BTreeSet<BindingId>) {
2350 match pattern {
2351 QueryPattern::Isa { binding, .. } => {
2352 bindings.insert(*binding);
2353 }
2354 QueryPattern::Has {
2355 owner, attribute, ..
2356 } => {
2357 bindings.extend([*owner, *attribute]);
2358 }
2359 QueryPattern::Links {
2360 relation, players, ..
2361 } => {
2362 bindings.insert(*relation);
2363 bindings.extend(players.iter().map(AssertionRolePlayer::player));
2364 }
2365 QueryPattern::Reachable { source, target, .. } => {
2366 bindings.extend([*source, *target]);
2367 }
2368 QueryPattern::FunctionCall { assigned, .. } => {
2369 bindings.insert(*assigned);
2370 }
2371 QueryPattern::Or { branches } => {
2372 let mut branches = branches.iter().map(|patterns| {
2373 let mut branch = BTreeSet::new();
2374 for pattern in patterns {
2375 collect_direct_positive_bindings(pattern, &mut branch);
2376 }
2377 branch
2378 });
2379 if let Some(mut intersection) = branches.next() {
2380 for branch in branches {
2381 intersection.retain(|binding| branch.contains(binding));
2382 }
2383 bindings.extend(intersection);
2384 }
2385 }
2386 QueryPattern::Value { .. } | QueryPattern::Not { .. } | QueryPattern::Try { .. } => {}
2387 }
2388}
2389
2390fn collect_negation_positive_bindings(pattern: &QueryPattern, bindings: &mut BTreeSet<BindingId>) {
2394 match pattern {
2395 QueryPattern::Not { patterns } => {
2396 for child in patterns {
2397 collect_direct_positive_bindings(child, bindings);
2398 collect_negation_positive_bindings(child, bindings);
2399 }
2400 }
2401 QueryPattern::Or { branches } => {
2402 for branch in branches {
2403 for child in branch {
2404 collect_negation_positive_bindings(child, bindings);
2405 }
2406 }
2407 }
2408 QueryPattern::Isa { .. }
2409 | QueryPattern::Has { .. }
2410 | QueryPattern::Links { .. }
2411 | QueryPattern::Value { .. }
2412 | QueryPattern::Try { .. }
2413 | QueryPattern::Reachable { .. }
2414 | QueryPattern::FunctionCall { .. } => {}
2415 }
2416}
2417
2418fn check_operand(
2419 operand: &QueryOperand,
2420 binding_count: usize,
2421 input_count: usize,
2422) -> Result<(), Diagnostic> {
2423 match operand {
2424 QueryOperand::Binding { binding } => check_binding(*binding, binding_count),
2425 QueryOperand::Literal { .. } => Ok(()),
2426 QueryOperand::Input { column } => {
2427 if usize::from(column.get()) < input_count {
2428 Ok(())
2429 } else {
2430 Err(failure(
2431 DiagnosticCategory::InvalidContract,
2432 "query_plan_unknown_input_column",
2433 "pattern references an undeclared input column",
2434 ))
2435 }
2436 }
2437 }
2438}
2439
2440fn check_binding(binding: BindingId, binding_count: usize) -> Result<(), Diagnostic> {
2441 if usize::from(binding.get()) < binding_count {
2442 Ok(())
2443 } else {
2444 Err(failure(
2445 DiagnosticCategory::InvalidContract,
2446 "query_plan_unknown_binding",
2447 "pattern references an undeclared binding",
2448 ))
2449 }
2450}
2451
2452fn derive_capabilities(
2453 pipeline: &[ReadStage],
2454 functions: &[LocalFunction],
2455 inputs: &[InputColumn],
2456 output: &QueryOutput,
2457 compatibility: Option<&QueryPlanV2Compatibility>,
2458) -> Result<CapabilitySet, Diagnostic> {
2459 let mut capabilities = CapabilitySet::new();
2460 insert_capability(&mut capabilities, CAP_PLAN)?;
2461 if !functions.is_empty() {
2462 insert_capability(&mut capabilities, CAP_LOCAL_FUNCTIONS)?;
2463 for function in functions {
2464 for pattern in function.body() {
2465 collect_pattern_capabilities(pattern, &mut capabilities)?;
2466 }
2467 }
2468 }
2469 match output {
2470 QueryOutput::Rows { .. } => {
2471 insert_capability(&mut capabilities, CAP_OUTPUT_ROWS)?;
2472 }
2473 QueryOutput::Documents { .. } => {
2474 insert_capability(&mut capabilities, CAP_OUTPUT_DOCUMENTS)?;
2475 }
2476 }
2477 if !inputs.is_empty() {
2478 insert_capability(&mut capabilities, CAP_INPUT_COLUMNS)?;
2479 }
2480 for stage in pipeline {
2481 match stage {
2482 ReadStage::Match { patterns } => {
2483 for pattern in patterns {
2484 collect_pattern_capabilities(pattern, &mut capabilities)?;
2485 }
2486 }
2487 ReadStage::Select { .. } => {
2488 insert_capability(&mut capabilities, CAP_STAGE_SELECT)?;
2489 }
2490 ReadStage::Require { .. } => {
2491 insert_capability(&mut capabilities, CAP_STAGE_REQUIRE)?;
2492 }
2493 ReadStage::Distinct => {
2494 insert_capability(&mut capabilities, CAP_STAGE_DISTINCT)?;
2495 }
2496 ReadStage::Reduce { .. } => {
2497 insert_capability(&mut capabilities, CAP_STAGE_REDUCE)?;
2498 }
2499 ReadStage::Sort { .. } => {
2500 insert_capability(&mut capabilities, CAP_STAGE_SORT)?;
2501 }
2502 ReadStage::Offset { .. } => {
2503 insert_capability(&mut capabilities, CAP_STAGE_OFFSET)?;
2504 }
2505 ReadStage::Limit { .. } => {
2506 insert_capability(&mut capabilities, CAP_STAGE_LIMIT)?;
2507 }
2508 }
2509 }
2510 if let Some(compatibility) = compatibility {
2511 insert_capability(&mut capabilities, v2::CAP_PLAN_V2)?;
2512 compatibility.add_capabilities(&mut capabilities)?;
2513 let vocabulary = query_plan_v2_capability_vocabulary();
2514 let unknown = capabilities.missing_from(&vocabulary);
2515 if !unknown.is_empty() {
2516 return Err(failure(
2517 DiagnosticCategory::Integrity,
2518 "query_plan_v2_capability_inventory_incomplete",
2519 "V2 syntax derived a capability absent from the exhaustive vocabulary",
2520 ));
2521 }
2522 }
2523 Ok(capabilities)
2524}
2525
2526fn collect_pattern_capabilities(
2527 pattern: &QueryPattern,
2528 capabilities: &mut CapabilitySet,
2529) -> Result<(), Diagnostic> {
2530 match pattern {
2531 QueryPattern::Isa {
2532 include_subtypes, ..
2533 } => {
2534 insert_capability(capabilities, CAP_ISA)?;
2535 if *include_subtypes {
2536 insert_capability(capabilities, CAP_ISA_SUBTYPES)?;
2537 }
2538 }
2539 QueryPattern::Has { .. } => insert_capability(capabilities, CAP_HAS)?,
2540 QueryPattern::Links { .. } => insert_capability(capabilities, CAP_LINKS)?,
2541 QueryPattern::Value { .. } => insert_capability(capabilities, CAP_VALUE)?,
2542 QueryPattern::Or { branches } => {
2543 insert_capability(capabilities, CAP_DISJUNCTION)?;
2544 for branch in branches {
2545 for child in branch {
2546 collect_pattern_capabilities(child, capabilities)?;
2547 }
2548 }
2549 }
2550 QueryPattern::Try { patterns } => {
2551 insert_capability(capabilities, CAP_TRY)?;
2552 for child in patterns {
2553 collect_pattern_capabilities(child, capabilities)?;
2554 }
2555 }
2556 QueryPattern::Reachable { .. } => {
2557 insert_capability(capabilities, CAP_REACHABLE)?;
2558 }
2559 QueryPattern::Not { patterns } => {
2560 insert_capability(capabilities, CAP_NEGATION)?;
2561 for child in patterns {
2562 collect_pattern_capabilities(child, capabilities)?;
2563 }
2564 }
2565 QueryPattern::FunctionCall { .. } => {
2566 insert_capability(capabilities, CAP_FUNCTION_CALL)?;
2567 }
2568 }
2569 Ok(())
2570}
2571
2572pub(crate) fn insert_capability(
2573 capabilities: &mut CapabilitySet,
2574 value: &'static str,
2575) -> Result<(), Diagnostic> {
2576 capabilities.insert(CapabilityId::new(value)?);
2577 Ok(())
2578}
2579
2580pub(crate) fn failure(
2581 category: DiagnosticCategory,
2582 code: &'static str,
2583 message: &'static str,
2584) -> Diagnostic {
2585 Diagnostic::new(
2586 category,
2587 DiagnosticCode::new(code).expect("static query-plan diagnostic code"),
2588 message,
2589 )
2590}