nodedb_lite/query/columnar_ops/writes/
ops.rs1use nodedb_physical::physical_plan::columnar::ColumnarInsertIntent;
5use nodedb_physical::physical_plan::document::types::UpdateValue;
6use nodedb_query::scan_filter::ScanFilter;
7use nodedb_types::columnar::ColumnarSchema;
8use nodedb_types::result::QueryResult;
9use nodedb_types::value::Value;
10
11use crate::error::LiteError;
12use crate::query::engine::LiteQueryEngine;
13use crate::query::text_index::{index_columnar_rows, remove_columnar_rows};
14use crate::storage::engine::StorageEngine;
15
16use super::super::reads::row_to_object;
17use super::payload::decode_payload;
18use super::rows::{apply_conflict_updates, find_row};
19
20pub struct InsertParams<'a> {
22 pub payload: &'a [u8],
23 pub format: &'a str,
24 pub intent: ColumnarInsertIntent,
25 pub on_conflict_updates: &'a [(
26 String,
27 nodedb_physical::physical_plan::document::types::UpdateValue,
28 )],
29 pub surrogates: &'a [nodedb_types::Surrogate],
30 pub schema_bytes: &'a [u8],
31}
32
33pub async fn insert<S: StorageEngine>(
44 engine: &LiteQueryEngine<S>,
45 collection: &str,
46 params: InsertParams<'_>,
47) -> Result<(QueryResult, Vec<Vec<Value>>), LiteError> {
48 let InsertParams {
49 payload,
50 format,
51 intent,
52 on_conflict_updates,
53 surrogates,
54 schema_bytes,
55 } = params;
56 let schema = engine
57 .columnar
58 .schema(collection)
59 .ok_or_else(|| LiteError::collection_not_found("columnar", collection))?;
60
61 let effective_schema: ColumnarSchema = if !schema_bytes.is_empty() {
63 zerompk::from_msgpack(schema_bytes).unwrap_or(schema)
64 } else {
65 schema
66 };
67
68 let col_names: Vec<String> = effective_schema
69 .columns
70 .iter()
71 .map(|c| c.name.clone())
72 .collect();
73 let pk_idx = effective_schema
75 .columns
76 .iter()
77 .position(|c| c.primary_key)
78 .unwrap_or(0);
79
80 let rows = decode_payload(payload, format, &col_names)?;
81
82 let mut affected: u64 = 0;
83 let mut inserted_rows: Vec<Vec<Value>> = Vec::new();
84
85 for (row_idx, row_values) in rows.into_iter().enumerate() {
86 let _surrogate = surrogates
87 .get(row_idx)
88 .copied()
89 .unwrap_or(nodedb_types::Surrogate(0));
90
91 match intent {
92 ColumnarInsertIntent::Insert => {
93 engine.columnar.insert(collection, &row_values)?;
94 inserted_rows.push(row_values);
95 affected += 1;
96 }
97 ColumnarInsertIntent::InsertIfAbsent => {
98 if let Some(pk) = row_values.get(pk_idx)
99 && find_row(engine, collection, pk).await?.is_some()
100 {
101 continue;
102 }
103 engine.columnar.insert(collection, &row_values)?;
104 inserted_rows.push(row_values);
105 affected += 1;
106 }
107 ColumnarInsertIntent::InsertUnique => {
108 if let Some(pk) = row_values.get(pk_idx)
111 && find_row(engine, collection, pk).await?.is_some()
112 {
113 return Err(LiteError::UniqueViolation {
114 collection: collection.to_string(),
115 detail: format!(
116 "{} = {pk:?}",
117 col_names.get(pk_idx).map(String::as_str).unwrap_or("pk")
118 ),
119 });
120 }
121 engine.columnar.insert(collection, &row_values)?;
122 inserted_rows.push(row_values);
123 affected += 1;
124 }
125 ColumnarInsertIntent::Put => {
126 if on_conflict_updates.is_empty() {
127 if let Some(pk) = row_values.get(pk_idx) {
129 let _ = engine.columnar.delete(collection, pk);
130 }
131 engine.columnar.insert(collection, &row_values)?;
132 inserted_rows.push(row_values);
133 affected += 1;
134 } else {
135 let existing = match row_values.get(pk_idx) {
137 Some(pk) => find_row(engine, collection, pk).await?,
138 None => None,
139 };
140 let merged = match existing {
141 Some(existing) => {
142 let incoming_obj = row_to_object(&col_names, &row_values);
143 apply_conflict_updates(
144 existing,
145 &incoming_obj,
146 on_conflict_updates,
147 &col_names,
148 )?
149 }
150 None => row_values,
151 };
152 if let Some(pk) = merged.get(pk_idx) {
153 let _ = engine.columnar.delete(collection, pk);
154 }
155 engine.columnar.insert(collection, &merged)?;
156 inserted_rows.push(merged);
157 affected += 1;
158 }
159 }
160 }
161 }
162
163 index_columnar_rows(engine, collection, inserted_rows.iter().map(Vec::as_slice))?;
164 Ok((
165 QueryResult {
166 columns: Vec::new(),
167 rows: Vec::new(),
168 rows_affected: affected,
169 command: Some("INSERT".into()),
170 },
171 inserted_rows,
172 ))
173}
174
175pub async fn update<S: StorageEngine>(
182 engine: &LiteQueryEngine<S>,
183 collection: &str,
184 filters_bytes: &[u8],
185 updates: &[(String, UpdateValue)],
186) -> Result<QueryResult, LiteError> {
187 let schema = engine
188 .columnar
189 .schema(collection)
190 .ok_or_else(|| LiteError::collection_not_found("columnar", collection))?;
191
192 let col_names: Vec<String> = schema.columns.iter().map(|c| c.name.clone()).collect();
193 let pk_idx = schema
194 .columns
195 .iter()
196 .position(|c| c.primary_key)
197 .unwrap_or(0);
198
199 let filters: Vec<ScanFilter> = if filters_bytes.is_empty() {
200 Vec::new()
201 } else {
202 zerompk::from_msgpack(filters_bytes).map_err(|e| LiteError::Serialization {
203 detail: format!("decode update filters: {e}"),
204 })?
205 };
206
207 let mut assignments: Vec<(usize, Assigned<'_>)> = Vec::with_capacity(updates.len());
209 for (field, update) in updates {
210 let Some(col_idx) = col_names.iter().position(|n| n == field) else {
211 continue;
212 };
213 let assigned = match update {
214 UpdateValue::Literal(bytes) => {
215 Assigned::Literal(nodedb_types::value_from_msgpack(bytes).map_err(|e| {
216 LiteError::Serialization {
217 detail: format!("decode update value for field '{field}': {e}"),
218 }
219 })?)
220 }
221 UpdateValue::Expr(expr) => Assigned::Expr(expr),
222 };
223 assignments.push((col_idx, assigned));
224 }
225
226 let all_rows = engine.columnar.list_rows(collection).await?;
227
228 let mut pending: Vec<(Value, Vec<Value>)> = Vec::new();
229 for row in all_rows {
230 let doc = row_to_object(&col_names, &row);
231 if !ScanFilter::all_match_value(&filters, &doc)? {
232 continue;
233 }
234 let pk = row.get(pk_idx).cloned().unwrap_or(Value::Null);
235 let mut new_values = row;
236 for (col_idx, assigned) in &assignments {
237 let value = match assigned {
238 Assigned::Literal(value) => value.clone(),
239 Assigned::Expr(expr) => expr.eval(&doc)?,
240 };
241 if let Some(slot) = new_values.get_mut(*col_idx) {
242 *slot = value;
243 }
244 }
245 pending.push((pk, new_values));
246 }
247
248 let mut affected: u64 = 0;
249 for (pk, new_values) in pending {
250 engine.columnar.update(collection, &pk, &new_values)?;
251 index_columnar_rows(engine, collection, [new_values.as_slice()])?;
252 affected += 1;
253 }
254
255 Ok(QueryResult {
256 columns: Vec::new(),
257 rows: Vec::new(),
258 rows_affected: affected,
259 command: Some("UPDATE".into()),
260 })
261}
262
263enum Assigned<'a> {
265 Literal(Value),
266 Expr(&'a nodedb_query::expr::SqlExpr),
267}
268
269pub async fn delete<S: StorageEngine>(
271 engine: &LiteQueryEngine<S>,
272 collection: &str,
273 filters_bytes: &[u8],
274) -> Result<QueryResult, LiteError> {
275 let schema = engine
276 .columnar
277 .schema(collection)
278 .ok_or_else(|| LiteError::collection_not_found("columnar", collection))?;
279
280 let col_names: Vec<String> = schema.columns.iter().map(|c| c.name.clone()).collect();
281 let pk_idx = schema
282 .columns
283 .iter()
284 .position(|c| c.primary_key)
285 .unwrap_or(0);
286
287 let filters: Vec<ScanFilter> = if filters_bytes.is_empty() {
288 Vec::new()
289 } else {
290 zerompk::from_msgpack(filters_bytes).map_err(|e| LiteError::Serialization {
291 detail: format!("decode delete filters: {e}"),
292 })?
293 };
294
295 let all_rows = engine.columnar.list_rows(collection).await?;
296
297 let mut pks_to_delete: Vec<Value> = Vec::new();
298 for row in all_rows {
299 let doc = row_to_object(&col_names, &row);
300 let matches = filters.is_empty() || ScanFilter::all_match_value(&filters, &doc)?;
301 if matches {
302 pks_to_delete.push(row.get(pk_idx).cloned().unwrap_or(Value::Null));
303 }
304 }
305
306 let mut affected: u64 = 0;
307 for pk in &pks_to_delete {
308 if engine.columnar.delete(collection, pk)? {
309 affected += 1;
310 }
311 }
312 remove_columnar_rows(engine, collection, &pks_to_delete)?;
313
314 Ok(QueryResult {
315 columns: Vec::new(),
316 rows: Vec::new(),
317 rows_affected: affected,
318 command: Some("DELETE".into()),
319 })
320}
321
322#[cfg(test)]
323mod tests {
324 use std::collections::HashMap;
325
326 use nodedb_types::columnar::{ColumnDef, ColumnType, ColumnarProfile};
327
328 use super::*;
329 use crate::query::engine::test_engine;
330
331 async fn seed(engine: &LiteQueryEngine<crate::PagedbStorageMem>) {
332 let schema = ColumnarSchema::new(vec![
333 ColumnDef::required("sku", ColumnType::String).with_primary_key(),
334 ColumnDef::nullable("qty", ColumnType::Int64),
335 ])
336 .expect("schema");
337 engine
338 .columnar
339 .create_collection("items", schema, ColumnarProfile::Plain, false)
340 .await
341 .expect("create");
342 }
343
344 fn payload(sku: &str, qty: i64) -> Vec<u8> {
345 let mut row = HashMap::new();
346 row.insert("sku".to_string(), Value::String(sku.into()));
347 row.insert("qty".to_string(), Value::Integer(qty));
348 zerompk::to_msgpack_vec(&Value::Object(row)).expect("encode")
349 }
350
351 async fn run(
352 engine: &LiteQueryEngine<crate::PagedbStorageMem>,
353 intent: ColumnarInsertIntent,
354 bytes: &[u8],
355 ) -> Result<u64, LiteError> {
356 insert(
357 engine,
358 "items",
359 InsertParams {
360 payload: bytes,
361 format: "msgpack",
362 intent,
363 on_conflict_updates: &[],
364 surrogates: &[],
365 schema_bytes: &[],
366 },
367 )
368 .await
369 .map(|(r, _)| r.rows_affected)
370 }
371
372 #[tokio::test]
373 async fn insert_unique_duplicate_pk_is_a_unique_violation() {
374 let engine = test_engine().await;
375 seed(&engine).await;
376 let first = run(
377 &engine,
378 ColumnarInsertIntent::InsertUnique,
379 &payload("a", 1),
380 )
381 .await
382 .expect("first insert");
383 assert_eq!(first, 1);
384 let err = run(
385 &engine,
386 ColumnarInsertIntent::InsertUnique,
387 &payload("a", 2),
388 )
389 .await
390 .expect_err("duplicate");
391 assert!(matches!(err, LiteError::UniqueViolation { .. }), "{err}");
392 let rows = engine.columnar.list_rows("items").await.expect("rows");
393 assert_eq!(rows.len(), 1);
394 assert_eq!(rows[0][1], Value::Integer(1), "the first row is kept");
395 }
396
397 #[tokio::test]
398 async fn insert_if_absent_duplicate_pk_is_skipped() {
399 let engine = test_engine().await;
400 seed(&engine).await;
401 run(&engine, ColumnarInsertIntent::Insert, &payload("a", 1))
402 .await
403 .expect("first insert");
404 let n = run(
405 &engine,
406 ColumnarInsertIntent::InsertIfAbsent,
407 &payload("a", 2),
408 )
409 .await
410 .expect("if absent");
411 assert_eq!(n, 0);
412 }
413}