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: HeapFileImplementations§
Source§impl Table
impl Table
pub fn create(schema: Schema, data_dir: &Path) -> Result<Self>
Sourcepub fn open(schema: Schema, data_dir: &Path) -> Result<Self>
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.
Sourcepub fn schema(&self) -> &Schema
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.
Sourcepub fn index(&self, col_name: &str) -> Option<&BTree>
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.
Sourcepub fn index_mut(&mut self, col_name: &str) -> Option<&mut BTree>
pub fn index_mut(&mut self, col_name: &str) -> Option<&mut BTree>
Mutable counterpart to Self::index.
Sourcepub fn has_index(&self, col_name: &str) -> bool
pub fn has_index(&self, col_name: &str) -> bool
true if this table has an index on the named column.
Sourcepub fn indexes_is_empty(&self) -> bool
pub fn indexes_is_empty(&self) -> bool
true if this table has no secondary indexes at all.
Sourcepub fn insert(&mut self, values: &Row) -> Result<RowId>
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:
for (col_name, btree) in &mut self.indexeswalked an FxHashMap by iterator (cheap but not free), andself.schema.column_index(col_name)walkedschema.columnsdoing 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.
pub fn get(&self, rid: RowId) -> Option<Row>
Sourcepub fn get_projected(
&self,
rid: RowId,
column_indices: &[usize],
) -> Result<Option<Vec<Value>>>
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.
Sourcepub fn delete(&mut self, rid: RowId) -> Result<()>
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.
Sourcepub fn delete_many(&mut self, rids: &[RowId]) -> Result<u64>
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-rowdeletepath. 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.
Sourcepub fn scan_delete_matching<P>(&mut self, pred: P) -> Result<u64>
pub fn scan_delete_matching<P>(&mut self, pred: P) -> Result<u64>
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.
Sourcepub fn scan_delete_matching_with_hook<P, H>(
&mut self,
pred: P,
user_hook: H,
) -> Result<u64>
pub fn scan_delete_matching_with_hook<P, H>( &mut self, pred: P, user_hook: H, ) -> Result<u64>
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.
Sourcepub fn scan_patch_matching_with_hook<P, M, H>(
&mut self,
pred: P,
try_mutate: M,
hook: H,
) -> Result<(u64, Vec<RowId>)>
pub fn scan_patch_matching_with_hook<P, M, H>( &mut self, pred: P, try_mutate: M, hook: H, ) -> Result<(u64, Vec<RowId>)>
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.
Sourcepub fn update(&mut self, rid: RowId, values: &Row) -> Result<RowId>
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:
- read+wrote the page twice (once to clear the slot, once to fill it again — usually on a different page),
- did an O(N) scan over
pages_with_spacefor every insert, - 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.
Sourcepub fn update_hinted(
&mut self,
rid: RowId,
values: &Row,
changed_col_indices: Option<&[usize]>,
) -> Result<RowId>
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.
Sourcepub fn with_row_bytes_mut<F>(&mut self, rid: RowId, f: F) -> Result<bool>
pub fn with_row_bytes_mut<F>(&mut self, rid: RowId, f: F) -> Result<bool>
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.
Sourcepub fn patch_var_col_in_place(
&mut self,
rid: RowId,
col_idx: usize,
new_value: Option<&[u8]>,
) -> Result<bool>
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.
Sourcepub fn row_layout(&self) -> &RowLayout
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.
Sourcepub fn has_indexed_col(&self, col_idx: usize) -> bool
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.
Sourcepub fn format_version(&self) -> u16
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.
Sourcepub fn has_overflow_rows(&self) -> bool
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).
pub fn scan(&self) -> impl Iterator<Item = (RowId, Row)> + '_
Sourcepub fn for_each_row_raw<F>(&self, f: F)
pub fn for_each_row_raw<F>(&self, f: F)
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).
Sourcepub fn try_for_each_row_raw<F>(&self, f: F)
pub fn try_for_each_row_raw<F>(&self, f: F)
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).
pub fn index_lookup(&self, col_name: &str, key: &Value) -> Option<(RowId, Row)>
Sourcepub fn index_lookup_all(&self, col_name: &str, key: &Value) -> Vec<RowId>
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.
Sourcepub fn is_index_unique(&self, col_name: &str) -> Option<bool>
pub fn is_index_unique(&self, col_name: &str) -> Option<bool>
Check if an index on the given column is unique.
Sourcepub fn create_index(&mut self, col_name: &str, data_dir: &Path) -> Result<()>
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).
Sourcepub fn create_index_with_unique(
&mut self,
col_name: &str,
data_dir: &Path,
unique: bool,
) -> Result<()>
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.