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    RangeScan {
38        collection: String,
39        field: String,
40        lower: Option<SqlValue>,
41        upper: Option<SqlValue>,
42        limit: usize,
43    },
44
45    // ── Writes ──
46    Insert {
47        collection: String,
48        engine: EngineType,
49        rows: Vec<Vec<(String, SqlValue)>>,
50        /// Column defaults from schema: `(column_name, default_expr)`.
51        /// Used to auto-generate values for missing columns (e.g. `id` with `UUID_V7`).
52        column_defaults: Vec<(String, String)>,
53    },
54    /// KV INSERT: key and value are fundamentally separate.
55    /// Each entry is `(key, value_columns)`.
56    KvInsert {
57        collection: String,
58        entries: Vec<(SqlValue, Vec<(String, SqlValue)>)>,
59        /// TTL in seconds (0 = no expiry). Extracted from `ttl` column if present.
60        ttl_secs: u64,
61    },
62    /// UPSERT: insert or merge if document exists.
63    Upsert {
64        collection: String,
65        engine: EngineType,
66        rows: Vec<Vec<(String, SqlValue)>>,
67        column_defaults: Vec<(String, String)>,
68        /// `ON CONFLICT (...) DO UPDATE SET field = expr` assignments.
69        /// When empty, upsert is a plain merge: new columns overwrite existing.
70        /// When non-empty, the engine applies these per-row against the
71        /// *existing* document instead of merging the inserted values.
72        on_conflict_updates: Vec<(String, SqlExpr)>,
73    },
74    InsertSelect {
75        target: String,
76        source: Box<SqlPlan>,
77        limit: usize,
78    },
79    Update {
80        collection: String,
81        engine: EngineType,
82        assignments: Vec<(String, SqlExpr)>,
83        filters: Vec<Filter>,
84        target_keys: Vec<SqlValue>,
85        returning: bool,
86    },
87    Delete {
88        collection: String,
89        engine: EngineType,
90        filters: Vec<Filter>,
91        target_keys: Vec<SqlValue>,
92    },
93    Truncate {
94        collection: String,
95        restart_identity: bool,
96    },
97
98    // ── Joins ──
99    Join {
100        left: Box<SqlPlan>,
101        right: Box<SqlPlan>,
102        on: Vec<(String, String)>,
103        join_type: JoinType,
104        condition: Option<SqlExpr>,
105        limit: usize,
106        /// Post-join projection: column names to keep (empty = all columns).
107        projection: Vec<Projection>,
108        /// Post-join filters (from WHERE clause).
109        filters: Vec<Filter>,
110    },
111
112    // ── Aggregation ──
113    Aggregate {
114        input: Box<SqlPlan>,
115        group_by: Vec<SqlExpr>,
116        aggregates: Vec<AggregateExpr>,
117        having: Vec<Filter>,
118        limit: usize,
119    },
120
121    // ── Timeseries ──
122    TimeseriesScan {
123        collection: String,
124        time_range: (i64, i64),
125        bucket_interval_ms: i64,
126        group_by: Vec<String>,
127        aggregates: Vec<AggregateExpr>,
128        filters: Vec<Filter>,
129        projection: Vec<Projection>,
130        gap_fill: String,
131        limit: usize,
132        tiered: bool,
133    },
134    TimeseriesIngest {
135        collection: String,
136        rows: Vec<Vec<(String, SqlValue)>>,
137    },
138
139    // ── Search (first-class) ──
140    VectorSearch {
141        collection: String,
142        field: String,
143        query_vector: Vec<f32>,
144        top_k: usize,
145        ef_search: usize,
146        filters: Vec<Filter>,
147    },
148    MultiVectorSearch {
149        collection: String,
150        query_vector: Vec<f32>,
151        top_k: usize,
152        ef_search: usize,
153    },
154    TextSearch {
155        collection: String,
156        query: String,
157        top_k: usize,
158        fuzzy: bool,
159        filters: Vec<Filter>,
160    },
161    HybridSearch {
162        collection: String,
163        query_vector: Vec<f32>,
164        query_text: String,
165        top_k: usize,
166        ef_search: usize,
167        vector_weight: f32,
168        fuzzy: bool,
169    },
170    SpatialScan {
171        collection: String,
172        field: String,
173        predicate: SpatialPredicate,
174        query_geometry: Vec<u8>,
175        distance_meters: f64,
176        attribute_filters: Vec<Filter>,
177        limit: usize,
178        projection: Vec<Projection>,
179    },
180
181    // ── Composite ──
182    Union {
183        inputs: Vec<SqlPlan>,
184        distinct: bool,
185    },
186    Intersect {
187        left: Box<SqlPlan>,
188        right: Box<SqlPlan>,
189        all: bool,
190    },
191    Except {
192        left: Box<SqlPlan>,
193        right: Box<SqlPlan>,
194        all: bool,
195    },
196    RecursiveScan {
197        collection: String,
198        base_filters: Vec<Filter>,
199        recursive_filters: Vec<Filter>,
200        max_iterations: usize,
201        distinct: bool,
202        limit: usize,
203    },
204
205    /// Non-recursive CTE: execute each definition, then the outer query.
206    Cte {
207        /// CTE definitions: `(name, subquery_plan)`.
208        definitions: Vec<(String, SqlPlan)>,
209        /// The outer query that references CTE names.
210        outer: Box<SqlPlan>,
211    },
212}
213
214/// Database engine type for a collection.
215#[derive(Debug, Clone, Copy, PartialEq, Eq)]
216pub enum EngineType {
217    DocumentSchemaless,
218    DocumentStrict,
219    KeyValue,
220    Columnar,
221    Timeseries,
222    Spatial,
223}
224
225/// SQL join type.
226#[derive(Debug, Clone, Copy, PartialEq, Eq)]
227pub enum JoinType {
228    Inner,
229    Left,
230    Right,
231    Full,
232    Semi,
233    Anti,
234    Cross,
235}
236
237impl JoinType {
238    pub fn as_str(&self) -> &'static str {
239        match self {
240            Self::Inner => "inner",
241            Self::Left => "left",
242            Self::Right => "right",
243            Self::Full => "full",
244            Self::Semi => "semi",
245            Self::Anti => "anti",
246            Self::Cross => "cross",
247        }
248    }
249}
250
251/// Spatial predicate types.
252#[derive(Debug, Clone, Copy, PartialEq, Eq)]
253pub enum SpatialPredicate {
254    DWithin,
255    Contains,
256    Intersects,
257    Within,
258}
259
260/// A filter predicate.
261#[derive(Debug, Clone)]
262pub struct Filter {
263    pub expr: FilterExpr,
264}
265
266/// Filter expression tree.
267#[derive(Debug, Clone)]
268pub enum FilterExpr {
269    Comparison {
270        field: String,
271        op: CompareOp,
272        value: SqlValue,
273    },
274    Like {
275        field: String,
276        pattern: String,
277    },
278    InList {
279        field: String,
280        values: Vec<SqlValue>,
281    },
282    Between {
283        field: String,
284        low: SqlValue,
285        high: SqlValue,
286    },
287    IsNull {
288        field: String,
289    },
290    IsNotNull {
291        field: String,
292    },
293    And(Vec<Filter>),
294    Or(Vec<Filter>),
295    Not(Box<Filter>),
296    /// Raw expression filter (for complex predicates that don't fit simple patterns).
297    Expr(SqlExpr),
298}
299
300/// Comparison operators.
301#[derive(Debug, Clone, Copy, PartialEq, Eq)]
302pub enum CompareOp {
303    Eq,
304    Ne,
305    Gt,
306    Ge,
307    Lt,
308    Le,
309}
310
311/// Projection item in SELECT.
312#[derive(Debug, Clone)]
313pub enum Projection {
314    /// Simple column reference: `SELECT name`
315    Column(String),
316    /// All columns: `SELECT *`
317    Star,
318    /// Qualified star: `SELECT t.*`
319    QualifiedStar(String),
320    /// Computed expression: `SELECT price * qty AS total`
321    Computed { expr: SqlExpr, alias: String },
322}
323
324/// Sort key for ORDER BY.
325#[derive(Debug, Clone)]
326pub struct SortKey {
327    pub expr: SqlExpr,
328    pub ascending: bool,
329    pub nulls_first: bool,
330}
331
332/// Aggregate expression: `COUNT(*)`, `SUM(amount)`, etc.
333#[derive(Debug, Clone)]
334pub struct AggregateExpr {
335    pub function: String,
336    pub args: Vec<SqlExpr>,
337    pub alias: String,
338    pub distinct: bool,
339}
340
341/// Window function specification.
342#[derive(Debug, Clone)]
343pub struct WindowSpec {
344    pub function: String,
345    pub args: Vec<SqlExpr>,
346    pub partition_by: Vec<SqlExpr>,
347    pub order_by: Vec<SortKey>,
348    pub alias: String,
349}
350
351/// SQL value literal.
352#[derive(Debug, Clone, PartialEq)]
353pub enum SqlValue {
354    Int(i64),
355    Float(f64),
356    String(String),
357    Bool(bool),
358    Null,
359    Bytes(Vec<u8>),
360    Array(Vec<SqlValue>),
361}
362
363/// SQL expression tree.
364#[derive(Debug, Clone)]
365pub enum SqlExpr {
366    /// Column reference, optionally qualified: `name` or `users.name`
367    Column { table: Option<String>, name: String },
368    /// Literal value.
369    Literal(SqlValue),
370    /// Binary operation: `a + b`, `x > 5`
371    BinaryOp {
372        left: Box<SqlExpr>,
373        op: BinaryOp,
374        right: Box<SqlExpr>,
375    },
376    /// Unary operation: `-x`, `NOT flag`
377    UnaryOp { op: UnaryOp, expr: Box<SqlExpr> },
378    /// Function call: `COUNT(*)`, `vector_distance(field, ARRAY[...])`
379    Function {
380        name: String,
381        args: Vec<SqlExpr>,
382        distinct: bool,
383    },
384    /// CASE WHEN ... THEN ... ELSE ... END
385    Case {
386        operand: Option<Box<SqlExpr>>,
387        when_then: Vec<(SqlExpr, SqlExpr)>,
388        else_expr: Option<Box<SqlExpr>>,
389    },
390    /// CAST(expr AS type)
391    Cast { expr: Box<SqlExpr>, to_type: String },
392    /// Subquery expression (IN, EXISTS, scalar)
393    Subquery(Box<SqlPlan>),
394    /// Wildcard `*`
395    Wildcard,
396    /// `IS NULL` / `IS NOT NULL`
397    IsNull { expr: Box<SqlExpr>, negated: bool },
398    /// `expr IN (values)`
399    InList {
400        expr: Box<SqlExpr>,
401        list: Vec<SqlExpr>,
402        negated: bool,
403    },
404    /// `expr BETWEEN low AND high`
405    Between {
406        expr: Box<SqlExpr>,
407        low: Box<SqlExpr>,
408        high: Box<SqlExpr>,
409        negated: bool,
410    },
411    /// `expr LIKE pattern`
412    Like {
413        expr: Box<SqlExpr>,
414        pattern: Box<SqlExpr>,
415        negated: bool,
416    },
417    /// Array literal: `ARRAY[1.0, 2.0, 3.0]`
418    ArrayLiteral(Vec<SqlExpr>),
419}
420
421/// Binary operators.
422#[derive(Debug, Clone, Copy, PartialEq, Eq)]
423pub enum BinaryOp {
424    // Arithmetic
425    Add,
426    Sub,
427    Mul,
428    Div,
429    Mod,
430    // Comparison
431    Eq,
432    Ne,
433    Gt,
434    Ge,
435    Lt,
436    Le,
437    // Logical
438    And,
439    Or,
440    // String
441    Concat,
442}
443
444/// Unary operators.
445#[derive(Debug, Clone, Copy, PartialEq, Eq)]
446pub enum UnaryOp {
447    Neg,
448    Not,
449}
450
451/// SQL data type for schema resolution.
452#[derive(Debug, Clone, PartialEq, Eq)]
453pub enum SqlDataType {
454    Int64,
455    Float64,
456    String,
457    Bool,
458    Bytes,
459    Timestamp,
460    Decimal,
461    Uuid,
462    Vector(usize),
463    Geometry,
464}
465
466// ── Catalog trait ──
467// The `SqlCatalog` trait itself and its error type live in
468// `crate::catalog` to keep this file under the 500-line limit.
469// Re-exported here so downstream modules that `use crate::types::*`
470// keep resolving `SqlCatalog` without changing their imports.
471pub use crate::catalog::{SqlCatalog, SqlCatalogError};
472
473/// Metadata about a collection for query planning.
474#[derive(Debug, Clone)]
475pub struct CollectionInfo {
476    pub name: String,
477    pub engine: EngineType,
478    pub columns: Vec<ColumnInfo>,
479    pub primary_key: Option<String>,
480    pub has_auto_tier: bool,
481}
482
483/// Metadata about a single column.
484#[derive(Debug, Clone)]
485pub struct ColumnInfo {
486    pub name: String,
487    pub data_type: SqlDataType,
488    pub nullable: bool,
489    pub is_primary_key: bool,
490    /// Default value expression (e.g. "UUID_V7", "ULID", "NANOID(10)", "0", "'active'").
491    pub default: Option<String>,
492}