Skip to content

Commit 39f9127

Browse files
Auto-migration rules for views (#3484)
# Description of Changes <!-- Please describe your change, mention any related tickets, and so on here. --> Defines auto-migration rules for views. > What are they? Answer: You can always auto-migrate a view. It's just a matter of whether you have to disconnect clients. Removal of a view or changing the return type in a breaking way requires that we disconnect clients. TODO: Even if a view is updated in a completely compatible way, we still have to wipe or dirty the read set in order to force a re-computation because the behavior could have changed. # API and ABI breaking changes <!-- If this is an API or ABI breaking change, please apply the corresponding GitHub label. --> None # Expected complexity level and risk <!-- How complicated do you think these changes are? Grade on a scale from 1 to 5, where 1 is a trivial change, and 5 is a deep-reaching and complex change. This complexity rating applies not only to the complexity apparent in the diff, but also to its interactions with existing and future code. If you answered more than a 2, explain what is complex about the PR, and what other components it interacts with in potentially concerning ways. --> 2.5 Not just a mechanical change. Most of the time you can think of views as though they were tables. However for auto-migration there is a key distinction. Views don't have observably persistent state. While we can persist state internally, we can always derive a view's result set from the database state. Hence auto-migration rules are not as strict. # Testing <!-- Describe any testing you've done, and any testing you'd like your reviewers to do, so that you're confident that all the changes work as expected! --> - [x] Auto-migration planner tests
1 parent af69e58 commit 39f9127

17 files changed

Lines changed: 1188 additions & 76 deletions

crates/core/src/db/relational_db.rs

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1055,13 +1055,30 @@ impl RelationalDB {
10551055
Ok(self.inner.create_table_mut_tx(tx, schema)?)
10561056
}
10571057

1058-
pub fn create_view_table(
1058+
pub fn drop_table(&self, tx: &mut MutTx, table_id: TableId) -> Result<(), DBError> {
1059+
let table_name = self
1060+
.table_name_from_id_mut(tx, table_id)?
1061+
.map(|name| name.to_string())
1062+
.unwrap_or_default();
1063+
Ok(self.inner.drop_table_mut_tx(tx, table_id).map(|_| {
1064+
DB_METRICS
1065+
.rdb_num_table_rows
1066+
.with_label_values(&self.database_identity, &table_id.into(), &table_name)
1067+
.set(0)
1068+
})?)
1069+
}
1070+
1071+
pub fn create_view(
10591072
&self,
10601073
tx: &mut MutTx,
10611074
module_def: &ModuleDef,
10621075
view_def: &ViewDef,
10631076
) -> Result<(ViewId, TableId), DBError> {
1064-
Ok(tx.create_view_with_backing_table(module_def, view_def)?)
1077+
Ok(tx.create_view(module_def, view_def)?)
1078+
}
1079+
1080+
pub fn drop_view(&self, tx: &mut MutTx, view_id: ViewId) -> Result<(), DBError> {
1081+
Ok(tx.drop_view(view_id)?)
10651082
}
10661083

10671084
pub fn create_table_for_test_with_the_works(
@@ -1141,19 +1158,6 @@ impl RelationalDB {
11411158
self.create_table_for_test_with_the_works(name, schema, &indexes[..], &[], StAccess::Public)
11421159
}
11431160

1144-
pub fn drop_table(&self, tx: &mut MutTx, table_id: TableId) -> Result<(), DBError> {
1145-
let table_name = self
1146-
.table_name_from_id_mut(tx, table_id)?
1147-
.map(|name| name.to_string())
1148-
.unwrap_or_default();
1149-
Ok(self.inner.drop_table_mut_tx(tx, table_id).map(|_| {
1150-
DB_METRICS
1151-
.rdb_num_table_rows
1152-
.with_label_values(&self.database_identity, &table_id.into(), &table_name)
1153-
.set(0)
1154-
})?)
1155-
}
1156-
11571161
/// Rename a table.
11581162
///
11591163
/// Sets the name of the table to `new_name` regardless of the previous value. This is a
@@ -1164,6 +1168,10 @@ impl RelationalDB {
11641168
Ok(self.inner.rename_table_mut_tx(tx, table_id, new_name)?)
11651169
}
11661170

1171+
pub fn view_id_from_name_mut(&self, tx: &MutTx, view_name: &str) -> Result<Option<ViewId>, DBError> {
1172+
Ok(self.inner.view_id_from_name_mut_tx(tx, view_name)?)
1173+
}
1174+
11671175
pub fn table_id_from_name_mut(&self, tx: &MutTx, table_name: &str) -> Result<Option<TableId>, DBError> {
11681176
Ok(self.inner.table_id_from_name_mut_tx(tx, table_name)?)
11691177
}

crates/core/src/db/update.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use spacetimedb_lib::identity::AuthCtx;
77
use spacetimedb_lib::AlgebraicValue;
88
use spacetimedb_primitives::{ColSet, TableId};
99
use spacetimedb_schema::auto_migrate::{AutoMigratePlan, ManualMigratePlan, MigratePlan};
10-
use spacetimedb_schema::def::TableDef;
10+
use spacetimedb_schema::def::{TableDef, ViewDef};
1111
use spacetimedb_schema::schema::{column_schemas_from_defs, IndexSchema, Schema, SequenceSchema, TableSchema};
1212

1313
/// The logger used for by [`update_database`] and friends.
@@ -137,6 +137,17 @@ fn auto_migrate_database(
137137

138138
stdb.create_table(tx, table_schema)?;
139139
}
140+
spacetimedb_schema::auto_migrate::AutoMigrateStep::AddView(view_name) => {
141+
let view_def: &ViewDef = plan.new.expect_lookup(view_name);
142+
stdb.create_view(tx, plan.new, view_def)?;
143+
}
144+
spacetimedb_schema::auto_migrate::AutoMigrateStep::RemoveView(view_name) => {
145+
let view_id = stdb.view_id_from_name_mut(tx, view_name)?.unwrap();
146+
stdb.drop_view(tx, view_id)?;
147+
}
148+
spacetimedb_schema::auto_migrate::AutoMigrateStep::UpdateView(_) => {
149+
unimplemented!("Recompute view and update its backing table")
150+
}
140151
spacetimedb_schema::auto_migrate::AutoMigrateStep::AddIndex(index_name) => {
141152
let table_def = plan.new.stored_in_table_def(index_name).unwrap();
142153
let index_def = table_def.indexes.get(index_name).unwrap();

crates/core/src/host/module_host.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -422,7 +422,7 @@ pub fn create_table_from_view_def(
422422
module_def: &ModuleDef,
423423
view_def: &ViewDef,
424424
) -> anyhow::Result<()> {
425-
stdb.create_view_table(tx, module_def, view_def)
425+
stdb.create_view(tx, module_def, view_def)
426426
.with_context(|| format!("failed to create table for view {}", &view_def.name))?;
427427
Ok(())
428428
}

crates/datastore/src/locking_tx_datastore/datastore.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ use spacetimedb_durability::TxOffset;
3535
use spacetimedb_lib::{db::auth::StAccess, metrics::ExecutionMetrics};
3636
use spacetimedb_lib::{ConnectionId, Identity};
3737
use spacetimedb_paths::server::SnapshotDirPath;
38-
use spacetimedb_primitives::{ColList, ConstraintId, IndexId, SequenceId, TableId};
38+
use spacetimedb_primitives::{ColList, ConstraintId, IndexId, SequenceId, TableId, ViewId};
3939
use spacetimedb_sats::{
4040
algebraic_value::de::ValueDeserializer, bsatn, buffer::BufReader, AlgebraicValue, ProductValue,
4141
};
@@ -512,6 +512,10 @@ impl MutTxDatastore for Locking {
512512
tx.rename_table(table_id, new_name)
513513
}
514514

515+
fn view_id_from_name_mut_tx(&self, tx: &Self::MutTx, view_name: &str) -> Result<Option<ViewId>> {
516+
tx.view_id_from_name(view_name)
517+
}
518+
515519
fn table_id_from_name_mut_tx(&self, tx: &Self::MutTx, table_name: &str) -> Result<Option<TableId>> {
516520
tx.table_id_from_name(table_name)
517521
}

crates/datastore/src/locking_tx_datastore/mut_tx.rs

Lines changed: 66 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@ use super::{
99
SharedMutexGuard, SharedWriteGuard,
1010
};
1111
use crate::system_tables::{
12-
system_tables, ConnectionIdViaU128, StConnectionCredentialsFields, StConnectionCredentialsRow, StViewFields,
13-
StViewParamRow, ST_CONNECTION_CREDENTIALS_ID, ST_VIEW_COLUMN_ID, ST_VIEW_ID, ST_VIEW_PARAM_ID,
12+
system_tables, ConnectionIdViaU128, StConnectionCredentialsFields, StConnectionCredentialsRow, StViewColumnFields,
13+
StViewFields, StViewParamFields, StViewParamRow, ST_CONNECTION_CREDENTIALS_ID, ST_VIEW_COLUMN_ID, ST_VIEW_ID,
14+
ST_VIEW_PARAM_ID,
1415
};
1516
use crate::traits::{InsertFlags, RowTypeForTable, TxData, UpdateFlags};
1617
use crate::{
@@ -46,7 +47,7 @@ use spacetimedb_sats::{
4647
AlgebraicType, AlgebraicValue, ProductType, ProductValue, WithTypespace,
4748
};
4849
use spacetimedb_schema::{
49-
def::{ColumnDef, ModuleDef, ViewDef},
50+
def::{ModuleDef, ViewColumnDef, ViewDef},
5051
schema::{ColumnSchema, ConstraintSchema, IndexSchema, RowLevelSecuritySchema, SequenceSchema, TableSchema},
5152
};
5253
use spacetimedb_table::{
@@ -184,7 +185,7 @@ impl MutTxId {
184185
Ok(())
185186
}
186187

187-
/// Create a backing table for a view.
188+
/// Create a backing table for a view and update the system tables.
188189
///
189190
/// Requires:
190191
/// - Everything [`Self::create_table`] requires.
@@ -193,11 +194,7 @@ impl MutTxId {
193194
/// - Everything [`Self::create_table`] ensures.
194195
/// - The returned [`ViewId`] is unique and not [`ViewId::SENTINEL`].
195196
/// - All view metadata maintained by the datastore is created atomically
196-
pub fn create_view_with_backing_table(
197-
&mut self,
198-
module_def: &ModuleDef,
199-
view_def: &ViewDef,
200-
) -> Result<(ViewId, TableId)> {
197+
pub fn create_view(&mut self, module_def: &ModuleDef, view_def: &ViewDef) -> Result<(ViewId, TableId)> {
201198
let table_schema = TableSchema::from_view_def(module_def, view_def);
202199
let table_id = self.create_table(table_schema)?;
203200

@@ -206,16 +203,35 @@ impl MutTxId {
206203
is_anonymous,
207204
is_public,
208205
params,
209-
columns,
206+
return_columns,
210207
..
211208
} = view_def;
212209

213210
let view_id = self.insert_into_st_view(name.clone().into(), table_id, *is_public, *is_anonymous)?;
214211
self.insert_into_st_view_param(view_id, params)?;
215-
self.insert_into_st_view_column(view_id, columns)?;
212+
self.insert_into_st_view_column(view_id, return_columns)?;
216213
Ok((view_id, table_id))
217214
}
218215

216+
/// Drop the backing table of a view and update the system tables.
217+
pub fn drop_view(&mut self, view_id: ViewId) -> Result<()> {
218+
// Drop the view's metadata
219+
self.drop_st_view(view_id)?;
220+
self.drop_st_view_param(view_id)?;
221+
self.drop_st_view_column(view_id)?;
222+
223+
// Drop the view's backing table if materialized
224+
if let StViewRow {
225+
table_id: Some(table_id),
226+
..
227+
} = self.lookup_st_view(view_id)?
228+
{
229+
return self.drop_table(table_id);
230+
};
231+
232+
Ok(())
233+
}
234+
219235
/// Create a table.
220236
///
221237
/// Requires:
@@ -320,6 +336,15 @@ impl MutTxId {
320336
})
321337
}
322338

339+
fn lookup_st_view(&self, view_id: ViewId) -> Result<StViewRow> {
340+
let row = self
341+
.iter_by_col_eq(ST_VIEW_ID, StViewFields::ViewId, &view_id.into())?
342+
.next()
343+
.ok_or_else(|| TableError::IdNotFound(SystemTable::st_view, view_id.into()))?;
344+
345+
StViewRow::try_from(row)
346+
}
347+
323348
/// Insert a row into `st_view`, auto-increments and returns the [`ViewId`].
324349
fn insert_into_st_view(
325350
&mut self,
@@ -365,7 +390,7 @@ impl MutTxId {
365390
}
366391

367392
/// For each column or field returned in a view, insert a row into `st_view_column`.
368-
fn insert_into_st_view_column(&mut self, view_id: ViewId, columns: &[ColumnDef]) -> Result<()> {
393+
fn insert_into_st_view_column(&mut self, view_id: ViewId, columns: &[ViewColumnDef]) -> Result<()> {
369394
for def in columns {
370395
self.insert_via_serialize_bsatn(
371396
ST_VIEW_COLUMN_ID,
@@ -425,6 +450,26 @@ impl MutTxId {
425450
self.delete_col_eq(ST_COLUMN_ID, StColumnFields::TableId.col_id(), &table_id.into())
426451
}
427452

453+
/// Drops the row in `st_table` for this `table_id`
454+
fn drop_st_table(&mut self, table_id: TableId) -> Result<()> {
455+
self.delete_col_eq(ST_TABLE_ID, StTableFields::TableId.col_id(), &table_id.into())
456+
}
457+
458+
/// Drops the row in `st_view` for this `view_id`
459+
fn drop_st_view(&mut self, view_id: ViewId) -> Result<()> {
460+
self.delete_col_eq(ST_VIEW_ID, StViewFields::ViewId.col_id(), &view_id.into())
461+
}
462+
463+
/// Drops the rows in `st_view_param` for this `view_id`
464+
fn drop_st_view_param(&mut self, view_id: ViewId) -> Result<()> {
465+
self.delete_col_eq(ST_VIEW_PARAM_ID, StViewParamFields::ViewId.col_id(), &view_id.into())
466+
}
467+
468+
/// Drops the rows in `st_view_column` for this `view_id`
469+
fn drop_st_view_column(&mut self, view_id: ViewId) -> Result<()> {
470+
self.delete_col_eq(ST_VIEW_COLUMN_ID, StViewColumnFields::ViewId.col_id(), &view_id.into())
471+
}
472+
428473
pub fn drop_table(&mut self, table_id: TableId) -> Result<()> {
429474
self.clear_table(table_id)?;
430475

@@ -443,7 +488,7 @@ impl MutTxId {
443488
}
444489

445490
// Drop the table and their columns
446-
self.delete_col_eq(ST_TABLE_ID, StTableFields::TableId.col_id(), &table_id.into())?;
491+
self.drop_st_table(table_id)?;
447492
self.drop_st_column(table_id)?;
448493

449494
if let Some(schedule) = &schema.schedule {
@@ -492,6 +537,14 @@ impl MutTxId {
492537
Ok(ret)
493538
}
494539

540+
pub fn view_id_from_name(&self, view_name: &str) -> Result<Option<ViewId>> {
541+
let view_name = &view_name.into();
542+
let row = self
543+
.iter_by_col_eq(ST_VIEW_ID, StViewFields::ViewName, view_name)?
544+
.next();
545+
Ok(row.map(|row| row.read_col(StViewFields::ViewId).unwrap()))
546+
}
547+
495548
pub fn table_id_from_name(&self, table_name: &str) -> Result<Option<TableId>> {
496549
let table_name = &table_name.into();
497550
let row = self

crates/datastore/src/system_tables.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ pub const ST_RESERVED_SEQUENCE_RANGE: u32 = 4096;
107107
#[derive(Debug, Display)]
108108
pub enum SystemTable {
109109
st_table,
110+
st_view,
110111
st_column,
111112
st_sequence,
112113
st_index,
@@ -749,6 +750,13 @@ pub struct StViewRow {
749750
pub is_anonymous: bool,
750751
}
751752

753+
impl TryFrom<RowRef<'_>> for StViewRow {
754+
type Error = DatastoreError;
755+
fn try_from(row: RowRef<'_>) -> Result<Self, DatastoreError> {
756+
read_via_bsatn(row)
757+
}
758+
}
759+
752760
/// A wrapper around `AlgebraicType` that acts like `AlgegbraicType::bytes()` for serialization purposes.
753761
#[derive(Debug, Clone, PartialEq, Eq)]
754762
pub struct AlgebraicTypeViaBytes(pub AlgebraicType);

crates/datastore/src/traits.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -509,6 +509,7 @@ pub trait MutTxDatastore: TxDatastore + MutTx {
509509
fn schema_for_table_mut_tx(&self, tx: &Self::MutTx, table_id: TableId) -> Result<Arc<TableSchema>>;
510510
fn drop_table_mut_tx(&self, tx: &mut Self::MutTx, table_id: TableId) -> Result<()>;
511511
fn rename_table_mut_tx(&self, tx: &mut Self::MutTx, table_id: TableId, new_name: &str) -> Result<()>;
512+
fn view_id_from_name_mut_tx(&self, tx: &Self::MutTx, view_name: &str) -> Result<Option<ViewId>>;
512513
fn table_id_from_name_mut_tx(&self, tx: &Self::MutTx, table_name: &str) -> Result<Option<TableId>>;
513514
fn table_id_exists_mut_tx(&self, tx: &Self::MutTx, table_id: &TableId) -> bool;
514515
fn table_name_from_id_mut_tx<'a>(&'a self, tx: &'a Self::MutTx, table_id: TableId) -> Result<Option<Cow<'a, str>>>;

crates/sats/src/convert.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use crate::sum_value::SumTag;
22
use crate::{i256, u256};
33
use crate::{AlgebraicType, AlgebraicValue, ProductType, ProductValue};
4-
use spacetimedb_primitives::{ColId, ConstraintId, IndexId, ScheduleId, SequenceId, TableId};
4+
use spacetimedb_primitives::{ColId, ConstraintId, IndexId, ScheduleId, SequenceId, TableId, ViewId};
55

66
impl crate::Value for AlgebraicValue {
77
type Type = AlgebraicType;
@@ -64,6 +64,7 @@ macro_rules! system_id {
6464
};
6565
}
6666
system_id!(TableId);
67+
system_id!(ViewId);
6768
system_id!(ColId);
6869
system_id!(SequenceId);
6970
system_id!(IndexId);

0 commit comments

Comments
 (0)