Skip to content

Commit a2ca083

Browse files
Add args column to view backing tables (clockworklabs#5300)
# Description of Changes Review clockworklabs#5287 first. This patch updates view backing table schemas to have a single private column `arg_hash`. Previously sender-scoped views had a `sender` column for the calling identity, however now that's been replaced with a single unified `arg_hash` column that encodes the calling identity within it. When we add parameterized views, the view args will also be encoded in this hash and stored in this column. This column exists for both anonymous and sender scoped views meaning that the backing tables for all views now have the same number of private columns - one. This hash is now used as a runtime variable that the query engine uses to evaluate view table scans. In order to keep the diff small, this patch does update view read sets with this new hash value. That change has a larger blast radius and will be done in the next set of changes. # API and ABI breaking changes N/A # Expected complexity level and risk 2 # Testing Existing coverage.
1 parent 0f7615f commit a2ca083

12 files changed

Lines changed: 227 additions & 236 deletions

File tree

crates/core/src/db/relational_db.rs

Lines changed: 53 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ use spacetimedb_sats::{AlgebraicType, AlgebraicValue, ProductType, ProductValue}
5050
use spacetimedb_schema::def::{ModuleDef, TableDef, ViewDef};
5151
use spacetimedb_schema::reducer_name::ReducerName;
5252
use spacetimedb_schema::schema::{
53-
ColumnSchema, IndexSchema, RowLevelSecuritySchema, Schema, SequenceSchema, TableSchema,
53+
ColumnSchema, IndexSchema, RowLevelSecuritySchema, Schema, SequenceSchema, TableSchema, VIEW_ARG_HASH_COL,
5454
};
5555
use spacetimedb_schema::table_name::TableName;
5656
use spacetimedb_snapshot::{DynSnapshotRepo, ReconstructedSnapshot, SnapshotError, SnapshotRepository};
@@ -1559,10 +1559,10 @@ impl RelationalDB {
15591559
Ok(None)
15601560
}
15611561

1562-
/// Write `rows` into a (sender) view's backing table.
1562+
/// Write `rows` into a sender-scoped view's backing table.
15631563
///
15641564
/// # Process
1565-
/// 1. Delete all rows for `sender` from the view's backing table
1565+
/// 1. Delete all rows for `sender`'s implicit argument hash from the view's backing table
15661566
/// 2. Insert the new rows into the backing table
15671567
///
15681568
/// # Arguments
@@ -1578,39 +1578,24 @@ impl RelationalDB {
15781578
sender: Identity,
15791579
rows: Vec<ProductValue>,
15801580
) -> Result<(), DBError> {
1581-
// Delete rows for `sender` from the backing table
1582-
let rows_to_delete = self
1583-
.iter_by_col_eq_mut(tx, table_id, ColId(0), &sender.into())?
1584-
.map(|res| res.pointer())
1585-
.collect::<Vec<_>>();
1586-
self.delete(tx, table_id, rows_to_delete);
1587-
1588-
self.write_view_rows(tx, table_id, rows, Some(sender))?;
1589-
1590-
Ok(())
1581+
let arg_hash = MutTxId::view_arg_hash(sender);
1582+
self.materialize_view_arg_hash(tx, table_id, arg_hash, rows)
15911583
}
15921584

1593-
/// Write `rows` into an anonymous view's backing table.
1594-
///
1595-
/// # Process
1596-
/// 1. Clear the view's backing table
1597-
/// 2. Insert the new rows into the backing table
1598-
///
1599-
/// # Arguments
1600-
/// * `tx` - Mutable transaction context
1601-
/// * `table_id` - The id of the view's backing table
1602-
/// * `rows` - Product values to insert
1603-
#[allow(clippy::too_many_arguments)]
1604-
pub fn materialize_anonymous_view(
1585+
fn materialize_view_arg_hash(
16051586
&self,
16061587
tx: &mut MutTxId,
16071588
table_id: TableId,
1589+
arg_hash: AlgebraicValue,
16081590
rows: Vec<ProductValue>,
16091591
) -> Result<(), DBError> {
1610-
// Clear entire backing table
1611-
self.clear_table(tx, table_id)?;
1592+
let rows_to_delete = self
1593+
.iter_by_col_eq_mut(tx, table_id, VIEW_ARG_HASH_COL, &arg_hash)?
1594+
.map(|res| res.pointer())
1595+
.collect::<Vec<_>>();
1596+
self.delete(tx, table_id, rows_to_delete);
16121597

1613-
self.write_view_rows(tx, table_id, rows, None)?;
1598+
self.write_view_rows(tx, table_id, rows, &arg_hash)?;
16141599

16151600
Ok(())
16161601
}
@@ -1626,10 +1611,11 @@ impl RelationalDB {
16261611
view_call: ViewCallInfo,
16271612
rows: Vec<ProductValue>,
16281613
) -> Result<(), DBError> {
1629-
match view_call.sender {
1630-
Some(sender) => self.materialize_view(tx, table_id, sender, rows)?,
1631-
None => self.materialize_anonymous_view(tx, table_id, rows)?,
1632-
}
1614+
let arg_hash = match view_call.sender {
1615+
Some(sender) => MutTxId::view_arg_hash(sender),
1616+
None => MutTxId::anonymous_view_arg_hash(),
1617+
};
1618+
self.materialize_view_arg_hash(tx, table_id, arg_hash, rows)?;
16331619
tx.replace_view_read_set(view_call);
16341620

16351621
Ok(())
@@ -1640,34 +1626,18 @@ impl RelationalDB {
16401626
tx: &mut MutTxId,
16411627
table_id: TableId,
16421628
rows: Vec<ProductValue>,
1643-
sender: Option<Identity>,
1629+
arg_hash: &AlgebraicValue,
16441630
) -> Result<(), DBError> {
1645-
match sender {
1646-
Some(sender) => {
1647-
for product in rows {
1648-
let value = ProductValue::from_iter(std::iter::once(sender.into()).chain(product.elements));
1649-
self.insert(
1650-
tx,
1651-
table_id,
1652-
&value
1653-
.to_bsatn_vec()
1654-
.map_err(|_| ViewError::SerializeRow)
1655-
.map_err(DatastoreError::from)?,
1656-
)?;
1657-
}
1658-
}
1659-
None => {
1660-
for product in rows {
1661-
self.insert(
1662-
tx,
1663-
table_id,
1664-
&product
1665-
.to_bsatn_vec()
1666-
.map_err(|_| ViewError::SerializeRow)
1667-
.map_err(DatastoreError::from)?,
1668-
)?;
1669-
}
1670-
}
1631+
for product in rows {
1632+
let value = ProductValue::from_iter(std::iter::once(arg_hash.clone()).chain(product.elements));
1633+
self.insert(
1634+
tx,
1635+
table_id,
1636+
&value
1637+
.to_bsatn_vec()
1638+
.map_err(|_| ViewError::SerializeRow)
1639+
.map_err(DatastoreError::from)?,
1640+
)?;
16711641
}
16721642

16731643
Ok(())
@@ -2256,8 +2226,8 @@ pub mod tests_utils {
22562226
row: ProductValue,
22572227
) -> Result<RowRef<'a>, DBError> {
22582228
let meta_cols = match sender {
2259-
Some(identity) => vec![identity.into()],
2260-
None => vec![],
2229+
Some(identity) => vec![MutTxId::view_arg_hash(identity)],
2230+
None => vec![MutTxId::anonymous_view_arg_hash()],
22612231
};
22622232
let cols = meta_cols.into_iter().chain(row.elements);
22632233
let row = ProductValue::from_iter(cols);
@@ -2512,8 +2482,9 @@ mod tests {
25122482

25132483
fn project_views(stdb: &TestDB, table_id: TableId, sender: Identity) -> Vec<ProductValue> {
25142484
let tx = begin_tx(stdb);
2485+
let arg_hash = MutTxId::view_arg_hash(sender);
25152486

2516-
stdb.iter_by_col_eq(&tx, table_id, 0, &sender.into())
2487+
stdb.iter_by_col_eq(&tx, table_id, VIEW_ARG_HASH_COL, &arg_hash)
25172488
.unwrap()
25182489
.map(|row| {
25192490
let pv = row.to_product_value();
@@ -2526,10 +2497,16 @@ mod tests {
25262497

25272498
fn project_anonymous_views(stdb: &TestDB, table_id: TableId) -> Vec<ProductValue> {
25282499
let tx = begin_tx(stdb);
2500+
let arg_hash = MutTxId::anonymous_view_arg_hash();
25292501

2530-
stdb.iter(&tx, table_id)
2502+
stdb.iter_by_col_eq(&tx, table_id, VIEW_ARG_HASH_COL, &arg_hash)
25312503
.unwrap()
2532-
.map(|row| row.to_product_value())
2504+
.map(|row| {
2505+
let pv = row.to_product_value();
2506+
ProductValue {
2507+
elements: pv.elements.iter().skip(1).cloned().collect(),
2508+
}
2509+
})
25332510
.collect()
25342511
}
25352512

@@ -2737,7 +2714,12 @@ mod tests {
27372714
let mut tx = begin_mut_tx(&stdb);
27382715
tx.subscribe_view(view_id, ArgId::SENTINEL, stale_sender)?;
27392716
tx.subscribe_view(view_id, ArgId::SENTINEL, live_sender)?;
2740-
stdb.materialize_anonymous_view(&mut tx, table_id, vec![product![42u8]])?;
2717+
stdb.materialize_view_call(
2718+
&mut tx,
2719+
table_id,
2720+
ViewCallInfo { view_id, sender: None },
2721+
vec![product![42u8]],
2722+
)?;
27412723
stdb.commit_tx(tx)?;
27422724

27432725
let mut tx = begin_mut_tx(&stdb);
@@ -2786,7 +2768,12 @@ mod tests {
27862768

27872769
let mut tx = begin_mut_tx(&stdb);
27882770
tx.subscribe_view(view_id, ArgId::SENTINEL, sender)?;
2789-
stdb.materialize_anonymous_view(&mut tx, table_id, vec![product![42u8]])?;
2771+
stdb.materialize_view_call(
2772+
&mut tx,
2773+
table_id,
2774+
ViewCallInfo { view_id, sender: None },
2775+
vec![product![42u8]],
2776+
)?;
27902777
stdb.commit_tx(tx)?;
27912778

27922779
let mut tx = begin_mut_tx(&stdb);

crates/core/src/host/module_host.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3309,7 +3309,7 @@ impl ModuleHost {
33093309
let delta_tx = DeltaTx::from(tx);
33103310
let params = ExecutionParams::from_auth(auth);
33113311
let plan_fragments = optimized.iter();
3312-
let (rows, _, metrics) = if returns_view_table && num_private_cols > 0 {
3312+
let (rows, _, metrics) = if returns_view_table {
33133313
execute_plan_for_view::<F>(plan_fragments, num_cols, num_private_cols, &delta_tx, &params, rlb_pool)
33143314
} else {
33153315
execute_plan::<F>(optimized.iter(), &delta_tx, &params, rlb_pool)

crates/core/src/subscription/module_subscription_actor.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2040,6 +2040,7 @@ mod tests {
20402040
use spacetimedb_client_api_messages::energy::FunctionBudget;
20412041
use spacetimedb_client_api_messages::websocket::{common::RowListLen as _, v1 as ws_v1, v2 as ws_v2};
20422042
use spacetimedb_data_structures::map::{HashCollectionExt as _, HashMap};
2043+
use spacetimedb_datastore::locking_tx_datastore::MutTxId;
20432044
use spacetimedb_datastore::system_tables::{StRowLevelSecurityRow, ST_ROW_LEVEL_SECURITY_ID};
20442045
use spacetimedb_execution::dml::MutDatastore;
20452046
use spacetimedb_lib::bsatn::ToBsatn;
@@ -2536,7 +2537,8 @@ mod tests {
25362537
subs.remove_subscriber(client_id_for_b);
25372538

25382539
// Delete the backing row and verify the surviving subscriber still receives the view delta.
2539-
let _ = commit_tx(&db, &subs, [(view_table_id, product![id_for_a, 7_u8])], [])?;
2540+
let arg_hash = MutTxId::view_arg_hash(id_for_a);
2541+
let _ = commit_tx(&db, &subs, [(view_table_id, product![arg_hash, 7_u8])], [])?;
25402542

25412543
let schema = ProductType::from([AlgebraicType::U8]);
25422544
assert_v2_tx_update_for_table(

crates/datastore/src/error.rs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,6 @@ pub enum ViewError {
5757
TypeMismatchOption,
5858
#[error("expected ProductValue in view result")]
5959
ExpectedProduct,
60-
#[error("failed to serialize view arguments")]
61-
SerializeArgs,
6260
}
6361

6462
#[derive(Error, Debug)]

crates/datastore/src/locking_tx_datastore/mut_tx.rs

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,9 @@ use spacetimedb_execution::{dml::MutDatastore, Datastore, DeltaStore, Row};
4343
use spacetimedb_lib::{
4444
db::raw_def::v9::RawSql,
4545
db::{auth::StAccess, raw_def::SEQUENCE_ALLOCATION_STEP},
46+
empty_view_arg_hash_value,
4647
metrics::ExecutionMetrics,
47-
ConnectionId, Identity, Timestamp,
48+
sender_view_arg_hash_value, ConnectionId, Identity, Timestamp,
4849
};
4950
use spacetimedb_primitives::{
5051
col_list, ArgId, ColId, ColList, ColSet, ConstraintId, IndexId, ScheduleId, SequenceId, TableId, ViewId,
@@ -57,7 +58,10 @@ use spacetimedb_schema::{
5758
def::{ModuleDef, ViewColumnDef, ViewDef, ViewParamDef},
5859
identifier::Identifier,
5960
reducer_name::ReducerName,
60-
schema::{ColumnSchema, ConstraintSchema, IndexSchema, RowLevelSecuritySchema, SequenceSchema, TableSchema},
61+
schema::{
62+
ColumnSchema, ConstraintSchema, IndexSchema, RowLevelSecuritySchema, SequenceSchema, TableSchema,
63+
VIEW_ARG_HASH_COL,
64+
},
6165
table_name::TableName,
6266
};
6367
use spacetimedb_table::{
@@ -2369,6 +2373,16 @@ impl<'a, I: Iterator<Item = RowRef<'a>>> Iterator for FilterDeleted<'a, I> {
23692373
}
23702374

23712375
impl MutTxId {
2376+
/// Returns the hash value for an anonymous view's empty arguments.
2377+
pub fn anonymous_view_arg_hash() -> AlgebraicValue {
2378+
empty_view_arg_hash_value()
2379+
}
2380+
2381+
/// Returns the hash value for a sender-scoped view's arguments.
2382+
pub fn view_arg_hash(sender: Identity) -> AlgebraicValue {
2383+
sender_view_arg_hash_value(sender)
2384+
}
2385+
23722386
/// Does this caller have an entry for `view_id` in `st_view_sub`?
23732387
pub fn is_view_materialized(&self, view_id: ViewId, arg_id: ArgId, sender: Identity) -> Result<bool> {
23742388
use StViewSubFields::*;
@@ -2542,22 +2556,27 @@ impl MutTxId {
25422556
} = self.lookup_st_view(view_id)?;
25432557
let table_id = table_id.expect("views have backing table");
25442558

2545-
if is_anonymous {
2546-
if !self.has_other_st_view_sub_entries(view_id, sub_row_ptr)? {
2547-
self.clear_table(table_id)?;
2548-
self.drop_view_from_committed_read_set(view_id);
2549-
}
2550-
} else {
2559+
let drop_materialization = !is_anonymous || !self.has_other_st_view_sub_entries(view_id, sub_row_ptr)?;
2560+
if drop_materialization {
2561+
let arg_hash = if is_anonymous {
2562+
Self::anonymous_view_arg_hash()
2563+
} else {
2564+
Self::view_arg_hash(sender)
2565+
};
25512566
let rows_to_delete = self
2552-
.iter_by_col_eq(table_id, 0, &sender.into())?
2567+
.iter_by_col_eq(table_id, VIEW_ARG_HASH_COL, &arg_hash)?
25532568
.map(|res| res.pointer())
25542569
.collect::<Vec<_>>();
25552570

25562571
for row_ptr in rows_to_delete {
25572572
self.delete(table_id, row_ptr)?;
25582573
}
25592574

2560-
self.drop_view_with_sender_from_committed_read_set(view_id, sender);
2575+
if is_anonymous {
2576+
self.drop_view_from_committed_read_set(view_id);
2577+
} else {
2578+
self.drop_view_with_sender_from_committed_read_set(view_id, sender);
2579+
}
25612580
}
25622581

25632582
// Finally, delete the subscription row

crates/datastore/src/locking_tx_datastore/state_view.rs

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@ use crate::system_tables::{
77
StConnectionCredentialsFields, StConnectionCredentialsRow, StConstraintFields, StConstraintRow, StEventTableFields,
88
StEventTableRow, StIndexAccessorFields, StIndexAccessorRow, StIndexFields, StIndexRow, StScheduledFields,
99
StScheduledRow, StSequenceFields, StSequenceRow, StTableAccessorFields, StTableAccessorRow, StTableFields,
10-
StTableRow, StViewFields, StViewParamFields, StViewRow, SystemTable, ST_COLUMN_ACCESSOR_ID, ST_COLUMN_ID,
10+
StTableRow, StViewFields, StViewRow, SystemTable, ST_COLUMN_ACCESSOR_ID, ST_COLUMN_ID,
1111
ST_CONNECTION_CREDENTIALS_ID, ST_CONSTRAINT_ID, ST_EVENT_TABLE_ID, ST_INDEX_ACCESSOR_ID, ST_INDEX_ID,
12-
ST_SCHEDULED_ID, ST_SEQUENCE_ID, ST_TABLE_ACCESSOR_ID, ST_TABLE_ID, ST_VIEW_ID, ST_VIEW_PARAM_ID,
12+
ST_SCHEDULED_ID, ST_SEQUENCE_ID, ST_TABLE_ACCESSOR_ID, ST_TABLE_ID, ST_VIEW_ID,
1313
};
1414
use anyhow::anyhow;
1515
use core::ops::RangeBounds;
@@ -240,14 +240,9 @@ pub trait StateView {
240240
.map(|mut iter| {
241241
iter.next().map(|row| -> Result<_> {
242242
let row = StViewRow::try_from(row)?;
243-
let has_args = self
244-
.iter_by_col_eq(ST_VIEW_PARAM_ID, StViewParamFields::ViewId, &row.view_id.into())?
245-
.next()
246-
.is_some();
247243

248244
Ok(ViewDefInfo {
249245
view_id: row.view_id,
250-
has_args,
251246
is_anonymous: row.is_anonymous,
252247
})
253248
})

crates/execution/src/lib.rs

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
use anyhow::Result;
22
use core::hash::{Hash, Hasher};
33
use core::ops::RangeBounds;
4-
use spacetimedb_lib::{identity::AuthCtx, query::Delta, AlgebraicType, Identity};
5-
use spacetimedb_physical_plan::plan::{ParamResolver, ProjectField, TupleField};
4+
use spacetimedb_lib::{hash_sender_view_args, identity::AuthCtx, query::Delta, AlgebraicType, Identity};
5+
use spacetimedb_physical_plan::plan::{
6+
ParamResolver, ParamSlot, ProjectField, TupleField, PARAM_SENDER, PARAM_VIEW_ARG_HASH,
7+
};
68
use spacetimedb_primitives::{ColList, IndexId, TableId};
79
use spacetimedb_sats::bsatn::{BufReservedFill, EncodeError, ToBsatn};
810
use spacetimedb_sats::buffer::BufWriter;
911
use spacetimedb_sats::product_value::InvalidFieldError;
10-
use spacetimedb_sats::{impl_serialize, AlgebraicValue, ProductValue};
11-
use spacetimedb_sql_parser::ast::Parameter;
12+
use spacetimedb_sats::{impl_serialize, u256, AlgebraicValue, ProductValue};
1213
use spacetimedb_table::{static_assert_size, table::RowRef};
1314

1415
pub mod dml;
@@ -17,11 +18,15 @@ pub mod pipelined;
1718
#[derive(Debug, Clone, Copy)]
1819
pub struct ExecutionParams {
1920
sender: Identity,
21+
view_arg_hash: u256,
2022
}
2123

2224
impl ExecutionParams {
2325
pub fn from_sender(sender: Identity) -> Self {
24-
Self { sender }
26+
Self {
27+
sender,
28+
view_arg_hash: hash_sender_view_args(sender).to_u256(),
29+
}
2530
}
2631

2732
pub fn from_auth(auth: &AuthCtx) -> Self {
@@ -30,11 +35,14 @@ impl ExecutionParams {
3035
}
3136

3237
impl ParamResolver for ExecutionParams {
33-
fn resolve_param(&self, param: Parameter, ty: &AlgebraicType) -> AlgebraicValue {
38+
fn resolve_param(&self, param: ParamSlot, ty: &AlgebraicType) -> AlgebraicValue {
3439
match param {
35-
Parameter::Sender if ty.is_identity() => self.sender.into(),
36-
Parameter::Sender if ty.is_bytes() => AlgebraicValue::Bytes(self.sender.to_be_byte_array().into()),
37-
Parameter::Sender => panic!("unsupported type for :sender: {ty:?}"),
40+
PARAM_SENDER if ty.is_identity() => self.sender.into(),
41+
PARAM_SENDER if ty.is_bytes() => AlgebraicValue::Bytes(self.sender.to_be_byte_array().into()),
42+
PARAM_SENDER => panic!("unsupported type for :sender: {ty:?}"),
43+
PARAM_VIEW_ARG_HASH if matches!(ty, AlgebraicType::U256) => AlgebraicValue::U256(self.view_arg_hash.into()),
44+
PARAM_VIEW_ARG_HASH => panic!("unsupported type for view arg hash: {ty:?}"),
45+
ParamSlot(slot) => panic!("unknown physical plan parameter slot: {slot}"),
3846
}
3947
}
4048
}

0 commit comments

Comments
 (0)