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