Skip to content

Commit 6f428f3

Browse files
authored
Make the key of Table.indexes be IndexId (#2124)
1 parent a54ea3a commit 6f428f3

File tree

9 files changed

+266
-221
lines changed

9 files changed

+266
-221
lines changed

crates/core/src/db/datastore/locking_tx_datastore/committed_state.rs

Lines changed: 34 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,13 @@ use spacetimedb_lib::{
3232
db::auth::{StAccess, StTableType},
3333
Identity,
3434
};
35-
use spacetimedb_primitives::{ColList, ColSet, TableId};
36-
use spacetimedb_sats::{AlgebraicType, AlgebraicValue, ProductValue};
35+
use spacetimedb_primitives::{ColList, ColSet, IndexId, TableId};
36+
use spacetimedb_sats::{AlgebraicValue, ProductValue};
3737
use spacetimedb_schema::schema::TableSchema;
3838
use spacetimedb_table::{
3939
blob_store::{BlobStore, HashMapBlobStore},
4040
indexes::{RowPointer, SquashedOffset},
41-
table::{IndexScanIter, InsertError, RowRef, Table},
41+
table::{IndexScanIter, InsertError, RowRef, Table, TableAndIndex},
4242
MemoryUsage,
4343
};
4444
use std::collections::{BTreeMap, BTreeSet};
@@ -375,19 +375,19 @@ impl CommittedState {
375375
.collect();
376376

377377
for index_row in rows {
378-
let Some((table, blob_store)) = self.get_table_and_blob_store(index_row.table_id) else {
378+
let index_id = index_row.index_id;
379+
let table_id = index_row.table_id;
380+
let Some((table, blob_store)) = self.get_table_and_blob_store(table_id) else {
379381
panic!("Cannot create index for table which doesn't exist in committed state");
380382
};
381383
let columns = match index_row.index_algorithm {
382384
StIndexAlgorithm::BTree { columns } => columns,
383385
_ => unimplemented!("Only BTree indexes are supported"),
384386
};
385-
let is_unique = unique_constraints.contains(&(index_row.table_id, (&columns).into()));
386-
387-
let index = table.new_index(index_row.index_id, &columns, is_unique)?;
388-
table.insert_index(blob_store, columns.clone(), index);
389-
self.index_id_map
390-
.insert(index_row.index_id, (index_row.table_id, columns));
387+
let is_unique = unique_constraints.contains(&(table_id, (&columns).into()));
388+
let index = table.new_index(columns.clone(), is_unique)?;
389+
table.insert_index(blob_store, index_id, index);
390+
self.index_id_map.insert(index_id, table_id);
391391
}
392392
Ok(())
393393
}
@@ -429,13 +429,36 @@ impl CommittedState {
429429
Ok(())
430430
}
431431

432+
/// When there's an index on `cols`,
433+
/// returns an iterator over the [BTreeIndex] that yields all the [`RowRef`]s
434+
/// that match the specified `range` in the indexed column.
435+
///
436+
/// Matching is defined by `Ord for AlgebraicValue`.
437+
///
438+
/// For a unique index this will always yield at most one `RowRef`.
439+
/// When there is no index this returns `None`.
432440
pub(super) fn index_seek<'a>(
433441
&'a self,
434442
table_id: TableId,
435443
cols: &ColList,
436444
range: &impl RangeBounds<AlgebraicValue>,
437445
) -> Option<IndexScanIter<'a>> {
438-
self.tables.get(&table_id)?.index_seek(&self.blob_store, cols, range)
446+
self.tables
447+
.get(&table_id)?
448+
.get_index_by_cols_with_table(&self.blob_store, cols)
449+
.map(|i| i.seek(range))
450+
}
451+
452+
/// Returns the table associated with the given `index_id`, if any.
453+
pub(super) fn get_table_for_index(&self, index_id: IndexId) -> Option<TableId> {
454+
self.index_id_map.get(&index_id).copied()
455+
}
456+
457+
/// Returns the table for `table_id` combined with the index for `index_id`, if both exist.
458+
pub(super) fn get_index_by_id_with_table(&self, table_id: TableId, index_id: IndexId) -> Option<TableAndIndex<'_>> {
459+
self.tables
460+
.get(&table_id)?
461+
.get_index_by_id_with_table(&self.blob_store, index_id)
439462
}
440463

441464
// TODO(perf, deep-integration): Make this method `unsafe`. Add the following to the docs:
@@ -637,13 +660,6 @@ impl CommittedState {
637660
let blob_store = &mut self.blob_store;
638661
(table, blob_store)
639662
}
640-
641-
/// Returns the table and index associated with the given `table_id` and `col_list`, if any.
642-
pub(super) fn get_table_and_index_type(&self, table_id: TableId, col_list: &ColList) -> Option<&AlgebraicType> {
643-
let table = self.tables.get(&table_id)?;
644-
let index = table.indexes.get(col_list)?;
645-
Some(&index.key_type)
646-
}
647663
}
648664

649665
pub struct CommittedIndexIterWithDeletedMutTx<'a> {

crates/core/src/db/datastore/locking_tx_datastore/mut_tx.rs

Lines changed: 51 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ use spacetimedb_schema::{
4343
use spacetimedb_table::{
4444
blob_store::{BlobStore, HashMapBlobStore},
4545
indexes::{RowPointer, SquashedOffset},
46-
table::{IndexScanIter, InsertError, RowRef, Table},
46+
table::{IndexScanIter, InsertError, RowRef, Table, TableAndIndex},
4747
};
4848
use std::{
4949
sync::Arc,
@@ -395,15 +395,15 @@ impl MutTxId {
395395
//
396396
// Ensure adding the index does not cause a unique constraint violation due to
397397
// the existing rows having the same value for some column(s).
398-
let mut insert_index = table.new_index(index.index_id, &columns, is_unique)?;
398+
let mut insert_index = table.new_index(columns.clone(), is_unique)?;
399399
let mut build_from_rows = |table: &Table, bs: &dyn BlobStore| -> Result<()> {
400-
if let Some(violation) = insert_index.build_from_rows(&columns, table.scan_rows(bs))? {
400+
if let Some(violation) = insert_index.build_from_rows(table.scan_rows(bs))? {
401401
let violation = table
402402
.get_row_ref(bs, violation)
403403
.expect("row came from scanning the table")
404404
.project(&columns)
405405
.expect("`cols` should consist of valid columns for this table");
406-
return Err(IndexError::from(table.build_error_unique(&insert_index, &columns, violation)).into());
406+
return Err(IndexError::from(table.build_error_unique(&insert_index, index_id, violation)).into());
407407
}
408408
Ok(())
409409
};
@@ -421,16 +421,17 @@ impl MutTxId {
421421
build_from_rows(commit_table, commit_blob_store)?;
422422
}
423423

424-
table.add_index(columns.clone(), insert_index);
425-
// Associate `index_id -> (table_id, col_list)` for fast lookup.
426-
idx_map.insert(index_id, (table_id, columns.clone()));
427-
428424
log::trace!(
429425
"INDEX CREATED: {} for table: {} and col(s): {:?}",
430426
index_id,
431427
table_id,
432428
columns
433429
);
430+
431+
table.add_index(index_id, insert_index);
432+
// Associate `index_id -> table_id` for fast lookup.
433+
idx_map.insert(index_id, table_id);
434+
434435
// Update the table's schema.
435436
// This won't clone-write when creating a table but likely to otherwise.
436437
table.with_mut_schema(|s| s.indexes.push(index));
@@ -455,16 +456,10 @@ impl MutTxId {
455456
// Remove the index in the transaction's insert table.
456457
// By altering the insert table, this gets moved over to the committed state on merge.
457458
let (table, blob_store, idx_map, ..) = self.get_or_create_insert_table_mut(table_id)?;
458-
if let Some(col) = table
459-
.indexes
460-
.iter()
461-
.find(|(_, idx)| idx.index_id == index_id)
462-
.map(|(cols, _)| cols.clone())
463-
{
459+
if table.delete_index(blob_store, index_id) {
464460
// This likely will do a clone-write as over time?
465461
// The schema might have found other referents.
466-
table.with_mut_schema(|s| s.indexes.retain(|x| x.index_algorithm.columns() != &col));
467-
table.delete_index(blob_store, &col);
462+
table.with_mut_schema(|s| s.indexes.retain(|x| x.index_id != index_id));
468463
}
469464
// Remove the `index_id -> (table_id, col_list)` association.
470465
idx_map.remove(&index_id);
@@ -497,21 +492,20 @@ impl MutTxId {
497492
rstart: &[u8],
498493
rend: &[u8],
499494
) -> Result<(TableId, BTreeScan<'a>)> {
500-
// Extract the table and index type for the tx state.
501-
let (table_id, col_list, tx_idx_key_type) = self
502-
.get_table_and_index_type(index_id)
503-
.ok_or_else(|| IndexError::NotFound(index_id))?;
495+
// Extract the table id, index type, and commit/tx indices.
496+
let (table_id_and_index_ty, commit_index, tx_index) = self.get_table_and_index_type(index_id);
497+
let (table_id, index_ty) = table_id_and_index_ty.ok_or_else(|| IndexError::NotFound(index_id))?;
504498

505499
// TODO(centril): Once we have more index types than `btree`,
506500
// we'll need to enforce that `index_id` refers to a btree index.
507501

508502
// We have the index key type, so we can decode everything.
509-
let bounds = Self::btree_decode_bounds(tx_idx_key_type, prefix, prefix_elems, rstart, rend)
510-
.map_err(IndexError::Decode)?;
503+
let bounds =
504+
Self::btree_decode_bounds(index_ty, prefix, prefix_elems, rstart, rend).map_err(IndexError::Decode)?;
511505

512506
// Get an index seek iterator for the tx and committed state.
513-
let tx_iter = self.tx_state.index_seek(table_id, col_list, &bounds);
514-
let commit_iter = self.committed_state_write_lock.index_seek(table_id, col_list, &bounds);
507+
let tx_iter = tx_index.map(|i| i.seek(&bounds));
508+
let commit_iter = commit_index.map(|i| i.seek(&bounds));
515509

516510
// Chain together the indexed rows in the tx and committed state,
517511
// but don't yield rows deleted in the tx state.
@@ -521,7 +515,7 @@ impl MutTxId {
521515
None => Left(iter),
522516
Some(deletes) => Right(IndexScanFilterDeleted { iter, deletes }),
523517
});
524-
// this is effectively just `tx_iter.into_iter().flatten().chain(commit_iter.into_iter().flatten())`,
518+
// This is effectively just `tx_iter.into_iter().flatten().chain(commit_iter.into_iter().flatten())`,
525519
// but with all the branching and `Option`s flattened to just one layer.
526520
let iter = match (tx_iter, commit_iter) {
527521
(None, None) => Empty(iter::empty()),
@@ -534,34 +528,46 @@ impl MutTxId {
534528
Ok((table_id, BTreeScan { inner: iter }))
535529
}
536530

537-
/// Translate `index_id` to the table id, the column list and index key type.
538-
fn get_table_and_index_type(&self, index_id: IndexId) -> Option<(TableId, &ColList, &AlgebraicType)> {
531+
/// Translate `index_id` to the table id, index type, and commit/tx indices.
532+
fn get_table_and_index_type(
533+
&self,
534+
index_id: IndexId,
535+
) -> (
536+
Option<(TableId, &AlgebraicType)>,
537+
Option<TableAndIndex<'_>>,
538+
Option<TableAndIndex<'_>>,
539+
) {
539540
// The order of querying the committed vs. tx state for the translation is not important.
540541
// But it is vastly more likely that it is in the committed state,
541542
// so query that first to avoid two lookups.
542-
let &(table_id, ref col_list) = self
543-
.committed_state_write_lock
544-
.index_id_map
545-
.get(&index_id)
546-
.or_else(|| self.tx_state.index_id_map.get(&index_id))?;
547-
548-
// The tx state must have the index.
543+
//
544+
// Also, the tx state must have the index.
549545
// If the index was e.g., dropped from the tx state but exists physically in the committed state,
550546
// the index does not exist, semantically.
551547
// TODO: handle the case where the table has been dropped in this transaction.
552-
let key_ty = if let Some(key_ty) = self
548+
let commit_table_id = self
553549
.committed_state_write_lock
554-
.get_table_and_index_type(table_id, col_list)
555-
{
556-
if self.tx_state_removed_index(index_id) {
557-
return None;
558-
}
559-
key_ty
550+
.get_table_for_index(index_id)
551+
.filter(|_| !self.tx_state_removed_index(index_id));
552+
553+
let (table_id, commit_index, tx_index) = if let t_id @ Some(table_id) = commit_table_id {
554+
// Index found for commit state, might also exist for tx state.
555+
let commit_index = self
556+
.committed_state_write_lock
557+
.get_index_by_id_with_table(table_id, index_id);
558+
let tx_index = self.tx_state.get_index_by_id_with_table(table_id, index_id);
559+
(t_id, commit_index, tx_index)
560+
} else if let t_id @ Some(table_id) = self.tx_state.get_table_for_index(index_id) {
561+
// Index might exist for tx state.
562+
let tx_index = self.tx_state.get_index_by_id_with_table(table_id, index_id);
563+
(t_id, None, tx_index)
560564
} else {
561-
self.tx_state.get_table_and_index_type(table_id, col_list)?
565+
// No index in either side.
566+
(None, None, None)
562567
};
563-
564-
Some((table_id, col_list, key_ty))
568+
let index_ty = commit_index.or(tx_index).map(|index| &index.index().key_type);
569+
let table_id_and_index_ty = table_id.zip(index_ty);
570+
(table_id_and_index_ty, commit_index, tx_index)
565571
}
566572

567573
/// Returns whether the index with `index_id` was removed in this transaction.
@@ -1540,7 +1546,7 @@ impl StateView for MutTxId {
15401546
// TODO(george): It's unclear that we truly support dynamically creating an index
15411547
// yet. In particular, I don't know if creating an index in a transaction and
15421548
// rolling it back will leave the index in place.
1543-
if let Some(inserted_rows) = self.tx_state.index_seek(table_id, &cols, &range) {
1549+
if let Some(inserted_rows) = self.tx_state.index_seek_by_cols(table_id, &cols, &range) {
15441550
let committed_rows = self.committed_state_write_lock.index_seek(table_id, &cols, &range);
15451551
// The current transaction has modified this table, and the table is indexed.
15461552
Ok(if let Some(del_table) = self.tx_state.get_delete_table(table_id) {

crates/core/src/db/datastore/locking_tx_datastore/tx.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ impl TxId {
100100
// Do not change its return type to a bare `u64`.
101101
pub(crate) fn num_distinct_values(&self, table_id: TableId, cols: &ColList) -> Option<NonZeroU64> {
102102
let table = self.committed_state_shared_lock.get_table(table_id)?;
103-
let index = table.indexes.get(cols)?;
103+
let (_, index) = table.get_index_by_cols(cols)?;
104104
NonZeroU64::new(index.num_keys() as u64)
105105
}
106106
}

crates/core/src/db/datastore/locking_tx_datastore/tx_state.rs

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
11
use core::ops::RangeBounds;
22
use spacetimedb_data_structures::map::{IntMap, IntSet};
33
use spacetimedb_primitives::{ColList, IndexId, TableId};
4-
use spacetimedb_sats::{AlgebraicType, AlgebraicValue};
4+
use spacetimedb_sats::AlgebraicValue;
55
use spacetimedb_table::{
66
blob_store::{BlobStore, HashMapBlobStore},
77
indexes::{RowPointer, SquashedOffset},
88
static_assert_size,
9-
table::{IndexScanIter, RowRef, Table},
9+
table::{IndexScanIter, RowRef, Table, TableAndIndex},
1010
};
1111
use std::collections::{btree_map, BTreeMap, BTreeSet};
1212

1313
pub(super) type DeleteTable = BTreeSet<RowPointer>;
1414

1515
/// A mapping to find the actual index given an `IndexId`.
16-
pub(super) type IndexIdMap = IntMap<IndexId, (TableId, ColList)>;
16+
pub(super) type IndexIdMap = IntMap<IndexId, TableId>;
1717
pub(super) type RemovedIndexIdSet = IntSet<IndexId>;
1818

1919
/// `TxState` tracks all of the modifications made during a particular transaction.
@@ -89,22 +89,35 @@ impl TxState {
8989
}
9090

9191
/// When there's an index on `cols`,
92-
/// returns an iterator over the [BTreeIndex] that yields all the `RowId`s
93-
/// that match the specified `value` in the indexed column.
92+
/// returns an iterator over the [BTreeIndex] that yields all the [`RowRef`]s
93+
/// that match the specified `range` in the indexed column.
9494
///
9595
/// Matching is defined by `Ord for AlgebraicValue`.
9696
///
97-
/// For a unique index this will always yield at most one `RowId`.
97+
/// For a unique index this will always yield at most one `RowRef`.
9898
/// When there is no index this returns `None`.
99-
pub(super) fn index_seek<'a>(
99+
pub(super) fn index_seek_by_cols<'a>(
100100
&'a self,
101101
table_id: TableId,
102102
cols: &ColList,
103103
range: &impl RangeBounds<AlgebraicValue>,
104104
) -> Option<IndexScanIter<'a>> {
105105
self.insert_tables
106106
.get(&table_id)?
107-
.index_seek(&self.blob_store, cols, range)
107+
.get_index_by_cols_with_table(&self.blob_store, cols)
108+
.map(|i| i.seek(range))
109+
}
110+
111+
/// Returns the table associated with the given `index_id`, if any.
112+
pub(super) fn get_table_for_index(&self, index_id: IndexId) -> Option<TableId> {
113+
self.index_id_map.get(&index_id).copied()
114+
}
115+
116+
/// Returns the table for `table_id` combined with the index for `index_id`, if both exist.
117+
pub(super) fn get_index_by_id_with_table(&self, table_id: TableId, index_id: IndexId) -> Option<TableAndIndex<'_>> {
118+
self.insert_tables
119+
.get(&table_id)?
120+
.get_index_by_id_with_table(&self.blob_store, index_id)
108121
}
109122

110123
// TODO(perf, deep-integration): Make this unsafe. Add the following to the docs:
@@ -203,11 +216,4 @@ impl TxState {
203216
let delete_table = unsafe { delete_table.unwrap_unchecked() };
204217
(tx_table, tx_blob_store, delete_table)
205218
}
206-
207-
/// Returns the table and index associated with the given `table_id` and `col_list`, if any.
208-
pub(super) fn get_table_and_index_type(&self, table_id: TableId, col_list: &ColList) -> Option<&AlgebraicType> {
209-
let table = self.insert_tables.get(&table_id)?;
210-
let index = table.indexes.get(col_list)?;
211-
Some(&index.key_type)
212-
}
213219
}

0 commit comments

Comments
 (0)