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}