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 Phase 4 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, QueryRowCardinalityV2,
41    QueryStableOrderV2, QueryWindowV2, ReleasedValueKindV2,
42};
43
44/// The exact wire discriminator for first-format query plans.
45pub const QUERY_PLAN_FORMAT_V1: &str = "typebridge.query-plan/v1";
46/// The exact wire discriminator for additive query plans.
47pub const QUERY_PLAN_FORMAT_V2: &str = "typebridge.query-plan/v2";
48/// Domain separating query-plan fingerprints from every other digest.
49pub const QUERY_PLAN_FINGERPRINT_DOMAIN: &str = "typebridge.query.plan";
50/// V1 canonicalization version retained for source compatibility.
51pub const QUERY_PLAN_CANONICALIZATION: &str = "typebridge.query-plan-c14n/v1";
52/// The V1 canonicalization version.
53pub const QUERY_PLAN_CANONICALIZATION_V1: &str = QUERY_PLAN_CANONICALIZATION;
54/// The additive V2 canonicalization version.
55pub const QUERY_PLAN_CANONICALIZATION_V2: &str = "typebridge.query-plan-c14n/v2";
56
57const CAP_PLAN: &str = "query.plan";
58const CAP_ISA: &str = "query.pattern.isa";
59const CAP_ISA_SUBTYPES: &str = "query.pattern.isa-subtypes";
60const CAP_HAS: &str = "query.pattern.has";
61const CAP_LINKS: &str = "query.pattern.links";
62const CAP_VALUE: &str = "query.pattern.value";
63const CAP_NEGATION: &str = "query.pattern.negation";
64const CAP_DISJUNCTION: &str = "query.pattern.disjunction";
65const CAP_INPUT_COLUMNS: &str = "query.input.columns";
66const CAP_STAGE_SELECT: &str = "query.stage.select";
67const CAP_STAGE_REQUIRE: &str = "query.stage.require";
68const CAP_STAGE_DISTINCT: &str = "query.stage.distinct";
69const CAP_STAGE_SORT: &str = "query.stage.sort";
70const CAP_STAGE_OFFSET: &str = "query.stage.offset";
71const CAP_STAGE_LIMIT: &str = "query.stage.limit";
72const CAP_OUTPUT_ROWS: &str = "query.output.rows";
73const CAP_FUNCTION_CALL: &str = "query.pattern.function-call";
74const CAP_STAGE_REDUCE: &str = "query.stage.reduce";
75const CAP_TRY: &str = "query.pattern.try";
76const CAP_OUTPUT_DOCUMENTS: &str = "query.output.documents";
77const CAP_LOCAL_FUNCTIONS: &str = "query.function.local";
78const CAP_REACHABLE: &str = "query.pattern.reachable";
79const CAP_INPUT_GIVEN_ROWS: &str = "query.input.given-rows";
80
81/// Return every capability the first query-plan vocabulary can require.
82#[must_use]
83pub fn query_plan_capability_vocabulary() -> CapabilitySet {
84    [
85        CAP_PLAN,
86        CAP_ISA,
87        CAP_ISA_SUBTYPES,
88        CAP_HAS,
89        CAP_LINKS,
90        CAP_VALUE,
91        CAP_NEGATION,
92        CAP_INPUT_COLUMNS,
93        CAP_STAGE_SELECT,
94        CAP_STAGE_REQUIRE,
95        CAP_STAGE_DISTINCT,
96        CAP_STAGE_SORT,
97        CAP_STAGE_OFFSET,
98        CAP_STAGE_LIMIT,
99        CAP_OUTPUT_ROWS,
100        CAP_FUNCTION_CALL,
101        CAP_STAGE_REDUCE,
102        CAP_TRY,
103        CAP_OUTPUT_DOCUMENTS,
104        CAP_LOCAL_FUNCTIONS,
105        CAP_REACHABLE,
106    ]
107    .into_iter()
108    .map(|value| CapabilityId::new(value).expect("static capability id is canonical"))
109    .collect()
110}
111
112/// Return every capability the public low-level V2 authoring surface can derive.
113///
114/// This is deliberately narrower than [`query_plan_v2_capability_vocabulary`]:
115/// native low-level authoring adds only the V2 plan discriminator and closed
116/// disjunction syntax to the first query-plan vocabulary. Model-query
117/// compatibility capabilities are authored through a separate facade.
118#[must_use]
119pub fn query_plan_authoring_capability_vocabulary() -> CapabilitySet {
120    query_plan_capability_vocabulary()
121        .into_iter()
122        .chain([v2::CAP_PLAN_V2, v2::CAP_DISJUNCTION].map(|value| {
123            CapabilityId::new(value).expect("static V2 authoring capability is canonical")
124        }))
125        .collect()
126}
127
128/// Return every capability an additive V2 plan can syntax-derive.
129///
130/// The V1 vocabulary stays byte- and value-exact. This V2 inventory is a
131/// separate completeness guard containing the V1 vocabulary plus the closed
132/// model-query compatibility vocabulary.
133#[must_use]
134pub fn query_plan_v2_capability_vocabulary() -> CapabilitySet {
135    query_plan_capability_vocabulary()
136        .into_iter()
137        .chain(
138            [
139                v2::CAP_PLAN_V2,
140                v2::CAP_DISJUNCTION,
141                v2::CAP_STRING_OPERATORS,
142                v2::CAP_LINKS_SUBTYPES,
143                v2::CAP_IID,
144                v2::CAP_CROSS_JOIN,
145                v2::CAP_OUTPUT_NAMED,
146                v2::CAP_OUTPUT_COLLECT,
147                v2::CAP_OUTPUT_COLLECT_DISTINCT,
148                v2::CAP_OUTPUT_HYDRATED,
149                v2::CAP_EXACTLY_ONE,
150                v2::CAP_PAGE,
151                v2::CAP_DISTINCT_COUNT,
152                v2::CAP_DISTINCT_EXISTS,
153                v2::CAP_STABLE_SELECTED,
154                v2::CAP_STABLE_ROOT,
155                v2::CAP_STABLE_COLLECTION,
156                v2::CAP_SAME_SNAPSHOT_HYDRATION,
157                v2::CAP_BATCH_IDENTITY_REBIND,
158            ]
159            .into_iter()
160            .map(|value| {
161                CapabilityId::new(value).expect("static V2 query capability is canonical")
162            }),
163        )
164        .collect()
165}
166
167/// Return the transport capability exact `given` invocations require.
168///
169/// Plans never require this capability — it is derived from the invocation's
170/// row count and values by [`QueryInvocation::transport_capabilities`], so it
171/// is not part of [`query_plan_capability_vocabulary`]. Batches, explicit
172/// absence, and datetime-tz values select this transport. Executors advertise
173/// it only when their provider can transport explicit input rows, which makes
174/// admission truthful at preflight instead of failing after a transaction
175/// exists.
176#[must_use]
177pub fn query_given_rows_capability() -> CapabilityId {
178    CapabilityId::new(CAP_INPUT_GIVEN_ROWS).expect("static capability id is canonical")
179}
180
181/// One dense typed input column identity.
182#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
183#[serde(transparent)]
184pub struct InputColumnId(u16);
185
186impl InputColumnId {
187    /// Construct a dense input column ordinal.
188    #[must_use]
189    pub const fn new(value: u16) -> Self {
190        Self(value)
191    }
192
193    /// Return the dense ordinal.
194    #[must_use]
195    pub const fn get(self) -> u16 {
196        self.0
197    }
198}
199
200impl fmt::Display for InputColumnId {
201    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
202        write!(formatter, "{}", self.0)
203    }
204}
205
206/// One typed input column declaration owned by the reusable plan.
207///
208/// Input declarations belong to the plan; input rows belong to the
209/// invocation. Changing an input value never creates a new plan identity.
210#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
211pub struct InputColumn {
212    id: InputColumnId,
213    optional: bool,
214    public_name: QueryVariable,
215    value_type: ValueTypeTag,
216}
217
218impl InputColumn {
219    /// Construct one typed input column.
220    #[must_use]
221    pub const fn new(
222        id: InputColumnId,
223        public_name: QueryVariable,
224        value_type: ValueTypeTag,
225        optional: bool,
226    ) -> Self {
227        Self {
228            id,
229            optional,
230            public_name,
231            value_type,
232        }
233    }
234
235    /// Return the dense column identity.
236    #[must_use]
237    pub const fn id(&self) -> InputColumnId {
238        self.id
239    }
240
241    /// Return the binding-facing public column name.
242    #[must_use]
243    pub const fn public_name(&self) -> &QueryVariable {
244        &self.public_name
245    }
246
247    /// Return the exact scalar type every row value must carry.
248    #[must_use]
249    pub const fn value_type(&self) -> ValueTypeTag {
250        self.value_type
251    }
252
253    /// Return whether invocation rows may omit this column's value.
254    #[must_use]
255    pub const fn optional(&self) -> bool {
256        self.optional
257    }
258}
259
260/// One typed operand inside a query value comparison.
261#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
262#[serde(tag = "kind", rename_all = "snake_case")]
263pub enum QueryOperand {
264    /// Read the scalar value of an attribute binding.
265    Binding {
266        /// The referenced dense binding.
267        binding: BindingId,
268    },
269    /// Compare with an exact canonical literal.
270    Literal {
271        /// The canonical scalar literal.
272        value: CanonicalValue,
273    },
274    /// Read one declared invocation input column.
275    Input {
276        /// The referenced dense input column.
277        column: InputColumnId,
278    },
279}
280
281/// The closed typed pattern algebra of the first public vocabulary.
282///
283/// Exactly the V1-parity graph shapes: typed `isa` with optional subtype
284/// inclusion, effective-ownership `has`, role-qualified `links`, typed value
285/// comparison, and negation over a closed conjunction. No variant accepts an
286/// arbitrary TypeQL fragment.
287#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
288#[serde(tag = "kind", rename_all = "snake_case")]
289pub enum QueryPattern {
290    /// Constrain a binding to one schema type, optionally including subtypes.
291    Isa {
292        /// The constrained binding.
293        binding: BindingId,
294        /// Whether transitive subtypes are admitted.
295        include_subtypes: bool,
296        /// The exact schema type.
297        type_id: TypeId,
298    },
299    /// Bind an owned attribute through an effective ownership.
300    Has {
301        /// The bound attribute instance.
302        attribute: BindingId,
303        /// The exact attribute type.
304        attribute_id: AttributeId,
305        /// The owning binding.
306        owner: BindingId,
307    },
308    /// Bind a relation and role-qualified players.
309    Links {
310        /// Role-qualified player bindings.
311        players: Vec<AssertionRolePlayer>,
312        /// The bound relation instance.
313        relation: BindingId,
314        /// The exact relation type.
315        relation_id: TypeId,
316    },
317    /// Compare two exact typed scalar operands.
318    Value {
319        /// The closed comparator.
320        comparator: ValueComparator,
321        /// The left operand.
322        left: QueryOperand,
323        /// The right operand.
324        right: QueryOperand,
325    },
326    /// Require at least one child pattern.
327    ///
328    /// A binding is positively established after this pattern only when every
329    /// branch establishes it. Branch-local bindings never leak into the outer
330    /// row environment.
331    Or {
332        /// Non-empty alternative conjunctions in source order.
333        branches: Vec<Vec<QueryPattern>>,
334    },
335    /// Negate a closed nested conjunction.
336    Not {
337        /// The negated conjunction.
338        patterns: Vec<QueryPattern>,
339    },
340    /// Optionally match a nested conjunction without filtering rows.
341    ///
342    /// Bindings established only inside the body survive as optional: rows
343    /// where the body matched carry them, other rows carry an explicit
344    /// absence. The first optional vocabulary admits `isa`, `has`, `links`,
345    /// and value comparisons in the body, at the root conjunction only.
346    Try {
347        /// The optional conjunction.
348        patterns: Vec<QueryPattern>,
349    },
350    /// Existential bounded reachability along one role-directed relation.
351    ///
352    /// Holds when the target is reachable from the source in
353    /// `min_depth..=max_depth` hops, each hop one relation instance from the
354    /// `role_from` player to the `role_to` player. A zero-hop branch is exact
355    /// concept identity. Positive branches are finite directed walks: repeated
356    /// vertices or relation instances are admitted, so cycles cannot make
357    /// execution unbounded. Proof-path identity is existential and never
358    /// enters the selected row. Lowering unrolls every admitted length
359    /// provider-side, never by repeated client queries.
360    Reachable {
361        /// The inclusive minimum hop count. Zero admits source/target identity.
362        min_depth: u8,
363        /// The inclusive finite maximum hop count.
364        max_depth: u8,
365        /// The exact relation type of every hop.
366        relation: TypeId,
367        /// The role the hop starts from.
368        role_from: RoleId,
369        /// The role the hop arrives at.
370        role_to: RoleId,
371        /// The established start binding.
372        source: BindingId,
373        /// The established end binding.
374        target: BindingId,
375    },
376    /// Assign one scalar schema-function result to a binding.
377    ///
378    /// The first function vocabulary admits scalar, non-optional returns
379    /// only; tuple and stream returns stay reserved behind later
380    /// capabilities.
381    FunctionCall {
382        /// Ordered call arguments.
383        arguments: Vec<QueryOperand>,
384        /// The binding assigned from the scalar return.
385        assigned: BindingId,
386        /// The exact schema function identity.
387        function: FunctionId,
388    },
389}
390
391/// The sort direction of one order term.
392#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
393#[serde(rename_all = "snake_case")]
394pub enum OrderDirection {
395    /// Smallest first.
396    Ascending,
397    /// Largest first.
398    Descending,
399}
400
401/// One typed sort key.
402#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
403pub struct OrderTerm {
404    binding: BindingId,
405    direction: OrderDirection,
406}
407
408impl OrderTerm {
409    /// Construct one sort key.
410    #[must_use]
411    pub const fn new(binding: BindingId, direction: OrderDirection) -> Self {
412        Self { binding, direction }
413    }
414
415    /// Return the sorted binding.
416    #[must_use]
417    pub const fn binding(&self) -> BindingId {
418        self.binding
419    }
420
421    /// Return the sort direction.
422    #[must_use]
423    pub const fn direction(&self) -> OrderDirection {
424        self.direction
425    }
426}
427
428/// The closed reducer vocabulary of the first reduce stage.
429///
430/// Reducers that can observe an empty stream (`max`, `min`, `mean`,
431/// `median`, `std`) are admitted only under group bindings, where every
432/// group is witnessed by at least one row; `count` and `sum` stay total on
433/// empty streams.
434#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
435#[serde(rename_all = "snake_case")]
436pub enum Reducer {
437    /// Count surviving rows.
438    Count,
439    /// The largest input value.
440    Max,
441    /// The arithmetic mean of input values.
442    Mean,
443    /// The statistical median of input values.
444    Median,
445    /// The smallest input value.
446    Min,
447    /// The sample standard deviation of input values.
448    Std,
449    /// The total of input values.
450    Sum,
451}
452
453impl Reducer {
454    /// Whether this reducer yields a defined result on an empty stream.
455    #[must_use]
456    pub const fn total_without_groups(self) -> bool {
457        matches!(self, Self::Count | Self::Sum)
458    }
459
460    /// Whether this reducer consumes an input binding.
461    #[must_use]
462    pub const fn requires_input(self) -> bool {
463        !matches!(self, Self::Count)
464    }
465}
466
467/// One reduce-stage assignment producing a fresh value binding.
468#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
469pub struct ReduceAssignment {
470    assigned: BindingId,
471    input: Option<BindingId>,
472    reducer: Reducer,
473}
474
475impl ReduceAssignment {
476    /// Assign one reducer result to a fresh declared binding.
477    #[must_use]
478    pub const fn new(assigned: BindingId, reducer: Reducer, input: Option<BindingId>) -> Self {
479        Self {
480            assigned,
481            input,
482            reducer,
483        }
484    }
485
486    /// Return the fresh binding the reducer result is assigned to.
487    #[must_use]
488    pub const fn assigned(&self) -> BindingId {
489        self.assigned
490    }
491
492    /// Return the reduced input binding, absent for bare `count`.
493    #[must_use]
494    pub const fn input(&self) -> Option<BindingId> {
495        self.input
496    }
497
498    /// Return the reducer applied within each group.
499    #[must_use]
500    pub const fn reducer(&self) -> Reducer {
501        self.reducer
502    }
503}
504
505/// One ordered read stage of the first public vocabulary.
506///
507/// The canonical stage order is fixed: one `match`, then at most one each of
508/// `select`, `require`, `distinct`, `reduce`, `sort`, `offset`, and `limit`,
509/// in that order. Later stage kinds (documents) are reserved.
510#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
511#[serde(tag = "kind", rename_all = "snake_case")]
512pub enum ReadStage {
513    /// The single pattern conjunction producing the row environment.
514    Match {
515        /// The closed positive/negative conjunction.
516        patterns: Vec<QueryPattern>,
517    },
518    /// Restrict the visible row environment to these bindings.
519    Select {
520        /// Canonical-sorted visible bindings.
521        bindings: Vec<BindingId>,
522    },
523    /// Require optional bindings to be present in every surviving row.
524    Require {
525        /// Canonical-sorted required bindings.
526        bindings: Vec<BindingId>,
527    },
528    /// Deduplicate the visible row environment.
529    Distinct,
530    /// Collapse rows into grouped reducer results.
531    ///
532    /// The surviving row environment is exactly the group bindings plus the
533    /// assigned reducer results; every other binding ends here.
534    Reduce {
535        /// Reducer assignments, each producing a fresh value binding.
536        assignments: Vec<ReduceAssignment>,
537        /// Canonical-sorted group-key bindings; empty for a global reduce.
538        groups: Vec<BindingId>,
539    },
540    /// Impose an explicit total row order.
541    Sort {
542        /// Ordered sort keys.
543        terms: Vec<OrderTerm>,
544    },
545    /// Skip an exact number of ordered rows.
546    Offset {
547        /// The number of rows skipped.
548        rows: u64,
549    },
550    /// Truncate to an exact number of ordered rows.
551    Limit {
552        /// The maximum number of rows returned.
553        rows: u64,
554    },
555}
556
557impl ReadStage {
558    const fn ordinal(&self) -> u8 {
559        match self {
560            Self::Match { .. } => 0,
561            Self::Select { .. } => 1,
562            Self::Require { .. } => 2,
563            Self::Distinct => 3,
564            Self::Reduce { .. } => 4,
565            Self::Sort { .. } => 5,
566            Self::Offset { .. } => 6,
567            Self::Limit { .. } => 7,
568        }
569    }
570}
571
572/// The declared scalar return of one plan-local function.
573///
574/// The first local vocabulary admits only reducers that stay total on an
575/// empty body stream (`count`, `sum`), so every call yields a value.
576#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
577pub struct LocalReturn {
578    input: BindingId,
579    reducer: Reducer,
580    value_type: ValueTypeTag,
581}
582
583impl LocalReturn {
584    /// Declare one total reducer return over a body binding.
585    #[must_use]
586    pub const fn new(reducer: Reducer, input: BindingId, value_type: ValueTypeTag) -> Self {
587        Self {
588            input,
589            reducer,
590            value_type,
591        }
592    }
593
594    /// Return the reduced body binding.
595    #[must_use]
596    pub const fn input(&self) -> BindingId {
597        self.input
598    }
599
600    /// Return the reducer.
601    #[must_use]
602    pub const fn reducer(&self) -> Reducer {
603        self.reducer
604    }
605
606    /// Return the declared scalar result type.
607    #[must_use]
608    pub const fn value_type(&self) -> ValueTypeTag {
609        self.value_type
610    }
611}
612
613/// One plan-local function defined from the closed pattern algebra.
614///
615/// The function owns a private dense binding space; its parameters are the
616/// leading bindings, each declared against one schema type label. The first
617/// local vocabulary keeps bodies flat (`isa`, `has`, `links`, value) and
618/// returns one total reducer result.
619#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
620pub struct LocalFunction {
621    bindings: Vec<AssertionBinding>,
622    body: Vec<QueryPattern>,
623    name: FunctionId,
624    parameters: Vec<Label>,
625    returns: LocalReturn,
626}
627
628impl LocalFunction {
629    /// Declare one plan-local function.
630    #[must_use]
631    pub const fn new(
632        name: FunctionId,
633        bindings: Vec<AssertionBinding>,
634        parameters: Vec<Label>,
635        body: Vec<QueryPattern>,
636        returns: LocalReturn,
637    ) -> Self {
638        Self {
639            bindings,
640            body,
641            name,
642            parameters,
643            returns,
644        }
645    }
646
647    /// Return the function name.
648    #[must_use]
649    pub const fn name(&self) -> &FunctionId {
650        &self.name
651    }
652
653    /// Return the private dense binding table.
654    #[must_use]
655    pub fn bindings(&self) -> &[AssertionBinding] {
656        &self.bindings
657    }
658
659    /// Return the schema type label of each leading parameter binding.
660    #[must_use]
661    pub fn parameters(&self) -> &[Label] {
662        &self.parameters
663    }
664
665    /// Return the closed body conjunction.
666    #[must_use]
667    pub fn body(&self) -> &[QueryPattern] {
668        &self.body
669    }
670
671    /// Return the declared reducer result.
672    #[must_use]
673    pub const fn returns(&self) -> &LocalReturn {
674        &self.returns
675    }
676}
677
678/// One value source of a fetched document field.
679#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
680#[serde(tag = "kind", rename_all = "snake_case")]
681pub enum DocumentSource {
682    /// The scalar value of one visible binding; optional bindings
683    /// fetch as an explicit JSON null where absent.
684    Binding {
685        /// The projected scalar binding.
686        binding: BindingId,
687    },
688    /// Every value of one attribute on one mandatory owner binding,
689    /// fetched as a typed list (empty where the owner has none).
690    AttributeList {
691        /// The listed attribute.
692        attribute: AttributeId,
693        /// The mandatory owner binding.
694        owner: BindingId,
695    },
696}
697
698/// One key-value field of a fetched document.
699#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
700pub struct DocumentField {
701    key: QueryVariable,
702    source: DocumentSource,
703}
704
705impl DocumentField {
706    /// Bind one document key to its value source.
707    #[must_use]
708    pub const fn new(key: QueryVariable, source: DocumentSource) -> Self {
709        Self { key, source }
710    }
711
712    /// Return the document key.
713    #[must_use]
714    pub const fn key(&self) -> &QueryVariable {
715        &self.key
716    }
717
718    /// Return the value source.
719    #[must_use]
720    pub const fn source(&self) -> &DocumentSource {
721        &self.source
722    }
723}
724
725/// The explicit output category of the first public vocabulary.
726#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
727#[serde(tag = "kind", rename_all = "snake_case")]
728pub enum QueryOutput {
729    /// Project ordered typed row columns.
730    Rows {
731        /// The visible bindings projected, in output order.
732        columns: Vec<BindingId>,
733    },
734    /// Fetch one flat typed JSON document per surviving row.
735    Documents {
736        /// The document fields, in output order.
737        fields: Vec<DocumentField>,
738    },
739}
740
741/// A reusable, invocation-free typed read program.
742#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
743pub struct QueryPlan {
744    bindings: Vec<AssertionBinding>,
745    #[serde(skip_serializing_if = "Option::is_none")]
746    compatibility: Option<QueryPlanV2Compatibility>,
747    format: String,
748    functions: Vec<LocalFunction>,
749    inputs: Vec<InputColumn>,
750    managed_semantics: ManagedSemanticSchemaFingerprint,
751    output: QueryOutput,
752    pipeline: Vec<ReadStage>,
753    required_capabilities: CapabilitySet,
754}
755
756impl QueryPlan {
757    /// Validate and construct one canonical plan under fixed protocol limits.
758    pub fn new(
759        bindings: Vec<AssertionBinding>,
760        inputs: Vec<InputColumn>,
761        pipeline: Vec<ReadStage>,
762        output: QueryOutput,
763        managed_semantics: ManagedSemanticSchemaFingerprint,
764    ) -> Result<Self, Diagnostic> {
765        Self::new_with_limits(
766            bindings,
767            Vec::new(),
768            inputs,
769            pipeline,
770            output,
771            None,
772            managed_semantics,
773            StructuralLimits::CANONICAL,
774        )
775    }
776
777    /// Validate and construct one plan carrying plan-local functions.
778    pub fn new_with_functions(
779        bindings: Vec<AssertionBinding>,
780        functions: Vec<LocalFunction>,
781        inputs: Vec<InputColumn>,
782        pipeline: Vec<ReadStage>,
783        output: QueryOutput,
784        managed_semantics: ManagedSemanticSchemaFingerprint,
785    ) -> Result<Self, Diagnostic> {
786        Self::new_with_limits(
787            bindings,
788            functions,
789            inputs,
790            pipeline,
791            output,
792            None,
793            managed_semantics,
794            StructuralLimits::CANONICAL,
795        )
796    }
797
798    /// Validate and construct one additive V2 low-level plan.
799    ///
800    /// This constructor carries the required empty V2 compatibility object;
801    /// there is no host-controlled format switch or legacy mode.
802    pub fn new_v2(
803        bindings: Vec<AssertionBinding>,
804        inputs: Vec<InputColumn>,
805        pipeline: Vec<ReadStage>,
806        output: QueryOutput,
807        managed_semantics: ManagedSemanticSchemaFingerprint,
808    ) -> Result<Self, Diagnostic> {
809        Self::new_v2_with_functions(
810            bindings,
811            Vec::new(),
812            inputs,
813            pipeline,
814            output,
815            QueryPlanV2Compatibility::native(),
816            managed_semantics,
817        )
818    }
819
820    /// Validate and construct one additive V2 plan with local functions and
821    /// an explicit Rust-owned compatibility contract.
822    pub fn new_v2_with_functions(
823        bindings: Vec<AssertionBinding>,
824        functions: Vec<LocalFunction>,
825        inputs: Vec<InputColumn>,
826        pipeline: Vec<ReadStage>,
827        output: QueryOutput,
828        compatibility: QueryPlanV2Compatibility,
829        managed_semantics: ManagedSemanticSchemaFingerprint,
830    ) -> Result<Self, Diagnostic> {
831        Self::new_with_limits(
832            bindings,
833            functions,
834            inputs,
835            pipeline,
836            output,
837            Some(compatibility),
838            managed_semantics,
839            StructuralLimits::CANONICAL,
840        )
841    }
842
843    #[expect(
844        clippy::too_many_arguments,
845        reason = "the shared constructor receives every independently versioned plan component"
846    )]
847    fn new_with_limits(
848        bindings: Vec<AssertionBinding>,
849        functions: Vec<LocalFunction>,
850        inputs: Vec<InputColumn>,
851        pipeline: Vec<ReadStage>,
852        output: QueryOutput,
853        compatibility: Option<QueryPlanV2Compatibility>,
854        managed_semantics: ManagedSemanticSchemaFingerprint,
855        limits: StructuralLimits,
856    ) -> Result<Self, Diagnostic> {
857        if compatibility.is_none() {
858            validate_v1_reachability(&pipeline)?;
859        }
860        Self::validate_plan_structure(
861            &bindings,
862            &functions,
863            &inputs,
864            &pipeline,
865            &output,
866            compatibility.as_ref(),
867            limits,
868        )?;
869        let required_capabilities = derive_capabilities(
870            &pipeline,
871            &functions,
872            &inputs,
873            &output,
874            compatibility.as_ref(),
875        )?;
876        let format = if compatibility.is_some() {
877            QUERY_PLAN_FORMAT_V2
878        } else {
879            QUERY_PLAN_FORMAT_V1
880        };
881        Ok(Self {
882            bindings,
883            compatibility,
884            format: format.to_owned(),
885            functions,
886            inputs,
887            managed_semantics,
888            output,
889            pipeline,
890            required_capabilities,
891        })
892    }
893
894    /// Re-check this plan's whole structure under caller-supplied limits.
895    ///
896    /// The exact construction-time traversal runs again — bindings,
897    /// inputs, the root pipeline, and every local function under one
898    /// aggregate predicate-node budget — so a caller passing stricter
899    /// [`StructuralLimits`] gets every field enforced, not a subset.
900    pub fn check_structural_limits(&self, limits: StructuralLimits) -> Result<(), Diagnostic> {
901        Self::validate_plan_structure(
902            &self.bindings,
903            &self.functions,
904            &self.inputs,
905            &self.pipeline,
906            &self.output,
907            self.compatibility.as_ref(),
908            limits,
909        )
910    }
911
912    /// The construction-time structural traversal, shared with
913    /// [`Self::check_structural_limits`] so caller-supplied limits enforce
914    /// exactly what construction enforces.
915    fn validate_plan_structure(
916        bindings: &[AssertionBinding],
917        functions: &[LocalFunction],
918        inputs: &[InputColumn],
919        pipeline: &[ReadStage],
920        output: &QueryOutput,
921        compatibility: Option<&QueryPlanV2Compatibility>,
922        limits: StructuralLimits,
923    ) -> Result<(), Diagnostic> {
924        if bindings.is_empty() || !limits.allows_bindings(bindings.len()) {
925            return Err(failure(
926                DiagnosticCategory::ResourceLimit,
927                "query_plan_binding_limit",
928                "plan binding count is empty or exceeds the structural ceiling",
929            ));
930        }
931        let mut names = BTreeSet::new();
932        for (index, binding) in bindings.iter().enumerate() {
933            if usize::from(binding.id().get()) != index {
934                return Err(failure(
935                    DiagnosticCategory::InvalidContract,
936                    "query_plan_bindings_not_dense",
937                    "plan binding IDs must be ordered dense zero-based ordinals",
938                ));
939            }
940            validate_query_name_limit(
941                binding.variable(),
942                limits,
943                "a query variable name exceeds the structural ceiling",
944            )?;
945            if !names.insert(binding.variable().clone()) {
946                return Err(failure(
947                    DiagnosticCategory::InvalidContract,
948                    "query_plan_duplicate_variable",
949                    "plan query variables must be unique",
950                ));
951            }
952        }
953        if !limits.allows_bindings(inputs.len().max(1)) {
954            return Err(failure(
955                DiagnosticCategory::ResourceLimit,
956                "query_plan_input_limit",
957                "plan input column count exceeds the structural ceiling",
958            ));
959        }
960        for (index, column) in inputs.iter().enumerate() {
961            if usize::from(column.id().get()) != index {
962                return Err(failure(
963                    DiagnosticCategory::InvalidContract,
964                    "query_plan_inputs_not_dense",
965                    "input column IDs must be ordered dense zero-based ordinals",
966                ));
967            }
968            validate_query_name_limit(
969                column.public_name(),
970                limits,
971                "an input column name exceeds the structural ceiling",
972            )?;
973            if !names.insert(column.public_name().clone()) {
974                return Err(failure(
975                    DiagnosticCategory::InvalidContract,
976                    "query_plan_duplicate_variable",
977                    "input column names must not collide with query variables",
978                ));
979            }
980        }
981
982        let mut nodes = 0usize;
983        validate_local_functions(functions, limits, &mut nodes)?;
984
985        let (mandatory, optional, has_sort) =
986            validate_pipeline(pipeline, bindings.len(), inputs.len(), limits, &mut nodes)?;
987        let visible: BTreeSet<BindingId> = mandatory.union(&optional).copied().collect();
988
989        match output {
990            QueryOutput::Rows { columns } => {
991                if columns.is_empty() || !limits.allows_selected_slots(columns.len()) {
992                    return Err(failure(
993                        DiagnosticCategory::ResourceLimit,
994                        "query_plan_output_limit",
995                        "output column count is empty or exceeds the structural ceiling",
996                    ));
997                }
998                let mut seen = BTreeSet::new();
999                for column in columns {
1000                    if !visible.contains(column) {
1001                        return Err(failure(
1002                            DiagnosticCategory::InvalidContract,
1003                            "query_plan_output_not_visible",
1004                            "output projects a binding outside the visible row environment",
1005                        ));
1006                    }
1007                    if !seen.insert(*column) {
1008                        return Err(failure(
1009                            DiagnosticCategory::InvalidContract,
1010                            "query_plan_duplicate_output_column",
1011                            "output projects one binding twice",
1012                        ));
1013                    }
1014                }
1015            }
1016            QueryOutput::Documents { fields } => {
1017                if fields.is_empty() || !limits.allows_selected_slots(fields.len()) {
1018                    return Err(failure(
1019                        DiagnosticCategory::ResourceLimit,
1020                        "query_plan_output_limit",
1021                        "output column count is empty or exceeds the structural ceiling",
1022                    ));
1023                }
1024                let mut keys = BTreeSet::new();
1025                for field in fields {
1026                    validate_query_name_limit(
1027                        field.key(),
1028                        limits,
1029                        "a document output key exceeds the structural ceiling",
1030                    )?;
1031                    if !keys.insert(field.key().clone()) {
1032                        return Err(failure(
1033                            DiagnosticCategory::InvalidContract,
1034                            "query_plan_duplicate_output_column",
1035                            "documents fetch one key twice",
1036                        ));
1037                    }
1038                    match field.source() {
1039                        DocumentSource::Binding { binding } => {
1040                            if !visible.contains(binding) {
1041                                return Err(failure(
1042                                    DiagnosticCategory::InvalidContract,
1043                                    "query_plan_output_not_visible",
1044                                    "output projects a binding outside the visible row environment",
1045                                ));
1046                            }
1047                        }
1048                        DocumentSource::AttributeList { owner, .. } => {
1049                            // A list reaches through its owner per row;
1050                            // absence would have no list to fetch from.
1051                            if !mandatory.contains(owner) {
1052                                return Err(failure(
1053                                    DiagnosticCategory::InvalidContract,
1054                                    "query_plan_output_not_visible",
1055                                    "attribute lists require a mandatory owner binding",
1056                                ));
1057                            }
1058                        }
1059                    }
1060                }
1061            }
1062        }
1063        // Offset and limit consume an ordered stream; without an explicit
1064        // sort there is no stable total order to consume. Fail closed rather
1065        // than inherit provider iteration order.
1066        if !has_sort
1067            && pipeline
1068                .iter()
1069                .any(|stage| matches!(stage, ReadStage::Offset { .. } | ReadStage::Limit { .. }))
1070        {
1071            return Err(failure(
1072                DiagnosticCategory::InvalidContract,
1073                "query_plan_unordered_truncation",
1074                "offset and limit require an explicit total sort order",
1075            ));
1076        }
1077        if let Some(compatibility) = compatibility {
1078            compatibility.validate(bindings.len(), limits)?;
1079        }
1080
1081        Ok(())
1082    }
1083
1084    /// Return the exact wire format discriminator.
1085    #[must_use]
1086    pub fn format(&self) -> &str {
1087        &self.format
1088    }
1089
1090    /// Return dense binding declarations.
1091    #[must_use]
1092    pub fn bindings(&self) -> &[AssertionBinding] {
1093        &self.bindings
1094    }
1095
1096    /// Return the additive V2 compatibility contract.
1097    ///
1098    /// V1 plans return `None`; every V2 plan returns `Some`, including native
1099    /// low-level plans whose compatibility object is empty.
1100    #[must_use]
1101    pub const fn v2_compatibility(&self) -> Option<&QueryPlanV2Compatibility> {
1102        self.compatibility.as_ref()
1103    }
1104
1105    /// Return dense typed input column declarations.
1106    #[must_use]
1107    pub fn inputs(&self) -> &[InputColumn] {
1108        &self.inputs
1109    }
1110
1111    /// Return the plan-local functions.
1112    #[must_use]
1113    pub fn functions(&self) -> &[LocalFunction] {
1114        &self.functions
1115    }
1116
1117    /// Return the ordered read pipeline.
1118    pub fn pipeline(&self) -> &[ReadStage] {
1119        &self.pipeline
1120    }
1121
1122    /// Return the explicit output category.
1123    #[must_use]
1124    pub const fn output(&self) -> &QueryOutput {
1125        &self.output
1126    }
1127
1128    /// Return the exact managed semantics this plan was authored against.
1129    #[must_use]
1130    pub const fn managed_semantics(&self) -> &ManagedSemanticSchemaFingerprint {
1131        &self.managed_semantics
1132    }
1133
1134    /// Return open capabilities derived from syntax, never named manually.
1135    #[must_use]
1136    pub const fn required_capabilities(&self) -> &CapabilitySet {
1137        &self.required_capabilities
1138    }
1139
1140    /// Encode exact canonical bytes.
1141    pub fn canonical_bytes(&self) -> Result<Vec<u8>, Diagnostic> {
1142        match self.format.as_str() {
1143            QUERY_PLAN_FORMAT_V1 => to_canonical_json(&QueryPlanV1Projection::new(self)?),
1144            QUERY_PLAN_FORMAT_V2 => to_canonical_json(self),
1145            _ => Err(failure(
1146                DiagnosticCategory::InvalidContract,
1147                "query_plan_format_unsupported",
1148                "query plan wire format is unsupported",
1149            )),
1150        }
1151    }
1152
1153    /// Compute the canonical domain-separated plan fingerprint.
1154    pub fn fingerprint(&self) -> Result<QueryPlanFingerprint, Diagnostic> {
1155        QueryPlanFingerprint::compute(self)
1156    }
1157}
1158
1159/// A borrowed projection through the frozen V1 field set.
1160///
1161/// This projection must precede the bounded canonical codec. Serializing the
1162/// widened in-memory V2 shape first would make V1 acceptance depend on bytes
1163/// (`min_depth`) that are absent from the released V1 wire.
1164#[derive(Serialize)]
1165struct QueryPlanV1Projection<'a> {
1166    bindings: &'a [AssertionBinding],
1167    format: &'a str,
1168    functions: Vec<LocalFunctionV1Projection<'a>>,
1169    inputs: &'a [InputColumn],
1170    managed_semantics: &'a ManagedSemanticSchemaFingerprint,
1171    output: &'a QueryOutput,
1172    pipeline: Vec<ReadStageV1Projection<'a>>,
1173    required_capabilities: &'a CapabilitySet,
1174}
1175
1176impl<'a> QueryPlanV1Projection<'a> {
1177    fn new(plan: &'a QueryPlan) -> Result<Self, Diagnostic> {
1178        Ok(Self {
1179            bindings: plan.bindings(),
1180            format: plan.format(),
1181            functions: plan
1182                .functions()
1183                .iter()
1184                .map(LocalFunctionV1Projection::new)
1185                .collect::<Result<Vec<_>, _>>()?,
1186            inputs: plan.inputs(),
1187            managed_semantics: plan.managed_semantics(),
1188            output: plan.output(),
1189            pipeline: plan
1190                .pipeline()
1191                .iter()
1192                .map(ReadStageV1Projection::new)
1193                .collect::<Result<Vec<_>, _>>()?,
1194            required_capabilities: plan.required_capabilities(),
1195        })
1196    }
1197}
1198
1199#[derive(Serialize)]
1200struct LocalFunctionV1Projection<'a> {
1201    bindings: &'a [AssertionBinding],
1202    body: Vec<QueryPatternV1Projection<'a>>,
1203    name: &'a FunctionId,
1204    parameters: &'a [Label],
1205    returns: &'a LocalReturn,
1206}
1207
1208impl<'a> LocalFunctionV1Projection<'a> {
1209    fn new(function: &'a LocalFunction) -> Result<Self, Diagnostic> {
1210        Ok(Self {
1211            bindings: function.bindings(),
1212            body: function
1213                .body()
1214                .iter()
1215                .map(QueryPatternV1Projection::new)
1216                .collect::<Result<Vec<_>, _>>()?,
1217            name: function.name(),
1218            parameters: function.parameters(),
1219            returns: function.returns(),
1220        })
1221    }
1222}
1223
1224#[derive(Serialize)]
1225#[serde(tag = "kind", rename_all = "snake_case")]
1226enum ReadStageV1Projection<'a> {
1227    Match {
1228        patterns: Vec<QueryPatternV1Projection<'a>>,
1229    },
1230    Select {
1231        bindings: &'a [BindingId],
1232    },
1233    Require {
1234        bindings: &'a [BindingId],
1235    },
1236    Distinct,
1237    Reduce {
1238        assignments: &'a [ReduceAssignment],
1239        groups: &'a [BindingId],
1240    },
1241    Sort {
1242        terms: &'a [OrderTerm],
1243    },
1244    Offset {
1245        rows: u64,
1246    },
1247    Limit {
1248        rows: u64,
1249    },
1250}
1251
1252impl<'a> ReadStageV1Projection<'a> {
1253    fn new(stage: &'a ReadStage) -> Result<Self, Diagnostic> {
1254        Ok(match stage {
1255            ReadStage::Match { patterns } => Self::Match {
1256                patterns: patterns
1257                    .iter()
1258                    .map(QueryPatternV1Projection::new)
1259                    .collect::<Result<Vec<_>, _>>()?,
1260            },
1261            ReadStage::Select { bindings } => Self::Select { bindings },
1262            ReadStage::Require { bindings } => Self::Require { bindings },
1263            ReadStage::Distinct => Self::Distinct,
1264            ReadStage::Reduce {
1265                assignments,
1266                groups,
1267            } => Self::Reduce {
1268                assignments,
1269                groups,
1270            },
1271            ReadStage::Sort { terms } => Self::Sort { terms },
1272            ReadStage::Offset { rows } => Self::Offset { rows: *rows },
1273            ReadStage::Limit { rows } => Self::Limit { rows: *rows },
1274        })
1275    }
1276}
1277
1278#[derive(Serialize)]
1279#[serde(tag = "kind", rename_all = "snake_case")]
1280enum QueryPatternV1Projection<'a> {
1281    Isa {
1282        binding: BindingId,
1283        include_subtypes: bool,
1284        type_id: &'a TypeId,
1285    },
1286    Has {
1287        attribute: BindingId,
1288        attribute_id: &'a AttributeId,
1289        owner: BindingId,
1290    },
1291    Links {
1292        players: &'a [AssertionRolePlayer],
1293        relation: BindingId,
1294        relation_id: &'a TypeId,
1295    },
1296    Value {
1297        comparator: ValueComparator,
1298        left: &'a QueryOperand,
1299        right: &'a QueryOperand,
1300    },
1301    Not {
1302        patterns: Vec<QueryPatternV1Projection<'a>>,
1303    },
1304    Try {
1305        patterns: Vec<QueryPatternV1Projection<'a>>,
1306    },
1307    Reachable {
1308        max_depth: u8,
1309        relation: &'a TypeId,
1310        role_from: &'a RoleId,
1311        role_to: &'a RoleId,
1312        source: BindingId,
1313        target: BindingId,
1314    },
1315    FunctionCall {
1316        arguments: &'a [QueryOperand],
1317        assigned: BindingId,
1318        function: &'a FunctionId,
1319    },
1320}
1321
1322impl<'a> QueryPatternV1Projection<'a> {
1323    fn new(pattern: &'a QueryPattern) -> Result<Self, Diagnostic> {
1324        Ok(match pattern {
1325            QueryPattern::Isa {
1326                binding,
1327                include_subtypes,
1328                type_id,
1329            } => Self::Isa {
1330                binding: *binding,
1331                include_subtypes: *include_subtypes,
1332                type_id,
1333            },
1334            QueryPattern::Has {
1335                attribute,
1336                attribute_id,
1337                owner,
1338            } => Self::Has {
1339                attribute: *attribute,
1340                attribute_id,
1341                owner: *owner,
1342            },
1343            QueryPattern::Links {
1344                players,
1345                relation,
1346                relation_id,
1347            } => Self::Links {
1348                players,
1349                relation: *relation,
1350                relation_id,
1351            },
1352            QueryPattern::Value {
1353                comparator,
1354                left,
1355                right,
1356            } => Self::Value {
1357                comparator: *comparator,
1358                left,
1359                right,
1360            },
1361            QueryPattern::Or { .. } => {
1362                return Err(failure(
1363                    DiagnosticCategory::InvalidContract,
1364                    "query_plan_v1_disjunction_unsupported",
1365                    "ordinary disjunction is additive in query-plan V2",
1366                ));
1367            }
1368            QueryPattern::Not { patterns } => Self::Not {
1369                patterns: patterns
1370                    .iter()
1371                    .map(Self::new)
1372                    .collect::<Result<Vec<_>, _>>()?,
1373            },
1374            QueryPattern::Try { patterns } => Self::Try {
1375                patterns: patterns
1376                    .iter()
1377                    .map(Self::new)
1378                    .collect::<Result<Vec<_>, _>>()?,
1379            },
1380            QueryPattern::Reachable {
1381                max_depth,
1382                relation,
1383                role_from,
1384                role_to,
1385                source,
1386                target,
1387                ..
1388            } => Self::Reachable {
1389                max_depth: *max_depth,
1390                relation,
1391                role_from,
1392                role_to,
1393                source: *source,
1394                target: *target,
1395            },
1396            QueryPattern::FunctionCall {
1397                arguments,
1398                assigned,
1399                function,
1400            } => Self::FunctionCall {
1401                arguments,
1402                assigned: *assigned,
1403                function,
1404            },
1405        })
1406    }
1407}
1408
1409fn validate_v1_reachability(pipeline: &[ReadStage]) -> Result<(), Diagnostic> {
1410    fn validate_patterns(patterns: &[QueryPattern]) -> Result<(), Diagnostic> {
1411        for pattern in patterns {
1412            match pattern {
1413                QueryPattern::Or { .. } => {
1414                    return Err(failure(
1415                        DiagnosticCategory::InvalidContract,
1416                        "query_plan_v1_disjunction_unsupported",
1417                        "ordinary disjunction is additive in query-plan V2",
1418                    ));
1419                }
1420                QueryPattern::Reachable { min_depth, .. } if *min_depth != 1 => {
1421                    return Err(failure(
1422                        DiagnosticCategory::InvalidContract,
1423                        "query_plan_v1_reachable_min_depth",
1424                        "V1 reachability always starts at one hop",
1425                    ));
1426                }
1427                QueryPattern::Not { patterns } | QueryPattern::Try { patterns } => {
1428                    validate_patterns(patterns)?;
1429                }
1430                QueryPattern::Isa { .. }
1431                | QueryPattern::Has { .. }
1432                | QueryPattern::Links { .. }
1433                | QueryPattern::Value { .. }
1434                | QueryPattern::Reachable { .. }
1435                | QueryPattern::FunctionCall { .. } => {}
1436            }
1437        }
1438        Ok(())
1439    }
1440
1441    for stage in pipeline {
1442        if let ReadStage::Match { patterns } = stage {
1443            validate_patterns(patterns)?;
1444        }
1445    }
1446    Ok(())
1447}
1448
1449/// One rectangular invocation input row.
1450///
1451/// Values are positional by dense input column ordinal. `None` is admitted
1452/// only where the declaring column is optional; there is no other
1453/// null-shaped default.
1454#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
1455#[serde(transparent)]
1456pub struct InputRow {
1457    values: Vec<Option<CanonicalValue>>,
1458}
1459
1460impl InputRow {
1461    /// Construct one positional input row.
1462    #[must_use]
1463    pub const fn new(values: Vec<Option<CanonicalValue>>) -> Self {
1464        Self { values }
1465    }
1466
1467    /// Return positional values by dense column ordinal.
1468    #[must_use]
1469    pub fn values(&self) -> &[Option<CanonicalValue>] {
1470        &self.values
1471    }
1472}
1473
1474/// The closed operation vocabulary of the first public revision.
1475#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
1476#[serde(rename_all = "snake_case")]
1477pub enum QueryOperation {
1478    /// Stream the projected rows.
1479    Rows,
1480    /// Count the projected rows.
1481    Count,
1482    /// Report whether at least one row exists.
1483    Exists,
1484}
1485
1486/// One executable invocation of a reusable validated plan.
1487///
1488/// The invocation carries values and an operation; the plan carries all
1489/// reusable structure. Binding is by exact plan fingerprint, so a plan edit
1490/// invalidates every outstanding invocation instead of silently reshaping it.
1491#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
1492pub struct QueryInvocation {
1493    inputs: Vec<InputRow>,
1494    operation: QueryOperation,
1495    plan_fingerprint: QueryPlanFingerprint,
1496}
1497
1498impl QueryInvocation {
1499    /// Validate one rectangular input batch against its exact plan.
1500    pub fn new(
1501        plan: &QueryPlan,
1502        operation: QueryOperation,
1503        inputs: Vec<InputRow>,
1504    ) -> Result<Self, Diagnostic> {
1505        Self::new_with_limits(plan, operation, inputs, StructuralLimits::CANONICAL)
1506    }
1507
1508    fn new_with_limits(
1509        plan: &QueryPlan,
1510        operation: QueryOperation,
1511        inputs: Vec<InputRow>,
1512        limits: StructuralLimits,
1513    ) -> Result<Self, Diagnostic> {
1514        if plan.inputs().is_empty() {
1515            if !inputs.is_empty() {
1516                return Err(failure(
1517                    DiagnosticCategory::InvalidContract,
1518                    "query_invocation_unexpected_inputs",
1519                    "the plan declares no input columns yet the invocation carries rows",
1520                ));
1521            }
1522        } else {
1523            if inputs.is_empty() {
1524                return Err(failure(
1525                    DiagnosticCategory::InvalidContract,
1526                    "query_invocation_missing_inputs",
1527                    "the plan declares input columns and requires at least one row",
1528                ));
1529            }
1530            if !limits.allows_input_rows(inputs.len()) {
1531                return Err(failure(
1532                    DiagnosticCategory::ResourceLimit,
1533                    "query_invocation_row_limit",
1534                    "invocation input row count exceeds the structural ceiling",
1535                ));
1536            }
1537            let input_bytes = serde_json::to_vec(&inputs).map_err(|_| {
1538                failure(
1539                    DiagnosticCategory::InvalidContract,
1540                    "query_invocation_inputs_unencodable",
1541                    "invocation input rows cannot be encoded",
1542                )
1543            })?;
1544            if !limits.allows_input_bytes(input_bytes.len()) {
1545                return Err(failure(
1546                    DiagnosticCategory::ResourceLimit,
1547                    "query_invocation_input_byte_limit",
1548                    "invocation input rows exceed the structural byte ceiling",
1549                ));
1550            }
1551            for row in &inputs {
1552                if row.values().len() != plan.inputs().len() {
1553                    return Err(failure(
1554                        DiagnosticCategory::InvalidContract,
1555                        "query_invocation_row_arity",
1556                        "input row does not carry exactly the declared column set",
1557                    ));
1558                }
1559                for (column, value) in plan.inputs().iter().zip(row.values()) {
1560                    match value {
1561                        None if column.optional() => {}
1562                        None => {
1563                            return Err(failure(
1564                                DiagnosticCategory::InvalidContract,
1565                                "query_invocation_missing_value",
1566                                "a required input column carries no value",
1567                            ));
1568                        }
1569                        Some(value) if value.value_type() == column.value_type() => {}
1570                        Some(_) => {
1571                            return Err(failure(
1572                                DiagnosticCategory::InvalidContract,
1573                                "query_invocation_value_type",
1574                                "input value type differs from the declared column type",
1575                            ));
1576                        }
1577                    }
1578                }
1579            }
1580        }
1581        Ok(Self {
1582            inputs,
1583            operation,
1584            plan_fingerprint: plan.fingerprint()?,
1585        })
1586    }
1587
1588    /// Return the rectangular validated input rows.
1589    #[must_use]
1590    pub fn inputs(&self) -> &[InputRow] {
1591        &self.inputs
1592    }
1593
1594    /// Return the requested operation.
1595    #[must_use]
1596    pub const fn operation(&self) -> QueryOperation {
1597        self.operation
1598    }
1599
1600    /// Return the exact plan fingerprint this invocation binds.
1601    #[must_use]
1602    pub const fn plan_fingerprint(&self) -> &QueryPlanFingerprint {
1603        &self.plan_fingerprint
1604    }
1605
1606    /// Return whether this invocation still binds the supplied plan.
1607    pub fn binds(&self, plan: &QueryPlan) -> Result<bool, Diagnostic> {
1608        Ok(self.plan_fingerprint == plan.fingerprint()?)
1609    }
1610
1611    /// Return capabilities this invocation's transport requires beyond
1612    /// the plan's own.
1613    ///
1614    /// A multi-row input batch rides the native `given` transport. A single
1615    /// row with an absent optional cell also requires `given`, because inline
1616    /// literal substitution has no absence spelling. Datetime-tz inputs use
1617    /// `given` at every cardinality so named and fixed/second-offset values
1618    /// have one exact lowering. Both client preflight and executor admission
1619    /// check this set against the advertisement so untransportable
1620    /// invocations fail before any I/O or provider resource.
1621    #[must_use]
1622    pub fn transport_capabilities(&self) -> CapabilitySet {
1623        let mut capabilities = CapabilitySet::new();
1624        if self.inputs.len() > 1
1625            || self.inputs.first().is_some_and(|row| {
1626                row.values().iter().any(|value| {
1627                    value.is_none() || matches!(value, Some(CanonicalValue::DateTimeTz(_)))
1628                })
1629            })
1630        {
1631            capabilities.insert(query_given_rows_capability());
1632        }
1633        capabilities
1634    }
1635}
1636
1637/// Fingerprint of exact canonical query-plan bytes.
1638#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
1639#[serde(transparent)]
1640pub struct QueryPlanFingerprint(Fingerprint);
1641
1642impl QueryPlanFingerprint {
1643    /// Compute the fixed-domain fingerprint of a trusted plan.
1644    pub fn compute(plan: &QueryPlan) -> Result<Self, Diagnostic> {
1645        let canonicalization = match plan.format() {
1646            QUERY_PLAN_FORMAT_V1 => QUERY_PLAN_CANONICALIZATION_V1,
1647            QUERY_PLAN_FORMAT_V2 => QUERY_PLAN_CANONICALIZATION_V2,
1648            _ => {
1649                return Err(failure(
1650                    DiagnosticCategory::InvalidContract,
1651                    "query_plan_format_unsupported",
1652                    "query plan format has no fingerprint canonicalization",
1653                ));
1654            }
1655        };
1656        Ok(Self(Fingerprint::compute(
1657            FingerprintDomain::new(QUERY_PLAN_FINGERPRINT_DOMAIN)?,
1658            CanonicalizationVersion::new(canonicalization)?,
1659            None,
1660            &plan.canonical_bytes()?,
1661        )))
1662    }
1663
1664    /// Return the generic fingerprint.
1665    #[must_use]
1666    pub const fn as_fingerprint(&self) -> &Fingerprint {
1667        &self.0
1668    }
1669}
1670
1671/// Decode canonical bytes through private constructor-rebuilding wire types.
1672pub fn decode_query_plan(bytes: &[u8]) -> Result<QueryPlan, Diagnostic> {
1673    crate::query_plan_wire::decode_query_plan(bytes)
1674}
1675
1676/// Decode exact canonical invocation bytes and bind them to one trusted plan.
1677///
1678/// Reconstruction checks the serialized fingerprint before rebuilding the
1679/// invocation through the ordinary constructor, then requires byte-for-byte
1680/// canonical round-trip identity.
1681pub fn decode_query_invocation(
1682    plan: &QueryPlan,
1683    bytes: &[u8],
1684) -> Result<QueryInvocation, Diagnostic> {
1685    crate::query_invocation_wire::decode_query_invocation(plan, bytes)
1686}
1687
1688fn validate_local_functions(
1689    functions: &[LocalFunction],
1690    limits: StructuralLimits,
1691    nodes: &mut usize,
1692) -> Result<(), Diagnostic> {
1693    if !limits.allows_bindings(functions.len().max(1)) {
1694        return Err(failure(
1695            DiagnosticCategory::ResourceLimit,
1696            "query_plan_local_function_limit",
1697            "plan-local function count exceeds the structural ceiling",
1698        ));
1699    }
1700    let mut names = BTreeSet::new();
1701    for function in functions {
1702        if !names.insert(function.name().clone()) {
1703            return Err(failure(
1704                DiagnosticCategory::InvalidContract,
1705                "query_plan_duplicate_local_function",
1706                "plan-local function names must be unique",
1707            ));
1708        }
1709        let bindings = function.bindings();
1710        if bindings.is_empty() || !limits.allows_bindings(bindings.len()) {
1711            return Err(failure(
1712                DiagnosticCategory::ResourceLimit,
1713                "query_plan_binding_limit",
1714                "plan binding count is empty or exceeds the structural ceiling",
1715            ));
1716        }
1717        let mut local_names = BTreeSet::new();
1718        for (index, binding) in bindings.iter().enumerate() {
1719            if usize::from(binding.id().get()) != index {
1720                return Err(failure(
1721                    DiagnosticCategory::InvalidContract,
1722                    "query_plan_bindings_not_dense",
1723                    "plan binding IDs must be ordered dense zero-based ordinals",
1724                ));
1725            }
1726            validate_query_name_limit(
1727                binding.variable(),
1728                limits,
1729                "a local query variable name exceeds the structural ceiling",
1730            )?;
1731            if !local_names.insert(binding.variable().clone()) {
1732                return Err(failure(
1733                    DiagnosticCategory::InvalidContract,
1734                    "query_plan_duplicate_variable",
1735                    "plan query variables must be unique",
1736                ));
1737            }
1738        }
1739        if function.parameters().is_empty() || function.parameters().len() > bindings.len() {
1740            return Err(failure(
1741                DiagnosticCategory::InvalidContract,
1742                "query_plan_local_function_parameters",
1743                "parameters must be a non-empty prefix of the local bindings",
1744            ));
1745        }
1746        if function.body().is_empty() || function.body().len() > limits.boolean_terms {
1747            return Err(failure(
1748                DiagnosticCategory::ResourceLimit,
1749                "query_plan_pattern_limit",
1750                "plan root conjunction is empty or exceeds the term ceiling",
1751            ));
1752        }
1753        for pattern in function.body() {
1754            // The first local vocabulary keeps bodies flat.
1755            if matches!(
1756                pattern,
1757                QueryPattern::Or { .. }
1758                    | QueryPattern::Not { .. }
1759                    | QueryPattern::Try { .. }
1760                    | QueryPattern::Reachable { .. }
1761                    | QueryPattern::FunctionCall { .. }
1762            ) {
1763                return Err(failure(
1764                    DiagnosticCategory::InvalidContract,
1765                    "query_plan_local_function_body_unsupported",
1766                    "local bodies admit only isa, has, links, and value patterns",
1767                ));
1768            }
1769            // The predicate-node budget is one aggregate ceiling for the
1770            // whole plan: local bodies charge the same counter the root
1771            // conjunction does instead of resetting per function.
1772            inspect_pattern(pattern, 1, bindings.len(), 0, limits, nodes)?;
1773        }
1774        let returns = function.returns();
1775        check_binding(returns.input(), bindings.len())?;
1776        if !returns.reducer().total_without_groups() {
1777            return Err(failure(
1778                DiagnosticCategory::InvalidContract,
1779                "query_plan_local_function_return_partial",
1780                "local returns admit only reducers total on empty streams",
1781            ));
1782        }
1783        let declared_valid = match returns.reducer() {
1784            Reducer::Count => returns.value_type() == ValueTypeTag::Long,
1785            Reducer::Sum => matches!(
1786                returns.value_type(),
1787                ValueTypeTag::Long | ValueTypeTag::Double
1788            ),
1789            Reducer::Max | Reducer::Min | Reducer::Mean | Reducer::Median | Reducer::Std => false,
1790        };
1791        if !declared_valid {
1792            return Err(failure(
1793                DiagnosticCategory::InvalidContract,
1794                "query_plan_local_function_return_type",
1795                "declared return type does not fit the reducer",
1796            ));
1797        }
1798    }
1799    Ok(())
1800}
1801
1802fn validate_query_name_limit(
1803    name: &QueryVariable,
1804    limits: StructuralLimits,
1805    message: &'static str,
1806) -> Result<(), Diagnostic> {
1807    if name.as_str().len() > limits.output_name_bytes {
1808        return Err(failure(
1809            DiagnosticCategory::ResourceLimit,
1810            "query_plan_name_limit",
1811            message,
1812        ));
1813    }
1814    Ok(())
1815}
1816
1817fn validate_pipeline(
1818    pipeline: &[ReadStage],
1819    binding_count: usize,
1820    input_count: usize,
1821    limits: StructuralLimits,
1822    nodes: &mut usize,
1823) -> Result<(BTreeSet<BindingId>, BTreeSet<BindingId>, bool), Diagnostic> {
1824    let Some((first, rest)) = pipeline.split_first() else {
1825        return Err(failure(
1826            DiagnosticCategory::InvalidContract,
1827            "query_plan_empty_pipeline",
1828            "a read pipeline requires at least its match stage",
1829        ));
1830    };
1831    let ReadStage::Match { patterns } = first else {
1832        return Err(failure(
1833            DiagnosticCategory::InvalidContract,
1834            "query_plan_match_not_first",
1835            "the pattern conjunction must be the first pipeline stage",
1836        ));
1837    };
1838    if patterns.is_empty() || patterns.len() > limits.boolean_terms {
1839        return Err(failure(
1840            DiagnosticCategory::ResourceLimit,
1841            "query_plan_pattern_limit",
1842            "plan root conjunction is empty or exceeds the term ceiling",
1843        ));
1844    }
1845    for pattern in patterns {
1846        inspect_pattern(pattern, 1, binding_count, input_count, limits, nodes)?;
1847    }
1848
1849    // Keep every pattern reference for freshness checks, but derive the row
1850    // environment only from bindings positively established in the root
1851    // conjunction. Negation-local witnesses never become row columns merely
1852    // because their binding IDs are referenced in a nested scope.
1853    let mut pattern_bound = BTreeSet::new();
1854    for pattern in patterns {
1855        collect_pattern_bindings(pattern, &mut pattern_bound);
1856    }
1857    // Bindings established only inside a try body are optional: rows carry
1858    // them or an explicit absence. Each optional binding belongs to exactly
1859    // one try body; sharing one across bodies has no single presence source.
1860    let mut root_mandatory = BTreeSet::new();
1861    let mut scoped_positive = BTreeSet::new();
1862    for pattern in patterns {
1863        collect_direct_positive_bindings(pattern, &mut root_mandatory);
1864        collect_negation_positive_bindings(pattern, &mut scoped_positive);
1865    }
1866    let mut optional = BTreeSet::new();
1867    for pattern in patterns {
1868        let QueryPattern::Try { patterns } = pattern else {
1869            continue;
1870        };
1871        let mut body_positive = BTreeSet::new();
1872        for child in patterns {
1873            collect_direct_positive_bindings(child, &mut body_positive);
1874        }
1875        for local in body_positive.difference(&root_mandatory) {
1876            if !optional.insert(*local) {
1877                return Err(failure(
1878                    DiagnosticCategory::InvalidContract,
1879                    "query_plan_try_binding_shared",
1880                    "an optional binding belongs to exactly one try body",
1881                ));
1882            }
1883        }
1884    }
1885    if !scoped_positive.is_disjoint(&optional) {
1886        return Err(failure(
1887            DiagnosticCategory::InvalidContract,
1888            "query_plan_try_binding_shared",
1889            "an optional binding cannot also be a negation-local witness",
1890        ));
1891    }
1892    let mut mandatory = root_mandatory;
1893    let mut previous_ordinal = 0u8;
1894    let mut has_sort = false;
1895    for stage in rest {
1896        let ordinal = stage.ordinal();
1897        if ordinal <= previous_ordinal {
1898            return Err(failure(
1899                DiagnosticCategory::InvalidContract,
1900                "query_plan_stage_order",
1901                "pipeline stages must follow the canonical order exactly once each",
1902            ));
1903        }
1904        previous_ordinal = ordinal;
1905        match stage {
1906            ReadStage::Match { .. } => unreachable!("ordinal zero cannot follow"),
1907            ReadStage::Select { bindings } => {
1908                let union: BTreeSet<BindingId> = mandatory.union(&optional).copied().collect();
1909                let selected = canonical_stage_set(bindings, &union, "select")?;
1910                mandatory.retain(|id| selected.contains(id));
1911                optional.retain(|id| selected.contains(id));
1912            }
1913            ReadStage::Require { bindings } => {
1914                // The provider contract for requiring an optional binding is
1915                // unproven (TypeDB 3.12.1 stalls on it); the first optional
1916                // vocabulary reserves require to mandatory bindings.
1917                let union: BTreeSet<BindingId> = mandatory.union(&optional).copied().collect();
1918                let required = canonical_stage_set(bindings, &union, "require")?;
1919                if required.iter().any(|id| optional.contains(id)) {
1920                    return Err(failure(
1921                        DiagnosticCategory::InvalidContract,
1922                        "query_plan_require_optional_reserved",
1923                        "requiring an optional binding is reserved in this vocabulary",
1924                    ));
1925                }
1926            }
1927            ReadStage::Distinct => {}
1928            ReadStage::Reduce {
1929                assignments,
1930                groups,
1931            } => {
1932                if assignments.is_empty()
1933                    || assignments.len() > limits.boolean_terms
1934                    || groups.len() > limits.boolean_terms
1935                {
1936                    return Err(failure(
1937                        DiagnosticCategory::ResourceLimit,
1938                        "query_plan_reduce_term_limit",
1939                        "reduce has no assignments or exceeds the term ceiling",
1940                    ));
1941                }
1942                let mut previous = None;
1943                let mut next_visible = BTreeSet::new();
1944                for group in groups {
1945                    if previous.is_some_and(|previous: BindingId| previous >= *group) {
1946                        return Err(failure(
1947                            DiagnosticCategory::InvalidContract,
1948                            "query_plan_stage_set_not_canonical",
1949                            "stage binding sets must be strictly ascending",
1950                        ));
1951                    }
1952                    previous = Some(*group);
1953                    // Grouping by an optional binding would key groups on
1954                    // absence; group keys stay mandatory.
1955                    if !mandatory.contains(group) {
1956                        return Err(failure(
1957                            DiagnosticCategory::InvalidContract,
1958                            "query_plan_stage_unknown_binding",
1959                            "reduce groups a binding outside the mandatory row environment",
1960                        ));
1961                    }
1962                    next_visible.insert(*group);
1963                }
1964                for assignment in assignments {
1965                    check_binding(assignment.assigned(), binding_count)?;
1966                    if pattern_bound.contains(&assignment.assigned())
1967                        || !next_visible.insert(assignment.assigned())
1968                    {
1969                        return Err(failure(
1970                            DiagnosticCategory::InvalidContract,
1971                            "query_plan_reduce_assigned_bound",
1972                            "reduce must assign a fresh binding free of patterns and groups",
1973                        ));
1974                    }
1975                    match assignment.input() {
1976                        Some(input) => {
1977                            if !mandatory.contains(&input) && !optional.contains(&input) {
1978                                return Err(failure(
1979                                    DiagnosticCategory::InvalidContract,
1980                                    "query_plan_stage_unknown_binding",
1981                                    "reduce consumes a binding outside the visible row environment",
1982                                ));
1983                            }
1984                            // Count and sum skip absent inputs and stay
1985                            // total; max, min, and mean can observe a group
1986                            // whose optional input never matched.
1987                            if optional.contains(&input)
1988                                && !assignment.reducer().total_without_groups()
1989                            {
1990                                return Err(failure(
1991                                    DiagnosticCategory::InvalidContract,
1992                                    "query_plan_reduce_optional_input",
1993                                    "this reducer is undefined over an optional input",
1994                                ));
1995                            }
1996                        }
1997                        None => {
1998                            if assignment.reducer().requires_input() {
1999                                return Err(failure(
2000                                    DiagnosticCategory::InvalidContract,
2001                                    "query_plan_reduce_missing_input",
2002                                    "this reducer consumes an input binding",
2003                                ));
2004                            }
2005                        }
2006                    }
2007                    if groups.is_empty() && !assignment.reducer().total_without_groups() {
2008                        return Err(failure(
2009                            DiagnosticCategory::InvalidContract,
2010                            "query_plan_reduce_requires_groups",
2011                            "reducers undefined on empty streams require group bindings",
2012                        ));
2013                    }
2014                }
2015                mandatory = next_visible;
2016                optional.clear();
2017            }
2018            ReadStage::Sort { terms } => {
2019                if terms.is_empty() || !limits.allows_order_terms(terms.len()) {
2020                    return Err(failure(
2021                        DiagnosticCategory::ResourceLimit,
2022                        "query_plan_sort_term_limit",
2023                        "sort has no terms or exceeds the term ceiling",
2024                    ));
2025                }
2026                let mut sorted = BTreeSet::new();
2027                for term in terms {
2028                    // Absence has no defined position in a total order;
2029                    // sort keys stay mandatory.
2030                    if !mandatory.contains(&term.binding()) {
2031                        return Err(failure(
2032                            DiagnosticCategory::InvalidContract,
2033                            "query_plan_stage_unknown_binding",
2034                            "sort references a binding outside the mandatory row environment",
2035                        ));
2036                    }
2037                    if !sorted.insert(term.binding()) {
2038                        return Err(failure(
2039                            DiagnosticCategory::InvalidContract,
2040                            "query_plan_duplicate_sort_binding",
2041                            "sort references one binding twice",
2042                        ));
2043                    }
2044                }
2045                has_sort = true;
2046            }
2047            ReadStage::Offset { .. } | ReadStage::Limit { .. } => {}
2048        }
2049    }
2050    Ok((mandatory, optional, has_sort))
2051}
2052
2053fn canonical_stage_set(
2054    bindings: &[BindingId],
2055    visible: &BTreeSet<BindingId>,
2056    stage: &'static str,
2057) -> Result<BTreeSet<BindingId>, Diagnostic> {
2058    if bindings.is_empty() {
2059        return Err(failure(
2060            DiagnosticCategory::InvalidContract,
2061            "query_plan_empty_stage_set",
2062            stage,
2063        ));
2064    }
2065    let mut set = BTreeSet::new();
2066    let mut previous = None;
2067    for binding in bindings {
2068        if previous.is_some_and(|previous: BindingId| previous >= *binding) {
2069            return Err(failure(
2070                DiagnosticCategory::InvalidContract,
2071                "query_plan_stage_set_not_canonical",
2072                "stage binding sets must be strictly ascending",
2073            ));
2074        }
2075        previous = Some(*binding);
2076        if !visible.contains(binding) {
2077            return Err(failure(
2078                DiagnosticCategory::InvalidContract,
2079                "query_plan_stage_unknown_binding",
2080                "stage references a binding outside the visible row environment",
2081            ));
2082        }
2083        set.insert(*binding);
2084    }
2085    Ok(set)
2086}
2087
2088fn inspect_pattern(
2089    pattern: &QueryPattern,
2090    depth: usize,
2091    binding_count: usize,
2092    input_count: usize,
2093    limits: StructuralLimits,
2094    nodes: &mut usize,
2095) -> Result<(), Diagnostic> {
2096    *nodes += 1;
2097    if !limits.allows_predicate_nodes(*nodes) {
2098        return Err(failure(
2099            DiagnosticCategory::ResourceLimit,
2100            "query_plan_pattern_node_limit",
2101            "plan pattern count exceeds the structural ceiling",
2102        ));
2103    }
2104    if !limits.allows_predicate_depth(depth) {
2105        return Err(failure(
2106            DiagnosticCategory::ResourceLimit,
2107            "query_plan_pattern_depth_limit",
2108            "plan pattern depth exceeds the structural ceiling",
2109        ));
2110    }
2111    match pattern {
2112        QueryPattern::Isa { binding, .. } => check_binding(*binding, binding_count),
2113        QueryPattern::Has {
2114            owner, attribute, ..
2115        } => {
2116            check_binding(*owner, binding_count)?;
2117            check_binding(*attribute, binding_count)
2118        }
2119        QueryPattern::Links {
2120            relation, players, ..
2121        } => {
2122            check_binding(*relation, binding_count)?;
2123            if players.is_empty() || players.len() > limits.boolean_terms {
2124                return Err(failure(
2125                    DiagnosticCategory::ResourceLimit,
2126                    "query_plan_role_player_limit",
2127                    "links pattern has no players or exceeds the term ceiling",
2128                ));
2129            }
2130            for player in players {
2131                check_binding(player.player(), binding_count)?;
2132            }
2133            Ok(())
2134        }
2135        QueryPattern::Value { left, right, .. } => {
2136            check_operand(left, binding_count, input_count)?;
2137            check_operand(right, binding_count, input_count)
2138        }
2139        QueryPattern::Or { branches } => {
2140            if branches.is_empty()
2141                || branches.len() > limits.boolean_terms
2142                || branches
2143                    .iter()
2144                    .any(|branch| branch.is_empty() || branch.len() > limits.boolean_terms)
2145            {
2146                return Err(failure(
2147                    DiagnosticCategory::ResourceLimit,
2148                    "query_plan_disjunction_term_limit",
2149                    "disjunction branches are empty or exceed the boolean-term ceiling",
2150                ));
2151            }
2152            for branch in branches {
2153                for child in branch {
2154                    inspect_pattern(child, depth + 1, binding_count, input_count, limits, nodes)?;
2155                }
2156            }
2157            Ok(())
2158        }
2159        QueryPattern::Not { patterns } => {
2160            if patterns.is_empty() || patterns.len() > limits.boolean_terms {
2161                return Err(failure(
2162                    DiagnosticCategory::ResourceLimit,
2163                    "query_plan_negation_term_limit",
2164                    "negation is empty or exceeds the boolean-term ceiling",
2165                ));
2166            }
2167            for child in patterns {
2168                inspect_pattern(child, depth + 1, binding_count, input_count, limits, nodes)?;
2169            }
2170            Ok(())
2171        }
2172        QueryPattern::Try { patterns } => {
2173            if depth > 1 {
2174                return Err(failure(
2175                    DiagnosticCategory::InvalidContract,
2176                    "query_plan_try_not_root",
2177                    "optional blocks are admitted only in the root conjunction",
2178                ));
2179            }
2180            if patterns.is_empty() || patterns.len() > limits.boolean_terms {
2181                return Err(failure(
2182                    DiagnosticCategory::ResourceLimit,
2183                    "query_plan_try_term_limit",
2184                    "optional block is empty or exceeds the boolean-term ceiling",
2185                ));
2186            }
2187            for child in patterns {
2188                if matches!(
2189                    child,
2190                    QueryPattern::Or { .. }
2191                        | QueryPattern::Not { .. }
2192                        | QueryPattern::Try { .. }
2193                        | QueryPattern::Reachable { .. }
2194                        | QueryPattern::FunctionCall { .. }
2195                ) {
2196                    return Err(failure(
2197                        DiagnosticCategory::InvalidContract,
2198                        "query_plan_try_body_unsupported",
2199                        "the first optional vocabulary admits only isa, has, links, and value patterns",
2200                    ));
2201                }
2202                inspect_pattern(child, depth + 1, binding_count, input_count, limits, nodes)?;
2203            }
2204            Ok(())
2205        }
2206        QueryPattern::Reachable {
2207            min_depth,
2208            max_depth,
2209            source,
2210            target,
2211            ..
2212        } => {
2213            if depth > 1 {
2214                return Err(failure(
2215                    DiagnosticCategory::InvalidContract,
2216                    "query_plan_reachable_not_root",
2217                    "bounded reachability is admitted only in the root conjunction",
2218                ));
2219            }
2220            if *min_depth == 1 && *max_depth == 0 {
2221                return Err(failure(
2222                    DiagnosticCategory::ResourceLimit,
2223                    "query_plan_reachable_depth",
2224                    "reachability requires a finite hop bound within the depth ceiling",
2225                ));
2226            }
2227            if min_depth > max_depth {
2228                return Err(failure(
2229                    DiagnosticCategory::InvalidContract,
2230                    "query_plan_reachable_bounds",
2231                    "reachability minimum depth must not exceed its maximum depth",
2232                ));
2233            }
2234            if !limits.allows_predicate_depth(usize::from(*max_depth)) {
2235                return Err(failure(
2236                    DiagnosticCategory::ResourceLimit,
2237                    "query_plan_reachable_depth",
2238                    "reachability requires a finite hop bound within the depth ceiling",
2239                ));
2240            }
2241            // Lowering unrolls one branch per admitted path length. Charge
2242            // every emitted hop against the shared node budget. The zero-hop
2243            // identity branch is one clause, so a compact plan cannot smuggle
2244            // hundreds of thousands of emitted clauses past the ceiling.
2245            let first_positive = usize::from((*min_depth).max(1));
2246            let bound = usize::from(*max_depth);
2247            let expanded_hops = if first_positive <= bound {
2248                (first_positive..=bound).fold(0usize, usize::saturating_add)
2249            } else {
2250                0
2251            };
2252            let expanded_clauses = expanded_hops.saturating_add(usize::from(*min_depth == 0));
2253            *nodes = nodes.saturating_add(expanded_clauses.saturating_sub(1));
2254            if !limits.allows_predicate_nodes(*nodes) {
2255                return Err(failure(
2256                    DiagnosticCategory::ResourceLimit,
2257                    "query_plan_reachable_expansion_limit",
2258                    "reachability expansion exceeds the plan pattern-node ceiling",
2259                ));
2260            }
2261            check_binding(*source, binding_count)?;
2262            check_binding(*target, binding_count)
2263        }
2264        QueryPattern::FunctionCall {
2265            arguments,
2266            assigned,
2267            ..
2268        } => {
2269            if depth > 1 {
2270                return Err(failure(
2271                    DiagnosticCategory::InvalidContract,
2272                    "query_plan_function_in_negation",
2273                    "function calls are admitted only in the root conjunction",
2274                ));
2275            }
2276            if arguments.len() > limits.boolean_terms {
2277                return Err(failure(
2278                    DiagnosticCategory::ResourceLimit,
2279                    "query_plan_function_argument_limit",
2280                    "function call arguments exceed the term ceiling",
2281                ));
2282            }
2283            for argument in arguments {
2284                check_operand(argument, binding_count, input_count)?;
2285            }
2286            check_binding(*assigned, binding_count)
2287        }
2288    }
2289}
2290
2291fn collect_pattern_bindings(pattern: &QueryPattern, bindings: &mut BTreeSet<BindingId>) {
2292    let mut operand = |operand: &QueryOperand| {
2293        if let QueryOperand::Binding { binding } = operand {
2294            bindings.insert(*binding);
2295        }
2296    };
2297    match pattern {
2298        QueryPattern::Isa { binding, .. } => {
2299            bindings.insert(*binding);
2300        }
2301        QueryPattern::Has {
2302            owner, attribute, ..
2303        } => {
2304            bindings.insert(*owner);
2305            bindings.insert(*attribute);
2306        }
2307        QueryPattern::Links {
2308            relation, players, ..
2309        } => {
2310            bindings.insert(*relation);
2311            for player in players {
2312                bindings.insert(player.player());
2313            }
2314        }
2315        QueryPattern::Value { left, right, .. } => {
2316            operand(left);
2317            operand(right);
2318        }
2319        QueryPattern::Or { branches } => {
2320            for branch in branches {
2321                for child in branch {
2322                    collect_pattern_bindings(child, bindings);
2323                }
2324            }
2325        }
2326        QueryPattern::Not { patterns } | QueryPattern::Try { patterns } => {
2327            for child in patterns {
2328                collect_pattern_bindings(child, bindings);
2329            }
2330        }
2331        QueryPattern::Reachable { source, target, .. } => {
2332            bindings.insert(*source);
2333            bindings.insert(*target);
2334        }
2335        QueryPattern::FunctionCall {
2336            arguments,
2337            assigned,
2338            ..
2339        } => {
2340            for argument in arguments {
2341                operand(argument);
2342            }
2343            bindings.insert(*assigned);
2344        }
2345    }
2346}
2347
2348/// Collect bindings positively established by one pattern in its current
2349/// lexical scope. References used only as value/function operands are not
2350/// producers, and nested scopes are deliberately not traversed.
2351fn collect_direct_positive_bindings(pattern: &QueryPattern, bindings: &mut BTreeSet<BindingId>) {
2352    match pattern {
2353        QueryPattern::Isa { binding, .. } => {
2354            bindings.insert(*binding);
2355        }
2356        QueryPattern::Has {
2357            owner, attribute, ..
2358        } => {
2359            bindings.extend([*owner, *attribute]);
2360        }
2361        QueryPattern::Links {
2362            relation, players, ..
2363        } => {
2364            bindings.insert(*relation);
2365            bindings.extend(players.iter().map(AssertionRolePlayer::player));
2366        }
2367        QueryPattern::Reachable { source, target, .. } => {
2368            bindings.extend([*source, *target]);
2369        }
2370        QueryPattern::FunctionCall { assigned, .. } => {
2371            bindings.insert(*assigned);
2372        }
2373        QueryPattern::Or { branches } => {
2374            let mut branches = branches.iter().map(|patterns| {
2375                let mut branch = BTreeSet::new();
2376                for pattern in patterns {
2377                    collect_direct_positive_bindings(pattern, &mut branch);
2378                }
2379                branch
2380            });
2381            if let Some(mut intersection) = branches.next() {
2382                for branch in branches {
2383                    intersection.retain(|binding| branch.contains(binding));
2384                }
2385                bindings.extend(intersection);
2386            }
2387        }
2388        QueryPattern::Value { .. } | QueryPattern::Not { .. } | QueryPattern::Try { .. } => {}
2389    }
2390}
2391
2392/// Collect bindings established only inside negation scopes, including nested
2393/// negations. Optional blocks cannot occur below a negation in this vocabulary,
2394/// so every nested positive producer is a scoped witness.
2395fn collect_negation_positive_bindings(pattern: &QueryPattern, bindings: &mut BTreeSet<BindingId>) {
2396    match pattern {
2397        QueryPattern::Not { patterns } => {
2398            for child in patterns {
2399                collect_direct_positive_bindings(child, bindings);
2400                collect_negation_positive_bindings(child, bindings);
2401            }
2402        }
2403        QueryPattern::Or { branches } => {
2404            for branch in branches {
2405                for child in branch {
2406                    collect_negation_positive_bindings(child, bindings);
2407                }
2408            }
2409        }
2410        QueryPattern::Isa { .. }
2411        | QueryPattern::Has { .. }
2412        | QueryPattern::Links { .. }
2413        | QueryPattern::Value { .. }
2414        | QueryPattern::Try { .. }
2415        | QueryPattern::Reachable { .. }
2416        | QueryPattern::FunctionCall { .. } => {}
2417    }
2418}
2419
2420fn check_operand(
2421    operand: &QueryOperand,
2422    binding_count: usize,
2423    input_count: usize,
2424) -> Result<(), Diagnostic> {
2425    match operand {
2426        QueryOperand::Binding { binding } => check_binding(*binding, binding_count),
2427        QueryOperand::Literal { .. } => Ok(()),
2428        QueryOperand::Input { column } => {
2429            if usize::from(column.get()) < input_count {
2430                Ok(())
2431            } else {
2432                Err(failure(
2433                    DiagnosticCategory::InvalidContract,
2434                    "query_plan_unknown_input_column",
2435                    "pattern references an undeclared input column",
2436                ))
2437            }
2438        }
2439    }
2440}
2441
2442fn check_binding(binding: BindingId, binding_count: usize) -> Result<(), Diagnostic> {
2443    if usize::from(binding.get()) < binding_count {
2444        Ok(())
2445    } else {
2446        Err(failure(
2447            DiagnosticCategory::InvalidContract,
2448            "query_plan_unknown_binding",
2449            "pattern references an undeclared binding",
2450        ))
2451    }
2452}
2453
2454fn derive_capabilities(
2455    pipeline: &[ReadStage],
2456    functions: &[LocalFunction],
2457    inputs: &[InputColumn],
2458    output: &QueryOutput,
2459    compatibility: Option<&QueryPlanV2Compatibility>,
2460) -> Result<CapabilitySet, Diagnostic> {
2461    let mut capabilities = CapabilitySet::new();
2462    insert_capability(&mut capabilities, CAP_PLAN)?;
2463    if !functions.is_empty() {
2464        insert_capability(&mut capabilities, CAP_LOCAL_FUNCTIONS)?;
2465        for function in functions {
2466            for pattern in function.body() {
2467                collect_pattern_capabilities(pattern, &mut capabilities)?;
2468            }
2469        }
2470    }
2471    match output {
2472        QueryOutput::Rows { .. } => {
2473            insert_capability(&mut capabilities, CAP_OUTPUT_ROWS)?;
2474        }
2475        QueryOutput::Documents { .. } => {
2476            insert_capability(&mut capabilities, CAP_OUTPUT_DOCUMENTS)?;
2477        }
2478    }
2479    if !inputs.is_empty() {
2480        insert_capability(&mut capabilities, CAP_INPUT_COLUMNS)?;
2481    }
2482    for stage in pipeline {
2483        match stage {
2484            ReadStage::Match { patterns } => {
2485                for pattern in patterns {
2486                    collect_pattern_capabilities(pattern, &mut capabilities)?;
2487                }
2488            }
2489            ReadStage::Select { .. } => {
2490                insert_capability(&mut capabilities, CAP_STAGE_SELECT)?;
2491            }
2492            ReadStage::Require { .. } => {
2493                insert_capability(&mut capabilities, CAP_STAGE_REQUIRE)?;
2494            }
2495            ReadStage::Distinct => {
2496                insert_capability(&mut capabilities, CAP_STAGE_DISTINCT)?;
2497            }
2498            ReadStage::Reduce { .. } => {
2499                insert_capability(&mut capabilities, CAP_STAGE_REDUCE)?;
2500            }
2501            ReadStage::Sort { .. } => {
2502                insert_capability(&mut capabilities, CAP_STAGE_SORT)?;
2503            }
2504            ReadStage::Offset { .. } => {
2505                insert_capability(&mut capabilities, CAP_STAGE_OFFSET)?;
2506            }
2507            ReadStage::Limit { .. } => {
2508                insert_capability(&mut capabilities, CAP_STAGE_LIMIT)?;
2509            }
2510        }
2511    }
2512    if let Some(compatibility) = compatibility {
2513        insert_capability(&mut capabilities, v2::CAP_PLAN_V2)?;
2514        compatibility.add_capabilities(&mut capabilities)?;
2515        let vocabulary = query_plan_v2_capability_vocabulary();
2516        let unknown = capabilities.missing_from(&vocabulary);
2517        if !unknown.is_empty() {
2518            return Err(failure(
2519                DiagnosticCategory::Integrity,
2520                "query_plan_v2_capability_inventory_incomplete",
2521                "V2 syntax derived a capability absent from the exhaustive vocabulary",
2522            ));
2523        }
2524    }
2525    Ok(capabilities)
2526}
2527
2528fn collect_pattern_capabilities(
2529    pattern: &QueryPattern,
2530    capabilities: &mut CapabilitySet,
2531) -> Result<(), Diagnostic> {
2532    match pattern {
2533        QueryPattern::Isa {
2534            include_subtypes, ..
2535        } => {
2536            insert_capability(capabilities, CAP_ISA)?;
2537            if *include_subtypes {
2538                insert_capability(capabilities, CAP_ISA_SUBTYPES)?;
2539            }
2540        }
2541        QueryPattern::Has { .. } => insert_capability(capabilities, CAP_HAS)?,
2542        QueryPattern::Links { .. } => insert_capability(capabilities, CAP_LINKS)?,
2543        QueryPattern::Value { .. } => insert_capability(capabilities, CAP_VALUE)?,
2544        QueryPattern::Or { branches } => {
2545            insert_capability(capabilities, CAP_DISJUNCTION)?;
2546            for branch in branches {
2547                for child in branch {
2548                    collect_pattern_capabilities(child, capabilities)?;
2549                }
2550            }
2551        }
2552        QueryPattern::Try { patterns } => {
2553            insert_capability(capabilities, CAP_TRY)?;
2554            for child in patterns {
2555                collect_pattern_capabilities(child, capabilities)?;
2556            }
2557        }
2558        QueryPattern::Reachable { .. } => {
2559            insert_capability(capabilities, CAP_REACHABLE)?;
2560        }
2561        QueryPattern::Not { patterns } => {
2562            insert_capability(capabilities, CAP_NEGATION)?;
2563            for child in patterns {
2564                collect_pattern_capabilities(child, capabilities)?;
2565            }
2566        }
2567        QueryPattern::FunctionCall { .. } => {
2568            insert_capability(capabilities, CAP_FUNCTION_CALL)?;
2569        }
2570    }
2571    Ok(())
2572}
2573
2574pub(crate) fn insert_capability(
2575    capabilities: &mut CapabilitySet,
2576    value: &'static str,
2577) -> Result<(), Diagnostic> {
2578    capabilities.insert(CapabilityId::new(value)?);
2579    Ok(())
2580}
2581
2582pub(crate) fn failure(
2583    category: DiagnosticCategory,
2584    code: &'static str,
2585    message: &'static str,
2586) -> Diagnostic {
2587    Diagnostic::new(
2588        category,
2589        DiagnosticCode::new(code).expect("static query-plan diagnostic code"),
2590        message,
2591    )
2592}