Skip to main content

nodedb_physical/physical_plan/
columnar.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Columnar engine base operations dispatched to the Data Plane.
4//!
5//! `ColumnarOp` is the base for all columnar-profile collections:
6//! - **Plain columnar**: analytics collections without time semantics.
7//! - **Timeseries**: extends with `time_range` + `bucket_interval_ms` (via `TimeseriesOp`).
8//! - **Spatial**: extends with R-tree + OGC predicates (via `SpatialOp`).
9//!
10//! All profiles share the same `ColumnarMemtable` → `SegmentWriter` infrastructure.
11
12use nodedb_types::{
13    QualifiedCollection, RlsWriteCheck, Surrogate, SurrogateBitmap, SystemTimeScope,
14};
15
16use crate::physical_plan::document::ReturningSpec;
17
18/// Intent carried on `ColumnarOp::Insert` — see enum docs.
19#[derive(
20    Clone,
21    Copy,
22    Debug,
23    PartialEq,
24    Eq,
25    serde::Serialize,
26    serde::Deserialize,
27    zerompk::ToMessagePack,
28    zerompk::FromMessagePack,
29)]
30pub enum ColumnarInsertIntent {
31    /// Plain `INSERT`. On columnar, duplicate PK is **not** an error —
32    /// the prior row is tombstoned via the segment's delete bitmap and
33    /// the new row is appended. Cross-engine SQL consistency is kept on
34    /// the read side (`SELECT WHERE pk = X` returns one row) rather than
35    /// the insert-error side, matching ClickHouse's append + dedup model.
36    Insert,
37    /// `INSERT ... ON CONFLICT DO NOTHING`: silent no-op on duplicate.
38    InsertIfAbsent,
39    /// `UPSERT` / `INSERT ... ON CONFLICT (pk) DO UPDATE`. Behaves like
40    /// `Insert` when `on_conflict_updates` is empty (overwrite); when
41    /// non-empty, the handler reads the existing row, applies the
42    /// assignments (with `EXCLUDED.col` bound to the incoming row), and
43    /// writes the merged result.
44    Put,
45    /// Plain `INSERT` on a collection whose `PRIMARY KEY` is declared on a
46    /// natural key column (not `id` / `document_id`). Duplicate PK refuses
47    /// the row with `RejectedConstraint` (SQLSTATE 23505) instead of the
48    /// `Insert` tombstone-and-append behavior above.
49    InsertUnique,
50}
51
52/// Base columnar physical operations.
53#[derive(
54    Debug,
55    Clone,
56    PartialEq,
57    serde::Serialize,
58    serde::Deserialize,
59    zerompk::ToMessagePack,
60    zerompk::FromMessagePack,
61)]
62pub enum ColumnarOp {
63    /// Read rows from columnar memtable + segments.
64    ///
65    /// Applies filters, projects columns, respects limit.
66    /// No time-range semantics — that's `TimeseriesOp::Scan`.
67    Scan {
68        collection: QualifiedCollection,
69        projection: Vec<String>,
70        limit: usize,
71        filters: Vec<u8>,
72        rls_filters: Vec<u8>,
73        /// ORDER BY terms, each an expression, applied against matching
74        /// rows before the `limit` is enforced. Empty for scans with no
75        /// ORDER BY.
76        sort_keys: Vec<crate::physical_plan::SortKeySpec>,
77        /// System-time selection. `Current` = current-state read; `AsOf(ms)` =
78        /// rows whose `_ts_system` is ≤ ms; `AllVersions` = every `_ts_system`
79        /// row ordered ascending (audit log), system-time column projected.
80        /// Only meaningful for collections created `WITH BITEMPORAL`.
81        #[serde(default)]
82        system_time: SystemTimeScope,
83        /// Bitemporal valid-time predicate: keep rows whose
84        /// `[_ts_valid_from, _ts_valid_until)` interval contains this
85        /// point. `None` = no valid-time filter.
86        #[serde(default)]
87        valid_at_ms: Option<i64>,
88        /// Optional surrogate prefilter injected by a cross-engine sub-plan.
89        /// When present, the scan skips rows whose surrogate is absent from
90        /// this bitmap. `None` = no prefilter; full collection is scanned.
91        #[serde(default)]
92        prefilter: Option<SurrogateBitmap>,
93        /// MessagePack-serialized `Vec<ComputedColumn>` for scalar projection
94        /// expressions (e.g. JSON arrow operators). Empty when no computed
95        /// columns are present in the query.
96        #[serde(default)]
97        computed_columns: Vec<u8>,
98    },
99
100    /// Insert rows into a columnar memtable.
101    ///
102    /// Accepts JSON or MessagePack payload. The memtable is created on
103    /// first insert with schema inferred from the payload.
104    ///
105    /// `intent` distinguishes plain `INSERT` (upsert-semantics on columnar:
106    /// duplicate PK tombstones the prior row and appends the new one) from
107    /// `ON CONFLICT DO NOTHING` (`InsertIfAbsent` — silent skip on dup) and
108    /// `UPSERT` / `ON CONFLICT (pk) DO UPDATE` (`Put` — optionally with
109    /// per-row merges in `on_conflict_updates`).
110    Insert {
111        collection: QualifiedCollection,
112        /// Row data. Format determined by `format` field.
113        payload: Vec<u8>,
114        /// "json" for JSON array of objects, "msgpack" for MessagePack,
115        /// "ilp" for InfluxDB Line Protocol (delegated to timeseries path).
116        format: String,
117        /// INSERT / INSERT IF ABSENT / UPSERT distinction. See
118        /// `ColumnarInsertIntent` for semantics per variant.
119        intent: ColumnarInsertIntent,
120        /// `ON CONFLICT (pk) DO UPDATE SET field = expr` assignments.
121        /// Carried only when `intent == Put`; empty for `Insert`,
122        /// `InsertIfAbsent`, and plain `UPSERT` (whole-row overwrite).
123        /// Each `UpdateValue` is either a literal msgpack bytes payload
124        /// or a `SqlExpr` the handler evaluates against the existing row
125        /// plus the would-be-inserted row (with `EXCLUDED.col` resolution).
126        on_conflict_updates: Vec<(String, super::document::UpdateValue)>,
127        /// Per-row stable cross-engine identities, parallel to the rows
128        /// in `payload`. CP-side assigner populates this in row order
129        /// before dispatch. `vec![]` only in test fixtures (and length
130        /// must equal the row count when populated).
131        surrogates: Vec<Surrogate>,
132        /// MessagePack-serialized `ColumnarSchema` from the DDL catalog.
133        /// When non-empty, the Data Plane uses this schema to initialize the
134        /// memtable engine instead of inferring the schema from the payload.
135        /// This is required for columns whose SQL type is ambiguous at the
136        /// Value level (e.g. JSON arrives as `Value::String` but must be
137        /// stored in `ColumnData::Json`, not `ColumnData::String`).
138        /// Empty `vec![]` only in legacy test fixtures that do not carry schema.
139        #[serde(default)]
140        schema_bytes: Vec<u8>,
141        /// Sync provenance: identifies the originating peer and sequence for idempotency.
142        #[serde(default)]
143        provenance: Option<nodedb_types::sync::wire::SyncProvenance>,
144        /// WAL record LSN for deduplication. Set by the WAL catch-up task so the
145        /// Data Plane can skip records that have already been ingested or flushed
146        /// to disk. `None` for live ingest (always accepted).
147        #[serde(default)]
148        wal_lsn: Option<u64>,
149        /// Compiled row-level-security WRITE predicate (`Vec<ScanFilter>` as
150        /// MessagePack), or the reason no predicate is attached. Carried only
151        /// for the ON CONFLICT DO UPDATE shape, whose merged post-image
152        /// exists only inside the handler; a plain insert's rows are decided
153        /// at plan time instead.
154        rls_write_check: RlsWriteCheck,
155        /// When `Some`, return the STORED post-image of each written row
156        /// projected per spec — the row assembled from the values that reached
157        /// the engine, in schema order, so it matches what a `SELECT` on the
158        /// same key produces. Never the caller's submitted body: an `ON
159        /// CONFLICT DO UPDATE` merges against the stored row, so an echo of the
160        /// request would report a row that does not exist.
161        #[serde(default)]
162        returning: Option<ReturningSpec>,
163        /// Read filters gating the rows `returning` emits. Distinct from
164        /// `rls_write_check` above, which decides whether the write happens at
165        /// all: this one bounds what may be shown back, so a `RETURNING` row set
166        /// never exceeds a `SELECT` by the same principal. A collection can
167        /// carry a read policy and no write policy, in which case the write is
168        /// unrestricted and only the visible row set shrinks.
169        #[serde(default)]
170        rls_filters: Vec<u8>,
171    },
172
173    /// Update rows matching filter predicates.
174    ///
175    /// Uses `MutationEngine` for plain/spatial profiles.
176    Update {
177        collection: QualifiedCollection,
178        /// Serialized `Vec<ScanFilter>` (MessagePack).
179        filters: Vec<u8>,
180        /// Field assignments: `(column_name, value)`. An `Expr` value
181        /// evaluates against each matched row's pre-image.
182        updates: Vec<(String, super::document::UpdateValue)>,
183        /// Compiled row-level-security WRITE predicate, evaluated against each
184        /// row's post-image once the assignments have been applied, or the
185        /// reason no predicate is attached.
186        rls_write_check: RlsWriteCheck,
187    },
188
189    /// Delete rows matching filter predicates.
190    ///
191    /// Uses `MutationEngine` for plain/spatial profiles.
192    Delete {
193        collection: QualifiedCollection,
194        /// Serialized `Vec<ScanFilter>` (MessagePack).
195        filters: Vec<u8>,
196        /// Compiled row-level-security WRITE predicate, evaluated against the
197        /// pre-image of every row this removes, or the reason no predicate is
198        /// attached.
199        rls_write_check: RlsWriteCheck,
200    },
201
202    /// Update rows the Control Plane already resolved to concrete images.
203    ///
204    /// Built only for a collection carrying a write policy. The Control
205    /// Plane resolved the predicate to a row set and decided the policy
206    /// against each row's exact post-image while the writing identity was
207    /// live, so this carries the verdict, not a predicate to re-evaluate.
208    /// The Data Plane applies exactly these rows and evaluates nothing.
209    ResolvedUpdate {
210        collection: QualifiedCollection,
211        /// (primary key, full post-image) for each row the Control Plane
212        /// resolved and the write policy admitted.
213        rows: Vec<(nodedb_types::Value, Vec<nodedb_types::Value>)>,
214        rls_write_check: RlsWriteCheck,
215    },
216
217    /// Delete rows the Control Plane already resolved to concrete keys.
218    ///
219    /// Built only for a collection carrying a write policy. The Control
220    /// Plane resolved the predicate to a row set and decided the policy
221    /// against each row's pre-image while the writing identity was live,
222    /// so this carries the verdict, not a predicate to re-evaluate. The
223    /// Data Plane removes exactly these rows and evaluates nothing.
224    ResolvedDelete {
225        collection: QualifiedCollection,
226        /// Primary key of each row the write policy admitted for removal.
227        pks: Vec<nodedb_types::Value>,
228        rls_write_check: RlsWriteCheck,
229    },
230
231    /// Resolve a predicate UPDATE/DELETE to the concrete rows it would write,
232    /// deciding the write policy against those exact images.
233    ///
234    /// Read-only: it mutates nothing. The Control Plane runs this before
235    /// proposing, then proposes the resolved rows instead of the predicate,
236    /// because a follower has no writing identity to evaluate a predicate
237    /// against.
238    ResolveDml {
239        collection: QualifiedCollection,
240        /// Serialized `Vec<ScanFilter>` — the statement's WHERE clause.
241        filters: Vec<u8>,
242        /// Field assignments for an UPDATE. Empty for a DELETE.
243        updates: Vec<(String, super::document::UpdateValue)>,
244        /// True for UPDATE, false for DELETE.
245        is_update: bool,
246        rls_write_check: RlsWriteCheck,
247    },
248
249    /// Cursor-paginated raw scan for the clone materializer.
250    ///
251    /// Returns `(surrogate, row_value_bytes)` pairs plus next-cursor in one
252    /// payload. Honors `system_as_of_ms` so the materializer reads source
253    /// as-of the clone's `as_of_lsn` for bitemporal collections.
254    MaterializeScan {
255        collection: QualifiedCollection,
256        cursor: Vec<u8>,
257        count: usize,
258        system_as_of_ms: Option<i64>,
259    },
260
261    /// `TRUNCATE` of a columnar or spatial collection: every row, every
262    /// bitemporal version, every flushed segment, and every R-tree entry
263    /// the collection's geometry columns produced. Reports the number of
264    /// live rows that existed.
265    Truncate {
266        collection: QualifiedCollection,
267        /// `TRUNCATE ... RESTART IDENTITY`: the Control Plane resets the
268        /// collection's sequences after the Data Plane clears the rows.
269        #[serde(default)]
270        restart_identity: bool,
271    },
272}