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