Skip to main content

Table

Struct Table 

Source
pub struct Table {
    pub heap: HeapFile,
    /* private fields */
}
Expand description

A table combines a heap file, schema, and optional indexes.

Mission C Phase 17: indexes used to live in a FxHashMap<String, BTree> alongside a parallel Vec<IndexedCol> of metadata. Every row insert paid an FxHash of the index column name to look the btree back out of the map. This phase collapses both data structures into a single Vec<IndexedCol> where each entry owns its btree inline — the hot write path walks one small vec and calls straight through to insert_int.

Mission C Phase 2: holds encode_scratch, a reusable buffer for [crate::row::encode_row_into]. Bench loops that push thousands of rows through insert/update reuse the same allocation across calls, cutting the allocator traffic to ~zero after the first row.

Fields§

§heap: HeapFile

Implementations§

Source§

impl Table

Source

pub fn create(schema: Schema, data_dir: &Path) -> Result<Self>

Source

pub fn open(schema: Schema, data_dir: &Path) -> Result<Self>

Reopen an existing table from disk. Caller supplies the schema (loaded from the catalog file). No index columns are supplied, so no index is rehydrated or rebuilt here; prefer open_with_indexes when the catalog knows which columns are indexed.

Source

pub fn schema(&self) -> &Schema

Return the table schema without exposing structural mutation. Schema changes are catalog-owned so prepared-query metadata is invalidated whenever column layout or table identity changes.

Source

pub fn index(&self, col_name: &str) -> Option<&BTree>

Look up an index by column name. Returns None if no index on this column. Used by the read-side executor paths (IndexScan, Project(IndexScan), etc.) that still need name-based resolution; the write-side hot paths iterate indexed_cols directly.

Source

pub fn index_mut(&mut self, col_name: &str) -> Option<&mut BTree>

Mutable counterpart to Self::index.

Source

pub fn has_index(&self, col_name: &str) -> bool

true if this table has an index on the named column.

Source

pub fn indexes_is_empty(&self) -> bool

true if this table has no secondary indexes at all.

Source

pub fn insert(&mut self, values: &Row) -> Result<RowId>

Mission C Phase 15: the hot insert path used to do two wasted things per secondary index, on every row:

  1. for (col_name, btree) in &mut self.indexes walked an FxHashMap by iterator (cheap but not free), and
  2. self.schema.column_index(col_name) walked schema.columns doing an O(n_cols) strcmp linear search to translate the column name back into its schema position.

For the insert_batch_1k bench (1K rows, User table, one index on id) that came out to ~6 strcmps * 1000 rows = 6K wasted comparisons per iteration, plus the HashMap iter overhead. We now iterate the precomputed indexed_cols slice directly, which hands us (col_idx, col_name, is_int) per entry, and route int keys straight through BTree::insert_int to skip the generic Value::Ord dispatch on every binary-search comparison.

Source

pub fn get(&self, rid: RowId) -> Option<Row>

Source

pub fn get_projected( &self, rid: RowId, column_indices: &[usize], ) -> Result<Option<Vec<Value>>>

Read only the requested logical columns from one row.

Output order exactly follows column_indices, including duplicates. Inline values are decoded directly from the row body. For a v2 row, an overflow chain is fetched and verified only when its column was requested; an unselected spilled value is never touched.

Source

pub fn delete(&mut self, rid: RowId) -> Result<()>

Delete a row. Mission C Phase 7: if the table has indexes, we used to call decode_row here — allocating Row + every column’s Value just to read the two or three columns that actually feed the index. Now we borrow the raw page bytes once and call decode_column for exactly the indexed columns, skipping the rest of the row entirely.

Mission C Phase 11: the Phase 7 version still allocated a Vec<(usize, Value)> per row so the btree mutations could happen after the hot-page borrow closed. That’s 3300 heap allocations per 100K-row delete_by_filter iteration — gone in Phase 11 via struct-field borrow splitting, so the btree lives alongside the page borrow inside the closure.

Source

pub fn delete_many(&mut self, rids: &[RowId]) -> Result<u64>

Mission C Phase 12: bulk delete a list of rids, batching the secondary-index maintenance.

For a 100K-row delete_by_filter that removes ~20% of the rows, the per-row Table::delete path pays ~4ms of pure Vec::remove memmove inside the btree: every call shifts up to 4KB of leaf entries. This helper collects the indexed-column keys first, deletes the heap slots one by one (hot-page writes), then compacts each btree in a single pass via [BTree::delete_many_int].

Restrictions / fall-through:

  • If the table has no indexes, this is equivalent to looping over heap.delete.
  • If any indexed column is not TypeId::Int, this falls back to the per-row delete path. The int-only constraint matches the only btree batch primitive we have (delete_many_int) and covers the overwhelmingly common case (primary keys, created_at, foreign keys).

Returns the number of rows removed.

Source

pub fn scan_delete_matching<P>(&mut self, pred: P) -> Result<u64>
where P: FnMut(&[u8]) -> bool,

Single-pass scan-and-delete driven by a raw-bytes predicate. Walks the heap once, marks matching rows deleted in place, and updates any int-keyed secondary indexes in a single batched delete_many_int per index at the end. Non-int secondary indexes fall back to per-key btree.delete, but still ride the same single heap pass.

Mission C Phase 16: this is the Table-level hook for [HeapFile::scan_delete_matching]. See that method for the fusion rationale. The executor’s Delete fast path routes Filter(SeqScan) / SeqScan-shaped delete plans here when the predicate compiles.

Source

pub fn scan_delete_matching_with_hook<P, H>( &mut self, pred: P, user_hook: H, ) -> Result<u64>
where P: FnMut(&[u8]) -> bool, H: FnMut(RowId, &[u8]),

Variant of Self::scan_delete_matching that lets the caller observe every matched row just before it’s marked deleted. Used by crate::catalog::Catalog::scan_delete_matching_logged to emit one WAL Delete record per victim in the same single-pass scan — no second walk over the heap, no per-row ensure_hot round-trip.

The user hook runs inside the heap’s pinned hot-page borrow, so it must not call back into the catalog / table / heap. The WAL append path only writes into an in-memory buffer and is safe.

Source

pub fn scan_patch_matching_with_hook<P, M, H>( &mut self, pred: P, try_mutate: M, hook: H, ) -> Result<(u64, Vec<RowId>)>
where P: FnMut(&[u8]) -> bool, M: FnMut(&mut [u8]) -> Option<u16>, H: FnMut(RowId, &[u8]),

Single-pass fused scan + in-place patch. Evaluates pred on raw row bytes and applies try_mutate to each match on the same hot page — no second pass. Returns (patched_count, fallback_rids).

The hook closure fires after each successful patch with the post-mutation bytes, used for WAL logging.

Perf sprint: this is the update analogue of scan_delete_matching_with_hook. Eliminates the two-pass collect-then-patch pattern that doubled ensure_hot calls for update_by_filter.

Source

pub fn update(&mut self, rid: RowId, values: &Row) -> Result<RowId>

Update a row in place when possible. Falls back to delete+insert only if the new encoding doesn’t fit in the current slot.

Mission D5: the previous implementation always did delete + insert, which:

  1. read+wrote the page twice (once to clear the slot, once to fill it again — usually on a different page),
  2. did an O(N) scan over pages_with_space for every insert,
  3. mutated every index even when the indexed column hadn’t changed.

On update_by_filter (50K matching rows, status-only update, no index on status) that turned ~1ms of work into 30 seconds — a catastrophic O(N²)-ish gap vs SQLite (6.7ms total). The fix is to (a) prefer heap.update which tries in-place first and (b) only touch indexes whose value actually changed.

Source

pub fn update_hinted( &mut self, rid: RowId, values: &Row, changed_col_indices: Option<&[usize]>, ) -> Result<RowId>

Same as update, but the caller can supply the set of column indices that actually changed. If supplied, the old-row read is skipped entirely when none of the changed columns is indexed.

Mission C Phase 2: update_by_filter hits this path ~50K times with a single-column assignment (status) on a table whose only index is on id. The old code called self.get(rid) unconditionally — a heap read + full decode every time — even though the result was always thrown away for non-indexed updates. Skipping that read is worth ~300ns/row, or ~15ms on a 50K-row update_by_filter.

Source

pub fn with_row_bytes_mut<F>(&mut self, rid: RowId, f: F) -> Result<bool>
where F: FnOnce(&mut [u8]),

Patch a row’s raw bytes in place. Caller guarantees the mutation does not change the row’s total length and does not touch any indexed column — indexes are NOT updated by this path.

Mission C Phase 4: see HeapFile::with_row_bytes_mut. This is the primitive that backs the executor’s single-column fixed-width update fast path.

Source

pub fn patch_var_col_in_place( &mut self, rid: RowId, col_idx: usize, new_value: Option<&[u8]>, ) -> Result<bool>

Patch a single var-length column in place, shrinking the row when the new value is smaller than the old one. Returns Ok(true) on success, Ok(false) when the new value would grow the row or the slot is gone (caller should fall back to the full update path).

The caller is responsible for ensuring no indexed column is touched by this patch — indexes are NOT maintained here.

Mission C Phase 10: backs the executor’s update_by_filter fast path for var-length single-column assignments.

Source

pub fn row_layout(&self) -> &RowLayout

Cached row layout for this table. Used by the executor to plan the byte-patch fast paths without re-walking the schema.

Source

pub fn has_indexed_col(&self, col_idx: usize) -> bool

Mission C Phase 15: does the given schema column index have an index attached? Used by the executor’s update fast-path planner to decide whether a byte-patch update is safe (no index to maintain). Linear scan over indexed_cols — typically 1–3 entries, so cheaper than a HashMap lookup by name.

Source

pub fn format_version(&self) -> u16

Heap on-disk format version. >= HEAP_FORMAT_VERSION_WITH_OVERFLOW (3) means the table has used overflow pages at least once, so it may hold v2 (spilled) rows. The executor uses this to route such tables away from the v1-only raw-byte fast paths (which cannot correctly read or patch a v2 row) and onto the reassembling decode paths.

Source

pub fn has_overflow_rows(&self) -> bool

Whether this table may hold v2 (spilled) rows: true once its heap has ever written an overflow chain. The executor gates the v1-only raw-byte read/patch fast paths on this — a spilled table takes the reassembling decode paths instead (correct for values of any size, including the >= 64KB values that cannot be re-inlined into a u16 v1 row).

Source

pub fn scan(&self) -> impl Iterator<Item = (RowId, Row)> + '_

Source

pub fn for_each_row_raw<F>(&self, f: F)
where F: FnMut(RowId, &[u8]),

Zero-copy scan that passes raw row bytes to the callback. v1/v0 rows are handed through untouched (zero copy). A v2 row is reassembled into an equivalent v1 (fully inline) row first — its spilled columns are fetched from the overflow chains — so every downstream consumer (decode_row, decode_column, compiled predicates) sees a v1 layout and needs no v2 awareness. Only the rare v2 rows pay the reassembly; v1 rows stay on the mmap zero-copy path. A row whose chain is corrupt is skipped (its typed error surfaces via get).

Source

pub fn try_for_each_row_raw<F>(&self, f: F)
where F: FnMut(RowId, &[u8]) -> ControlFlow<()>,

Zero-copy scan with early termination. The callback returns ControlFlow::Break(()) to stop. Used by Limit fast paths. v2 rows are reassembled to v1 first (see Self::for_each_row_raw).

Source

pub fn index_lookup(&self, col_name: &str, key: &Value) -> Option<(RowId, Row)>

Source

pub fn index_lookup_all(&self, col_name: &str, key: &Value) -> Vec<RowId>

Look up ALL matching rows for a column value. For unique indexes this returns 0 or 1 results. For non-unique indexes this returns all rows whose indexed column equals key.

Source

pub fn is_index_unique(&self, col_name: &str) -> Option<bool>

Check if an index on the given column is unique.

Source

pub fn create_index(&mut self, col_name: &str, data_dir: &Path) -> Result<()>

Create a non-unique secondary index on a column. Duplicate column values are supported via composite keys (column_value, RowId).

Source

pub fn create_index_with_unique( &mut self, col_name: &str, data_dir: &Path, unique: bool, ) -> Result<()>

Create an index on a column with an explicit uniqueness flag. unique = true creates a traditional unique index where duplicate key inserts overwrite (suitable for primary keys). unique = false creates a non-unique secondary index using composite keys.

Auto Trait Implementations§

§

impl Freeze for Table

§

impl RefUnwindSafe for Table

§

impl Send for Table

§

impl Sync for Table

§

impl Unpin for Table

§

impl UnsafeUnpin for Table

§

impl UnwindSafe for Table

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self>

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self>

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self>
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self>

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more