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