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        /// `ON CONFLICT DO NOTHING` semantics: when true, duplicate-PK rows
83        /// are silently skipped instead of raising `unique_violation`. Plain
84        /// `INSERT` (no `ON CONFLICT` clause) sets this to `false`.
85        if_absent: bool,
86    },
87    /// KV INSERT: key and value are fundamentally separate.
88    /// Each entry is `(key, value_columns)`.
89    KvInsert {
90        collection: String,
91        entries: Vec<(SqlValue, Vec<(String, SqlValue)>)>,
92        /// TTL in seconds (0 = no expiry). Extracted from `ttl` column if present.
93        ttl_secs: u64,
94        /// INSERT-vs-UPSERT distinction. `KvOp::Put` is a Redis-SET-style
95        /// upsert by design; to honor SQL `INSERT` semantics the planner must
96        /// tell the converter whether a duplicate key should raise (plain
97        /// `INSERT`, `Insert`), be silently skipped (`ON CONFLICT DO NOTHING`,
98        /// `InsertIfAbsent`), or overwrite (`UPSERT` / `ON CONFLICT DO
99        /// UPDATE`, `Put`).
100        intent: KvInsertIntent,
101        /// `ON CONFLICT (key) DO UPDATE SET field = expr` assignments, carried
102        /// through when `intent == Put` via the ON-CONFLICT-DO-UPDATE path.
103        /// Empty for plain UPSERT (whole-value overwrite) and for INSERT
104        /// variants.
105        on_conflict_updates: Vec<(String, SqlExpr)>,
106    },
107    /// UPSERT: insert or merge if document exists.
108    Upsert {
109        collection: String,
110        engine: EngineType,
111        rows: Vec<Vec<(String, SqlValue)>>,
112        column_defaults: Vec<(String, String)>,
113        /// `ON CONFLICT (...) DO UPDATE SET field = expr` assignments.
114        /// When empty, upsert is a plain merge: new columns overwrite existing.
115        /// When non-empty, the engine applies these per-row against the
116        /// *existing* document instead of merging the inserted values.
117        on_conflict_updates: Vec<(String, SqlExpr)>,
118    },
119    InsertSelect {
120        target: String,
121        source: Box<SqlPlan>,
122        limit: usize,
123    },
124    Update {
125        collection: String,
126        engine: EngineType,
127        assignments: Vec<(String, SqlExpr)>,
128        filters: Vec<Filter>,
129        target_keys: Vec<SqlValue>,
130        returning: bool,
131    },
132    Delete {
133        collection: String,
134        engine: EngineType,
135        filters: Vec<Filter>,
136        target_keys: Vec<SqlValue>,
137    },
138    Truncate {
139        collection: String,
140        restart_identity: bool,
141    },
142
143    // ── Joins ──
144    Join {
145        left: Box<SqlPlan>,
146        right: Box<SqlPlan>,
147        on: Vec<(String, String)>,
148        join_type: JoinType,
149        condition: Option<SqlExpr>,
150        limit: usize,
151        /// Post-join projection: column names to keep (empty = all columns).
152        projection: Vec<Projection>,
153        /// Post-join filters (from WHERE clause).
154        filters: Vec<Filter>,
155    },
156
157    // ── Aggregation ──
158    Aggregate {
159        input: Box<SqlPlan>,
160        group_by: Vec<SqlExpr>,
161        aggregates: Vec<AggregateExpr>,
162        having: Vec<Filter>,
163        limit: usize,
164    },
165
166    // ── Timeseries ──
167    TimeseriesScan {
168        collection: String,
169        time_range: (i64, i64),
170        bucket_interval_ms: i64,
171        group_by: Vec<String>,
172        aggregates: Vec<AggregateExpr>,
173        filters: Vec<Filter>,
174        projection: Vec<Projection>,
175        gap_fill: String,
176        limit: usize,
177        tiered: bool,
178    },
179    TimeseriesIngest {
180        collection: String,
181        rows: Vec<Vec<(String, SqlValue)>>,
182    },
183
184    // ── Search (first-class) ──
185    VectorSearch {
186        collection: String,
187        field: String,
188        query_vector: Vec<f32>,
189        top_k: usize,
190        ef_search: usize,
191        filters: Vec<Filter>,
192    },
193    MultiVectorSearch {
194        collection: String,
195        query_vector: Vec<f32>,
196        top_k: usize,
197        ef_search: usize,
198    },
199    TextSearch {
200        collection: String,
201        query: String,
202        top_k: usize,
203        fuzzy: bool,
204        filters: Vec<Filter>,
205    },
206    HybridSearch {
207        collection: String,
208        query_vector: Vec<f32>,
209        query_text: String,
210        top_k: usize,
211        ef_search: usize,
212        vector_weight: f32,
213        fuzzy: bool,
214    },
215    SpatialScan {
216        collection: String,
217        field: String,
218        predicate: SpatialPredicate,
219        query_geometry: Vec<u8>,
220        distance_meters: f64,
221        attribute_filters: Vec<Filter>,
222        limit: usize,
223        projection: Vec<Projection>,
224    },
225
226    // ── Composite ──
227    Union {
228        inputs: Vec<SqlPlan>,
229        distinct: bool,
230    },
231    Intersect {
232        left: Box<SqlPlan>,
233        right: Box<SqlPlan>,
234        all: bool,
235    },
236    Except {
237        left: Box<SqlPlan>,
238        right: Box<SqlPlan>,
239        all: bool,
240    },
241    RecursiveScan {
242        collection: String,
243        base_filters: Vec<Filter>,
244        recursive_filters: Vec<Filter>,
245        /// Equi-join link for tree-traversal recursion:
246        /// `(collection_field, working_table_field)`.
247        /// e.g. `("parent_id", "id")` means each iteration finds rows
248        /// where `collection.parent_id` matches a `working_table.id`.
249        join_link: Option<(String, String)>,
250        max_iterations: usize,
251        distinct: bool,
252        limit: usize,
253    },
254
255    /// Non-recursive CTE: execute each definition, then the outer query.
256    Cte {
257        /// CTE definitions: `(name, subquery_plan)`.
258        definitions: Vec<(String, SqlPlan)>,
259        /// The outer query that references CTE names.
260        outer: Box<SqlPlan>,
261    },
262}
263
264/// INSERT-vs-UPSERT intent carried on `SqlPlan::KvInsert`.
265///
266/// The KV engine's `KvOp::Put` is a Redis-SET-style upsert: write wins
267/// unconditionally. SQL requires `INSERT` to raise `unique_violation`
268/// on duplicate keys, so the plan must carry the caller's intent through
269/// to the Data Plane where the hash-index existence probe happens.
270#[derive(Debug, Clone, Copy, PartialEq, Eq)]
271pub enum KvInsertIntent {
272    /// Plain `INSERT`: duplicate key raises `SQLSTATE 23505`.
273    Insert,
274    /// `INSERT ... ON CONFLICT DO NOTHING`: duplicate key is a no-op.
275    InsertIfAbsent,
276    /// `UPSERT` / `INSERT ... ON CONFLICT (key) DO UPDATE` / RESP `SET`:
277    /// duplicate key overwrites. Also the shape used by the RESP SET path.
278    Put,
279}
280
281/// Database engine type for a collection.
282#[derive(Debug, Clone, Copy, PartialEq, Eq)]
283pub enum EngineType {
284    DocumentSchemaless,
285    DocumentStrict,
286    KeyValue,
287    Columnar,
288    Timeseries,
289    Spatial,
290}
291
292/// SQL join type.
293#[derive(Debug, Clone, Copy, PartialEq, Eq)]
294pub enum JoinType {
295    Inner,
296    Left,
297    Right,
298    Full,
299    Semi,
300    Anti,
301    Cross,
302}
303
304impl JoinType {
305    pub fn as_str(&self) -> &'static str {
306        match self {
307            Self::Inner => "inner",
308            Self::Left => "left",
309            Self::Right => "right",
310            Self::Full => "full",
311            Self::Semi => "semi",
312            Self::Anti => "anti",
313            Self::Cross => "cross",
314        }
315    }
316}
317
318/// Spatial predicate types.
319#[derive(Debug, Clone, Copy, PartialEq, Eq)]
320pub enum SpatialPredicate {
321    DWithin,
322    Contains,
323    Intersects,
324    Within,
325}
326
327/// A filter predicate.
328#[derive(Debug, Clone)]
329pub struct Filter {
330    pub expr: FilterExpr,
331}
332
333/// Filter expression tree.
334#[derive(Debug, Clone)]
335pub enum FilterExpr {
336    Comparison {
337        field: String,
338        op: CompareOp,
339        value: SqlValue,
340    },
341    Like {
342        field: String,
343        pattern: String,
344    },
345    InList {
346        field: String,
347        values: Vec<SqlValue>,
348    },
349    Between {
350        field: String,
351        low: SqlValue,
352        high: SqlValue,
353    },
354    IsNull {
355        field: String,
356    },
357    IsNotNull {
358        field: String,
359    },
360    And(Vec<Filter>),
361    Or(Vec<Filter>),
362    Not(Box<Filter>),
363    /// Raw expression filter (for complex predicates that don't fit simple patterns).
364    Expr(SqlExpr),
365}
366
367/// Comparison operators.
368#[derive(Debug, Clone, Copy, PartialEq, Eq)]
369pub enum CompareOp {
370    Eq,
371    Ne,
372    Gt,
373    Ge,
374    Lt,
375    Le,
376}
377
378/// Projection item in SELECT.
379#[derive(Debug, Clone)]
380pub enum Projection {
381    /// Simple column reference: `SELECT name`
382    Column(String),
383    /// All columns: `SELECT *`
384    Star,
385    /// Qualified star: `SELECT t.*`
386    QualifiedStar(String),
387    /// Computed expression: `SELECT price * qty AS total`
388    Computed { expr: SqlExpr, alias: String },
389}
390
391/// Sort key for ORDER BY.
392#[derive(Debug, Clone)]
393pub struct SortKey {
394    pub expr: SqlExpr,
395    pub ascending: bool,
396    pub nulls_first: bool,
397}
398
399/// Aggregate expression: `COUNT(*)`, `SUM(amount)`, etc.
400#[derive(Debug, Clone)]
401pub struct AggregateExpr {
402    pub function: String,
403    pub args: Vec<SqlExpr>,
404    pub alias: String,
405    pub distinct: bool,
406}
407
408/// Window function specification.
409#[derive(Debug, Clone)]
410pub struct WindowSpec {
411    pub function: String,
412    pub args: Vec<SqlExpr>,
413    pub partition_by: Vec<SqlExpr>,
414    pub order_by: Vec<SortKey>,
415    pub alias: String,
416}
417
418// ── SQL value / expression / operator types ──
419// Extracted to `crate::types_expr` so this file stays under the 500-line limit.
420// Re-exported so downstream `use crate::types::*` continues to resolve these
421// symbols without change.
422pub use crate::types_expr::{BinaryOp, SqlDataType, SqlExpr, SqlValue, UnaryOp};
423
424// ── Catalog trait ──
425// The `SqlCatalog` trait itself and its error type live in
426// `crate::catalog` to keep this file under the 500-line limit.
427// Re-exported here so downstream modules that `use crate::types::*`
428// keep resolving `SqlCatalog` without changing their imports.
429pub use crate::catalog::{SqlCatalog, SqlCatalogError};
430
431/// Metadata about a collection for query planning.
432#[derive(Debug, Clone)]
433pub struct CollectionInfo {
434    pub name: String,
435    pub engine: EngineType,
436    pub columns: Vec<ColumnInfo>,
437    pub primary_key: Option<String>,
438    pub has_auto_tier: bool,
439    /// Secondary indexes available for planner rewrites. Populated by the
440    /// catalog adapter from `StoredCollection.indexes`. `Building` entries
441    /// are included so the planner can see them but MUST be skipped when
442    /// choosing an index lookup — only `Ready` indexes back query rewrites.
443    pub indexes: Vec<IndexSpec>,
444}
445
446/// Secondary index metadata surfaced to the SQL planner.
447#[derive(Debug, Clone)]
448pub struct IndexSpec {
449    pub name: String,
450    /// Canonical field path (`$.email`, `$.user.name`, or plain column name
451    /// for strict documents — the catalog layer stores them uniformly).
452    pub field: String,
453    pub unique: bool,
454    pub case_insensitive: bool,
455    /// Build state. Only `Ready` indexes drive query rewrites.
456    pub state: IndexState,
457    /// Partial-index predicate as raw SQL text (`WHERE <expr>` body
458    /// without the keyword), or `None` for full indexes. The planner
459    /// uses this to reject rewrites whose WHERE clause doesn't entail
460    /// the predicate — matching against such a partial index would
461    /// omit rows the index didn't cover.
462    pub predicate: Option<String>,
463}
464
465/// Planner-facing index state. Mirrors the catalog variant but lives here
466/// so the SQL crate doesn't depend on `nodedb` internals.
467#[derive(Debug, Clone, Copy, PartialEq, Eq)]
468pub enum IndexState {
469    Building,
470    Ready,
471}
472
473/// Metadata about a single column.
474#[derive(Debug, Clone)]
475pub struct ColumnInfo {
476    pub name: String,
477    pub data_type: SqlDataType,
478    pub nullable: bool,
479    pub is_primary_key: bool,
480    /// Default value expression (e.g. "UUID_V7", "ULID", "NANOID(10)", "0", "'active'").
481    pub default: Option<String>,
482}