Skip to main content

cobble_table/
table.rs

1use crate::codec::KeyCodec;
2use crate::metadata::TableMetadata;
3use crate::{BucketHash, FieldId, LogicalType, Result, TableError, TableSchema, Value, ValueCodec};
4use bytes::Bytes;
5use cobble::{
6    ColumnFamilyOptions, Config, Db, DbIterator, ReadOnlyDb, ReadOptions, ScanOptions, Schema,
7    ShardSnapshotMetadata, ShardSnapshotRef, WriteOptions,
8};
9use std::collections::HashMap;
10use std::sync::{Arc, mpsc};
11
12pub(crate) struct CompiledTable {
13    schema: Arc<TableSchema>,
14    column_family_options: ColumnFamilyOptions,
15    key_positions: Vec<usize>,
16    key_types: Vec<LogicalType>,
17    bucket_key_fields: usize,
18    value_positions: Vec<usize>,
19    value_types: Vec<LogicalType>,
20    physical_columns: usize,
21    bucket_hash: BucketHash,
22}
23
24struct TableKeyData {
25    values: Vec<Value>,
26    bucket: u16,
27    encoded: Vec<u8>,
28}
29
30/// A validated and encoded primary key for a table.
31///
32/// Cloning a key is cheap and shares its encoded bytes and typed values.
33#[derive(Clone)]
34pub struct TableKey {
35    inner: Arc<TableKeyData>,
36}
37
38impl TableKey {
39    /// Return the bucket selected for this key.
40    #[must_use]
41    pub fn bucket(&self) -> u16 {
42        self.inner.bucket
43    }
44}
45
46/// Incrementally builds one table primary key in schema order.
47pub struct TableKeyBuilder {
48    compiled: Arc<CompiledTable>,
49    values: Vec<Value>,
50}
51
52enum ProjectedFieldSource {
53    Key(usize),
54    Value {
55        /// Position in the compact column list returned by the projection read options.
56        projected_column: usize,
57        /// Position in the table's physical value columns, used to select its logical type.
58        physical_column: usize,
59    },
60}
61
62struct ProjectionPlan {
63    sources: Vec<ProjectedFieldSource>,
64    has_key_fields: bool,
65}
66
67#[derive(Clone)]
68pub(crate) enum ReadBackend {
69    Writable(Arc<Db>),
70    Shard(Arc<ReadOnlyDb>),
71    Global(Arc<crate::runtime::GlobalReaderState>),
72}
73
74impl ReadBackend {
75    pub(crate) fn get_with_options(
76        &self,
77        bucket: u16,
78        key: &[u8],
79        options: &ReadOptions,
80    ) -> Result<Option<Vec<Option<Bytes>>>> {
81        match self {
82            Self::Writable(db) => Ok(db.get_with_options(bucket, key, options)?),
83            Self::Shard(db) => Ok(db.get_with_options(bucket, key, options)?),
84            Self::Global(state) => state.get(bucket, key, options),
85        }
86    }
87
88    pub(crate) fn multi_get_with_options(
89        &self,
90        keys: &[(u16, &[u8])],
91        options: &ReadOptions,
92    ) -> Result<Vec<Option<Vec<Option<Bytes>>>>> {
93        match self {
94            Self::Writable(db) => Ok(db.multi_get_with_options(keys, options)?),
95            Self::Shard(db) => Ok(db.multi_get_with_options(keys, options)?),
96            Self::Global(state) => state.multi_get(keys, options),
97        }
98    }
99
100    pub(crate) fn scan_with_options_bounds(
101        &self,
102        bucket: u16,
103        start: Option<&[u8]>,
104        end: Option<&[u8]>,
105        options: &ScanOptions,
106    ) -> Result<DbIterator> {
107        match self {
108            Self::Writable(db) => Ok(db.scan_with_options_bounds(bucket, start, end, options)?),
109            Self::Shard(db) => Ok(db.scan_with_options_bounds(bucket, start, end, options)?),
110            Self::Global(state) => state.scan(bucket, start, end, options),
111        }
112    }
113}
114
115/// A reusable typed projection over one table or fixed snapshot table.
116pub struct TableProjection {
117    backend: ReadBackend,
118    compiled: Arc<CompiledTable>,
119    plan: Arc<ProjectionPlan>,
120    read_options: ReadOptions,
121    scan_options: ScanOptions,
122}
123
124impl TableKeyBuilder {
125    /// Append the next primary-key field.
126    pub fn push(&mut self, value: Value) -> &mut Self {
127        self.values.push(value);
128        self
129    }
130
131    /// Validate and encode the complete primary key.
132    pub fn build(self) -> Result<TableKey> {
133        let mut encoded = Vec::new();
134        let prefix_end = KeyCodec::encode_row_with_prefix_validated(
135            &self.compiled.key_types,
136            &self.values,
137            self.compiled.bucket_key_fields,
138            &mut encoded,
139        )?;
140        let bucket = self.compiled.bucket_hash.bucket(&encoded[..prefix_end]);
141        Ok(TableKey {
142            inner: Arc::new(TableKeyData {
143                values: self.values,
144                bucket,
145                encoded,
146            }),
147        })
148    }
149}
150
151/// Typed access to one table-backed Cobble column family.
152pub struct Table {
153    db: Arc<Db>,
154    read_backend: ReadBackend,
155    name: String,
156    compiled: Arc<CompiledTable>,
157    read_options: ReadOptions,
158    scan_options: ScanOptions,
159    write_options: WriteOptions,
160}
161
162/// Typed read-only access to one table in a fixed shard snapshot.
163pub struct ReadOnlyTable {
164    typed: TypedRead,
165}
166
167pub(crate) struct TypedRead {
168    name: String,
169    compiled: Arc<CompiledTable>,
170    read_backend: ReadBackend,
171    read_options: ReadOptions,
172    scan_options: ScanOptions,
173}
174
175impl Table {
176    /// Create a table or reopen it when its persisted schema is identical.
177    pub fn create(db: Arc<Db>, name: impl Into<String>, schema: TableSchema) -> Result<Self> {
178        let name = validate_name(name.into())?;
179        let metadata = ensure_table_schema(db.as_ref(), &name, schema)?;
180        Self::from_metadata(db, name, metadata)
181    }
182
183    /// Open a table from metadata stored in its column-family options.
184    pub fn open(db: Arc<Db>, name: impl Into<String>) -> Result<Self> {
185        let name = validate_name(name.into())?;
186        let current = db.current_schema();
187        let metadata = load_table_metadata(&current, &name)?;
188        Self::from_metadata(db, name, metadata)
189    }
190
191    /// Return the persisted semantic schema of this table.
192    pub fn schema(&self) -> &TableSchema {
193        &self.compiled.schema
194    }
195
196    /// Return metrics for this table's backing database shard.
197    ///
198    /// Metrics are collected at database scope and include all column families; they are not
199    /// filtered to this table's physical column family.
200    pub fn metrics(&self) -> Vec<cobble::MetricSample> {
201        self.db.metrics()
202    }
203
204    /// Start building one primary key in schema order.
205    pub fn key_builder(&self) -> TableKeyBuilder {
206        TableKeyBuilder {
207            compiled: Arc::clone(&self.compiled),
208            values: Vec::with_capacity(self.compiled.key_positions.len()),
209        }
210    }
211
212    /// Compile a reusable read projection from top-level field names.
213    pub fn project_by_names<S: AsRef<str>>(&self, field_names: &[S]) -> Result<TableProjection> {
214        build_projection(
215            self.read_backend.clone(),
216            &self.name,
217            Arc::clone(&self.compiled),
218            field_names,
219        )
220    }
221
222    /// Write one full row in schema field order.
223    pub fn put(&self, row: &[Value]) -> Result<()> {
224        self.put_bound(row, &self.write_options)
225    }
226
227    /// Write one full row with caller options safely rebound to this table.
228    pub fn put_with_options(&self, row: &[Value], options: &WriteOptions) -> Result<()> {
229        let bound = self.rebound_write_options(options);
230        self.put_bound(row, &bound)
231    }
232
233    /// Delete one complete row.
234    pub fn delete(&self, key: &TableKey) -> Result<()> {
235        self.db.delete_row_with_options(
236            key.inner.bucket,
237            key.inner.encoded.as_slice(),
238            &self.write_options,
239        )?;
240        Ok(())
241    }
242
243    /// Delete complete rows in one batch without cloning encoded keys.
244    pub fn delete_batch(&self, keys: &[TableKey]) -> Result<()> {
245        if keys.is_empty() {
246            return Ok(());
247        }
248        let requests = keys
249            .iter()
250            .map(|key| (key.inner.bucket, key.inner.encoded.as_slice()))
251            .collect::<Vec<_>>();
252        self.db
253            .delete_rows_with_options(&requests, &self.write_options)?;
254        Ok(())
255    }
256
257    fn put_bound(&self, row: &[Value], options: &WriteOptions) -> Result<()> {
258        let (bucket, key, values) = encode_table_row(&self.compiled, row)?;
259        self.db
260            .put_columns_with_options(bucket, key, &values, options)?;
261        Ok(())
262    }
263
264    pub(crate) fn rebound_write_options(&self, options: &WriteOptions) -> WriteOptions {
265        let mut bound = self.write_options.clone();
266        bound.ttl_seconds = options.ttl_seconds;
267        bound.await_durable = options.await_durable;
268        bound
269    }
270
271    /// Read one row by primary key.
272    pub fn get(&self, key: &TableKey) -> Result<Option<Vec<Value>>> {
273        self.db
274            .get_with_options(key.inner.bucket, &key.inner.encoded, &self.read_options)?
275            .map(|columns| {
276                assemble_row_from_key_values(&self.compiled, &key.inner.values, &columns)
277            })
278            .transpose()
279    }
280
281    /// Read many primary keys with one core multi-get, preserving order and duplicates.
282    pub fn multi_get(&self, keys: &[TableKey]) -> Result<Vec<Option<Vec<Value>>>> {
283        let mut requests = Vec::with_capacity(keys.len());
284        for key in keys {
285            requests.push((key.inner.bucket, key.inner.encoded.as_slice()));
286        }
287        self.db
288            .multi_get_with_options(&requests, &self.read_options)?
289            .into_iter()
290            .zip(keys)
291            .map(|(columns, key)| {
292                columns
293                    .map(|columns| {
294                        assemble_row_from_key_values(&self.compiled, &key.inner.values, &columns)
295                    })
296                    .transpose()
297            })
298            .collect()
299    }
300
301    /// Scan all rows in one bucket.
302    pub fn scan(&self, bucket: u16) -> Result<TableScan> {
303        self.scan_bounds(bucket, None, None)
304    }
305
306    /// Scan one bucket from an inclusive primary-key bound to an exclusive bound.
307    pub fn scan_bounds(
308        &self,
309        bucket: u16,
310        start_key_inclusive: Option<&TableKey>,
311        end_key_exclusive: Option<&TableKey>,
312    ) -> Result<TableScan> {
313        validate_bound(bucket, start_key_inclusive)?;
314        validate_bound(bucket, end_key_exclusive)?;
315        Ok(TableScan {
316            inner: self.read_backend.scan_with_options_bounds(
317                bucket,
318                start_key_inclusive.map(|key| key.inner.encoded.as_slice()),
319                end_key_exclusive.map(|key| key.inner.encoded.as_slice()),
320                &self.scan_options,
321            )?,
322            _read_backend: self.read_backend.clone(),
323            compiled: Arc::clone(&self.compiled),
324        })
325    }
326
327    /// Start an asynchronous shard snapshot, returning its id immediately.
328    pub fn snapshot(&self) -> Result<u64> {
329        Ok(self.db.snapshot()?)
330    }
331
332    /// Receive either the completed shard input or its publication error.
333    pub fn snapshot_with_callback<F>(&self, callback: F) -> Result<u64>
334    where
335        F: Fn(cobble::Result<ShardSnapshotMetadata>) + Send + Sync + 'static,
336    {
337        Ok(self.db.snapshot_with_callback(callback)?)
338    }
339
340    /// Create a snapshot and wait for its callback, without polling.
341    pub fn snapshot_and_wait(&self) -> Result<ShardSnapshotMetadata> {
342        let (sender, receiver) = mpsc::sync_channel(1);
343        self.snapshot_with_callback(move |result| {
344            let _ = sender.send(result);
345        })?;
346        receiver
347            .recv()
348            .map_err(|err| TableError::internal(format!("snapshot callback disconnected: {err}")))?
349            .map_err(Into::into)
350    }
351
352    /// Return complete metadata for a completed shard snapshot.
353    pub fn shard_snapshot_metadata(&self, snapshot_id: u64) -> Result<ShardSnapshotMetadata> {
354        Ok(self.db.shard_snapshot_metadata(snapshot_id)?)
355    }
356
357    /// Refresh this writable handle from the local database schema.
358    ///
359    /// This does not consult a catalog or track a moving catalog version. Existing projections
360    /// remain bound to the layout they were compiled with and must be rebuilt after a change.
361    pub fn refresh_schema(&mut self) -> Result<bool> {
362        let metadata = load_table_metadata(&self.db.current_schema(), &self.name)?;
363        if self.compiled.column_family_options.metadata.as_ref() == Some(&metadata.to_value()?) {
364            return Ok(false);
365        }
366        self.compiled = compile_table(metadata, self.db.total_buckets())?;
367        (self.read_options, self.scan_options, self.write_options) =
368            build_bound_options(&self.name, &self.compiled);
369        Ok(true)
370    }
371
372    pub(crate) fn db(&self) -> &Arc<Db> {
373        &self.db
374    }
375
376    pub(crate) fn name(&self) -> &str {
377        &self.name
378    }
379
380    pub(crate) fn from_metadata(
381        db: Arc<Db>,
382        name: String,
383        metadata: TableMetadata,
384    ) -> Result<Self> {
385        let compiled = compile_table(metadata, db.total_buckets())?;
386        let (read_options, scan_options, write_options) = build_bound_options(&name, &compiled);
387        Ok(Self {
388            read_backend: ReadBackend::Writable(Arc::clone(&db)),
389            db,
390            name,
391            compiled,
392            read_options,
393            scan_options,
394            write_options,
395        })
396    }
397
398    #[cfg(feature = "ffi")]
399    pub(crate) fn ffi_schema_binding(&self) -> crate::ffi::TableSchemaBinding {
400        crate::ffi::TableSchemaBinding {
401            options: self.compiled.column_family_options.clone(),
402            physical_columns: self.compiled.physical_columns,
403        }
404    }
405
406    #[cfg(feature = "ffi")]
407    pub(crate) fn ffi_raw_access(&self) -> crate::ffi::RawTableAccess {
408        crate::ffi::RawTableAccess::new(
409            self.read_backend.clone(),
410            self.read_options.clone(),
411            self.scan_options.clone(),
412        )
413    }
414
415    #[cfg(feature = "ffi")]
416    pub(crate) fn ffi_write_options(&self) -> &WriteOptions {
417        &self.write_options
418    }
419
420    #[cfg(feature = "ffi")]
421    pub(crate) fn ffi_raw_projection(
422        &self,
423        field_names: &[String],
424    ) -> Result<crate::ffi::RawTableAccess> {
425        Ok(self.project_by_names(field_names)?.ffi_into_raw_access())
426    }
427}
428
429fn build_bound_options(
430    name: &str,
431    compiled: &CompiledTable,
432) -> (ReadOptions, ScanOptions, WriteOptions) {
433    (
434        ReadOptions::default()
435            .with_column_family(name)
436            .bound_to_column_family_schema(
437                compiled.column_family_options.clone(),
438                compiled.physical_columns,
439            ),
440        ScanOptions::default()
441            .with_column_family(name)
442            .bound_to_column_family_schema(
443                compiled.column_family_options.clone(),
444                compiled.physical_columns,
445            ),
446        WriteOptions::with_column_family(name).bound_to_column_family_schema(
447            compiled.column_family_options.clone(),
448            compiled.physical_columns,
449        ),
450    )
451}
452
453fn ensure_table_schema(db: &Db, name: &str, schema: TableSchema) -> Result<TableMetadata> {
454    let metadata = TableMetadata::compile(schema)?;
455    let expected_columns = metadata.layout.value_columns.len().max(1);
456    let current = db.current_schema();
457    if let Some(id) = current.column_family_ids().get(name).copied() {
458        let existing = load_metadata(&current.column_family_options_in_family(id))?;
459        if existing != metadata || current.num_columns_in_family(id) != Some(expected_columns) {
460            return Err(TableError::InvalidSchema(format!(
461                "column family '{name}' is not this table"
462            )));
463        }
464    } else {
465        let mut builder = db.update_schema();
466        builder.ensure_column_family_exists(name.to_string())?;
467        for column in 0..expected_columns {
468            builder.add_column(column, None, None, Some(name.to_string()))?;
469        }
470        builder.set_column_family_options(
471            Some(name.to_string()),
472            ColumnFamilyOptions {
473                metadata: Some(metadata.to_value()?),
474                ..ColumnFamilyOptions::default()
475            },
476        )?;
477        builder.commit();
478    }
479    Ok(metadata)
480}
481
482impl ReadOnlyTable {
483    /// Open a table from metadata stored in this snapshot's schema.
484    pub fn open(db: Arc<ReadOnlyDb>, name: impl Into<String>) -> Result<Self> {
485        let name = validate_name(name.into())?;
486        let current = db.current_schema();
487        let metadata = load_table_metadata(&current, &name)?;
488        Self::from_shard_metadata(db, name, metadata)
489    }
490
491    #[cfg(feature = "ffi")]
492    pub(crate) fn ffi_schema_binding(&self) -> crate::ffi::TableSchemaBinding {
493        crate::ffi::TableSchemaBinding {
494            options: self.typed.compiled.column_family_options.clone(),
495            physical_columns: self.typed.compiled.physical_columns,
496        }
497    }
498
499    #[cfg(feature = "ffi")]
500    pub(crate) fn ffi_shard_db(&self) -> Arc<ReadOnlyDb> {
501        match &self.typed.read_backend {
502            ReadBackend::Shard(db) => Arc::clone(db),
503            ReadBackend::Writable(_) | ReadBackend::Global(_) => {
504                unreachable!("read-only table must use a shard database")
505            }
506        }
507    }
508
509    pub(crate) fn from_shard_metadata(
510        db: Arc<ReadOnlyDb>,
511        name: String,
512        metadata: TableMetadata,
513    ) -> Result<Self> {
514        Ok(Self {
515            typed: TypedRead::from_shard_metadata(db, name, metadata)?,
516        })
517    }
518
519    /// Return the persisted semantic schema of this table.
520    pub fn schema(&self) -> &TableSchema {
521        self.typed.schema()
522    }
523
524    /// Start building one primary key in schema order.
525    pub fn key_builder(&self) -> TableKeyBuilder {
526        self.typed.key_builder()
527    }
528
529    /// Compile a reusable read projection from top-level field names.
530    pub fn project_by_names<S: AsRef<str>>(&self, field_names: &[S]) -> Result<TableProjection> {
531        self.typed.project_by_names(field_names)
532    }
533
534    /// Read one row by primary key.
535    pub fn get(&self, key: &TableKey) -> Result<Option<Vec<Value>>> {
536        self.typed.get(key)
537    }
538
539    /// Read many primary keys while preserving order and duplicates.
540    pub fn multi_get(&self, keys: &[TableKey]) -> Result<Vec<Option<Vec<Value>>>> {
541        self.typed.multi_get(keys)
542    }
543
544    /// Scan all rows in one bucket.
545    pub fn scan(&self, bucket: u16) -> Result<TableScan> {
546        self.typed.scan(bucket)
547    }
548
549    /// Scan one bucket from an inclusive primary-key bound to an exclusive bound.
550    pub fn scan_bounds(
551        &self,
552        bucket: u16,
553        start_key_inclusive: Option<&TableKey>,
554        end_key_exclusive: Option<&TableKey>,
555    ) -> Result<TableScan> {
556        self.typed
557            .scan_bounds(bucket, start_key_inclusive, end_key_exclusive)
558    }
559}
560
561impl TypedRead {
562    pub(crate) fn from_shard_metadata(
563        db: Arc<ReadOnlyDb>,
564        name: String,
565        metadata: TableMetadata,
566    ) -> Result<Self> {
567        let compiled = compile_table(metadata, db.total_buckets())?;
568        Ok(Self::new(name, compiled, ReadBackend::Shard(db)))
569    }
570
571    pub(crate) fn from_global_metadata(
572        state: Arc<crate::runtime::GlobalReaderState>,
573        name: String,
574        metadata: TableMetadata,
575    ) -> Result<Self> {
576        let total_buckets = state.total_buckets();
577        let compiled = compile_table(metadata, total_buckets)?;
578        Ok(Self::new(name, compiled, ReadBackend::Global(state)))
579    }
580
581    fn new(name: String, compiled: Arc<CompiledTable>, read_backend: ReadBackend) -> Self {
582        Self {
583            name: name.clone(),
584            compiled,
585            read_backend,
586            read_options: ReadOptions::default().with_column_family(name.clone()),
587            scan_options: ScanOptions::default().with_column_family(name),
588        }
589    }
590
591    pub(crate) fn global_state(&self) -> Option<&Arc<crate::runtime::GlobalReaderState>> {
592        match &self.read_backend {
593            ReadBackend::Global(state) => Some(state),
594            ReadBackend::Writable(_) | ReadBackend::Shard(_) => None,
595        }
596    }
597
598    /// Return the persisted semantic schema of this table.
599    pub fn schema(&self) -> &TableSchema {
600        &self.compiled.schema
601    }
602
603    pub(crate) fn schema_arc(&self) -> Arc<TableSchema> {
604        Arc::clone(&self.compiled.schema)
605    }
606
607    #[cfg(feature = "ffi")]
608    pub(crate) fn ffi_schema_binding(&self) -> crate::ffi::TableSchemaBinding {
609        crate::ffi::TableSchemaBinding {
610            options: self.compiled.column_family_options.clone(),
611            physical_columns: self.compiled.physical_columns,
612        }
613    }
614
615    /// Start building one primary key in schema order.
616    pub fn key_builder(&self) -> TableKeyBuilder {
617        TableKeyBuilder {
618            compiled: Arc::clone(&self.compiled),
619            values: Vec::with_capacity(self.compiled.key_positions.len()),
620        }
621    }
622
623    /// Compile a reusable read projection from top-level field names.
624    pub fn project_by_names<S: AsRef<str>>(&self, field_names: &[S]) -> Result<TableProjection> {
625        build_projection(
626            self.read_backend.clone(),
627            &self.name,
628            Arc::clone(&self.compiled),
629            field_names,
630        )
631    }
632
633    /// Read one row by primary key.
634    pub fn get(&self, key: &TableKey) -> Result<Option<Vec<Value>>> {
635        self.read_backend
636            .get_with_options(key.inner.bucket, &key.inner.encoded, &self.read_options)?
637            .map(|columns| {
638                assemble_row_from_key_values(&self.compiled, &key.inner.values, &columns)
639            })
640            .transpose()
641    }
642
643    /// Read many primary keys while preserving order and duplicates.
644    pub fn multi_get(&self, keys: &[TableKey]) -> Result<Vec<Option<Vec<Value>>>> {
645        let requests = keys
646            .iter()
647            .map(|key| (key.inner.bucket, key.inner.encoded.as_slice()))
648            .collect::<Vec<_>>();
649        self.read_backend
650            .multi_get_with_options(&requests, &self.read_options)?
651            .into_iter()
652            .zip(keys)
653            .map(|(columns, key)| {
654                columns
655                    .map(|columns| {
656                        assemble_row_from_key_values(&self.compiled, &key.inner.values, &columns)
657                    })
658                    .transpose()
659            })
660            .collect()
661    }
662
663    /// Scan all rows in one bucket.
664    pub fn scan(&self, bucket: u16) -> Result<TableScan> {
665        self.scan_bounds(bucket, None, None)
666    }
667
668    /// Scan one bucket from an inclusive primary-key bound to an exclusive bound.
669    pub fn scan_bounds(
670        &self,
671        bucket: u16,
672        start_key_inclusive: Option<&TableKey>,
673        end_key_exclusive: Option<&TableKey>,
674    ) -> Result<TableScan> {
675        validate_bound(bucket, start_key_inclusive)?;
676        validate_bound(bucket, end_key_exclusive)?;
677        Ok(TableScan {
678            inner: self.read_backend.scan_with_options_bounds(
679                bucket,
680                start_key_inclusive.map(|key| key.inner.encoded.as_slice()),
681                end_key_exclusive.map(|key| key.inner.encoded.as_slice()),
682                &self.scan_options,
683            )?,
684            _read_backend: self.read_backend.clone(),
685            compiled: Arc::clone(&self.compiled),
686        })
687    }
688}
689
690impl TableProjection {
691    #[cfg(feature = "ffi")]
692    pub(crate) fn ffi_into_raw_access(self) -> crate::ffi::RawTableAccess {
693        crate::ffi::RawTableAccess::new(self.backend, self.read_options, self.scan_options)
694    }
695    /// Read one projected row.
696    pub fn get(&self, key: &TableKey) -> Result<Option<Vec<Value>>> {
697        self.backend
698            .get_with_options(key.inner.bucket, &key.inner.encoded, &self.read_options)?
699            .map(|columns| {
700                assemble_projected_row(
701                    &self.compiled,
702                    &self.plan,
703                    Some(&key.inner.values),
704                    &columns,
705                )
706            })
707            .transpose()
708    }
709
710    /// Read projected rows in input order, preserving duplicates and misses.
711    pub fn multi_get(&self, keys: &[TableKey]) -> Result<Vec<Option<Vec<Value>>>> {
712        let requests = keys
713            .iter()
714            .map(|key| (key.inner.bucket, key.inner.encoded.as_slice()))
715            .collect::<Vec<_>>();
716        self.backend
717            .multi_get_with_options(&requests, &self.read_options)?
718            .into_iter()
719            .zip(keys)
720            .map(|(columns, key)| {
721                columns
722                    .map(|columns| {
723                        assemble_projected_row(
724                            &self.compiled,
725                            &self.plan,
726                            Some(&key.inner.values),
727                            &columns,
728                        )
729                    })
730                    .transpose()
731            })
732            .collect()
733    }
734
735    /// Scan projected rows in one bucket.
736    pub fn scan(&self, bucket: u16) -> Result<ProjectedTableScan> {
737        self.scan_bounds(bucket, None, None)
738    }
739
740    /// Scan projected rows over cached primary-key bounds.
741    pub fn scan_bounds(
742        &self,
743        bucket: u16,
744        start_key_inclusive: Option<&TableKey>,
745        end_key_exclusive: Option<&TableKey>,
746    ) -> Result<ProjectedTableScan> {
747        validate_bound(bucket, start_key_inclusive)?;
748        validate_bound(bucket, end_key_exclusive)?;
749        Ok(ProjectedTableScan {
750            inner: self.backend.scan_with_options_bounds(
751                bucket,
752                start_key_inclusive.map(|key| key.inner.encoded.as_slice()),
753                end_key_exclusive.map(|key| key.inner.encoded.as_slice()),
754                &self.scan_options,
755            )?,
756            _read_backend: self.backend.clone(),
757            compiled: Arc::clone(&self.compiled),
758            plan: Arc::clone(&self.plan),
759        })
760    }
761}
762
763/// Iterator over typed rows from a bucket-scoped table scan.
764pub struct TableScan {
765    inner: DbIterator,
766    // `inner` drops first, releasing its owned access guard before this can
767    // release the backend that owns the underlying read route.
768    _read_backend: ReadBackend,
769    compiled: Arc<CompiledTable>,
770}
771
772/// Iterator over projected typed rows from a bucket-scoped scan.
773pub struct ProjectedTableScan {
774    inner: DbIterator,
775    _read_backend: ReadBackend,
776    compiled: Arc<CompiledTable>,
777    plan: Arc<ProjectionPlan>,
778}
779
780impl Iterator for ProjectedTableScan {
781    type Item = Result<Vec<Value>>;
782
783    fn next(&mut self) -> Option<Self::Item> {
784        self.inner.next().map(|row| {
785            let (key, columns) = row?;
786            let key_values = self
787                .plan
788                .has_key_fields
789                .then(|| KeyCodec::decode_row_validated(&self.compiled.key_types, &key))
790                .transpose()?;
791            assemble_projected_row(&self.compiled, &self.plan, key_values.as_deref(), &columns)
792        })
793    }
794}
795
796impl Iterator for TableScan {
797    type Item = Result<Vec<Value>>;
798
799    fn next(&mut self) -> Option<Self::Item> {
800        self.inner.next().map(|row| {
801            let (key, columns) = row?;
802            decode_table_scan_row(&self.compiled, &key, &columns)
803        })
804    }
805}
806
807pub(crate) fn decode_table_scan_row(
808    compiled: &CompiledTable,
809    key: &[u8],
810    columns: &[Option<Bytes>],
811) -> Result<Vec<Value>> {
812    let mut row = vec![Value::Null; compiled.schema.fields.len()];
813    KeyCodec::decode_row_into_positions_validated(
814        &compiled.key_types,
815        key,
816        &compiled.key_positions,
817        &mut row,
818    )?;
819    decode_value_columns(compiled, &mut row, columns)?;
820    Ok(row)
821}
822
823fn assemble_row_from_key_values(
824    compiled: &CompiledTable,
825    key_values: &[Value],
826    columns: &[Option<Bytes>],
827) -> Result<Vec<Value>> {
828    debug_assert_eq!(key_values.len(), compiled.key_positions.len());
829    let mut row = vec![Value::Null; compiled.schema.fields.len()];
830    for (position, value) in compiled.key_positions.iter().zip(key_values) {
831        row[*position] = value.clone();
832    }
833    decode_value_columns(compiled, &mut row, columns)?;
834    Ok(row)
835}
836
837fn encode_table_row(
838    compiled: &CompiledTable,
839    row: &[Value],
840) -> Result<(u16, Vec<u8>, Vec<Vec<u8>>)> {
841    if row.len() != compiled.schema.fields.len() {
842        return Err(TableError::codec("row field count does not match schema"));
843    }
844    let (encoded, prefix_end) = KeyCodec::encode_row_from_positions_validated(
845        &compiled.key_types,
846        row,
847        &compiled.key_positions,
848        compiled.bucket_key_fields,
849    )?;
850    let mut values = Vec::with_capacity(compiled.physical_columns);
851    for (position, logical_type) in compiled.value_positions.iter().zip(&compiled.value_types) {
852        values.push(ValueCodec::encode_validated(logical_type, &row[*position])?);
853    }
854    if values.is_empty() {
855        values.push(vec![1]);
856    }
857    Ok((
858        compiled.bucket_hash.bucket(&encoded[..prefix_end]),
859        encoded,
860        values,
861    ))
862}
863
864fn assemble_projected_row(
865    compiled: &CompiledTable,
866    plan: &ProjectionPlan,
867    key_values: Option<&[Value]>,
868    columns: &[Option<Bytes>],
869) -> Result<Vec<Value>> {
870    let mut row = Vec::with_capacity(plan.sources.len());
871    for source in &plan.sources {
872        match source {
873            ProjectedFieldSource::Key(key_index) => {
874                row.push(key_values.expect("projection decoded key fields")[*key_index].clone());
875            }
876            ProjectedFieldSource::Value {
877                projected_column,
878                physical_column,
879            } => {
880                let value = columns
881                    .get(*projected_column)
882                    .and_then(|value| value.as_ref())
883                    .ok_or_else(|| TableError::codec("table row is missing a value column"))?;
884                row.push(ValueCodec::decode_bytes_validated(
885                    &compiled.value_types[*physical_column],
886                    value.clone(),
887                )?);
888            }
889        }
890    }
891    Ok(row)
892}
893
894fn decode_value_columns(
895    compiled: &CompiledTable,
896    row: &mut [Value],
897    columns: &[Option<Bytes>],
898) -> Result<()> {
899    for (column, (logical_type, position)) in compiled
900        .value_types
901        .iter()
902        .zip(&compiled.value_positions)
903        .enumerate()
904    {
905        let value = columns
906            .get(column)
907            .and_then(|value| value.as_ref())
908            .ok_or_else(|| TableError::codec("table row is missing a value column"))?;
909        row[*position] = ValueCodec::decode_bytes_validated(logical_type, value.clone())?;
910    }
911    Ok(())
912}
913
914fn build_projection<S: AsRef<str>>(
915    backend: ReadBackend,
916    name: &str,
917    compiled: Arc<CompiledTable>,
918    field_names: &[S],
919) -> Result<TableProjection> {
920    let (plan, read_options, scan_options) =
921        build_projection_parts(name, Arc::clone(&compiled), field_names)?;
922    Ok(TableProjection {
923        backend,
924        compiled,
925        plan,
926        read_options,
927        scan_options,
928    })
929}
930
931fn build_projection_parts<S: AsRef<str>>(
932    name: &str,
933    compiled: Arc<CompiledTable>,
934    field_names: &[S],
935) -> Result<(Arc<ProjectionPlan>, ReadOptions, ScanOptions)> {
936    if field_names.is_empty() {
937        return Err(TableError::InvalidSchema(
938            "table projection must contain at least one field".to_string(),
939        ));
940    }
941    let mut seen = std::collections::HashSet::with_capacity(field_names.len());
942    let mut sources = Vec::with_capacity(field_names.len());
943    let mut physical_columns = Vec::new();
944    let mut has_key_fields = false;
945    for field_name in field_names {
946        let field_name = field_name.as_ref();
947        if !seen.insert(field_name) {
948            return Err(TableError::InvalidSchema(format!(
949                "duplicate projection field: '{field_name}'"
950            )));
951        }
952        let schema_position = compiled
953            .schema
954            .fields
955            .iter()
956            .position(|field| field.name == field_name)
957            .ok_or_else(|| {
958                TableError::InvalidSchema(format!("projection field '{field_name}' does not exist"))
959            })?;
960        if let Some(key_index) = compiled
961            .key_positions
962            .iter()
963            .position(|position| *position == schema_position)
964        {
965            sources.push(ProjectedFieldSource::Key(key_index));
966            has_key_fields = true;
967        } else {
968            let physical_column = compiled
969                .value_positions
970                .iter()
971                .position(|position| *position == schema_position)
972                .expect("compiled table maps every non-key field");
973            let projected_column = physical_columns.len();
974            physical_columns.push(physical_column);
975            sources.push(ProjectedFieldSource::Value {
976                projected_column,
977                physical_column,
978            });
979        }
980    }
981    if physical_columns.is_empty() {
982        physical_columns.push(0);
983    }
984    Ok((
985        Arc::new(ProjectionPlan {
986            sources,
987            has_key_fields,
988        }),
989        ReadOptions::for_columns_in_family(name.to_string(), physical_columns.clone())
990            .bound_to_column_family_schema(
991                compiled.column_family_options.clone(),
992                compiled.physical_columns,
993            ),
994        ScanOptions::for_columns(physical_columns)
995            .with_column_family(name.to_string())
996            .bound_to_column_family_schema(
997                compiled.column_family_options.clone(),
998                compiled.physical_columns,
999            ),
1000    ))
1001}
1002
1003#[cfg(feature = "ffi")]
1004pub(crate) fn build_scan_options_for_fields<S: AsRef<str>>(
1005    name: &str,
1006    compiled: Arc<CompiledTable>,
1007    field_names: &[S],
1008) -> Result<ScanOptions> {
1009    let (_, _, scan_options) = build_projection_parts(name, compiled, field_names)?;
1010    Ok(scan_options)
1011}
1012
1013pub(crate) fn compile_table(
1014    metadata: TableMetadata,
1015    total_buckets: u32,
1016) -> Result<Arc<CompiledTable>> {
1017    metadata.validate()?;
1018    let column_family_options = ColumnFamilyOptions {
1019        metadata: Some(metadata.to_value()?),
1020        ..ColumnFamilyOptions::default()
1021    };
1022    let positions = metadata
1023        .schema
1024        .fields
1025        .iter()
1026        .enumerate()
1027        .map(|(position, field)| (field.id, position))
1028        .collect::<HashMap<FieldId, usize>>();
1029    let key_positions = metadata
1030        .layout
1031        .key_fields
1032        .iter()
1033        .map(|id| positions[id])
1034        .collect::<Vec<_>>();
1035    let key_types = key_positions
1036        .iter()
1037        .map(|position| metadata.schema.fields[*position].logical_type.clone())
1038        .collect::<Vec<_>>();
1039    let value_positions = metadata
1040        .layout
1041        .value_columns
1042        .iter()
1043        .map(|column| positions[&column.field_id])
1044        .collect::<Vec<_>>();
1045    let value_types = value_positions
1046        .iter()
1047        .map(|position| metadata.schema.fields[*position].logical_type.clone())
1048        .collect::<Vec<_>>();
1049    Ok(Arc::new(CompiledTable {
1050        schema: Arc::new(metadata.schema),
1051        column_family_options,
1052        key_positions,
1053        key_types,
1054        bucket_key_fields: metadata.layout.bucket_fields.len(),
1055        value_positions,
1056        value_types,
1057        physical_columns: metadata.layout.value_columns.len().max(1),
1058        bucket_hash: BucketHash::new(total_buckets)?,
1059    }))
1060}
1061
1062pub(crate) fn load_table_metadata(schema: &Schema, name: &str) -> Result<TableMetadata> {
1063    let id = schema
1064        .column_family_ids()
1065        .get(name)
1066        .copied()
1067        .ok_or_else(|| TableError::InvalidSchema(format!("unknown table '{name}'")))?;
1068    load_table_metadata_from_options(
1069        &schema.column_family_options_in_family(id),
1070        schema.num_columns_in_family(id),
1071        name,
1072    )
1073}
1074
1075pub(crate) fn load_table_metadata_from_snapshot(
1076    snapshot: &ShardSnapshotMetadata,
1077    name: &str,
1078) -> Result<TableMetadata> {
1079    let family = snapshot
1080        .column_families
1081        .get(name)
1082        .ok_or_else(|| TableError::InvalidSchema(format!("unknown table '{name}'")))?;
1083    load_table_metadata_from_options(&family.options, Some(family.num_columns), name)
1084}
1085
1086pub(crate) fn load_table_metadata_for_shard(
1087    config: &Config,
1088    shard: &ShardSnapshotRef,
1089    name: &str,
1090) -> Result<TableMetadata> {
1091    let snapshot =
1092        cobble::load_shard_snapshot_metadata(config, &shard.db_id, &shard.manifest_path)?;
1093    if snapshot.snapshot_id != shard.snapshot_id {
1094        return Err(TableError::InvalidSchema(format!(
1095            "shard manifest snapshot {} does not match assigned snapshot {}",
1096            snapshot.snapshot_id, shard.snapshot_id
1097        )));
1098    }
1099    load_table_metadata_from_snapshot(&snapshot, name)
1100}
1101
1102pub(crate) fn table_metadata_from_shard_snapshot(
1103    snapshot: &ShardSnapshotMetadata,
1104) -> Result<std::collections::BTreeMap<String, TableMetadata>> {
1105    let mut tables = std::collections::BTreeMap::new();
1106    for (name, family) in &snapshot.column_families {
1107        let is_table = family
1108            .options
1109            .metadata
1110            .as_ref()
1111            .and_then(|metadata| metadata.get("format"))
1112            .and_then(serde_json::Value::as_str)
1113            == Some(crate::metadata::TABLE_METADATA_FORMAT);
1114        if !is_table {
1115            continue;
1116        }
1117        tables.insert(
1118            name.clone(),
1119            load_table_metadata_from_options(&family.options, Some(family.num_columns), name)?,
1120        );
1121    }
1122    if tables.is_empty() {
1123        return Err(TableError::InvalidSchema(
1124            "table shard snapshot contains no table metadata".into(),
1125        ));
1126    }
1127    Ok(tables)
1128}
1129
1130fn load_table_metadata_from_options(
1131    options: &ColumnFamilyOptions,
1132    num_columns: Option<usize>,
1133    name: &str,
1134) -> Result<TableMetadata> {
1135    let metadata = load_metadata(options)?;
1136    if num_columns != Some(metadata.layout.value_columns.len().max(1)) {
1137        return Err(TableError::InvalidSchema(format!(
1138            "table '{name}' has an incompatible physical column count"
1139        )));
1140    }
1141    Ok(metadata)
1142}
1143
1144fn validate_bound(bucket: u16, key: Option<&TableKey>) -> Result<()> {
1145    if key.is_some_and(|key| key.inner.bucket != bucket) {
1146        return Err(TableError::codec(
1147            "table scan bound belongs to a different bucket",
1148        ));
1149    }
1150    Ok(())
1151}
1152
1153fn load_metadata(options: &ColumnFamilyOptions) -> Result<TableMetadata> {
1154    let metadata = options
1155        .metadata
1156        .as_ref()
1157        .ok_or_else(|| TableError::InvalidSchema("column family is not a table".to_string()))?;
1158    TableMetadata::from_value(metadata)
1159}
1160
1161pub(crate) fn validate_name(name: String) -> Result<String> {
1162    if name.is_empty() || name != name.trim() {
1163        return Err(TableError::InvalidSchema(
1164            "table name must be non-empty without surrounding whitespace".to_string(),
1165        ));
1166    }
1167    Ok(name)
1168}