Skip to main content

type_bridge_contract/
query_plan.rs

1//! Reusable typed query plans: the first public V2 read vocabulary.
2//!
3//! A [`QueryPlan`](crate::query_plan::QueryPlan) extends the minimal
4//! migration-assertion primitives into a
5//! reusable, invocation-free read program: dense typed bindings, declared
6//! typed inputs, one closed pattern conjunction, and an ordered pipeline of
7//! V1-parity stages (`select`, `require`, `distinct`, `sort`, `offset`,
8//! `limit`) ending in one explicit output. Later vocabulary — functions,
9//! reductions, documents, reachability — stays reserved behind independent
10//! capabilities and is absent from this format, not defaulted.
11//!
12//! The persisted assertion algebra keeps its exact meaning: this
13//! module defines its own pattern vocabulary rather than widening
14//! [`AssertionPattern`](crate::migration_assertion::AssertionPattern).
15
16use 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
45/// The exact wire discriminator for first-format query plans.
46pub const QUERY_PLAN_FORMAT_V1: &str = "typebridge.query-plan/v1";
47/// The exact wire discriminator for additive query plans.
48pub const QUERY_PLAN_FORMAT_V2: &str = "typebridge.query-plan/v2";
49/// Domain separating query-plan fingerprints from every other digest.
50pub const QUERY_PLAN_FINGERPRINT_DOMAIN: &str = "typebridge.query.plan";
51/// V1 canonicalization version retained for source compatibility.
52pub const QUERY_PLAN_CANONICALIZATION: &str = "typebridge.query-plan-c14n/v1";
53/// The V1 canonicalization version.
54pub const QUERY_PLAN_CANONICALIZATION_V1: &str = QUERY_PLAN_CANONICALIZATION;
55/// The additive V2 canonicalization version.
56pub 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/// Return every capability the first query-plan vocabulary can require.
83#[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/// Return every capability the public low-level V2 authoring surface can derive.
114///
115/// This is deliberately narrower than [`query_plan_v2_capability_vocabulary`]:
116/// native low-level authoring adds only the V2 plan discriminator and closed
117/// disjunction syntax to the first query-plan vocabulary. Model-query
118/// compatibility capabilities are authored through a separate facade.
119#[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/// Return every capability an additive V2 plan can syntax-derive.
130///
131/// The V1 vocabulary stays byte- and value-exact. This V2 inventory is a
132/// separate completeness guard containing the V1 vocabulary plus the closed
133/// model-query compatibility vocabulary.
134#[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/// Return the transport capability exact `given` invocations require.
170///
171/// Batches, explicit absence, datetime-tz values, and every schema-function
172/// call select this transport. The first three are derived by
173/// [`QueryInvocation::transport_capabilities`]; the last is plan syntax and
174/// therefore appears in the plan's required capabilities even when its exact
175/// call graph has no scalar input cell. Executors advertise it only when their
176/// provider can transport explicit input rows, making request-level admission
177/// consistent across direct and remote execution.
178#[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/// One dense typed input column identity.
184#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
185#[serde(transparent)]
186pub struct InputColumnId(u16);
187
188impl InputColumnId {
189    /// Construct a dense input column ordinal.
190    #[must_use]
191    pub const fn new(value: u16) -> Self {
192        Self(value)
193    }
194
195    /// Return the dense ordinal.
196    #[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/// One typed input column declaration owned by the reusable plan.
209///
210/// Input declarations belong to the plan; input rows belong to the
211/// invocation. Changing an input value never creates a new plan identity.
212#[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    /// Construct one typed input column.
222    #[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    /// Return the dense column identity.
238    #[must_use]
239    pub const fn id(&self) -> InputColumnId {
240        self.id
241    }
242
243    /// Return the binding-facing public column name.
244    #[must_use]
245    pub const fn public_name(&self) -> &QueryVariable {
246        &self.public_name
247    }
248
249    /// Return the exact scalar type every row value must carry.
250    #[must_use]
251    pub const fn value_type(&self) -> ValueTypeTag {
252        self.value_type
253    }
254
255    /// Return whether invocation rows may omit this column's value.
256    #[must_use]
257    pub const fn optional(&self) -> bool {
258        self.optional
259    }
260}
261
262/// One typed operand inside a query value comparison.
263#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
264#[serde(tag = "kind", rename_all = "snake_case")]
265pub enum QueryOperand {
266    /// Read the scalar value of an attribute binding.
267    Binding {
268        /// The referenced dense binding.
269        binding: BindingId,
270    },
271    /// Compare with an exact canonical literal.
272    Literal {
273        /// The canonical scalar literal.
274        value: CanonicalValue,
275    },
276    /// Read one declared invocation input column.
277    Input {
278        /// The referenced dense input column.
279        column: InputColumnId,
280    },
281}
282
283/// The closed typed pattern algebra of the first public vocabulary.
284///
285/// Exactly the V1-parity graph shapes: typed `isa` with optional subtype
286/// inclusion, effective-ownership `has`, role-qualified `links`, typed value
287/// comparison, and negation over a closed conjunction. No variant accepts an
288/// arbitrary TypeQL fragment.
289#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
290#[serde(tag = "kind", rename_all = "snake_case")]
291pub enum QueryPattern {
292    /// Constrain a binding to one schema type, optionally including subtypes.
293    Isa {
294        /// The constrained binding.
295        binding: BindingId,
296        /// Whether transitive subtypes are admitted.
297        include_subtypes: bool,
298        /// The exact schema type.
299        type_id: TypeId,
300    },
301    /// Bind an owned attribute through an effective ownership.
302    Has {
303        /// The bound attribute instance.
304        attribute: BindingId,
305        /// The exact attribute type.
306        attribute_id: AttributeId,
307        /// The owning binding.
308        owner: BindingId,
309    },
310    /// Bind a relation and role-qualified players.
311    Links {
312        /// Role-qualified player bindings.
313        players: Vec<AssertionRolePlayer>,
314        /// The bound relation instance.
315        relation: BindingId,
316        /// The exact relation type.
317        relation_id: TypeId,
318    },
319    /// Compare two exact typed scalar operands.
320    Value {
321        /// The closed comparator.
322        comparator: ValueComparator,
323        /// The left operand.
324        left: QueryOperand,
325        /// The right operand.
326        right: QueryOperand,
327    },
328    /// Require at least one child pattern.
329    ///
330    /// A binding is positively established after this pattern only when every
331    /// branch establishes it. Branch-local bindings never leak into the outer
332    /// row environment.
333    Or {
334        /// Non-empty alternative conjunctions in source order.
335        branches: Vec<Vec<QueryPattern>>,
336    },
337    /// Negate a closed nested conjunction.
338    Not {
339        /// The negated conjunction.
340        patterns: Vec<QueryPattern>,
341    },
342    /// Optionally match a nested conjunction without filtering rows.
343    ///
344    /// Bindings established only inside the body survive as optional: rows
345    /// where the body matched carry them, other rows carry an explicit
346    /// absence. The first optional vocabulary admits `isa`, `has`, `links`,
347    /// and value comparisons in the body, at the root conjunction only.
348    Try {
349        /// The optional conjunction.
350        patterns: Vec<QueryPattern>,
351    },
352    /// Existential bounded reachability along one role-directed relation.
353    ///
354    /// Holds when the target is reachable from the source in
355    /// `min_depth..=max_depth` hops, each hop one relation instance from the
356    /// `role_from` player to the `role_to` player. A zero-hop branch is exact
357    /// concept identity. Positive branches are finite directed walks: repeated
358    /// vertices or relation instances are admitted, so cycles cannot make
359    /// execution unbounded. Proof-path identity is existential and never
360    /// enters the selected row. Lowering unrolls every admitted length
361    /// provider-side, never by repeated client queries.
362    Reachable {
363        /// The inclusive minimum hop count. Zero admits source/target identity.
364        min_depth: u8,
365        /// The inclusive finite maximum hop count.
366        max_depth: u8,
367        /// The exact relation type of every hop.
368        relation: TypeId,
369        /// The role the hop starts from.
370        role_from: RoleId,
371        /// The role the hop arrives at.
372        role_to: RoleId,
373        /// The established start binding.
374        source: BindingId,
375        /// The established end binding.
376        target: BindingId,
377    },
378    /// Assign one scalar schema-function result to a binding.
379    ///
380    /// The first function vocabulary admits scalar, non-optional returns
381    /// only; tuple and stream returns stay reserved behind later
382    /// capabilities.
383    FunctionCall {
384        /// Ordered call arguments.
385        arguments: Vec<QueryOperand>,
386        /// The binding assigned from the scalar return.
387        assigned: BindingId,
388        /// The exact schema function identity.
389        function: FunctionId,
390    },
391}
392
393/// The sort direction of one order term.
394#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
395#[serde(rename_all = "snake_case")]
396pub enum OrderDirection {
397    /// Smallest first.
398    Ascending,
399    /// Largest first.
400    Descending,
401}
402
403/// One typed sort key.
404#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
405pub struct OrderTerm {
406    binding: BindingId,
407    direction: OrderDirection,
408}
409
410impl OrderTerm {
411    /// Construct one sort key.
412    #[must_use]
413    pub const fn new(binding: BindingId, direction: OrderDirection) -> Self {
414        Self { binding, direction }
415    }
416
417    /// Return the sorted binding.
418    #[must_use]
419    pub const fn binding(&self) -> BindingId {
420        self.binding
421    }
422
423    /// Return the sort direction.
424    #[must_use]
425    pub const fn direction(&self) -> OrderDirection {
426        self.direction
427    }
428}
429
430/// The closed reducer vocabulary of the first reduce stage.
431///
432/// Reducers that can observe an empty stream (`max`, `min`, `mean`,
433/// `median`, `std`) are admitted only under group bindings, where every
434/// group is witnessed by at least one row; `count` and `sum` stay total on
435/// empty streams.
436#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
437#[serde(rename_all = "snake_case")]
438pub enum Reducer {
439    /// Count surviving rows.
440    Count,
441    /// The largest input value.
442    Max,
443    /// The arithmetic mean of input values.
444    Mean,
445    /// The statistical median of input values.
446    Median,
447    /// The smallest input value.
448    Min,
449    /// The sample standard deviation of input values.
450    Std,
451    /// The total of input values.
452    Sum,
453}
454
455impl Reducer {
456    /// Whether this reducer yields a defined result on an empty stream.
457    #[must_use]
458    pub const fn total_without_groups(self) -> bool {
459        matches!(self, Self::Count | Self::Sum)
460    }
461
462    /// Whether this reducer consumes an input binding.
463    #[must_use]
464    pub const fn requires_input(self) -> bool {
465        !matches!(self, Self::Count)
466    }
467}
468
469/// One reduce-stage assignment producing a fresh value binding.
470#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
471pub struct ReduceAssignment {
472    assigned: BindingId,
473    input: Option<BindingId>,
474    reducer: Reducer,
475}
476
477impl ReduceAssignment {
478    /// Assign one reducer result to a fresh declared binding.
479    #[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    /// Return the fresh binding the reducer result is assigned to.
489    #[must_use]
490    pub const fn assigned(&self) -> BindingId {
491        self.assigned
492    }
493
494    /// Return the reduced input binding, absent for bare `count`.
495    #[must_use]
496    pub const fn input(&self) -> Option<BindingId> {
497        self.input
498    }
499
500    /// Return the reducer applied within each group.
501    #[must_use]
502    pub const fn reducer(&self) -> Reducer {
503        self.reducer
504    }
505}
506
507/// One ordered read stage of the first public vocabulary.
508///
509/// The canonical stage order is fixed: one `match`, then at most one each of
510/// `select`, `require`, `distinct`, `reduce`, `sort`, `offset`, and `limit`,
511/// in that order. Later stage kinds (documents) are reserved.
512#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
513#[serde(tag = "kind", rename_all = "snake_case")]
514pub enum ReadStage {
515    /// The single pattern conjunction producing the row environment.
516    Match {
517        /// The closed positive/negative conjunction.
518        patterns: Vec<QueryPattern>,
519    },
520    /// Restrict the visible row environment to these bindings.
521    Select {
522        /// Canonical-sorted visible bindings.
523        bindings: Vec<BindingId>,
524    },
525    /// Require optional bindings to be present in every surviving row.
526    Require {
527        /// Canonical-sorted required bindings.
528        bindings: Vec<BindingId>,
529    },
530    /// Deduplicate the visible row environment.
531    Distinct,
532    /// Collapse rows into grouped reducer results.
533    ///
534    /// The surviving row environment is exactly the group bindings plus the
535    /// assigned reducer results; every other binding ends here.
536    Reduce {
537        /// Reducer assignments, each producing a fresh value binding.
538        assignments: Vec<ReduceAssignment>,
539        /// Canonical-sorted group-key bindings; empty for a global reduce.
540        groups: Vec<BindingId>,
541    },
542    /// Impose an explicit total row order.
543    Sort {
544        /// Ordered sort keys.
545        terms: Vec<OrderTerm>,
546    },
547    /// Skip an exact number of ordered rows.
548    Offset {
549        /// The number of rows skipped.
550        rows: u64,
551    },
552    /// Truncate to an exact number of ordered rows.
553    Limit {
554        /// The maximum number of rows returned.
555        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/// The declared scalar return of one plan-local function.
575///
576/// The first local vocabulary admits only reducers that stay total on an
577/// empty body stream (`count`, `sum`), so every call yields a value.
578#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
579pub struct LocalReturn {
580    input: BindingId,
581    reducer: Reducer,
582    value_type: ValueTypeTag,
583}
584
585impl LocalReturn {
586    /// Declare one total reducer return over a body binding.
587    #[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    /// Return the reduced body binding.
597    #[must_use]
598    pub const fn input(&self) -> BindingId {
599        self.input
600    }
601
602    /// Return the reducer.
603    #[must_use]
604    pub const fn reducer(&self) -> Reducer {
605        self.reducer
606    }
607
608    /// Return the declared scalar result type.
609    #[must_use]
610    pub const fn value_type(&self) -> ValueTypeTag {
611        self.value_type
612    }
613}
614
615/// One plan-local function defined from the closed pattern algebra.
616///
617/// The function owns a private dense binding space; its parameters are the
618/// leading bindings, each declared against one schema type label. The first
619/// local vocabulary keeps bodies flat (`isa`, `has`, `links`, value) and
620/// returns one total reducer result.
621#[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    /// Declare one plan-local function.
632    #[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    /// Return the function name.
650    #[must_use]
651    pub const fn name(&self) -> &FunctionId {
652        &self.name
653    }
654
655    /// Return the private dense binding table.
656    #[must_use]
657    pub fn bindings(&self) -> &[AssertionBinding] {
658        &self.bindings
659    }
660
661    /// Return the schema type label of each leading parameter binding.
662    #[must_use]
663    pub fn parameters(&self) -> &[Label] {
664        &self.parameters
665    }
666
667    /// Return the closed body conjunction.
668    #[must_use]
669    pub fn body(&self) -> &[QueryPattern] {
670        &self.body
671    }
672
673    /// Return the declared reducer result.
674    #[must_use]
675    pub const fn returns(&self) -> &LocalReturn {
676        &self.returns
677    }
678}
679
680/// One value source of a fetched document field.
681#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
682#[serde(tag = "kind", rename_all = "snake_case")]
683pub enum DocumentSource {
684    /// The scalar value of one visible binding; optional bindings
685    /// fetch as an explicit JSON null where absent.
686    Binding {
687        /// The projected scalar binding.
688        binding: BindingId,
689    },
690    /// Every value of one attribute on one mandatory owner binding,
691    /// fetched as a typed list (empty where the owner has none).
692    AttributeList {
693        /// The listed attribute.
694        attribute: AttributeId,
695        /// The mandatory owner binding.
696        owner: BindingId,
697    },
698}
699
700/// One key-value field of a fetched document.
701#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
702pub struct DocumentField {
703    key: QueryVariable,
704    source: DocumentSource,
705}
706
707impl DocumentField {
708    /// Bind one document key to its value source.
709    #[must_use]
710    pub const fn new(key: QueryVariable, source: DocumentSource) -> Self {
711        Self { key, source }
712    }
713
714    /// Return the document key.
715    #[must_use]
716    pub const fn key(&self) -> &QueryVariable {
717        &self.key
718    }
719
720    /// Return the value source.
721    #[must_use]
722    pub const fn source(&self) -> &DocumentSource {
723        &self.source
724    }
725}
726
727/// The explicit output category of the first public vocabulary.
728#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
729#[serde(tag = "kind", rename_all = "snake_case")]
730pub enum QueryOutput {
731    /// Project ordered typed row columns.
732    Rows {
733        /// The visible bindings projected, in output order.
734        columns: Vec<BindingId>,
735    },
736    /// Fetch one flat typed JSON document per surviving row.
737    Documents {
738        /// The document fields, in output order.
739        fields: Vec<DocumentField>,
740    },
741}
742
743/// A reusable, invocation-free typed read program.
744#[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    /// Validate and construct one canonical plan under fixed protocol limits.
760    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    /// Validate and construct one plan carrying plan-local functions.
780    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    /// Validate and construct one additive V2 low-level plan.
801    ///
802    /// This constructor carries the required empty V2 compatibility object;
803    /// there is no host-controlled format switch or legacy mode.
804    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    /// Validate and construct one additive V2 plan with local functions and
823    /// an explicit Rust-owned compatibility contract.
824    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    /// Re-check this plan's whole structure under caller-supplied limits.
897    ///
898    /// The exact construction-time traversal runs again — bindings,
899    /// inputs, the root pipeline, and every local function under one
900    /// aggregate predicate-node budget — so a caller passing stricter
901    /// [`StructuralLimits`] gets every field enforced, not a subset.
902    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    /// The construction-time structural traversal, shared with
915    /// [`Self::check_structural_limits`] so caller-supplied limits enforce
916    /// exactly what construction enforces.
917    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                            // A list reaches through its owner per row;
1052                            // absence would have no list to fetch from.
1053                            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        // Offset and limit consume an ordered stream; without an explicit
1066        // sort there is no stable total order to consume. Fail closed rather
1067        // than inherit provider iteration order.
1068        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    /// Return the exact wire format discriminator.
1087    #[must_use]
1088    pub fn format(&self) -> &str {
1089        &self.format
1090    }
1091
1092    /// Return dense binding declarations.
1093    #[must_use]
1094    pub fn bindings(&self) -> &[AssertionBinding] {
1095        &self.bindings
1096    }
1097
1098    /// Return the additive V2 compatibility contract.
1099    ///
1100    /// V1 plans return `None`; every V2 plan returns `Some`, including native
1101    /// low-level plans whose compatibility object is empty.
1102    #[must_use]
1103    pub const fn v2_compatibility(&self) -> Option<&QueryPlanV2Compatibility> {
1104        self.compatibility.as_ref()
1105    }
1106
1107    /// Return dense typed input column declarations.
1108    #[must_use]
1109    pub fn inputs(&self) -> &[InputColumn] {
1110        &self.inputs
1111    }
1112
1113    /// Return the plan-local functions.
1114    #[must_use]
1115    pub fn functions(&self) -> &[LocalFunction] {
1116        &self.functions
1117    }
1118
1119    /// Return the ordered read pipeline.
1120    pub fn pipeline(&self) -> &[ReadStage] {
1121        &self.pipeline
1122    }
1123
1124    /// Return the explicit output category.
1125    #[must_use]
1126    pub const fn output(&self) -> &QueryOutput {
1127        &self.output
1128    }
1129
1130    /// Return the exact managed semantics this plan was authored against.
1131    #[must_use]
1132    pub const fn managed_semantics(&self) -> &ManagedSemanticSchemaFingerprint {
1133        &self.managed_semantics
1134    }
1135
1136    /// Return open capabilities derived from syntax, never named manually.
1137    #[must_use]
1138    pub const fn required_capabilities(&self) -> &CapabilitySet {
1139        &self.required_capabilities
1140    }
1141
1142    /// Encode exact canonical bytes.
1143    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    /// Compute the canonical domain-separated plan fingerprint.
1156    pub fn fingerprint(&self) -> Result<QueryPlanFingerprint, Diagnostic> {
1157        QueryPlanFingerprint::compute(self)
1158    }
1159}
1160
1161/// A borrowed projection through the frozen V1 field set.
1162///
1163/// This projection must precede the bounded canonical codec. Serializing the
1164/// widened in-memory V2 shape first would make V1 acceptance depend on bytes
1165/// (`min_depth`) that are absent from the released V1 wire.
1166#[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/// One rectangular invocation input row.
1452///
1453/// Values are positional by dense input column ordinal. `None` is admitted
1454/// only where the declaring column is optional; there is no other
1455/// null-shaped default.
1456#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
1457#[serde(transparent)]
1458pub struct InputRow {
1459    values: Vec<Option<CanonicalValue>>,
1460}
1461
1462impl InputRow {
1463    /// Construct one positional input row.
1464    #[must_use]
1465    pub const fn new(values: Vec<Option<CanonicalValue>>) -> Self {
1466        Self { values }
1467    }
1468
1469    /// Return positional values by dense column ordinal.
1470    #[must_use]
1471    pub fn values(&self) -> &[Option<CanonicalValue>] {
1472        &self.values
1473    }
1474}
1475
1476/// The closed operation vocabulary of the first public revision.
1477#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
1478#[serde(rename_all = "snake_case")]
1479pub enum QueryOperation {
1480    /// Stream the projected rows.
1481    Rows,
1482    /// Count the projected rows.
1483    Count,
1484    /// Report whether at least one row exists.
1485    Exists,
1486}
1487
1488/// One executable invocation of a reusable validated plan.
1489///
1490/// The invocation carries values and an operation; the plan carries all
1491/// reusable structure. Binding is by exact plan fingerprint, so a plan edit
1492/// invalidates every outstanding invocation instead of silently reshaping it.
1493#[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    /// Validate one rectangular input batch against its exact plan.
1502    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    /// Return the rectangular validated input rows.
1591    #[must_use]
1592    pub fn inputs(&self) -> &[InputRow] {
1593        &self.inputs
1594    }
1595
1596    /// Return the requested operation.
1597    #[must_use]
1598    pub const fn operation(&self) -> QueryOperation {
1599        self.operation
1600    }
1601
1602    /// Return the exact plan fingerprint this invocation binds.
1603    #[must_use]
1604    pub const fn plan_fingerprint(&self) -> &QueryPlanFingerprint {
1605        &self.plan_fingerprint
1606    }
1607
1608    /// Return whether this invocation still binds the supplied plan.
1609    pub fn binds(&self, plan: &QueryPlan) -> Result<bool, Diagnostic> {
1610        Ok(self.plan_fingerprint == plan.fingerprint()?)
1611    }
1612
1613    /// Return capabilities this invocation's transport requires beyond
1614    /// the plan's own.
1615    ///
1616    /// A multi-row input batch rides the native `given` transport. A single
1617    /// row with an absent optional cell also requires `given`, because inline
1618    /// literal substitution has no absence spelling. Datetime-tz inputs use
1619    /// `given` at every cardinality so named and fixed/second-offset values
1620    /// have one exact lowering. Both client preflight and executor admission
1621    /// check this set against the advertisement so untransportable
1622    /// invocations fail before any I/O or provider resource.
1623    #[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/// Fingerprint of exact canonical query-plan bytes.
1640#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
1641#[serde(transparent)]
1642pub struct QueryPlanFingerprint(Fingerprint);
1643
1644impl QueryPlanFingerprint {
1645    /// Compute the fixed-domain fingerprint of a trusted plan.
1646    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    /// Return the generic fingerprint.
1667    #[must_use]
1668    pub const fn as_fingerprint(&self) -> &Fingerprint {
1669        &self.0
1670    }
1671}
1672
1673/// Decode canonical bytes through private constructor-rebuilding wire types.
1674pub fn decode_query_plan(bytes: &[u8]) -> Result<QueryPlan, Diagnostic> {
1675    crate::query_plan_wire::decode_query_plan(bytes)
1676}
1677
1678/// Decode exact canonical invocation bytes and bind them to one trusted plan.
1679///
1680/// Reconstruction checks the serialized fingerprint before rebuilding the
1681/// invocation through the ordinary constructor, then requires byte-for-byte
1682/// canonical round-trip identity.
1683pub 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            // The first local vocabulary keeps bodies flat.
1757            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            // The predicate-node budget is one aggregate ceiling for the
1772            // whole plan: local bodies charge the same counter the root
1773            // conjunction does instead of resetting per function.
1774            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    // Keep every pattern reference for freshness checks, but derive the row
1852    // environment only from bindings positively established in the root
1853    // conjunction. Negation-local witnesses never become row columns merely
1854    // because their binding IDs are referenced in a nested scope.
1855    let mut pattern_bound = BTreeSet::new();
1856    for pattern in patterns {
1857        collect_pattern_bindings(pattern, &mut pattern_bound);
1858    }
1859    // Bindings established only inside a try body are optional: rows carry
1860    // them or an explicit absence. Each optional binding belongs to exactly
1861    // one try body; sharing one across bodies has no single presence source.
1862    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                // The provider contract for requiring an optional binding is
1917                // unproven (TypeDB 3.12.1 stalls on it); the first optional
1918                // vocabulary reserves require to mandatory bindings.
1919                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                    // Grouping by an optional binding would key groups on
1956                    // absence; group keys stay mandatory.
1957                    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                            // Count and sum skip absent inputs and stay
1987                            // total; max, min, and mean can observe a group
1988                            // whose optional input never matched.
1989                            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                    // Absence has no defined position in a total order;
2031                    // sort keys stay mandatory.
2032                    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            // Lowering unrolls one branch per admitted path length. Charge
2244            // every emitted hop against the shared node budget. The zero-hop
2245            // identity branch is one clause, so a compact plan cannot smuggle
2246            // hundreds of thousands of emitted clauses past the ceiling.
2247            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
2350/// Collect bindings positively established by one pattern in its current
2351/// lexical scope. References used only as value/function operands are not
2352/// producers, and nested scopes are deliberately not traversed.
2353fn 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
2394/// Collect bindings established only inside negation scopes, including nested
2395/// negations. Optional blocks cannot occur below a negation in this vocabulary,
2396/// so every nested positive producer is a scoped witness.
2397fn 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}