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