1use std::any::Any;
2use std::collections::HashMap;
3use std::fmt;
4use std::sync::Arc;
5use std::sync::Mutex;
6
7use async_trait::async_trait;
8use bytes::Bytes;
9use commonware_codec::Encode;
10use datafusion::arrow::array::{
11 ArrayRef, BooleanArray, Date32Array, Date64Array, Decimal128Array, Decimal256Array,
12 FixedSizeBinaryArray, Float64Array, Int64Array, LargeStringArray, ListArray, StringArray,
13 StringViewArray, TimestampMicrosecondArray, UInt64Array,
14};
15use datafusion::arrow::datatypes::{i256, SchemaRef};
16use datafusion::arrow::record_batch::RecordBatch;
17use datafusion::common::{DataFusionError, Result as DataFusionResult};
18use datafusion::datasource::sink::DataSink;
19use datafusion::execution::context::TaskContext;
20use datafusion::physical_plan::{DisplayAs, DisplayFormatType, SendableRecordBatchStream};
21use exoware_sdk::keys::Key;
22#[cfg(test)]
23use exoware_sdk::kv_codec::decode_stored_row;
24use exoware_sdk::kv_codec::{StoredRow, StoredValue};
25use exoware_sdk::{PrefixedStoreClient, StoreBatchUpload, StoreWriteBatch};
26use futures::{future::BoxFuture, TryStreamExt};
27
28use crate::builder::archived_non_pk_value_is_valid;
29use crate::codec::*;
30use crate::types::*;
31
32#[derive(Debug)]
33pub struct TableWriter {
34 model: Arc<TableModel>,
35 index_specs: Arc<Vec<ResolvedIndexSpec>>,
36}
37
38impl TableWriter {
39 pub fn encode_row(&self, values: Vec<CellValue>) -> Result<Vec<(Key, Vec<u8>)>, String> {
40 let row = KvRow { values };
41 if row.values.len() != self.model.columns.len() {
42 return Err(format!(
43 "expected {} values, got {}",
44 self.model.columns.len(),
45 row.values.len()
46 ));
47 }
48 let base_key = encode_primary_key_from_row(self.model.table_prefix, &row, &self.model)?;
49 let base_value = encode_base_row_value(&row, &self.model).map_err(|e| format!("{e}"))?;
50 let mut out = vec![(base_key, base_value)];
51 for spec in self.index_specs.iter() {
52 let idx_key =
53 encode_secondary_index_key(self.model.table_prefix, spec, &self.model, &row)?;
54 let idx_value = encode_secondary_index_value(&row, &self.model, spec)
55 .map_err(|e| format!("{e}"))?;
56 out.push((idx_key, idx_value));
57 }
58 Ok(out)
59 }
60}
61
62#[derive(Debug)]
63pub struct BatchWriter {
64 client: PrefixedStoreClient,
65 tables: HashMap<String, TableWriter>,
66 next_request_id: u64,
67 failed_prepared: Mutex<Vec<PreparedBatch>>,
68 pub(crate) pending_keys: Vec<Key>,
69 pub(crate) pending_values: Vec<Bytes>,
70}
71
72#[derive(Debug)]
73#[must_use]
74pub struct PreparedBatch {
75 request_id: u64,
76 entry_count: usize,
77 keys: Vec<Key>,
78 values: Vec<Bytes>,
79}
80
81impl PreparedBatch {
82 pub fn request_id(&self) -> u64 {
83 self.request_id
84 }
85
86 pub fn entry_count(&self) -> usize {
87 self.entry_count
88 }
89
90 pub fn is_empty(&self) -> bool {
91 self.keys.is_empty()
92 }
93}
94
95#[derive(Clone, Copy, Debug, PartialEq, Eq)]
96pub struct BatchReceipt {
97 pub writer_request_id: u64,
98 pub entry_count: usize,
99 pub store_sequence_number: u64,
100}
101
102impl BatchWriter {
103 pub(crate) fn new(
104 client: PrefixedStoreClient,
105 table_configs: &[(String, KvTableConfig)],
106 ) -> Self {
107 let mut tables = HashMap::new();
108 for (name, config) in table_configs {
109 let model = Arc::new(
110 TableModel::from_config(config).expect("config already validated by KvSchema"),
111 );
112 let index_specs = Arc::new(
113 model
114 .resolve_index_specs(&config.index_specs)
115 .expect("specs already validated by KvSchema"),
116 );
117 tables.insert(name.clone(), TableWriter { model, index_specs });
118 }
119 Self {
120 client,
121 tables,
122 next_request_id: 0,
123 failed_prepared: Mutex::new(Vec::new()),
124 pending_keys: Vec::new(),
125 pending_values: Vec::new(),
126 }
127 }
128
129 pub fn insert(
130 &mut self,
131 table_name: &str,
132 values: Vec<CellValue>,
133 ) -> Result<&mut Self, String> {
134 let writer = self
135 .tables
136 .get(table_name)
137 .ok_or_else(|| format!("unknown table '{table_name}'"))?;
138 let entries = writer.encode_row(values)?;
139 for (key, value) in entries {
140 self.pending_keys.push(key);
141 self.pending_values.push(value.into());
142 }
143 Ok(self)
144 }
145
146 pub fn pending_count(&self) -> usize {
147 self.pending_keys.len()
148 + self
149 .failed_prepared
150 .lock()
151 .expect("failed prepared mutex poisoned")
152 .iter()
153 .map(PreparedBatch::entry_count)
154 .sum::<usize>()
155 }
156
157 pub async fn flush(&mut self) -> DataFusionResult<u64> {
159 Ok(self
160 .flush_with_receipt()
161 .await?
162 .map(|receipt| receipt.store_sequence_number)
163 .unwrap_or(0))
164 }
165
166 pub async fn flush_with_receipt(&mut self) -> DataFusionResult<Option<BatchReceipt>> {
168 let Some(prepared) = self.prepare_flush()? else {
169 return Ok(None);
170 };
171 Ok(Some(self.commit_upload(prepared).await?))
172 }
173
174 pub fn prepare_flush(&mut self) -> DataFusionResult<Option<PreparedBatch>> {
175 if let Some(prepared) = self.take_failed_prepared() {
176 return Ok(Some(prepared));
177 }
178 if self.pending_keys.is_empty() {
179 return Ok(None);
180 }
181 let request_id = self.next_request_id;
182 self.next_request_id += 1;
183 Ok(Some(PreparedBatch {
184 request_id,
185 entry_count: self.pending_keys.len(),
186 keys: std::mem::take(&mut self.pending_keys),
187 values: std::mem::take(&mut self.pending_values),
188 }))
189 }
190
191 pub fn stage_flush(
192 &self,
193 prepared: &PreparedBatch,
194 batch: &mut StoreWriteBatch,
195 ) -> DataFusionResult<()> {
196 for (key, value) in prepared.keys.iter().zip(prepared.values.iter()) {
197 batch
198 .push(&self.client, key, value)
199 .map_err(|e| DataFusionError::External(Box::new(e)))?;
200 }
201 Ok(())
202 }
203
204 pub fn mark_flush_persisted(
205 &self,
206 prepared: PreparedBatch,
207 sequence_number: u64,
208 ) -> BatchReceipt {
209 BatchReceipt {
210 writer_request_id: prepared.request_id,
211 entry_count: prepared.entry_count(),
212 store_sequence_number: sequence_number,
213 }
214 }
215
216 pub fn mark_flush_failed(&self, prepared: PreparedBatch) {
217 self.failed_prepared
218 .lock()
219 .expect("failed prepared mutex poisoned")
220 .push(prepared);
221 }
222
223 fn take_failed_prepared(&self) -> Option<PreparedBatch> {
224 let mut failed = self
225 .failed_prepared
226 .lock()
227 .expect("failed prepared mutex poisoned");
228 let (idx, _) = failed
229 .iter()
230 .enumerate()
231 .min_by_key(|(_, prepared)| prepared.request_id)?;
232 Some(failed.remove(idx))
233 }
234}
235
236impl StoreBatchUpload for BatchWriter {
237 type Prepared = PreparedBatch;
238 type Receipt = BatchReceipt;
239 type Error = DataFusionError;
240
241 fn store_client(&self) -> &PrefixedStoreClient {
242 &self.client
243 }
244
245 fn stage_upload(
246 &self,
247 prepared: &mut Self::Prepared,
248 batch: &mut StoreWriteBatch,
249 ) -> Result<(), Self::Error> {
250 self.stage_flush(prepared, batch)
251 }
252
253 fn commit_error(&self, error: exoware_sdk::ClientError) -> Self::Error {
254 DataFusionError::External(Box::new(error))
255 }
256
257 fn mark_upload_persisted<'a>(
258 &'a self,
259 prepared: Self::Prepared,
260 sequence_number: u64,
261 ) -> BoxFuture<'a, Self::Receipt>
262 where
263 Self: Sync + 'a,
264 Self::Prepared: 'a,
265 {
266 Box::pin(async move { self.mark_flush_persisted(prepared, sequence_number) })
267 }
268
269 fn mark_upload_failed<'a>(
270 &'a self,
271 prepared: Self::Prepared,
272 _error: String,
273 ) -> BoxFuture<'a, ()>
274 where
275 Self: Sync + 'a,
276 Self::Prepared: 'a,
277 {
278 Box::pin(async move {
279 self.mark_flush_failed(prepared);
280 })
281 }
282}
283
284#[derive(Debug)]
285pub(crate) struct KvIngestSink {
286 pub(crate) client: PrefixedStoreClient,
287 pub(crate) schema: SchemaRef,
288 pub(crate) model: Arc<TableModel>,
289 pub(crate) index_specs: Arc<Vec<ResolvedIndexSpec>>,
290}
291
292impl KvIngestSink {
293 pub(crate) fn new(
294 client: PrefixedStoreClient,
295 schema: SchemaRef,
296 model: Arc<TableModel>,
297 index_specs: Arc<Vec<ResolvedIndexSpec>>,
298 ) -> Self {
299 Self {
300 client,
301 schema,
302 model,
303 index_specs,
304 }
305 }
306}
307
308impl DisplayAs for KvIngestSink {
309 fn fmt_as(&self, _t: DisplayFormatType, f: &mut fmt::Formatter<'_>) -> fmt::Result {
310 write!(f, "KvIngestSink")
311 }
312}
313
314#[async_trait]
315impl DataSink for KvIngestSink {
316 fn as_any(&self) -> &dyn Any {
317 self
318 }
319
320 fn schema(&self) -> &SchemaRef {
321 &self.schema
322 }
323
324 async fn write_all(
325 &self,
326 data: SendableRecordBatchStream,
327 _context: &Arc<TaskContext>,
328 ) -> DataFusionResult<u64> {
329 let mut data = data;
330 let mut pending_keys: Vec<Key> = Vec::new();
331 let mut pending_values: Vec<Bytes> = Vec::new();
332 let mut logical_rows_written = 0u64;
333
334 while let Some(batch) = data.try_next().await? {
335 let encoded_entries = encode_insert_entries(&batch, &self.model, &self.index_specs)?;
336 logical_rows_written += batch.num_rows() as u64;
337 for (key, value) in encoded_entries {
338 pending_keys.push(key);
339 pending_values.push(value.into());
340 }
341 }
342
343 if !pending_keys.is_empty() {
344 flush_ingest_batch(&self.client, &mut pending_keys, &mut pending_values).await?;
345 }
346 Ok(logical_rows_written)
347 }
348}
349
350pub(crate) fn encode_insert_entries(
351 batch: &RecordBatch,
352 model: &TableModel,
353 index_specs: &[ResolvedIndexSpec],
354) -> DataFusionResult<Vec<(Key, Vec<u8>)>> {
355 let mut out = Vec::with_capacity(batch.num_rows() * (1 + index_specs.len()));
356 for row_idx in 0..batch.num_rows() {
357 let row = extract_row_from_batch(batch, row_idx, model)?;
358 let base_key = encode_primary_key_from_row(model.table_prefix, &row, model)
359 .map_err(DataFusionError::Execution)?;
360 let base_value = encode_base_row_value(&row, model)?;
361 out.push((base_key, base_value));
362
363 for spec in index_specs {
364 let secondary_key = encode_secondary_index_key(model.table_prefix, spec, model, &row)
365 .map_err(DataFusionError::Execution)?;
366 let secondary_value = encode_secondary_index_value(&row, model, spec)?;
367 out.push((secondary_key, secondary_value));
368 }
369 }
370 Ok(out)
371}
372
373pub(crate) fn extract_row_from_batch(
374 batch: &RecordBatch,
375 row_idx: usize,
376 model: &TableModel,
377) -> DataFusionResult<KvRow> {
378 let mut values = Vec::with_capacity(model.columns.len());
379 for col in &model.columns {
380 let array = required_column(batch, &col.name)?;
381 if col.nullable && array.is_null(row_idx) {
382 values.push(CellValue::Null);
383 continue;
384 }
385 let value = match col.kind {
386 ColumnKind::Int64 => CellValue::Int64(i64_value_at(array, row_idx, &col.name)?),
387 ColumnKind::UInt64 => CellValue::UInt64(uint64_value_at(array, row_idx, &col.name)?),
388 ColumnKind::Float64 => CellValue::Float64(f64_value_at(array, row_idx, &col.name)?),
389 ColumnKind::Boolean => CellValue::Boolean(bool_value_at(array, row_idx, &col.name)?),
390 ColumnKind::Date32 => CellValue::Date32(date32_value_at(array, row_idx, &col.name)?),
391 ColumnKind::Date64 => CellValue::Date64(date64_value_at(array, row_idx, &col.name)?),
392 ColumnKind::Timestamp => {
393 CellValue::Timestamp(timestamp_micros_value_at(array, row_idx, &col.name)?)
394 }
395 ColumnKind::Decimal128 => {
396 CellValue::Decimal128(decimal128_value_at(array, row_idx, &col.name)?)
397 }
398 ColumnKind::Decimal256 => {
399 CellValue::Decimal256(decimal256_value_at(array, row_idx, &col.name)?)
400 }
401 ColumnKind::Utf8 => CellValue::Utf8(string_value_at(array, row_idx, &col.name)?),
402 ColumnKind::FixedSizeBinary(_) => {
403 CellValue::FixedBinary(fixed_binary_value_at(array, row_idx, &col.name)?)
404 }
405 ColumnKind::List(elem) => list_value_at(array, row_idx, &col.name, elem)?,
406 };
407 values.push(value);
408 }
409 Ok(KvRow { values })
410}
411
412pub(crate) fn encode_base_row_value(row: &KvRow, model: &TableModel) -> DataFusionResult<Vec<u8>> {
413 let mut values = Vec::with_capacity(model.columns.len());
414 for (idx, col) in model.columns.iter().enumerate() {
415 if model.is_pk_column(idx) {
416 values.push(None);
417 continue;
418 }
419 values.push(encode_non_pk_cell_value(row.value_at(idx), col)?);
420 }
421 let stored_row = StoredRow { values };
422 Ok(stored_row.encode().to_vec())
423}
424
425pub(crate) fn encode_secondary_index_value(
426 row: &KvRow,
427 model: &TableModel,
428 spec: &ResolvedIndexSpec,
429) -> DataFusionResult<Vec<u8>> {
430 let mut values = Vec::with_capacity(model.columns.len());
431 for (idx, col) in model.columns.iter().enumerate() {
432 if model.is_pk_column(idx) || !spec.value_column_mask[idx] {
433 values.push(None);
434 continue;
435 }
436 values.push(encode_non_pk_cell_value(row.value_at(idx), col)?);
437 }
438 let stored_row = StoredRow { values };
439 Ok(stored_row.encode().to_vec())
440}
441
442pub(crate) fn encode_secondary_index_value_from_archived(
443 archived: &StoredRow,
444 model: &TableModel,
445 spec: &ResolvedIndexSpec,
446) -> DataFusionResult<Vec<u8>> {
447 if archived.values.len() != model.columns.len() {
448 return Err(DataFusionError::Execution(
449 "archived row column count mismatch".to_string(),
450 ));
451 }
452 let mut values = Vec::with_capacity(model.columns.len());
453 for (idx, col) in model.columns.iter().enumerate() {
454 if model.is_pk_column(idx) || !spec.value_column_mask[idx] {
455 values.push(None);
456 continue;
457 }
458 let stored_opt = archived.values.get(idx).and_then(|value| value.as_ref());
459 if !archived_non_pk_value_is_valid(col, stored_opt) {
460 return Err(DataFusionError::Execution(format!(
461 "invalid archived value for secondary index column '{}'",
462 col.name
463 )));
464 }
465 values.push(owned_stored_value_from_archived(stored_opt)?);
466 }
467 let stored_row = StoredRow { values };
468 Ok(stored_row.encode().to_vec())
469}
470
471pub(crate) fn encode_non_pk_cell_value(
472 value: &CellValue,
473 col: &ResolvedColumn,
474) -> DataFusionResult<Option<StoredValue>> {
475 match (col.kind, value) {
476 (_, CellValue::Null) => {
477 if !col.nullable {
478 return Err(DataFusionError::Execution(format!(
479 "column '{}' is not nullable but received NULL",
480 col.name
481 )));
482 }
483 Ok(None)
484 }
485 (ColumnKind::Int64, CellValue::Int64(v)) => Ok(Some(StoredValue::Int64(*v))),
486 (ColumnKind::UInt64, CellValue::UInt64(v)) => Ok(Some(StoredValue::UInt64(*v))),
487 (ColumnKind::Float64, CellValue::Float64(v)) => Ok(Some(StoredValue::Float64(*v))),
488 (ColumnKind::Boolean, CellValue::Boolean(v)) => Ok(Some(StoredValue::Boolean(*v))),
489 (ColumnKind::Date32, CellValue::Date32(v)) => Ok(Some(StoredValue::Int64(*v as i64))),
490 (ColumnKind::Date64, CellValue::Date64(v)) => Ok(Some(StoredValue::Int64(*v))),
491 (ColumnKind::Timestamp, CellValue::Timestamp(v)) => Ok(Some(StoredValue::Int64(*v))),
492 (ColumnKind::Decimal128, CellValue::Decimal128(v)) => {
493 Ok(Some(StoredValue::Bytes(v.to_le_bytes().to_vec())))
494 }
495 (ColumnKind::Decimal256, CellValue::Decimal256(v)) => {
496 Ok(Some(StoredValue::Bytes(v.to_le_bytes().to_vec())))
497 }
498 (ColumnKind::Utf8, CellValue::Utf8(v)) => Ok(Some(StoredValue::Utf8(v.clone()))),
499 (ColumnKind::FixedSizeBinary(n), CellValue::FixedBinary(v)) => {
500 if v.len() != n {
501 return Err(DataFusionError::Execution(format!(
502 "column '{}' expects FixedSizeBinary({n}) value with exactly {n} bytes, got {}",
503 col.name,
504 v.len()
505 )));
506 }
507 Ok(Some(StoredValue::Bytes(v.clone())))
508 }
509 (ColumnKind::List(elem), CellValue::List(items)) => {
510 let mut stored_items = Vec::with_capacity(items.len());
511 for item in items {
512 let stored_item = match (elem, item) {
513 (ListElementKind::Int64, CellValue::Int64(v)) => StoredValue::Int64(*v),
514 (ListElementKind::Float64, CellValue::Float64(v)) => StoredValue::Float64(*v),
515 (ListElementKind::Boolean, CellValue::Boolean(v)) => StoredValue::Boolean(*v),
516 (ListElementKind::Utf8, CellValue::Utf8(v)) => StoredValue::Utf8(v.clone()),
517 _ => {
518 return Err(DataFusionError::Execution(format!(
519 "column '{}' list element type mismatch (expected {:?}, got {:?})",
520 col.name, elem, item
521 )))
522 }
523 };
524 stored_items.push(stored_item);
525 }
526 Ok(Some(StoredValue::List(stored_items)))
527 }
528 _ => Err(DataFusionError::Execution(format!(
529 "column '{}' type mismatch (expected {:?}, got {:?})",
530 col.name, col.kind, value
531 ))),
532 }
533}
534
535pub(crate) fn owned_stored_value_from_archived(
536 stored_opt: Option<&StoredValue>,
537) -> DataFusionResult<Option<StoredValue>> {
538 let Some(stored) = stored_opt else {
539 return Ok(None);
540 };
541 Ok(Some(match stored {
542 StoredValue::Int64(v) => StoredValue::Int64(*v),
543 StoredValue::UInt64(v) => StoredValue::UInt64(*v),
544 StoredValue::Float64(v) => StoredValue::Float64(*v),
545 StoredValue::Boolean(v) => StoredValue::Boolean(*v),
546 StoredValue::Utf8(v) => StoredValue::Utf8(v.as_str().to_string()),
547 StoredValue::Bytes(v) => StoredValue::Bytes(v.as_slice().to_vec()),
548 StoredValue::List(items) => {
549 let mut out = Vec::with_capacity(items.len());
550 for item in items.iter() {
551 let owned = owned_stored_value_from_archived(Some(item))?.ok_or_else(|| {
552 DataFusionError::Execution(
553 "archived list item unexpectedly decoded as NULL".to_string(),
554 )
555 })?;
556 out.push(owned);
557 }
558 StoredValue::List(out)
559 }
560 }))
561}
562
563#[cfg(test)]
564pub(crate) fn decode_base_row(
565 pk_values: Vec<CellValue>,
566 value: &[u8],
567 model: &TableModel,
568) -> Option<KvRow> {
569 if pk_values.len() != model.primary_key_indices.len() {
570 return None;
571 }
572 let archived = decode_stored_row(value).ok()?;
573 if archived.values.len() != model.columns.len() {
574 return None;
575 }
576 let mut values = vec![CellValue::Null; model.columns.len()];
577 for (pk_pos, pk_value) in pk_values.into_iter().enumerate() {
578 let col_idx = *model.primary_key_indices.get(pk_pos)?;
579 values[col_idx] = pk_value;
580 }
581
582 for (idx, col) in model.columns.iter().enumerate() {
583 if model.is_pk_column(idx) {
584 continue;
585 }
586 let Some(stored) = archived.values[idx].as_ref() else {
587 if col.nullable {
588 continue;
589 }
590 return None;
591 };
592 values[idx] = match (col.kind, stored) {
593 (ColumnKind::Int64, StoredValue::Int64(v)) => CellValue::Int64(*v),
594 (ColumnKind::UInt64, StoredValue::UInt64(v)) => CellValue::UInt64(*v),
595 (ColumnKind::Float64, StoredValue::Float64(v)) => CellValue::Float64(*v),
596 (ColumnKind::Float64, StoredValue::Int64(v)) => CellValue::Float64(*v as f64),
597 (ColumnKind::Boolean, StoredValue::Boolean(v)) => CellValue::Boolean(*v),
598 (ColumnKind::Date32, StoredValue::Int64(v)) => CellValue::Date32(*v as i32),
599 (ColumnKind::Date64, StoredValue::Int64(v)) => CellValue::Date64(*v),
600 (ColumnKind::Timestamp, StoredValue::Int64(v)) => CellValue::Timestamp(*v),
601 (ColumnKind::Decimal128, StoredValue::Bytes(bytes)) => {
602 let arr: [u8; 16] = bytes.as_slice().try_into().ok()?;
603 CellValue::Decimal128(i128::from_le_bytes(arr))
604 }
605 (ColumnKind::Decimal256, StoredValue::Bytes(bytes)) => {
606 let arr: [u8; 32] = bytes.as_slice().try_into().ok()?;
607 CellValue::Decimal256(i256::from_le_bytes(arr))
608 }
609 (ColumnKind::Utf8, StoredValue::Utf8(v)) => CellValue::Utf8(v.as_str().to_string()),
610 (ColumnKind::FixedSizeBinary(_), StoredValue::Bytes(v)) => {
611 CellValue::FixedBinary(v.as_slice().to_vec())
612 }
613 (ColumnKind::List(elem), StoredValue::List(items)) => {
614 let mut cells = Vec::with_capacity(items.len());
615 for item in items.iter() {
616 cells.push(decode_list_element_archived(elem, item)?);
617 }
618 CellValue::List(cells)
619 }
620 _ => return None,
621 };
622 }
623 Some(KvRow { values })
624}
625
626pub(crate) fn decode_list_element_archived(
627 elem: ListElementKind,
628 stored: &StoredValue,
629) -> Option<CellValue> {
630 Some(match (elem, stored) {
631 (ListElementKind::Int64, StoredValue::Int64(v)) => CellValue::Int64(*v),
632 (ListElementKind::Float64, StoredValue::Float64(v)) => CellValue::Float64(*v),
633 (ListElementKind::Float64, StoredValue::Int64(v)) => CellValue::Float64(*v as f64),
634 (ListElementKind::Boolean, StoredValue::Boolean(v)) => CellValue::Boolean(*v),
635 (ListElementKind::Utf8, StoredValue::Utf8(v)) => CellValue::Utf8(v.as_str().to_string()),
636 _ => return None,
637 })
638}
639
640pub(crate) fn required_column<'a>(
641 batch: &'a RecordBatch,
642 name: &str,
643) -> DataFusionResult<&'a ArrayRef> {
644 batch.column_by_name(name).ok_or_else(|| {
645 DataFusionError::Execution(format!("insert batch is missing required column '{name}'"))
646 })
647}
648
649pub(crate) fn i64_value_at(
650 array: &ArrayRef,
651 row_idx: usize,
652 column_name: &str,
653) -> DataFusionResult<i64> {
654 if array.is_null(row_idx) {
655 return Err(DataFusionError::Execution(format!(
656 "column '{column_name}' cannot be NULL for kv table insert"
657 )));
658 }
659 let values = array.as_any().downcast_ref::<Int64Array>().ok_or_else(|| {
660 DataFusionError::Execution(format!(
661 "column '{column_name}' expected Int64, got {:?}",
662 array.data_type()
663 ))
664 })?;
665 Ok(values.value(row_idx))
666}
667
668pub(crate) fn string_value_at(
669 array: &ArrayRef,
670 row_idx: usize,
671 column_name: &str,
672) -> DataFusionResult<String> {
673 if array.is_null(row_idx) {
674 return Err(DataFusionError::Execution(format!(
675 "column '{column_name}' cannot be NULL for kv table insert"
676 )));
677 }
678 if let Some(values) = array.as_any().downcast_ref::<StringArray>() {
679 return Ok(values.value(row_idx).to_string());
680 }
681 if let Some(values) = array.as_any().downcast_ref::<LargeStringArray>() {
682 return Ok(values.value(row_idx).to_string());
683 }
684 if let Some(values) = array.as_any().downcast_ref::<StringViewArray>() {
685 return Ok(values.value(row_idx).to_string());
686 }
687 Err(DataFusionError::Execution(format!(
688 "column '{column_name}' expected string, got {:?}",
689 array.data_type()
690 )))
691}
692
693pub(crate) fn f64_value_at(
694 array: &ArrayRef,
695 row_idx: usize,
696 column_name: &str,
697) -> DataFusionResult<f64> {
698 if array.is_null(row_idx) {
699 return Err(DataFusionError::Execution(format!(
700 "column '{column_name}' cannot be NULL for kv table insert"
701 )));
702 }
703 let values = array
704 .as_any()
705 .downcast_ref::<Float64Array>()
706 .ok_or_else(|| {
707 DataFusionError::Execution(format!(
708 "column '{column_name}' expected Float64, got {:?}",
709 array.data_type()
710 ))
711 })?;
712 Ok(values.value(row_idx))
713}
714
715pub(crate) fn bool_value_at(
716 array: &ArrayRef,
717 row_idx: usize,
718 column_name: &str,
719) -> DataFusionResult<bool> {
720 if array.is_null(row_idx) {
721 return Err(DataFusionError::Execution(format!(
722 "column '{column_name}' cannot be NULL for kv table insert"
723 )));
724 }
725 let values = array
726 .as_any()
727 .downcast_ref::<BooleanArray>()
728 .ok_or_else(|| {
729 DataFusionError::Execution(format!(
730 "column '{column_name}' expected Boolean, got {:?}",
731 array.data_type()
732 ))
733 })?;
734 Ok(values.value(row_idx))
735}
736
737pub(crate) fn date32_value_at(
738 array: &ArrayRef,
739 row_idx: usize,
740 column_name: &str,
741) -> DataFusionResult<i32> {
742 if array.is_null(row_idx) {
743 return Err(DataFusionError::Execution(format!(
744 "column '{column_name}' cannot be NULL for kv table insert"
745 )));
746 }
747 let values = array
748 .as_any()
749 .downcast_ref::<Date32Array>()
750 .ok_or_else(|| {
751 DataFusionError::Execution(format!(
752 "column '{column_name}' expected Date32, got {:?}",
753 array.data_type()
754 ))
755 })?;
756 Ok(values.value(row_idx))
757}
758
759pub(crate) fn date64_value_at(
760 array: &ArrayRef,
761 row_idx: usize,
762 column_name: &str,
763) -> DataFusionResult<i64> {
764 if array.is_null(row_idx) {
765 return Err(DataFusionError::Execution(format!(
766 "column '{column_name}' cannot be NULL for kv table insert"
767 )));
768 }
769 let values = array
770 .as_any()
771 .downcast_ref::<Date64Array>()
772 .ok_or_else(|| {
773 DataFusionError::Execution(format!(
774 "column '{column_name}' expected Date64, got {:?}",
775 array.data_type()
776 ))
777 })?;
778 Ok(values.value(row_idx))
779}
780
781pub(crate) fn timestamp_micros_value_at(
782 array: &ArrayRef,
783 row_idx: usize,
784 column_name: &str,
785) -> DataFusionResult<i64> {
786 if array.is_null(row_idx) {
787 return Err(DataFusionError::Execution(format!(
788 "column '{column_name}' cannot be NULL for kv table insert"
789 )));
790 }
791 let values = array
792 .as_any()
793 .downcast_ref::<TimestampMicrosecondArray>()
794 .ok_or_else(|| {
795 DataFusionError::Execution(format!(
796 "column '{column_name}' expected TimestampMicrosecond, got {:?}",
797 array.data_type()
798 ))
799 })?;
800 Ok(values.value(row_idx))
801}
802
803pub(crate) fn decimal128_value_at(
804 array: &ArrayRef,
805 row_idx: usize,
806 column_name: &str,
807) -> DataFusionResult<i128> {
808 if array.is_null(row_idx) {
809 return Err(DataFusionError::Execution(format!(
810 "column '{column_name}' cannot be NULL for kv table insert"
811 )));
812 }
813 let values = array
814 .as_any()
815 .downcast_ref::<Decimal128Array>()
816 .ok_or_else(|| {
817 DataFusionError::Execution(format!(
818 "column '{column_name}' expected Decimal128, got {:?}",
819 array.data_type()
820 ))
821 })?;
822 Ok(values.value(row_idx))
823}
824
825pub(crate) fn uint64_value_at(
826 array: &ArrayRef,
827 row_idx: usize,
828 column_name: &str,
829) -> DataFusionResult<u64> {
830 if array.is_null(row_idx) {
831 return Err(DataFusionError::Execution(format!(
832 "column '{column_name}' cannot be NULL for kv table insert"
833 )));
834 }
835 let values = array
836 .as_any()
837 .downcast_ref::<UInt64Array>()
838 .ok_or_else(|| {
839 DataFusionError::Execution(format!(
840 "column '{column_name}' expected UInt64, got {:?}",
841 array.data_type()
842 ))
843 })?;
844 Ok(values.value(row_idx))
845}
846
847pub(crate) fn decimal256_value_at(
848 array: &ArrayRef,
849 row_idx: usize,
850 column_name: &str,
851) -> DataFusionResult<i256> {
852 if array.is_null(row_idx) {
853 return Err(DataFusionError::Execution(format!(
854 "column '{column_name}' cannot be NULL for kv table insert"
855 )));
856 }
857 let values = array
858 .as_any()
859 .downcast_ref::<Decimal256Array>()
860 .ok_or_else(|| {
861 DataFusionError::Execution(format!(
862 "column '{column_name}' expected Decimal256, got {:?}",
863 array.data_type()
864 ))
865 })?;
866 Ok(values.value(row_idx))
867}
868
869pub(crate) fn fixed_binary_value_at(
870 array: &ArrayRef,
871 row_idx: usize,
872 column_name: &str,
873) -> DataFusionResult<Vec<u8>> {
874 if array.is_null(row_idx) {
875 return Err(DataFusionError::Execution(format!(
876 "column '{column_name}' cannot be NULL for kv table insert"
877 )));
878 }
879 let values = array
880 .as_any()
881 .downcast_ref::<FixedSizeBinaryArray>()
882 .ok_or_else(|| {
883 DataFusionError::Execution(format!(
884 "column '{column_name}' expected FixedSizeBinary, got {:?}",
885 array.data_type()
886 ))
887 })?;
888 Ok(values.value(row_idx).to_vec())
889}
890
891pub(crate) fn list_value_at(
892 array: &ArrayRef,
893 row_idx: usize,
894 column_name: &str,
895 elem: ListElementKind,
896) -> DataFusionResult<CellValue> {
897 if array.is_null(row_idx) {
898 return Err(DataFusionError::Execution(format!(
899 "column '{column_name}' cannot be NULL for kv table insert"
900 )));
901 }
902 let list_array = array.as_any().downcast_ref::<ListArray>().ok_or_else(|| {
903 DataFusionError::Execution(format!(
904 "column '{column_name}' expected List, got {:?}",
905 array.data_type()
906 ))
907 })?;
908 let child = list_array.value(row_idx);
909 let mut items = Vec::with_capacity(child.len());
910 for i in 0..child.len() {
911 let item = match elem {
912 ListElementKind::Int64 => {
913 let arr = child.as_any().downcast_ref::<Int64Array>().ok_or_else(|| {
914 DataFusionError::Execution(format!(
915 "column '{column_name}' list element expected Int64"
916 ))
917 })?;
918 CellValue::Int64(arr.value(i))
919 }
920 ListElementKind::Float64 => {
921 let arr = child
922 .as_any()
923 .downcast_ref::<Float64Array>()
924 .ok_or_else(|| {
925 DataFusionError::Execution(format!(
926 "column '{column_name}' list element expected Float64"
927 ))
928 })?;
929 CellValue::Float64(arr.value(i))
930 }
931 ListElementKind::Boolean => {
932 let arr = child
933 .as_any()
934 .downcast_ref::<BooleanArray>()
935 .ok_or_else(|| {
936 DataFusionError::Execution(format!(
937 "column '{column_name}' list element expected Boolean"
938 ))
939 })?;
940 CellValue::Boolean(arr.value(i))
941 }
942 ListElementKind::Utf8 => {
943 let arr = child
944 .as_any()
945 .downcast_ref::<StringArray>()
946 .ok_or_else(|| {
947 DataFusionError::Execution(format!(
948 "column '{column_name}' list element expected Utf8"
949 ))
950 })?;
951 CellValue::Utf8(arr.value(i).to_string())
952 }
953 };
954 items.push(item);
955 }
956 Ok(CellValue::List(items))
957}
958
959pub(crate) async fn flush_ingest_batch(
960 client: &PrefixedStoreClient,
961 keys: &mut Vec<Key>,
962 values: &mut Vec<Bytes>,
963) -> DataFusionResult<u64> {
964 if keys.is_empty() {
965 return Ok(0);
966 }
967 let mut batch = StoreWriteBatch::new();
968 for (key, value) in keys.iter().zip(values.iter()) {
969 batch
970 .push(client, key, value)
971 .map_err(|e| DataFusionError::External(Box::new(e)))?;
972 }
973 let token = batch
974 .commit(client.client())
975 .await
976 .map_err(|e| DataFusionError::External(Box::new(e)))?;
977 keys.clear();
978 values.clear();
979 Ok(token)
980}
981
982#[cfg(test)]
983mod tests {
984 use bytes::Bytes;
985 use exoware_sdk::StoreClient;
986
987 use super::*;
988
989 #[test]
990 fn store_batch_upload_stage_preserves_rows_for_failed_retry() {
991 let writer = BatchWriter::new(
992 PrefixedStoreClient::empty(StoreClient::new("http://127.0.0.1:1")),
993 &[],
994 );
995 let mut prepared = PreparedBatch {
996 request_id: 7,
997 entry_count: 2,
998 keys: vec![Bytes::from_static(b"a"), Bytes::from_static(b"b")],
999 values: vec![Bytes::from_static(&[1]), Bytes::from_static(&[2, 3])],
1000 };
1001 let mut batch = StoreWriteBatch::new();
1002
1003 StoreBatchUpload::stage_upload(&writer, &mut prepared, &mut batch).expect("stage flush");
1004
1005 assert_eq!(batch.len(), 2);
1006 assert_eq!(prepared.entry_count(), 2);
1007 assert_eq!(prepared.keys.len(), 2);
1008 assert_eq!(prepared.values.len(), 2);
1009
1010 writer.mark_flush_failed(prepared);
1011 let mut retry = writer.take_failed_prepared().expect("failed batch queued");
1012 assert_eq!(retry.entry_count(), 2);
1013
1014 let mut retry_batch = StoreWriteBatch::new();
1015 StoreBatchUpload::stage_upload(&writer, &mut retry, &mut retry_batch).expect("stage retry");
1016 assert_eq!(retry_batch.len(), 2);
1017 }
1018}