1use std::collections::{HashMap, HashSet};
2use std::sync::Arc;
3
4use datafusion::arrow::datatypes::{i256, DataType, Field, Schema, SchemaRef, TimeUnit};
5use datafusion::prelude::SessionConfig;
6use exoware_sdk::keys::{Key, Prefix};
7use exoware_sdk::PrefixedStoreClient;
8use exoware_sdk::SerializableReadSession;
9
10use crate::codec::{primary_key_prefix, secondary_index_prefix};
11
12pub(crate) fn request_read_session(
14 config: &SessionConfig,
15 client: &PrefixedStoreClient,
16) -> Option<SerializableReadSession> {
17 config
18 .get_extension::<SerializableReadSession>()
19 .map(|request| request.with_client(client.clone()))
20}
21
22pub(crate) const FAMILY_PREFIX_LEN: usize = 1;
31pub(crate) const PRIMARY_FAMILY_DISCRIMINATOR: u8 = 0x00;
33pub(crate) const PRIMARY_KEY_BYTE_OFFSET: usize = FAMILY_PREFIX_LEN;
37pub(crate) const INDEX_KEY_BYTE_OFFSET: usize = FAMILY_PREFIX_LEN;
38pub(crate) const MAX_TABLES: usize = 16;
43pub(crate) const MAX_INDEX_SPECS: usize = 15;
44pub(crate) const STRING_KEY_INLINE_LIMIT: usize = 15;
45pub(crate) const STRING_KEY_TERMINATOR: u8 = 0x00;
46pub(crate) const STRING_KEY_ESCAPE_PREFIX: u8 = 0x01;
47pub(crate) const STRING_KEY_ESCAPE_FF: u8 = 0x02;
48pub(crate) const PAGE_SIZE: usize = 1_000;
49pub(crate) const BATCH_FLUSH_ROWS: usize = 2_048;
50pub(crate) const INDEX_BACKFILL_FLUSH_ENTRIES: usize = 4_096;
51
52#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
53pub struct IndexBackfillReport {
54 pub scanned_rows: u64,
55 pub indexes_backfilled: usize,
56 pub index_entries_written: u64,
57}
58
59#[derive(Debug, Clone, PartialEq, Eq)]
60pub struct IndexBackfillOptions {
61 pub row_batch_size: usize,
63 pub start_from_primary_key: Option<Key>,
64}
65
66impl Default for IndexBackfillOptions {
67 fn default() -> Self {
68 Self {
69 row_batch_size: PAGE_SIZE,
70 start_from_primary_key: None,
71 }
72 }
73}
74
75#[derive(Debug, Clone, PartialEq, Eq)]
76pub enum IndexBackfillEvent {
77 Started {
78 table_name: String,
79 indexes_backfilled: usize,
80 row_batch_size: usize,
81 start_cursor: Key,
82 },
83 Progress {
84 scanned_rows: u64,
85 index_entries_written: u64,
86 last_scanned_primary_key: Key,
87 next_cursor: Option<Key>,
88 },
89 Completed {
90 report: IndexBackfillReport,
91 },
92}
93
94#[derive(Debug, Clone, Copy, PartialEq, Eq)]
95pub(crate) enum ListElementKind {
96 Int64,
97 Float64,
98 Boolean,
99 Utf8,
100}
101
102#[derive(Debug, Clone, Copy, PartialEq, Eq)]
103pub(crate) enum ColumnKind {
104 Int64,
105 UInt64,
106 Float64,
107 Boolean,
108 Utf8,
109 Date32,
110 Date64,
111 Timestamp,
112 Decimal128,
113 Decimal256,
114 FixedSizeBinary(usize),
115 Binary,
116 List(ListElementKind),
117}
118
119impl ColumnKind {
120 pub(crate) fn from_data_type(data_type: &DataType) -> Result<Self, String> {
121 match data_type {
122 DataType::Int64 => Ok(Self::Int64),
123 DataType::UInt64 => Ok(Self::UInt64),
124 DataType::Float64 => Ok(Self::Float64),
125 DataType::Boolean => Ok(Self::Boolean),
126 DataType::Utf8 | DataType::LargeUtf8 | DataType::Utf8View => Ok(Self::Utf8),
127 DataType::Date32 => Ok(Self::Date32),
128 DataType::Date64 => Ok(Self::Date64),
129 DataType::Timestamp(_, _) => Ok(Self::Timestamp),
130 DataType::Decimal128(_, _) => Ok(Self::Decimal128),
131 DataType::Decimal256(_, _) => Ok(Self::Decimal256),
132 DataType::FixedSizeBinary(n) => Ok(Self::FixedSizeBinary(*n as usize)),
133 DataType::Binary | DataType::LargeBinary | DataType::BinaryView => Ok(Self::Binary),
134 DataType::List(field) | DataType::LargeList(field) => {
135 let inner = Self::from_data_type(field.data_type())?;
136 let elem = match inner {
137 Self::Int64 => ListElementKind::Int64,
138 Self::Float64 => ListElementKind::Float64,
139 Self::Boolean => ListElementKind::Boolean,
140 Self::Utf8 => ListElementKind::Utf8,
141 _ => {
142 return Err(format!(
143 "unsupported list element type {:?}; \
144 list elements must be Int64, Float64, Boolean, or Utf8",
145 field.data_type()
146 ))
147 }
148 };
149 Ok(Self::List(elem))
150 }
151 other => Err(format!(
152 "unsupported column type {other:?}; supported: \
153 Int64, UInt64, Float64, Boolean, Utf8, Date32, Date64, Timestamp, \
154 Decimal128, Decimal256, FixedSizeBinary, Binary, List"
155 )),
156 }
157 }
158
159 pub(crate) fn fixed_key_width(self) -> Option<usize> {
160 match self {
161 Self::Int64 => Some(8),
162 Self::UInt64 => Some(8),
163 Self::Float64 => Some(8),
164 Self::Boolean => Some(1),
165 Self::Utf8 => None,
166 Self::Date32 => Some(4),
167 Self::Date64 => Some(8),
168 Self::Timestamp => Some(8),
169 Self::Decimal128 => Some(16),
170 Self::Decimal256 => Some(32),
171 Self::FixedSizeBinary(n) => Some(n),
172 Self::Binary => None,
173 Self::List(_) => None,
174 }
175 }
176
177 pub(crate) fn key_width(self) -> usize {
178 self.fixed_key_width()
179 .unwrap_or(STRING_KEY_INLINE_LIMIT + 1)
180 }
181
182 pub(crate) fn indexable(self) -> bool {
183 !matches!(self, Self::List(_) | Self::Binary)
184 }
185}
186
187#[derive(Debug, Clone)]
188pub struct TableColumnConfig {
189 pub name: String,
190 pub data_type: DataType,
191 pub nullable: bool,
192}
193
194impl TableColumnConfig {
195 pub fn new(name: impl Into<String>, data_type: DataType, nullable: bool) -> Self {
196 Self {
197 name: name.into(),
198 data_type,
199 nullable,
200 }
201 }
202}
203
204#[derive(Debug, Clone, Copy, PartialEq, Eq)]
205pub enum IndexLayout {
206 Lexicographic,
207 ZOrder,
208}
209
210#[derive(Debug, Clone)]
211pub struct IndexSpec {
212 name: String,
213 key_columns: Vec<String>,
214 cover_columns: Vec<String>,
215 layout: IndexLayout,
216}
217
218impl IndexSpec {
219 #[cfg(test)]
220 pub(crate) fn new(name: impl Into<String>, key_columns: Vec<String>) -> Result<Self, String> {
221 Self::lexicographic(name, key_columns)
222 }
223
224 pub fn lexicographic(
225 name: impl Into<String>,
226 key_columns: Vec<String>,
227 ) -> Result<Self, String> {
228 let name = name.into();
229 if name.trim().is_empty() {
230 return Err("index name must not be empty".to_string());
231 }
232 if key_columns.is_empty() {
233 return Err("key_columns must not be empty".to_string());
234 }
235 Ok(Self {
236 name,
237 key_columns,
238 cover_columns: Vec::new(),
239 layout: IndexLayout::Lexicographic,
240 })
241 }
242
243 pub fn z_order(name: impl Into<String>, key_columns: Vec<String>) -> Result<Self, String> {
244 Self::lexicographic(name, key_columns).map(|spec| spec.with_layout(IndexLayout::ZOrder))
245 }
246
247 pub fn with_cover_columns(mut self, cover_columns: Vec<String>) -> Self {
248 self.cover_columns = cover_columns;
249 self
250 }
251
252 pub fn with_layout(mut self, layout: IndexLayout) -> Self {
253 self.layout = layout;
254 self
255 }
256
257 pub fn name(&self) -> &str {
258 &self.name
259 }
260
261 pub fn key_columns(&self) -> &[String] {
262 &self.key_columns
263 }
264
265 pub fn cover_columns(&self) -> &[String] {
266 &self.cover_columns
267 }
268
269 pub fn layout(&self) -> &IndexLayout {
270 &self.layout
271 }
272}
273
274pub fn default_orders_index_specs() -> Vec<IndexSpec> {
275 vec![IndexSpec::lexicographic(
276 "region_customer",
277 vec!["region".to_string(), "customer_id".to_string()],
278 )
279 .expect("default orders index must be valid")]
280}
281
282#[derive(Debug, Clone)]
283pub(crate) struct KvTableConfig {
284 pub(crate) table_prefix: u8,
285 pub(crate) columns: Vec<TableColumnConfig>,
286 pub(crate) primary_key_columns: Vec<String>,
287 pub(crate) index_specs: Vec<IndexSpec>,
288}
289
290impl KvTableConfig {
291 pub(crate) fn new(
292 table_prefix: u8,
293 columns: Vec<TableColumnConfig>,
294 primary_key_columns: Vec<String>,
295 index_specs: Vec<IndexSpec>,
296 ) -> Result<Self, String> {
297 if usize::from(table_prefix) >= MAX_TABLES {
298 return Err(format!(
299 "table prefix {table_prefix} exceeds max {} for key layout",
300 MAX_TABLES - 1
301 ));
302 }
303 if columns.is_empty() {
304 return Err("table config requires at least one column".to_string());
305 }
306 if primary_key_columns.is_empty() {
307 return Err("primary key must have at least one column".to_string());
308 }
309
310 let mut seen = HashSet::new();
311 let mut col_kinds = HashMap::new();
312 for col in &columns {
313 if col.name.trim().is_empty() {
314 return Err("column name must not be empty".to_string());
315 }
316 if !seen.insert(col.name.clone()) {
317 return Err(format!("duplicate column '{}'", col.name));
318 }
319 let kind = ColumnKind::from_data_type(&col.data_type)?;
320 col_kinds.insert(col.name.clone(), kind);
321 }
322
323 let mut total_pk_width = 0usize;
324 for pk_col in &primary_key_columns {
325 let kind = col_kinds
326 .get(pk_col)
327 .ok_or_else(|| format!("primary key column '{pk_col}' not found"))?;
328 match kind {
329 ColumnKind::Int64
330 | ColumnKind::UInt64
331 | ColumnKind::Utf8
332 | ColumnKind::FixedSizeBinary(_) => {}
333 _ => {
334 return Err(format!(
335 "primary key column '{pk_col}' must be Int64, UInt64, Utf8, or FixedSizeBinary"
336 ));
337 }
338 }
339 total_pk_width += kind.key_width();
340 }
341 if total_pk_width > primary_key_prefix(table_prefix)?.max_payload_len() {
342 return Err(format!(
343 "composite primary key is too wide ({total_pk_width} bytes) for key payload"
344 ));
345 }
346
347 Ok(Self {
348 table_prefix,
349 columns,
350 primary_key_columns,
351 index_specs,
352 })
353 }
354
355 pub(crate) fn to_schema(&self) -> SchemaRef {
356 Arc::new(Schema::new(
357 self.columns
358 .iter()
359 .map(|col| {
360 let dt = match &col.data_type {
361 DataType::Timestamp(_, tz) => {
362 DataType::Timestamp(TimeUnit::Microsecond, tz.clone())
363 }
364 DataType::LargeList(field) => DataType::List(field.clone()),
365 other => other.clone(),
366 };
367 Field::new(&col.name, dt, col.nullable)
368 })
369 .collect::<Vec<_>>(),
370 ))
371 }
372}
373
374#[derive(Debug, Clone)]
375pub(crate) struct ResolvedColumn {
376 pub(crate) name: String,
377 pub(crate) kind: ColumnKind,
378 pub(crate) nullable: bool,
379}
380
381#[derive(Debug, Clone)]
382pub(crate) struct ResolvedIndexSpec {
383 pub(crate) id: u8,
384 pub(crate) prefix: Prefix,
385 pub(crate) name: String,
386 pub(crate) layout: IndexLayout,
387 pub(crate) key_columns: Vec<usize>,
388 pub(crate) value_column_mask: Vec<bool>,
389 pub(crate) key_columns_width: usize,
390}
391
392#[derive(Debug, Clone)]
393pub(crate) struct TableModel {
394 pub(crate) table_prefix: u8,
395 pub(crate) primary_key_prefix: Prefix,
396 pub(crate) schema: SchemaRef,
397 pub(crate) columns: Vec<ResolvedColumn>,
398 pub(crate) columns_by_name: HashMap<String, usize>,
399 pub(crate) primary_key_indices: Vec<usize>,
400 pub(crate) primary_key_kinds: Vec<ColumnKind>,
401 pub(crate) primary_key_width: usize,
402}
403
404impl TableModel {
405 pub(crate) fn from_config(config: &KvTableConfig) -> Result<Self, String> {
406 let schema = config.to_schema();
407 let mut columns = Vec::with_capacity(config.columns.len());
408 let mut columns_by_name = HashMap::with_capacity(config.columns.len());
409
410 for (idx, col) in config.columns.iter().enumerate() {
411 let kind = ColumnKind::from_data_type(&col.data_type)?;
412 columns.push(ResolvedColumn {
413 name: col.name.clone(),
414 kind,
415 nullable: col.nullable,
416 });
417 columns_by_name.insert(col.name.clone(), idx);
418 }
419
420 let mut primary_key_indices = Vec::with_capacity(config.primary_key_columns.len());
421 let mut primary_key_kinds = Vec::with_capacity(config.primary_key_columns.len());
422 let mut primary_key_width = 0usize;
423 for pk_col in &config.primary_key_columns {
424 let idx = *columns_by_name
425 .get(pk_col)
426 .ok_or_else(|| format!("primary key column '{pk_col}' not found"))?;
427 let kind = columns[idx].kind;
428 primary_key_indices.push(idx);
429 primary_key_kinds.push(kind);
430 primary_key_width += kind.key_width();
431 }
432
433 Ok(Self {
434 table_prefix: config.table_prefix,
435 primary_key_prefix: primary_key_prefix(config.table_prefix)?,
436 schema,
437 columns,
438 columns_by_name,
439 primary_key_indices,
440 primary_key_kinds,
441 primary_key_width,
442 })
443 }
444
445 pub(crate) fn is_pk_column(&self, col_idx: usize) -> bool {
447 self.primary_key_indices.contains(&col_idx)
448 }
449
450 pub(crate) fn pk_position(&self, col_idx: usize) -> Option<usize> {
451 self.primary_key_indices
452 .iter()
453 .position(|&idx| idx == col_idx)
454 }
455
456 pub(crate) fn resolve_index_specs(
457 &self,
458 specs: &[IndexSpec],
459 ) -> Result<Vec<ResolvedIndexSpec>, String> {
460 let mut out = Vec::with_capacity(specs.len());
461 let mut names = HashSet::new();
462
463 for (idx, spec) in specs.iter().enumerate() {
464 if !names.insert(spec.name.clone()) {
465 return Err(format!("duplicate index name '{}'", spec.name));
466 }
467
468 let id = u8::try_from(idx + 1).map_err(|_| {
469 format!("too many index specs for key layout (max {MAX_INDEX_SPECS})")
470 })?;
471 if usize::from(id) > MAX_INDEX_SPECS {
472 return Err(format!(
473 "too many index specs for key layout (max {MAX_INDEX_SPECS})"
474 ));
475 }
476 let mut key_columns = Vec::with_capacity(spec.key_columns.len());
477 let mut key_columns_width = 0usize;
478 let mut value_column_mask = vec![false; self.columns.len()];
479 for col_name in &spec.key_columns {
480 let Some(col_idx) = self.columns_by_name.get(col_name).copied() else {
481 return Err(format!(
482 "index '{}' references unknown column '{}'",
483 spec.name, col_name
484 ));
485 };
486 if !self.columns[col_idx].kind.indexable() {
487 return Err(format!(
488 "index '{}' references non-indexable column '{}'",
489 spec.name, col_name
490 ));
491 }
492 if self.columns[col_idx].nullable {
493 return Err(format!(
494 "index '{}' references nullable column '{}'; \
495 nullable columns cannot be used in index keys",
496 spec.name, col_name
497 ));
498 }
499 if spec.layout == IndexLayout::ZOrder
504 && self.columns[col_idx].kind.fixed_key_width().is_none()
505 {
506 return Err(format!(
507 "index '{}' z-order key column '{}' must have a \
508 fixed-width kind; variable-width kinds (e.g. Utf8) \
509 cannot be used in z-order index keys",
510 spec.name, col_name
511 ));
512 }
513 key_columns.push(col_idx);
514 key_columns_width += self.columns[col_idx].kind.key_width();
515 if !self.is_pk_column(col_idx) {
516 value_column_mask[col_idx] = true;
517 }
518 }
519
520 for col_name in &spec.cover_columns {
521 let Some(col_idx) = self.columns_by_name.get(col_name).copied() else {
522 return Err(format!(
523 "index '{}' cover list references unknown column '{}'",
524 spec.name, col_name
525 ));
526 };
527 if self.is_pk_column(col_idx) {
528 return Err(format!(
529 "index '{}' cover column '{}' is a primary key column; \
530 PK columns are always available from key bytes",
531 spec.name, col_name
532 ));
533 }
534 if !value_column_mask[col_idx] {
535 value_column_mask[col_idx] = true;
536 }
537 }
538 let prefix = secondary_index_prefix(self.table_prefix, id)?;
539 if key_columns_width + self.primary_key_width > prefix.max_payload_len() {
540 return Err(format!(
541 "index '{}' key layout too wide for key payload",
542 spec.name
543 ));
544 }
545
546 out.push(ResolvedIndexSpec {
547 id,
548 prefix,
549 name: spec.name.clone(),
550 layout: spec.layout,
551 key_columns,
552 value_column_mask,
553 key_columns_width,
554 });
555 }
556
557 Ok(out)
558 }
559
560 pub(crate) fn column(&self, index: usize) -> &ResolvedColumn {
561 &self.columns[index]
562 }
563}
564
565#[derive(Debug, Clone)]
566pub enum CellValue {
567 Null,
568 Int64(i64),
569 UInt64(u64),
570 Float64(f64),
571 Boolean(bool),
572 Date32(i32),
573 Date64(i64),
574 Timestamp(i64),
575 Decimal128(i128),
576 Decimal256(i256),
577 Utf8(String),
578 FixedBinary(Vec<u8>),
579 Binary(Vec<u8>),
580 List(Vec<CellValue>),
581}
582
583#[derive(Debug, Clone)]
584pub(crate) struct KvRow {
585 pub(crate) values: Vec<CellValue>,
586}
587
588impl KvRow {
589 pub(crate) fn primary_key_values(&self, model: &TableModel) -> Vec<&CellValue> {
590 model
591 .primary_key_indices
592 .iter()
593 .map(|&idx| &self.values[idx])
594 .collect()
595 }
596
597 pub(crate) fn value_at(&self, idx: usize) -> &CellValue {
598 &self.values[idx]
599 }
600}
601
602#[derive(Debug, Clone, Default)]
603pub(crate) struct DecodedIndexEntry {
604 pub(crate) primary_key: Key,
605 pub(crate) primary_key_values: Vec<CellValue>,
606 pub(crate) values: HashMap<usize, CellValue>,
607}
608
609#[derive(Debug, Clone, PartialEq)]
610pub(crate) struct KeyRange {
611 pub(crate) start: Key,
612 pub(crate) end: Key,
613}
614
615#[derive(Debug, Clone)]
616pub(crate) struct IndexPlan {
617 pub(crate) spec_idx: usize,
618 pub(crate) ranges: Vec<KeyRange>,
620 pub(crate) constrained_prefix_len: usize,
621 pub(crate) constrained_column_count: usize,
622}
623
624#[derive(Debug, Clone)]
625pub(crate) struct KvTable {
626 pub(crate) client: PrefixedStoreClient,
627 pub(crate) model: Arc<TableModel>,
628 pub(crate) index_specs: Arc<Vec<ResolvedIndexSpec>>,
629}
630
631impl KvTable {
632 pub(crate) fn new(client: PrefixedStoreClient, config: KvTableConfig) -> Result<Self, String> {
633 let model = Arc::new(TableModel::from_config(&config)?);
634 let index_specs = Arc::new(model.resolve_index_specs(&config.index_specs)?);
635 Ok(Self {
636 client,
637 model,
638 index_specs,
639 })
640 }
641}