Skip to main content

nodedb_sql/
types.rs

1//! SqlPlan intermediate representation types.
2//!
3//! These types represent the output of the nodedb-sql planner. Both Origin
4//! (server) and Lite (embedded) map these to their own execution model.
5
6/// The top-level plan produced by the SQL planner.
7#[derive(Debug, Clone)]
8pub enum SqlPlan {
9    // ── Constant ──
10    /// Query with no FROM clause: SELECT 1, SELECT 'hello' AS name, etc.
11    /// Produces a single row with evaluated constant expressions.
12    ConstantResult {
13        columns: Vec<String>,
14        values: Vec<SqlValue>,
15    },
16
17    // ── Reads ──
18    Scan {
19        collection: String,
20        alias: Option<String>,
21        engine: EngineType,
22        filters: Vec<Filter>,
23        projection: Vec<Projection>,
24        sort_keys: Vec<SortKey>,
25        limit: Option<usize>,
26        offset: usize,
27        distinct: bool,
28        window_functions: Vec<WindowSpec>,
29    },
30    PointGet {
31        collection: String,
32        alias: Option<String>,
33        engine: EngineType,
34        key_column: String,
35        key_value: SqlValue,
36    },
37    /// Document fetch via a secondary index: equality predicate on an
38    /// indexed field. The executor performs an index lookup to resolve
39    /// matching document IDs, reads each document, and applies any
40    /// remaining filters, projection, sort, and limit.
41    ///
42    /// Emitted by `document_schemaless::plan_scan` /
43    /// `document_strict::plan_scan` when the WHERE clause contains a
44    /// single equality predicate on a `Ready` indexed field. Any
45    /// additional predicates fall through as post-filters.
46    DocumentIndexLookup {
47        collection: String,
48        alias: Option<String>,
49        engine: EngineType,
50        /// Indexed field path used for the lookup.
51        field: String,
52        /// Equality value from the WHERE clause.
53        value: SqlValue,
54        /// Remaining filters after extracting the equality used for lookup.
55        filters: Vec<Filter>,
56        projection: Vec<Projection>,
57        sort_keys: Vec<SortKey>,
58        limit: Option<usize>,
59        offset: usize,
60        distinct: bool,
61        window_functions: Vec<WindowSpec>,
62        /// Whether the chosen index is COLLATE NOCASE — the executor
63        /// lowercases the lookup value before probing.
64        case_insensitive: bool,
65    },
66    RangeScan {
67        collection: String,
68        field: String,
69        lower: Option<SqlValue>,
70        upper: Option<SqlValue>,
71        limit: usize,
72    },
73
74    // ── Writes ──
75    Insert {
76        collection: String,
77        engine: EngineType,
78        rows: Vec<Vec<(String, SqlValue)>>,
79        /// Column defaults from schema: `(column_name, default_expr)`.
80        /// Used to auto-generate values for missing columns (e.g. `id` with `UUID_V7`).
81        column_defaults: Vec<(String, String)>,
82    },
83    /// KV INSERT: key and value are fundamentally separate.
84    /// Each entry is `(key, value_columns)`.
85    KvInsert {
86        collection: String,
87        entries: Vec<(SqlValue, Vec<(String, SqlValue)>)>,
88        /// TTL in seconds (0 = no expiry). Extracted from `ttl` column if present.
89        ttl_secs: u64,
90    },
91    /// UPSERT: insert or merge if document exists.
92    Upsert {
93        collection: String,
94        engine: EngineType,
95        rows: Vec<Vec<(String, SqlValue)>>,
96        column_defaults: Vec<(String, String)>,
97        /// `ON CONFLICT (...) DO UPDATE SET field = expr` assignments.
98        /// When empty, upsert is a plain merge: new columns overwrite existing.
99        /// When non-empty, the engine applies these per-row against the
100        /// *existing* document instead of merging the inserted values.
101        on_conflict_updates: Vec<(String, SqlExpr)>,
102    },
103    InsertSelect {
104        target: String,
105        source: Box<SqlPlan>,
106        limit: usize,
107    },
108    Update {
109        collection: String,
110        engine: EngineType,
111        assignments: Vec<(String, SqlExpr)>,
112        filters: Vec<Filter>,
113        target_keys: Vec<SqlValue>,
114        returning: bool,
115    },
116    Delete {
117        collection: String,
118        engine: EngineType,
119        filters: Vec<Filter>,
120        target_keys: Vec<SqlValue>,
121    },
122    Truncate {
123        collection: String,
124        restart_identity: bool,
125    },
126
127    // ── Joins ──
128    Join {
129        left: Box<SqlPlan>,
130        right: Box<SqlPlan>,
131        on: Vec<(String, String)>,
132        join_type: JoinType,
133        condition: Option<SqlExpr>,
134        limit: usize,
135        /// Post-join projection: column names to keep (empty = all columns).
136        projection: Vec<Projection>,
137        /// Post-join filters (from WHERE clause).
138        filters: Vec<Filter>,
139    },
140
141    // ── Aggregation ──
142    Aggregate {
143        input: Box<SqlPlan>,
144        group_by: Vec<SqlExpr>,
145        aggregates: Vec<AggregateExpr>,
146        having: Vec<Filter>,
147        limit: usize,
148    },
149
150    // ── Timeseries ──
151    TimeseriesScan {
152        collection: String,
153        time_range: (i64, i64),
154        bucket_interval_ms: i64,
155        group_by: Vec<String>,
156        aggregates: Vec<AggregateExpr>,
157        filters: Vec<Filter>,
158        projection: Vec<Projection>,
159        gap_fill: String,
160        limit: usize,
161        tiered: bool,
162    },
163    TimeseriesIngest {
164        collection: String,
165        rows: Vec<Vec<(String, SqlValue)>>,
166    },
167
168    // ── Search (first-class) ──
169    VectorSearch {
170        collection: String,
171        field: String,
172        query_vector: Vec<f32>,
173        top_k: usize,
174        ef_search: usize,
175        filters: Vec<Filter>,
176    },
177    MultiVectorSearch {
178        collection: String,
179        query_vector: Vec<f32>,
180        top_k: usize,
181        ef_search: usize,
182    },
183    TextSearch {
184        collection: String,
185        query: String,
186        top_k: usize,
187        fuzzy: bool,
188        filters: Vec<Filter>,
189    },
190    HybridSearch {
191        collection: String,
192        query_vector: Vec<f32>,
193        query_text: String,
194        top_k: usize,
195        ef_search: usize,
196        vector_weight: f32,
197        fuzzy: bool,
198    },
199    SpatialScan {
200        collection: String,
201        field: String,
202        predicate: SpatialPredicate,
203        query_geometry: Vec<u8>,
204        distance_meters: f64,
205        attribute_filters: Vec<Filter>,
206        limit: usize,
207        projection: Vec<Projection>,
208    },
209
210    // ── Composite ──
211    Union {
212        inputs: Vec<SqlPlan>,
213        distinct: bool,
214    },
215    Intersect {
216        left: Box<SqlPlan>,
217        right: Box<SqlPlan>,
218        all: bool,
219    },
220    Except {
221        left: Box<SqlPlan>,
222        right: Box<SqlPlan>,
223        all: bool,
224    },
225    RecursiveScan {
226        collection: String,
227        base_filters: Vec<Filter>,
228        recursive_filters: Vec<Filter>,
229        /// Equi-join link for tree-traversal recursion:
230        /// `(collection_field, working_table_field)`.
231        /// e.g. `("parent_id", "id")` means each iteration finds rows
232        /// where `collection.parent_id` matches a `working_table.id`.
233        join_link: Option<(String, String)>,
234        max_iterations: usize,
235        distinct: bool,
236        limit: usize,
237    },
238
239    /// Non-recursive CTE: execute each definition, then the outer query.
240    Cte {
241        /// CTE definitions: `(name, subquery_plan)`.
242        definitions: Vec<(String, SqlPlan)>,
243        /// The outer query that references CTE names.
244        outer: Box<SqlPlan>,
245    },
246}
247
248/// Database engine type for a collection.
249#[derive(Debug, Clone, Copy, PartialEq, Eq)]
250pub enum EngineType {
251    DocumentSchemaless,
252    DocumentStrict,
253    KeyValue,
254    Columnar,
255    Timeseries,
256    Spatial,
257}
258
259/// SQL join type.
260#[derive(Debug, Clone, Copy, PartialEq, Eq)]
261pub enum JoinType {
262    Inner,
263    Left,
264    Right,
265    Full,
266    Semi,
267    Anti,
268    Cross,
269}
270
271impl JoinType {
272    pub fn as_str(&self) -> &'static str {
273        match self {
274            Self::Inner => "inner",
275            Self::Left => "left",
276            Self::Right => "right",
277            Self::Full => "full",
278            Self::Semi => "semi",
279            Self::Anti => "anti",
280            Self::Cross => "cross",
281        }
282    }
283}
284
285/// Spatial predicate types.
286#[derive(Debug, Clone, Copy, PartialEq, Eq)]
287pub enum SpatialPredicate {
288    DWithin,
289    Contains,
290    Intersects,
291    Within,
292}
293
294/// A filter predicate.
295#[derive(Debug, Clone)]
296pub struct Filter {
297    pub expr: FilterExpr,
298}
299
300/// Filter expression tree.
301#[derive(Debug, Clone)]
302pub enum FilterExpr {
303    Comparison {
304        field: String,
305        op: CompareOp,
306        value: SqlValue,
307    },
308    Like {
309        field: String,
310        pattern: String,
311    },
312    InList {
313        field: String,
314        values: Vec<SqlValue>,
315    },
316    Between {
317        field: String,
318        low: SqlValue,
319        high: SqlValue,
320    },
321    IsNull {
322        field: String,
323    },
324    IsNotNull {
325        field: String,
326    },
327    And(Vec<Filter>),
328    Or(Vec<Filter>),
329    Not(Box<Filter>),
330    /// Raw expression filter (for complex predicates that don't fit simple patterns).
331    Expr(SqlExpr),
332}
333
334/// Comparison operators.
335#[derive(Debug, Clone, Copy, PartialEq, Eq)]
336pub enum CompareOp {
337    Eq,
338    Ne,
339    Gt,
340    Ge,
341    Lt,
342    Le,
343}
344
345/// Projection item in SELECT.
346#[derive(Debug, Clone)]
347pub enum Projection {
348    /// Simple column reference: `SELECT name`
349    Column(String),
350    /// All columns: `SELECT *`
351    Star,
352    /// Qualified star: `SELECT t.*`
353    QualifiedStar(String),
354    /// Computed expression: `SELECT price * qty AS total`
355    Computed { expr: SqlExpr, alias: String },
356}
357
358/// Sort key for ORDER BY.
359#[derive(Debug, Clone)]
360pub struct SortKey {
361    pub expr: SqlExpr,
362    pub ascending: bool,
363    pub nulls_first: bool,
364}
365
366/// Aggregate expression: `COUNT(*)`, `SUM(amount)`, etc.
367#[derive(Debug, Clone)]
368pub struct AggregateExpr {
369    pub function: String,
370    pub args: Vec<SqlExpr>,
371    pub alias: String,
372    pub distinct: bool,
373}
374
375/// Window function specification.
376#[derive(Debug, Clone)]
377pub struct WindowSpec {
378    pub function: String,
379    pub args: Vec<SqlExpr>,
380    pub partition_by: Vec<SqlExpr>,
381    pub order_by: Vec<SortKey>,
382    pub alias: String,
383}
384
385/// SQL value literal.
386#[derive(Debug, Clone, PartialEq)]
387pub enum SqlValue {
388    Int(i64),
389    Float(f64),
390    String(String),
391    Bool(bool),
392    Null,
393    Bytes(Vec<u8>),
394    Array(Vec<SqlValue>),
395}
396
397/// SQL expression tree.
398#[derive(Debug, Clone)]
399pub enum SqlExpr {
400    /// Column reference, optionally qualified: `name` or `users.name`
401    Column { table: Option<String>, name: String },
402    /// Literal value.
403    Literal(SqlValue),
404    /// Binary operation: `a + b`, `x > 5`
405    BinaryOp {
406        left: Box<SqlExpr>,
407        op: BinaryOp,
408        right: Box<SqlExpr>,
409    },
410    /// Unary operation: `-x`, `NOT flag`
411    UnaryOp { op: UnaryOp, expr: Box<SqlExpr> },
412    /// Function call: `COUNT(*)`, `vector_distance(field, ARRAY[...])`
413    Function {
414        name: String,
415        args: Vec<SqlExpr>,
416        distinct: bool,
417    },
418    /// CASE WHEN ... THEN ... ELSE ... END
419    Case {
420        operand: Option<Box<SqlExpr>>,
421        when_then: Vec<(SqlExpr, SqlExpr)>,
422        else_expr: Option<Box<SqlExpr>>,
423    },
424    /// CAST(expr AS type)
425    Cast { expr: Box<SqlExpr>, to_type: String },
426    /// Subquery expression (IN, EXISTS, scalar)
427    Subquery(Box<SqlPlan>),
428    /// Wildcard `*`
429    Wildcard,
430    /// `IS NULL` / `IS NOT NULL`
431    IsNull { expr: Box<SqlExpr>, negated: bool },
432    /// `expr IN (values)`
433    InList {
434        expr: Box<SqlExpr>,
435        list: Vec<SqlExpr>,
436        negated: bool,
437    },
438    /// `expr BETWEEN low AND high`
439    Between {
440        expr: Box<SqlExpr>,
441        low: Box<SqlExpr>,
442        high: Box<SqlExpr>,
443        negated: bool,
444    },
445    /// `expr LIKE pattern`
446    Like {
447        expr: Box<SqlExpr>,
448        pattern: Box<SqlExpr>,
449        negated: bool,
450    },
451    /// Array literal: `ARRAY[1.0, 2.0, 3.0]`
452    ArrayLiteral(Vec<SqlExpr>),
453}
454
455/// Binary operators.
456#[derive(Debug, Clone, Copy, PartialEq, Eq)]
457pub enum BinaryOp {
458    // Arithmetic
459    Add,
460    Sub,
461    Mul,
462    Div,
463    Mod,
464    // Comparison
465    Eq,
466    Ne,
467    Gt,
468    Ge,
469    Lt,
470    Le,
471    // Logical
472    And,
473    Or,
474    // String
475    Concat,
476}
477
478/// Unary operators.
479#[derive(Debug, Clone, Copy, PartialEq, Eq)]
480pub enum UnaryOp {
481    Neg,
482    Not,
483}
484
485/// SQL data type for schema resolution.
486#[derive(Debug, Clone, PartialEq, Eq)]
487pub enum SqlDataType {
488    Int64,
489    Float64,
490    String,
491    Bool,
492    Bytes,
493    Timestamp,
494    Decimal,
495    Uuid,
496    Vector(usize),
497    Geometry,
498}
499
500// ── Catalog trait ──
501// The `SqlCatalog` trait itself and its error type live in
502// `crate::catalog` to keep this file under the 500-line limit.
503// Re-exported here so downstream modules that `use crate::types::*`
504// keep resolving `SqlCatalog` without changing their imports.
505pub use crate::catalog::{SqlCatalog, SqlCatalogError};
506
507/// Metadata about a collection for query planning.
508#[derive(Debug, Clone)]
509pub struct CollectionInfo {
510    pub name: String,
511    pub engine: EngineType,
512    pub columns: Vec<ColumnInfo>,
513    pub primary_key: Option<String>,
514    pub has_auto_tier: bool,
515    /// Secondary indexes available for planner rewrites. Populated by the
516    /// catalog adapter from `StoredCollection.indexes`. `Building` entries
517    /// are included so the planner can see them but MUST be skipped when
518    /// choosing an index lookup — only `Ready` indexes back query rewrites.
519    pub indexes: Vec<IndexSpec>,
520}
521
522/// Secondary index metadata surfaced to the SQL planner.
523#[derive(Debug, Clone)]
524pub struct IndexSpec {
525    pub name: String,
526    /// Canonical field path (`$.email`, `$.user.name`, or plain column name
527    /// for strict documents — the catalog layer stores them uniformly).
528    pub field: String,
529    pub unique: bool,
530    pub case_insensitive: bool,
531    /// Build state. Only `Ready` indexes drive query rewrites.
532    pub state: IndexState,
533    /// Partial-index predicate as raw SQL text (`WHERE <expr>` body
534    /// without the keyword), or `None` for full indexes. The planner
535    /// uses this to reject rewrites whose WHERE clause doesn't entail
536    /// the predicate — matching against such a partial index would
537    /// omit rows the index didn't cover.
538    pub predicate: Option<String>,
539}
540
541/// Planner-facing index state. Mirrors the catalog variant but lives here
542/// so the SQL crate doesn't depend on `nodedb` internals.
543#[derive(Debug, Clone, Copy, PartialEq, Eq)]
544pub enum IndexState {
545    Building,
546    Ready,
547}
548
549/// Metadata about a single column.
550#[derive(Debug, Clone)]
551pub struct ColumnInfo {
552    pub name: String,
553    pub data_type: SqlDataType,
554    pub nullable: bool,
555    pub is_primary_key: bool,
556    /// Default value expression (e.g. "UUID_V7", "ULID", "NANOID(10)", "0", "'active'").
557    pub default: Option<String>,
558}