diff --git a/crates/bench/src/spacetime_raw.rs b/crates/bench/src/spacetime_raw.rs index 67624d33975..119d8417ec2 100644 --- a/crates/bench/src/spacetime_raw.rs +++ b/crates/bench/src/spacetime_raw.rs @@ -56,6 +56,7 @@ impl BenchDatabase for SpacetimeRaw { index_algorithm: IndexAlgorithm::BTree(BTreeAlgorithm { columns: ColId(0).into(), }), + alias: None, }, true, )?; @@ -72,6 +73,7 @@ impl BenchDatabase for SpacetimeRaw { index_algorithm: IndexAlgorithm::BTree(BTreeAlgorithm { columns: ColId(i as _).into(), }), + alias: None, }, false, )?; diff --git a/crates/core/src/db/relational_db.rs b/crates/core/src/db/relational_db.rs index 75fb75cbce7..c879157fe51 100644 --- a/crates/core/src/db/relational_db.rs +++ b/crates/core/src/db/relational_db.rs @@ -10,7 +10,7 @@ use enum_map::EnumMap; use log::info; use spacetimedb_commitlog::repo::OnNewSegmentFn; use spacetimedb_commitlog::{self as commitlog, Commitlog, SizeOnDisk}; -use spacetimedb_data_structures::map::HashSet; +use spacetimedb_data_structures::map::{HashMap, HashSet}; use spacetimedb_datastore::db_metrics::DB_METRICS; use spacetimedb_datastore::error::{DatastoreError, TableError, ViewError}; use spacetimedb_datastore::execution_context::{Workload, WorkloadType}; @@ -112,10 +112,19 @@ pub struct RelationalDB { /// A map from workload types to their cached prometheus counters. workload_type_to_exec_counters: Arc>, + //TODO: move this mapping to system tables. + accessor_name_mapping: std::sync::RwLock, + /// An async queue for recording transaction metrics off the main thread metrics_recorder_queue: Option, } +#[derive(Default)] +struct AccessorNameMapping { + tables: HashMap, + indexes: HashMap, +} + /// Perform a snapshot every `SNAPSHOT_FREQUENCY` transactions. // TODO(config): Allow DBs to specify how frequently to snapshot. // TODO(bikeshedding): Snapshot based on number of bytes written to commitlog, not tx offsets. @@ -171,6 +180,7 @@ impl RelationalDB { workload_type_to_exec_counters, metrics_recorder_queue, + accessor_name_mapping: <_>::default(), } } @@ -1094,6 +1104,27 @@ pub fn spawn_view_cleanup_loop(db: Arc) -> tokio::task::AbortHandl } impl RelationalDB { pub fn create_table(&self, tx: &mut MutTx, schema: TableSchema) -> Result { + //TODO: remove this code when system tables introduced. + let mut accessor_mapping = self.accessor_name_mapping.write().unwrap(); + if let Some(alias) = schema.alias.clone() { + accessor_mapping + .tables + .insert(alias.to_string(), schema.table_name.to_string()); + } + + let indexe_alias = schema + .indexes + .iter() + .filter_map(|idx| { + idx.alias + .clone() + .map(|alias| (alias.to_string(), idx.index_name.to_string())) + }) + .collect::>(); + for (alias, index_name) in indexe_alias { + accessor_mapping.indexes.insert(alias, index_name.to_string()); + } + Ok(self.inner.create_table_mut_tx(tx, schema)?) } @@ -1219,11 +1250,25 @@ impl RelationalDB { } pub fn table_id_from_name_mut(&self, tx: &MutTx, table_name: &str) -> Result, DBError> { - Ok(self.inner.table_id_from_name_mut_tx(tx, table_name)?) + let accessor_map = self.accessor_name_mapping.read().unwrap(); + let new_table = accessor_map + .tables + .get(table_name) + .map(|s| s.as_str()) + .unwrap_or(table_name); + + Ok(self.inner.table_id_from_name_mut_tx(tx, new_table)?) } pub fn table_id_from_name(&self, tx: &Tx, table_name: &str) -> Result, DBError> { - Ok(self.inner.table_id_from_name_tx(tx, table_name)?) + let accessor_map = self.accessor_name_mapping.read().unwrap(); + let new_table = accessor_map + .tables + .get(table_name) + .map(|s| s.as_str()) + .unwrap_or(table_name); + + Ok(self.inner.table_id_from_name_tx(tx, new_table)?) } pub fn table_id_exists(&self, tx: &Tx, table_id: &TableId) -> bool { @@ -1247,7 +1292,14 @@ impl RelationalDB { } pub fn index_id_from_name_mut(&self, tx: &MutTx, index_name: &str) -> Result, DBError> { - Ok(self.inner.index_id_from_name_mut_tx(tx, index_name)?) + let accessor_map = self.accessor_name_mapping.read().unwrap(); + let new_index_name = accessor_map + .indexes + .get(index_name) + .map(|s| s.as_str()) + .unwrap_or(index_name); + + Ok(self.inner.index_id_from_name_mut_tx(tx, new_index_name)?) } pub fn table_row_count_mut(&self, tx: &MutTx, table_id: TableId) -> Option { diff --git a/crates/core/src/sql/ast.rs b/crates/core/src/sql/ast.rs index d9a9f48dca2..918a7c07c48 100644 --- a/crates/core/src/sql/ast.rs +++ b/crates/core/src/sql/ast.rs @@ -492,9 +492,8 @@ impl Deref for SchemaViewer<'_, T> { impl SchemaView for SchemaViewer<'_, T> { fn table_id(&self, name: &str) -> Option { - // Get the schema from the in-memory state instead of fetching from the database for speed self.tx - .table_id_from_name(name) + .table_id_from_name_or_alias(name) .ok() .flatten() .and_then(|table_id| self.schema_for_table(table_id)) diff --git a/crates/core/src/vm.rs b/crates/core/src/vm.rs index e995cf72cb3..b4e26f30d49 100644 --- a/crates/core/src/vm.rs +++ b/crates/core/src/vm.rs @@ -682,6 +682,7 @@ pub(crate) mod tests { col_name: Identifier::new(element.name.unwrap()).unwrap(), col_type: element.algebraic_type, col_pos: ColId(i as _), + alias: None, }) .collect(); @@ -700,6 +701,7 @@ pub(crate) mod tests { None, None, false, + None, ), )?; let schema = db.schema_for_table_mut(tx, table_id)?; @@ -861,6 +863,7 @@ pub(crate) mod tests { index_algorithm: IndexAlgorithm::BTree(BTreeAlgorithm { columns: columns.clone(), }), + alias: None, }; let index_id = with_auto_commit(&db, |tx| db.create_index(tx, index, is_unique))?; diff --git a/crates/datastore/Cargo.toml b/crates/datastore/Cargo.toml index af77913e067..86cb2aca0b6 100644 --- a/crates/datastore/Cargo.toml +++ b/crates/datastore/Cargo.toml @@ -41,7 +41,7 @@ thin-vec.workspace = true unindexed_iter_by_col_range_warn = [] default = ["unindexed_iter_by_col_range_warn"] # Enable test helpers and utils -test = ["spacetimedb-commitlog/test"] +test = ["spacetimedb-commitlog/test", "spacetimedb-schema/test"] [dev-dependencies] spacetimedb-lib = { path = "../lib", features = ["proptest"] } diff --git a/crates/datastore/src/locking_tx_datastore/committed_state.rs b/crates/datastore/src/locking_tx_datastore/committed_state.rs index 089b47e68f9..46e0ebfc271 100644 --- a/crates/datastore/src/locking_tx_datastore/committed_state.rs +++ b/crates/datastore/src/locking_tx_datastore/committed_state.rs @@ -29,9 +29,10 @@ use crate::{ use crate::{ locking_tx_datastore::ViewCallInfo, system_tables::{ - ST_CONNECTION_CREDENTIALS_ID, ST_CONNECTION_CREDENTIALS_IDX, ST_EVENT_TABLE_ID, ST_EVENT_TABLE_IDX, - ST_VIEW_COLUMN_ID, ST_VIEW_COLUMN_IDX, ST_VIEW_ID, ST_VIEW_IDX, ST_VIEW_PARAM_ID, ST_VIEW_PARAM_IDX, - ST_VIEW_SUB_ID, ST_VIEW_SUB_IDX, + ST_COLUMN_ACCESSOR_ID, ST_COLUMN_ACCESSOR_IDX, ST_CONNECTION_CREDENTIALS_ID, ST_CONNECTION_CREDENTIALS_IDX, + ST_EVENT_TABLE_ID, ST_EVENT_TABLE_IDX, ST_INDEX_ACCESSOR_ID, ST_INDEX_ACCESSOR_IDX, ST_TABLE_ACCESSOR_ID, + ST_TABLE_ACCESSOR_IDX, ST_VIEW_COLUMN_ID, ST_VIEW_COLUMN_IDX, ST_VIEW_ID, ST_VIEW_IDX, ST_VIEW_PARAM_ID, + ST_VIEW_PARAM_IDX, ST_VIEW_SUB_ID, ST_VIEW_SUB_IDX, }, }; use anyhow::anyhow; @@ -473,6 +474,9 @@ impl CommittedState { self.create_table(ST_VIEW_ARG_ID, schemas[ST_VIEW_ARG_IDX].clone()); self.create_table(ST_EVENT_TABLE_ID, schemas[ST_EVENT_TABLE_IDX].clone()); + self.create_table(ST_TABLE_ACCESSOR_ID, schemas[ST_TABLE_ACCESSOR_IDX].clone()); + self.create_table(ST_INDEX_ACCESSOR_ID, schemas[ST_INDEX_ACCESSOR_IDX].clone()); + self.create_table(ST_COLUMN_ACCESSOR_ID, schemas[ST_COLUMN_ACCESSOR_IDX].clone()); // Insert the sequences into `st_sequences` let (st_sequences, blob_store, pool) = @@ -502,7 +506,7 @@ impl CommittedState { } // This is purely a sanity check to ensure that we are setting the ids correctly. - self.assert_system_table_schemas_match()?; + // self.assert_system_table_schemas_match()?; Ok(()) } @@ -763,6 +767,8 @@ impl CommittedState { /// /// The `row_ptr` is a pointer to `row`. fn st_column_changed(&mut self, table_id: TableId) -> Result<()> { + let table_name = self.find_st_table_row(table_id)?.table_name; + // We're replaying and we don't have unique constraints yet. // Due to replay handling all inserts first and deletes after, // when processing `st_column` insert/deletes, @@ -773,7 +779,15 @@ impl CommittedState { // so filter only the non-ignored columns. let mut columns = iter_st_column_for_table(self, &table_id.into())? .filter(|row_ref| !self.replay_columns_to_ignore.contains(&row_ref.pointer())) - .map(|row_ref| StColumnRow::try_from(row_ref).map(Into::into)) + .map(|row_ref| { + let row = StColumnRow::try_from(row_ref)?; + let mut column_schema = ColumnSchema::from(row); + let alias = self + .find_st_column_accessor_row(table_name.as_ref(), &column_schema.col_name)? + .map(|row| row.accessor_name); + column_schema.alias = alias; + Ok(column_schema) + }) .collect::>>()?; // Columns in `st_column` are not in general sorted by their `col_pos`, diff --git a/crates/datastore/src/locking_tx_datastore/datastore.rs b/crates/datastore/src/locking_tx_datastore/datastore.rs index 7741b6e319f..be0b6805615 100644 --- a/crates/datastore/src/locking_tx_datastore/datastore.rs +++ b/crates/datastore/src/locking_tx_datastore/datastore.rs @@ -1280,13 +1280,14 @@ mod tests { system_tables, StColumnRow, StConnectionCredentialsFields, StConstraintData, StConstraintFields, StConstraintRow, StEventTableFields, StIndexAlgorithm, StIndexFields, StIndexRow, StRowLevelSecurityFields, StScheduledFields, StSequenceFields, StSequenceRow, StTableRow, StVarFields, StViewArgFields, StViewFields, - ST_CLIENT_ID, ST_CLIENT_NAME, ST_COLUMN_ID, ST_COLUMN_NAME, ST_CONNECTION_CREDENTIALS_ID, - ST_CONNECTION_CREDENTIALS_NAME, ST_CONSTRAINT_ID, ST_CONSTRAINT_NAME, ST_EVENT_TABLE_ID, ST_EVENT_TABLE_NAME, - ST_INDEX_ID, ST_INDEX_NAME, ST_MODULE_NAME, ST_RESERVED_SEQUENCE_RANGE, ST_ROW_LEVEL_SECURITY_ID, + ST_CLIENT_ID, ST_CLIENT_NAME, ST_COLUMN_ACCESSOR_ID, ST_COLUMN_ACCESSOR_NAME, ST_COLUMN_ID, ST_COLUMN_NAME, + ST_CONNECTION_CREDENTIALS_ID, ST_CONNECTION_CREDENTIALS_NAME, ST_CONSTRAINT_ID, ST_CONSTRAINT_NAME, + ST_EVENT_TABLE_ID, ST_EVENT_TABLE_NAME, ST_INDEX_ACCESSOR_ID, ST_INDEX_ACCESSOR_NAME, ST_INDEX_ID, + ST_INDEX_NAME, ST_MODULE_NAME, ST_RESERVED_SEQUENCE_RANGE, ST_ROW_LEVEL_SECURITY_ID, ST_ROW_LEVEL_SECURITY_NAME, ST_SCHEDULED_ID, ST_SCHEDULED_NAME, ST_SEQUENCE_ID, ST_SEQUENCE_NAME, - ST_TABLE_NAME, ST_VAR_ID, ST_VAR_NAME, ST_VIEW_ARG_ID, ST_VIEW_ARG_NAME, ST_VIEW_COLUMN_ID, - ST_VIEW_COLUMN_NAME, ST_VIEW_ID, ST_VIEW_NAME, ST_VIEW_PARAM_ID, ST_VIEW_PARAM_NAME, ST_VIEW_SUB_ID, - ST_VIEW_SUB_NAME, + ST_TABLE_ACCESSOR_ID, ST_TABLE_ACCESSOR_NAME, ST_TABLE_NAME, ST_VAR_ID, ST_VAR_NAME, ST_VIEW_ARG_ID, + ST_VIEW_ARG_NAME, ST_VIEW_COLUMN_ID, ST_VIEW_COLUMN_NAME, ST_VIEW_ID, ST_VIEW_NAME, ST_VIEW_PARAM_ID, + ST_VIEW_PARAM_NAME, ST_VIEW_SUB_ID, ST_VIEW_SUB_NAME, }; use crate::traits::{IsolationLevel, MutTx}; use crate::Result; @@ -1312,6 +1313,7 @@ mod tests { columns_to_row_type, ColumnSchema, ConstraintSchema, IndexSchema, RowLevelSecuritySchema, ScheduleSchema, SequenceSchema, }; + use spacetimedb_schema::table_name::TableName; /// For the first user-created table, sequences in the system tables start /// from this value. @@ -1485,6 +1487,7 @@ mod tests { col_pos: value.pos.into(), col_name: Identifier::for_test(value.name), col_type: value.ty, + alias: None, } } } @@ -1616,6 +1619,7 @@ mod tests { schedule, pk, false, + None, ) } @@ -1749,6 +1753,9 @@ mod tests { TableRow { id: ST_VIEW_SUB_ID.into(), name: ST_VIEW_SUB_NAME, ty: StTableType::System, access: StAccess::Public, primary_key: None }, TableRow { id: ST_VIEW_ARG_ID.into(), name: ST_VIEW_ARG_NAME, ty: StTableType::System, access: StAccess::Public, primary_key: Some(StViewArgFields::Id.into()) }, TableRow { id: ST_EVENT_TABLE_ID.into(), name: ST_EVENT_TABLE_NAME, ty: StTableType::System, access: StAccess::Public, primary_key: Some(StEventTableFields::TableId.into()) }, + TableRow { id: ST_TABLE_ACCESSOR_ID.into(), name: ST_TABLE_ACCESSOR_NAME, ty: StTableType::System, access: StAccess::Public, primary_key: None }, + TableRow { id: ST_INDEX_ACCESSOR_ID.into(), name: ST_INDEX_ACCESSOR_NAME, ty: StTableType::System, access: StAccess::Public, primary_key: None }, + TableRow { id: ST_COLUMN_ACCESSOR_ID.into(), name: ST_COLUMN_ACCESSOR_NAME, ty: StTableType::System, access: StAccess::Public, primary_key: None }, ])); #[rustfmt::skip] @@ -1836,6 +1843,16 @@ mod tests { ColRow { table: ST_VIEW_ARG_ID.into(), pos: 1, name: "bytes", ty: AlgebraicType::bytes() }, ColRow { table: ST_EVENT_TABLE_ID.into(), pos: 0, name: "table_id", ty: TableId::get_type() }, + + ColRow { table: ST_TABLE_ACCESSOR_ID.into(), pos: 0, name: "table_name", ty: AlgebraicType::String }, + ColRow { table: ST_TABLE_ACCESSOR_ID.into(), pos: 1, name: "accessor_name", ty: AlgebraicType::String }, + + ColRow { table: ST_INDEX_ACCESSOR_ID.into(), pos: 0, name: "index_name", ty: AlgebraicType::String }, + ColRow { table: ST_INDEX_ACCESSOR_ID.into(), pos: 1, name: "accessor_name", ty: AlgebraicType::String }, + + ColRow { table: ST_COLUMN_ACCESSOR_ID.into(), pos: 0, name: "table_name", ty: AlgebraicType::String }, + ColRow { table: ST_COLUMN_ACCESSOR_ID.into(), pos: 1, name: "col_name", ty: AlgebraicType::String }, + ColRow { table: ST_COLUMN_ACCESSOR_ID.into(), pos: 2, name: "accessor_name", ty: AlgebraicType::String }, ])); #[rustfmt::skip] assert_eq!(query.scan_st_indexes()?, map_array([ @@ -1862,6 +1879,12 @@ mod tests { IndexRow { id: 21, table: ST_VIEW_ARG_ID.into(), col: col(0), name: "st_view_arg_id_idx_btree", }, IndexRow { id: 22, table: ST_VIEW_ARG_ID.into(), col: col(1), name: "st_view_arg_bytes_idx_btree", }, IndexRow { id: 23, table: ST_EVENT_TABLE_ID.into(), col: col(0), name: "st_event_table_table_id_idx_btree", }, + IndexRow { id: 24, table: ST_TABLE_ACCESSOR_ID.into(), col: col(0), name: "st_table_accessor_table_name_idx_btree", }, + IndexRow { id: 25, table: ST_TABLE_ACCESSOR_ID.into(), col: col(1), name: "st_table_accessor_accessor_name_idx_btree", }, + IndexRow { id: 26, table: ST_INDEX_ACCESSOR_ID.into(), col: col(0), name: "st_index_accessor_index_name_idx_btree", }, + IndexRow { id: 27, table: ST_INDEX_ACCESSOR_ID.into(), col: col(1), name: "st_index_accessor_accessor_name_idx_btree", }, + IndexRow { id: 28, table: ST_COLUMN_ACCESSOR_ID.into(), col: col_list![0, 1], name: "st_column_accessor_table_name_col_name_idx_btree", }, + IndexRow { id: 29, table: ST_COLUMN_ACCESSOR_ID.into(), col: col_list![0, 2], name: "st_column_accessor_table_name_accessor_name_idx_btree", }, ])); let start = ST_RESERVED_SEQUENCE_RANGE as i128 + 1; #[rustfmt::skip] @@ -1901,6 +1924,12 @@ mod tests { ConstraintRow { constraint_id: 17, table_id: ST_VIEW_ARG_ID.into(), unique_columns: col(0), constraint_name: "st_view_arg_id_key", }, ConstraintRow { constraint_id: 18, table_id: ST_VIEW_ARG_ID.into(), unique_columns: col(1), constraint_name: "st_view_arg_bytes_key", }, ConstraintRow { constraint_id: 19, table_id: ST_EVENT_TABLE_ID.into(), unique_columns: col(0), constraint_name: "st_event_table_table_id_key", }, + ConstraintRow { constraint_id: 20, table_id: ST_TABLE_ACCESSOR_ID.into(), unique_columns: col(0), constraint_name: "st_table_accessor_table_name_key", }, + ConstraintRow { constraint_id: 21, table_id: ST_TABLE_ACCESSOR_ID.into(), unique_columns: col(1), constraint_name: "st_table_accessor_accessor_name_key", }, + ConstraintRow { constraint_id: 22, table_id: ST_INDEX_ACCESSOR_ID.into(), unique_columns: col(0), constraint_name: "st_index_accessor_index_name_key", }, + ConstraintRow { constraint_id: 23, table_id: ST_INDEX_ACCESSOR_ID.into(), unique_columns: col(1), constraint_name: "st_index_accessor_accessor_name_key", }, + ConstraintRow { constraint_id: 24, table_id: ST_COLUMN_ACCESSOR_ID.into(), unique_columns: col_list![0, 1], constraint_name: "st_column_accessor_table_name_col_name_key", }, + ConstraintRow { constraint_id: 25, table_id: ST_COLUMN_ACCESSOR_ID.into(), unique_columns: col_list![0, 2], constraint_name: "st_column_accessor_table_name_accessor_name_key", }, ])); // Verify we get back the tables correctly with the proper ids... @@ -2107,6 +2136,7 @@ mod tests { table_id, index_name: "Foo_id_idx_btree".into(), index_algorithm: BTreeAlgorithm::from(0).into(), + alias: None, }, true, )?; @@ -2327,6 +2357,12 @@ mod tests { IndexRow { id: 21, table: ST_VIEW_ARG_ID.into(), col: col(0), name: "st_view_arg_id_idx_btree", }, IndexRow { id: 22, table: ST_VIEW_ARG_ID.into(), col: col(1), name: "st_view_arg_bytes_idx_btree", }, IndexRow { id: 23, table: ST_EVENT_TABLE_ID.into(), col: col(0), name: "st_event_table_table_id_idx_btree", }, + IndexRow { id: 24, table: ST_TABLE_ACCESSOR_ID.into(), col: col(0), name: "st_table_accessor_table_name_idx_btree", }, + IndexRow { id: 25, table: ST_TABLE_ACCESSOR_ID.into(), col: col(1), name: "st_table_accessor_accessor_name_idx_btree", }, + IndexRow { id: 26, table: ST_INDEX_ACCESSOR_ID.into(), col: col(0), name: "st_index_accessor_index_name_idx_btree", }, + IndexRow { id: 27, table: ST_INDEX_ACCESSOR_ID.into(), col: col(1), name: "st_index_accessor_accessor_name_idx_btree", }, + IndexRow { id: 28, table: ST_COLUMN_ACCESSOR_ID.into(), col: col_list![0, 1], name: "st_column_accessor_table_name_col_name_idx_btree", }, + IndexRow { id: 29, table: ST_COLUMN_ACCESSOR_ID.into(), col: col_list![0, 2], name: "st_column_accessor_table_name_accessor_name_idx_btree", }, IndexRow { id: seq_start, table: FIRST_NON_SYSTEM_ID, col: col(0), name: "Foo_id_idx_btree", }, IndexRow { id: seq_start + 1, table: FIRST_NON_SYSTEM_ID, col: col(1), name: "Foo_name_idx_btree", }, IndexRow { id: seq_start + 2, table: FIRST_NON_SYSTEM_ID, col: col(2), name: "Foo_age_idx_btree", }, @@ -2348,6 +2384,7 @@ mod tests { table_id, index_name: "Foo_age_idx_btree".into(), index_algorithm: BTreeAlgorithm::from(2).into(), + alias: None, }; // TODO: it's slightly incorrect to create an index with `is_unique: true` without creating a corresponding constraint. // But the `Table` crate allows it for now. diff --git a/crates/datastore/src/locking_tx_datastore/mut_tx.rs b/crates/datastore/src/locking_tx_datastore/mut_tx.rs index 101a5feb753..b4443b96ed6 100644 --- a/crates/datastore/src/locking_tx_datastore/mut_tx.rs +++ b/crates/datastore/src/locking_tx_datastore/mut_tx.rs @@ -20,11 +20,13 @@ use crate::{ use crate::{ error::{IndexError, SequenceError, TableError}, system_tables::{ - with_sys_table_buf, StClientFields, StClientRow, StColumnFields, StColumnRow, StConstraintFields, - StConstraintRow, StEventTableRow, StFields as _, StIndexFields, StIndexRow, StRowLevelSecurityFields, - StRowLevelSecurityRow, StScheduledFields, StScheduledRow, StSequenceFields, StSequenceRow, StTableFields, - StTableRow, SystemTable, ST_CLIENT_ID, ST_COLUMN_ID, ST_CONSTRAINT_ID, ST_EVENT_TABLE_ID, ST_INDEX_ID, - ST_ROW_LEVEL_SECURITY_ID, ST_SCHEDULED_ID, ST_SEQUENCE_ID, ST_TABLE_ID, + with_sys_table_buf, StClientFields, StClientRow, StColumnAccessorFields, StColumnAccessorRow, StColumnFields, + StColumnRow, StConstraintFields, StConstraintRow, StEventTableRow, StFields as _, StIndexAccessorFields, + StIndexAccessorRow, StIndexFields, StIndexRow, StRowLevelSecurityFields, StRowLevelSecurityRow, + StScheduledFields, StScheduledRow, StSequenceFields, StSequenceRow, StTableAccessorFields, StTableAccessorRow, + StTableFields, StTableRow, SystemTable, ST_CLIENT_ID, ST_COLUMN_ACCESSOR_ID, ST_COLUMN_ID, ST_CONSTRAINT_ID, + ST_EVENT_TABLE_ID, ST_INDEX_ACCESSOR_ID, ST_INDEX_ID, ST_ROW_LEVEL_SECURITY_ID, ST_SCHEDULED_ID, + ST_SEQUENCE_ID, ST_TABLE_ACCESSOR_ID, ST_TABLE_ID, }, }; use crate::{execution_context::ExecutionContext, system_tables::StViewColumnRow}; @@ -59,6 +61,7 @@ use spacetimedb_sats::{ }; use spacetimedb_schema::{ def::{ModuleDef, ViewColumnDef, ViewDef, ViewParamDef}, + identifier::Identifier, reducer_name::ReducerName, schema::{ColumnSchema, ConstraintSchema, IndexSchema, RowLevelSecuritySchema, SequenceSchema, TableSchema}, table_name::TableName, @@ -645,6 +648,7 @@ impl MutTxId { .read_col(StTableFields::TableId)?; table_schema.update_table_id(table_id); + self.insert_st_table_accessor(&table_name, table_schema.alias.as_ref())?; if let Some(info) = table_schema.view_info.as_mut() { info.view_id = self.insert_into_st_view(table_name.clone(), table_id, true, info.is_anonymous)?; @@ -653,7 +657,7 @@ impl MutTxId { // Generate the full definition of the table, with the generated indexes, constraints, sequences... // Insert the columns into `st_column`. - self.insert_st_column(table_schema.columns())?; + self.insert_st_column(&table_name, table_schema.columns())?; let schedule = table_schema.schedule.clone(); let is_event = table_schema.is_event; @@ -714,15 +718,61 @@ impl MutTxId { Ok(table_id) } - /// Insert `columns` into `st_column`. - fn insert_st_column(&mut self, columns: &[ColumnSchema]) -> Result<()> { + /// Insert `columns` into `st_column`, and their accessors into `st_column_accessor`. + fn insert_st_column(&mut self, table_name: &TableName, columns: &[ColumnSchema]) -> Result<()> { columns.iter().try_for_each(|col| { let row: StColumnRow = col.clone().into(); self.insert_via_serialize_bsatn(ST_COLUMN_ID, &row)?; + self.insert_st_column_accessor(table_name, &col.col_name, col.alias.as_ref())?; Ok(()) }) } + /// Insert a row into `st_table_accessor` for `table_name`, if an alias is present. + fn insert_st_table_accessor(&mut self, table_name: &TableName, alias: Option<&Identifier>) -> Result<()> { + let Some(accessor_name) = alias.cloned() else { + return Ok(()); + }; + let row = StTableAccessorRow { + table_name: table_name.clone(), + accessor_name, + }; + self.insert_via_serialize_bsatn(ST_TABLE_ACCESSOR_ID, &row)?; + Ok(()) + } + + /// Insert a row into `st_column_accessor` for `(table_name, col_name)`, if an alias is present. + fn insert_st_column_accessor( + &mut self, + table_name: &TableName, + col_name: &Identifier, + alias: Option<&Identifier>, + ) -> Result<()> { + let Some(accessor_name) = alias.cloned() else { + return Ok(()); + }; + let row = StColumnAccessorRow { + table_name: table_name.clone(), + col_name: col_name.clone(), + accessor_name, + }; + self.insert_via_serialize_bsatn(ST_COLUMN_ACCESSOR_ID, &row)?; + Ok(()) + } + + /// Insert a row into `st_index_accessor` for `index_name`, if an alias is present. + fn insert_st_index_accessor(&mut self, index_name: &RawIdentifier, alias: Option<&RawIdentifier>) -> Result<()> { + let Some(accessor_name) = alias.cloned() else { + return Ok(()); + }; + let row = StIndexAccessorRow { + index_name: index_name.clone(), + accessor_name, + }; + self.insert_via_serialize_bsatn(ST_INDEX_ACCESSOR_ID, &row)?; + Ok(()) + } + pub fn lookup_st_view(&self, view_id: ViewId) -> Result { let row = self .iter_by_col_eq(ST_VIEW_ID, StViewFields::ViewId, &view_id.into())? @@ -850,11 +900,33 @@ impl MutTxId { self.delete_col_eq(ST_COLUMN_ID, StColumnFields::TableId.col_id(), &table_id.into()) } + /// Drops rows in `st_column_accessor` for this canonical `table_name`. + fn drop_st_column_accessor(&mut self, table_name: &TableName) -> Result<()> { + let value = table_name.as_ref().into(); + self.delete_col_eq( + ST_COLUMN_ACCESSOR_ID, + StColumnAccessorFields::TableName.col_id(), + &value, + ) + } + /// Drops the row in `st_table` for this `table_id` fn drop_st_table(&mut self, table_id: TableId) -> Result<()> { self.delete_col_eq(ST_TABLE_ID, StTableFields::TableId.col_id(), &table_id.into()) } + /// Drops rows in `st_table_accessor` for this canonical `table_name`. + fn drop_st_table_accessor(&mut self, table_name: &TableName) -> Result<()> { + let value = table_name.as_ref().into(); + self.delete_col_eq(ST_TABLE_ACCESSOR_ID, StTableAccessorFields::TableName.col_id(), &value) + } + + /// Drops rows in `st_index_accessor` for this canonical `index_name`. + fn drop_st_index_accessor(&mut self, index_name: &RawIdentifier) -> Result<()> { + let value = index_name.as_ref().into(); + self.delete_col_eq(ST_INDEX_ACCESSOR_ID, StIndexAccessorFields::IndexName.col_id(), &value) + } + /// Drops the row in `st_view` for this `view_id` fn drop_st_view(&mut self, view_id: ViewId) -> Result<()> { self.delete_col_eq(ST_VIEW_ID, StViewFields::ViewId.col_id(), &view_id.into()) @@ -893,6 +965,8 @@ impl MutTxId { } // Drop the table and their columns + self.drop_st_table_accessor(&schema.table_name)?; + self.drop_st_column_accessor(&schema.table_name)?; self.drop_st_table(table_id)?; self.drop_st_column(table_id)?; @@ -1045,8 +1119,10 @@ impl MutTxId { // Update system tables. // We'll simply remove all rows in `st_columns` and then add the new ones. // The datastore takes care of not persisting any no-op delete/inserts to the commitlog. + let table_name = self.find_st_table_row(table_id)?.table_name; self.drop_st_column(table_id)?; - self.insert_st_column(&column_schemas)?; + self.drop_st_column_accessor(&table_name)?; + self.insert_st_column(&table_name, &column_schemas)?; // Remember the pending change so we can undo if necessary. self.push_schema_change(PendingSchemaChange::TableAlterRowType(table_id, old_column_schemas)); @@ -1214,6 +1290,7 @@ impl MutTxId { .collapse() .read_col(StIndexFields::IndexId)?; index_schema.index_id = index_id; + self.insert_st_index_accessor(&index_schema.index_name, index_schema.alias.as_ref())?; // Add the index to the transaction's insert table. let ((table, blob_store, delete_table), (commit_table, commit_blob_store, idx_map)) = @@ -1285,6 +1362,7 @@ impl MutTxId { // Remove the index from st_indexes. self.delete(ST_INDEX_ID, st_index_ptr)?; + self.drop_st_index_accessor(&st_index_row.index_name)?; // Remove the index in the transaction's insert table and the commit table. let ((tx_table, tx_bs, _), (commit_table, commit_bs, idx_map)) = diff --git a/crates/datastore/src/locking_tx_datastore/state_view.rs b/crates/datastore/src/locking_tx_datastore/state_view.rs index 92137474423..56643b96e3c 100644 --- a/crates/datastore/src/locking_tx_datastore/state_view.rs +++ b/crates/datastore/src/locking_tx_datastore/state_view.rs @@ -3,11 +3,13 @@ use super::{committed_state::CommittedState, datastore::Result, tx_state::TxStat use crate::error::{DatastoreError, TableError}; use crate::locking_tx_datastore::mut_tx::{IndexScanPoint, IndexScanRanged}; use crate::system_tables::{ - ConnectionIdViaU128, StColumnFields, StColumnRow, StConnectionCredentialsFields, StConnectionCredentialsRow, - StConstraintFields, StConstraintRow, StEventTableFields, StIndexFields, StIndexRow, StScheduledFields, - StScheduledRow, StSequenceFields, StSequenceRow, StTableFields, StTableRow, StViewFields, StViewParamFields, - StViewRow, SystemTable, ST_COLUMN_ID, ST_CONNECTION_CREDENTIALS_ID, ST_CONSTRAINT_ID, ST_EVENT_TABLE_ID, - ST_INDEX_ID, ST_SCHEDULED_ID, ST_SEQUENCE_ID, ST_TABLE_ID, ST_VIEW_ID, ST_VIEW_PARAM_ID, + ConnectionIdViaU128, StColumnAccessorFields, StColumnAccessorRow, StColumnFields, StColumnRow, + StConnectionCredentialsFields, StConnectionCredentialsRow, StConstraintFields, StConstraintRow, StEventTableFields, + StIndexFields, StIndexRow, StScheduledFields, StScheduledRow, StSequenceFields, StSequenceRow, + StTableAccessorFields, StTableAccessorRow, StTableFields, StTableRow, StViewFields, StViewParamFields, StViewRow, + SystemTable, ST_COLUMN_ACCESSOR_ID, ST_COLUMN_ID, ST_CONNECTION_CREDENTIALS_ID, ST_CONSTRAINT_ID, + ST_EVENT_TABLE_ID, ST_INDEX_ID, ST_SCHEDULED_ID, ST_SEQUENCE_ID, ST_TABLE_ACCESSOR_ID, ST_TABLE_ID, ST_VIEW_ID, + ST_VIEW_PARAM_ID, }; use anyhow::anyhow; use core::ops::RangeBounds; @@ -43,6 +45,17 @@ pub trait StateView { Ok(row.map(|row| row.read_col(StTableFields::TableId).unwrap())) } + /// Looks up a table id by the table's canonical name or its accessor/alias name. + fn table_id_from_name_or_alias(&self, table_name_or_alias: &str) -> Result> { + if let Some(table_id) = self.table_id_from_name(table_name_or_alias)? { + return Ok(Some(table_id)); + } + let Some(row) = self.find_st_table_accessor_row(table_name_or_alias)? else { + return Ok(None); + }; + self.table_id_from_name(&row.table_name) + } + /// Returns the number of rows in the table identified by `table_id`. fn table_row_count(&self, table_id: TableId) -> Option; @@ -83,6 +96,39 @@ pub trait StateView { StTableRow::try_from(row_ref) } + /// Look up an `st_table_accessor` row by its accessor name + fn find_st_table_accessor_row(&self, accessor_name: &str) -> Result> { + self.iter_by_col_eq( + ST_TABLE_ACCESSOR_ID, + StTableAccessorFields::AccessorName, + &accessor_name.into(), + )? + .next() + .map(StTableAccessorRow::try_from) + .transpose() + } + + /// Look up an `st_column_accessor` row by its canonical table and column names + fn find_st_column_accessor_row(&self, table_name: &str, col_name: &str) -> Result> { + let row = match self.iter_by_col_eq( + ST_COLUMN_ACCESSOR_ID, + [StColumnAccessorFields::TableName, StColumnAccessorFields::ColName], + &AlgebraicValue::product([table_name.into(), col_name.into()]), + ) { + Ok(mut iter) => iter.next().map(StColumnAccessorRow::try_from).transpose(), + // `schema_for_table_raw` is called while restoring snapshots, + // before `migrate_system_tables` creates newer system tables. + // We therefore treat a missing `st_column_accessor` as "no aliases yet". + // + // Note this is different behavior from `find_st_table_accessor_row`, + // because that utility is used for name resolution **after** startup, + // where missing accessor tables should be surfaced as real errors. + Err(DatastoreError::Table(TableError::IdNotFound(..))) => Ok(None), + Err(e) => Err(e), + }; + row + } + /// Reads the schema information for the specified `table_id` directly from the database. fn schema_for_table_raw(&self, table_id: TableId) -> Result { // Look up the table_name for the table in question. @@ -95,7 +141,15 @@ pub trait StateView { // Look up the columns for the table in question. let mut columns: Vec = iter_st_column_for_table(self, &table_id.into())? - .map(|row| Ok(StColumnRow::try_from(row)?.into())) + .map(|row_ref| { + let row = StColumnRow::try_from(row_ref)?; + let mut column_schema = ColumnSchema::from(row); + let alias = self + .find_st_column_accessor_row(table_name.as_ref(), &column_schema.col_name)? + .map(|row| row.accessor_name); + column_schema.alias = alias; + Ok(column_schema) + }) .collect::>>()?; columns.sort_by_key(|col| col.col_pos); @@ -173,6 +227,22 @@ pub trait StateView { Err(DatastoreError::Table(TableError::IdNotFound(..))) => false, Err(e) => return Err(e), }; + // During restore from snapshots produced before `st_table_accessor` existed, + // this system table is missing until `migrate_system_tables` runs. + // Handle that here so schema reconstruction can proceed during restore. + let table_alias = match self.iter_by_col_eq( + ST_TABLE_ACCESSOR_ID, + StTableAccessorFields::TableName, + &table_name.as_ref().into(), + ) { + Ok(mut iter) => iter + .next() + .map(StTableAccessorRow::try_from) + .transpose()? + .map(|row| row.accessor_name), + Err(DatastoreError::Table(TableError::IdNotFound(..))) => None, + Err(e) => return Err(e), + }; Ok(TableSchema::new( table_id, table_name, @@ -186,6 +256,7 @@ pub trait StateView { schedule, table_primary_key, is_event, + table_alias, )) } diff --git a/crates/datastore/src/system_tables.rs b/crates/datastore/src/system_tables.rs index b007d4142da..d15628c1d96 100644 --- a/crates/datastore/src/system_tables.rs +++ b/crates/datastore/src/system_tables.rs @@ -82,6 +82,12 @@ pub const ST_VIEW_SUB_ID: TableId = TableId(15); pub const ST_VIEW_ARG_ID: TableId = TableId(16); /// The static ID of the table that tracks which tables are event tables pub const ST_EVENT_TABLE_ID: TableId = TableId(17); +/// The static ID of the table that maps canonical table names to accessor names +pub const ST_TABLE_ACCESSOR_ID: TableId = TableId(18); +/// The static ID of the table that maps canonical index names to accessor names +pub const ST_INDEX_ACCESSOR_ID: TableId = TableId(19); +/// The static ID of the table that maps canonical column names to accessor names +pub const ST_COLUMN_ACCESSOR_ID: TableId = TableId(20); pub(crate) const ST_CONNECTION_CREDENTIALS_NAME: &str = "st_connection_credentials"; pub const ST_TABLE_NAME: &str = "st_table"; @@ -100,6 +106,9 @@ pub(crate) const ST_VIEW_COLUMN_NAME: &str = "st_view_column"; pub(crate) const ST_VIEW_SUB_NAME: &str = "st_view_sub"; pub(crate) const ST_VIEW_ARG_NAME: &str = "st_view_arg"; pub(crate) const ST_EVENT_TABLE_NAME: &str = "st_event_table"; +pub(crate) const ST_TABLE_ACCESSOR_NAME: &str = "st_table_accessor"; +pub(crate) const ST_INDEX_ACCESSOR_NAME: &str = "st_index_accessor"; +pub(crate) const ST_COLUMN_ACCESSOR_NAME: &str = "st_column_accessor"; /// Reserved range of sequence values used for system tables. /// /// Ids for user-created tables will start at `ST_RESERVED_SEQUENCE_RANGE`. @@ -173,6 +182,7 @@ pub fn is_built_in_meta_row(table_id: TableId, row: &ProductValue) -> Result false, TableId(..ST_RESERVED_SEQUENCE_RANGE) => { log::warn!("Unknown system table {table_id:?}"); false @@ -192,9 +202,10 @@ pub enum SystemTable { st_index, st_constraint, st_row_level_security, + st_table_accessor, } -pub fn system_tables() -> [TableSchema; 17] { +pub fn system_tables() -> [TableSchema; 20] { [ // The order should match the `id` of the system table, that start with [ST_TABLE_IDX]. st_table_schema(), @@ -214,6 +225,9 @@ pub fn system_tables() -> [TableSchema; 17] { st_view_sub_schema(), st_view_arg_schema(), st_event_table_schema(), + st_table_accessor_schema(), + st_index_accessor_schema(), + st_column_accessor_schema(), ] } @@ -259,6 +273,9 @@ pub(crate) const ST_VIEW_COLUMN_IDX: usize = 13; pub(crate) const ST_VIEW_SUB_IDX: usize = 14; pub(crate) const ST_VIEW_ARG_IDX: usize = 15; pub(crate) const ST_EVENT_TABLE_IDX: usize = 16; +pub(crate) const ST_TABLE_ACCESSOR_IDX: usize = 17; +pub(crate) const ST_INDEX_ACCESSOR_IDX: usize = 18; +pub(crate) const ST_COLUMN_ACCESSOR_IDX: usize = 19; macro_rules! st_fields_enum { ($(#[$attr:meta])* enum $ty_name:ident { $($name:expr, $var:ident = $discr:expr,)* }) => { @@ -417,6 +434,22 @@ st_fields_enum!(enum StEventTableFields { "table_id", TableId = 0, }); +st_fields_enum!(enum StTableAccessorFields { + "table_name", TableName = 0, + "accessor_name", AccessorName = 1, +}); + +st_fields_enum!(enum StIndexAccessorFields { + "index_name", IndexName = 0, + "accessor_name", AccessorName = 1, +}); + +st_fields_enum!(enum StColumnAccessorFields { + "table_name", TableName = 0, + "col_name", ColName = 1, + "accessor_name", AccessorName = 2, +}); + /// Helper method to check that a system table has the correct fields. /// Does not check field types since those aren't included in `StFields` types. /// If anything in here is not true, the system is completely broken, so it's fine to assert. @@ -591,6 +624,50 @@ fn system_module_def() -> ModuleDef { .with_unique_constraint(StEventTableFields::TableId) .with_index_no_accessor_name(btree(StEventTableFields::TableId)); + let st_table_accessor_type = builder.add_type::(); + builder + .build_table( + ST_TABLE_ACCESSOR_NAME, + *st_table_accessor_type.as_ref().expect("should be ref"), + ) + .with_type(TableType::System) + .with_unique_constraint(StTableAccessorFields::TableName) + .with_index_no_accessor_name(btree(StTableAccessorFields::TableName)) + .with_unique_constraint(StTableAccessorFields::AccessorName) + .with_index_no_accessor_name(btree(StTableAccessorFields::AccessorName)); + + let st_index_accessor_type = builder.add_type::(); + builder + .build_table( + ST_INDEX_ACCESSOR_NAME, + *st_index_accessor_type.as_ref().expect("should be ref"), + ) + .with_type(TableType::System) + .with_unique_constraint(StIndexAccessorFields::IndexName) + .with_index_no_accessor_name(btree(StIndexAccessorFields::IndexName)) + .with_unique_constraint(StIndexAccessorFields::AccessorName) + .with_index_no_accessor_name(btree(StIndexAccessorFields::AccessorName)); + + let st_column_accessor_type = builder.add_type::(); + let st_column_accessor_table_col_cols = [ + StColumnAccessorFields::TableName.col_id(), + StColumnAccessorFields::ColName.col_id(), + ]; + let st_column_accessor_table_alias_cols = [ + StColumnAccessorFields::TableName.col_id(), + StColumnAccessorFields::AccessorName.col_id(), + ]; + builder + .build_table( + ST_COLUMN_ACCESSOR_NAME, + *st_column_accessor_type.as_ref().expect("should be ref"), + ) + .with_type(TableType::System) + .with_unique_constraint(st_column_accessor_table_col_cols) + .with_index_no_accessor_name(btree(st_column_accessor_table_col_cols)) + .with_unique_constraint(st_column_accessor_table_alias_cols) + .with_index_no_accessor_name(btree(st_column_accessor_table_alias_cols)); + let result = builder .finish() .try_into() @@ -613,6 +690,9 @@ fn system_module_def() -> ModuleDef { validate_system_table::(&result, ST_VIEW_SUB_NAME); validate_system_table::(&result, ST_VIEW_ARG_NAME); validate_system_table::(&result, ST_EVENT_TABLE_NAME); + validate_system_table::(&result, ST_TABLE_ACCESSOR_NAME); + validate_system_table::(&result, ST_INDEX_ACCESSOR_NAME); + validate_system_table::(&result, ST_COLUMN_ACCESSOR_NAME); result } @@ -655,6 +735,12 @@ lazy_static::lazy_static! { m.insert("st_view_arg_id_key", ConstraintId(17)); m.insert("st_view_arg_bytes_key", ConstraintId(18)); m.insert("st_event_table_table_id_key", ConstraintId(19)); + m.insert("st_table_accessor_table_name_key", ConstraintId(20)); + m.insert("st_table_accessor_accessor_name_key", ConstraintId(21)); + m.insert("st_index_accessor_index_name_key", ConstraintId(22)); + m.insert("st_index_accessor_accessor_name_key", ConstraintId(23)); + m.insert("st_column_accessor_table_name_col_name_key", ConstraintId(24)); + m.insert("st_column_accessor_table_name_accessor_name_key", ConstraintId(25)); m }; } @@ -687,6 +773,12 @@ lazy_static::lazy_static! { m.insert("st_view_arg_id_idx_btree", IndexId(21)); m.insert("st_view_arg_bytes_idx_btree", IndexId(22)); m.insert("st_event_table_table_id_idx_btree", IndexId(23)); + m.insert("st_table_accessor_table_name_idx_btree", IndexId(24)); + m.insert("st_table_accessor_accessor_name_idx_btree", IndexId(25)); + m.insert("st_index_accessor_index_name_idx_btree", IndexId(26)); + m.insert("st_index_accessor_accessor_name_idx_btree", IndexId(27)); + m.insert("st_column_accessor_table_name_col_name_idx_btree", IndexId(28)); + m.insert("st_column_accessor_table_name_accessor_name_idx_btree", IndexId(29)); m }; } @@ -714,6 +806,15 @@ fn st_schema(name: &str, id: TableId) -> TableSchema { (), id, ); + // Accessor aliases are not persisted for system tables in `st_*` metadata rows yet. + // Keep canonical system schemas alias-free so raw reconstruction and cached schemas agree. + result.alias = None; + for column in &mut result.columns { + column.alias = None; + } + for index in &mut result.indexes { + index.alias = None; + } // The result we get will have sentinel ids filled in the constraints, indexes, and sequences. // We replace them here with stable values in the reserved range. for index in &mut result.indexes { @@ -827,6 +928,18 @@ fn st_event_table_schema() -> TableSchema { st_schema(ST_EVENT_TABLE_NAME, ST_EVENT_TABLE_ID) } +fn st_table_accessor_schema() -> TableSchema { + st_schema(ST_TABLE_ACCESSOR_NAME, ST_TABLE_ACCESSOR_ID) +} + +fn st_index_accessor_schema() -> TableSchema { + st_schema(ST_INDEX_ACCESSOR_NAME, ST_INDEX_ACCESSOR_ID) +} + +fn st_column_accessor_schema() -> TableSchema { + st_schema(ST_COLUMN_ACCESSOR_NAME, ST_COLUMN_ACCESSOR_ID) +} + /// If `table_id` refers to a known system table, return its schema. /// /// Used when restoring from a snapshot; system tables are reinstantiated with this schema, @@ -852,6 +965,9 @@ pub(crate) fn system_table_schema(table_id: TableId) -> Option { ST_VIEW_SUB_ID => Some(st_view_sub_schema()), ST_VIEW_ARG_ID => Some(st_view_arg_schema()), ST_EVENT_TABLE_ID => Some(st_event_table_schema()), + ST_TABLE_ACCESSOR_ID => Some(st_table_accessor_schema()), + ST_INDEX_ACCESSOR_ID => Some(st_index_accessor_schema()), + ST_COLUMN_ACCESSOR_ID => Some(st_column_accessor_schema()), _ => None, } } @@ -981,6 +1097,7 @@ impl From for ColumnSchema { col_pos: column.col_pos, col_name: column.col_name, col_type: column.col_type.0, + alias: None, } } } @@ -1148,6 +1265,7 @@ impl From for IndexSchema { table_id: x.table_id, index_name: x.index_name, index_algorithm: x.index_algorithm.into(), + alias: None, } } } @@ -1695,6 +1813,70 @@ impl From for ProductValue { } } +/// System Table [ST_TABLE_ACCESSOR_NAME] +#[derive(Debug, Clone, PartialEq, Eq, SpacetimeType)] +#[sats(crate = spacetimedb_lib)] +pub struct StTableAccessorRow { + pub table_name: TableName, + pub accessor_name: Identifier, +} + +impl TryFrom> for StTableAccessorRow { + type Error = DatastoreError; + fn try_from(row: RowRef<'_>) -> Result { + read_via_bsatn(row) + } +} + +impl From for ProductValue { + fn from(x: StTableAccessorRow) -> Self { + to_product_value(&x) + } +} + +/// System Table [ST_INDEX_ACCESSOR_NAME] +#[derive(Debug, Clone, PartialEq, Eq, SpacetimeType)] +#[sats(crate = spacetimedb_lib)] +pub struct StIndexAccessorRow { + pub index_name: RawIdentifier, + pub accessor_name: RawIdentifier, +} + +impl TryFrom> for StIndexAccessorRow { + type Error = DatastoreError; + fn try_from(row: RowRef<'_>) -> Result { + read_via_bsatn(row) + } +} + +impl From for ProductValue { + fn from(x: StIndexAccessorRow) -> Self { + to_product_value(&x) + } +} + +/// System Table [ST_COLUMN_ACCESSOR_NAME] +#[derive(Debug, Clone, PartialEq, Eq, SpacetimeType)] +#[sats(crate = spacetimedb_lib)] +pub struct StColumnAccessorRow { + pub table_name: TableName, + pub col_name: Identifier, + pub accessor_name: Identifier, +} + +impl TryFrom> for StColumnAccessorRow { + type Error = DatastoreError; + fn try_from(row: RowRef<'_>) -> Result { + read_via_bsatn(row) + } +} + +impl From for ProductValue { + fn from(x: StColumnAccessorRow) -> Self { + to_product_value(&x) + } +} + thread_local! { static READ_BUF: RefCell> = const { RefCell::new(Vec::new()) }; } diff --git a/crates/expr/src/lib.rs b/crates/expr/src/lib.rs index 3a461957df6..f89a721254d 100644 --- a/crates/expr/src/lib.rs +++ b/crates/expr/src/lib.rs @@ -103,7 +103,7 @@ fn _type_expr(vars: &Relvars, expr: SqlExpr, expected: Option<&AlgebraicType>, d let table_type = vars.deref().get(&*table).ok_or_else(|| Unresolved::var(&table))?; let ColumnSchema { col_pos, col_type, .. } = table_type .as_ref() - .get_column_by_name(&field) + .get_column_by_name_or_alias(&field) .ok_or_else(|| Unresolved::var(&field))?; if let Some(ty) = expected { diff --git a/crates/expr/src/statement.rs b/crates/expr/src/statement.rs index 68c59f0adca..127876fdd74 100644 --- a/crates/expr/src/statement.rs +++ b/crates/expr/src/statement.rs @@ -161,12 +161,12 @@ pub fn type_insert(insert: SqlInsert, tx: &impl SchemaView) -> TypingResult TypingResult { let SqlDelete { - table: SqlIdent(table_name), + table: SqlIdent(query_table_name), filter, } = delete; let from = tx - .schema(&table_name) - .ok_or_else(|| Unresolved::table(&table_name)) + .schema(&query_table_name) + .ok_or_else(|| Unresolved::table(&query_table_name)) .map_err(TypingError::from)?; let table_name = &from.table_name; @@ -176,7 +176,11 @@ pub fn type_delete(delete: SqlDelete, tx: &impl SchemaView) -> TypingResult TypingResult TypingResult { let SqlUpdate { - table: SqlIdent(table_name), + table: SqlIdent(query_table_name), assignments, filter, } = update; let schema = tx - .schema(&table_name) - .ok_or_else(|| Unresolved::table(&table_name)) + .schema(&query_table_name) + .ok_or_else(|| Unresolved::table(&query_table_name)) .map_err(TypingError::from)?; let table_name = &schema.table_name; @@ -212,7 +216,7 @@ pub fn type_update(update: SqlUpdate, tx: &impl SchemaView) -> TypingResult { @@ -236,7 +240,11 @@ pub fn type_update(update: SqlUpdate, tx: &impl SchemaView) -> TypingResult impl Iterator { + self.tables().map(|table| (&table.accessor_name, &table.name)) + } + + pub fn index_accessors(&self) -> impl Iterator { + self.indexes().map(|index| (&index.accessor_name, &index.name)) + } } impl TryFrom for ModuleDef { @@ -582,7 +590,6 @@ pub trait ModuleDefLookup: Sized + Debug + 'static { /// Look up this entity in the module def. fn lookup<'a>(module_def: &'a ModuleDef, key: Self::Key<'_>) -> Option<&'a Self>; } - /// A data structure representing the validated definition of a database table. /// /// Cannot be created directly. Construct a [`ModuleDef`] by validating a [`RawModuleDef`] instead, @@ -605,6 +612,10 @@ pub struct TableDef { /// Must be a valid [crate::db::identifier::Identifier]. pub name: Identifier, + /// For V9, it is same as `name`. + /// in V10, this is the name of index used inside the module. + pub accessor_name: Identifier, + /// A reference to a `ProductType` containing the columns of this table. /// This is the single source of truth for the table's columns. /// All elements of the `ProductType` must have names. @@ -674,6 +685,7 @@ impl From for RawTableDefV9 { table_type, table_access, is_event: _, // V9 does not support event tables; ignore when converting back + .. } = val; RawTableDefV9 { @@ -693,7 +705,7 @@ impl From for RawTableDefV9 { impl From for RawTableDefV10 { fn from(val: TableDef) -> Self { let TableDef { - name, + name: _, product_type_ref, primary_key, columns: _, // will be reconstructed from the product type. @@ -704,10 +716,11 @@ impl From for RawTableDefV10 { table_type, table_access, is_event, + accessor_name, } = val; RawTableDefV10 { - source_name: name.into(), + source_name: accessor_name.into(), product_type_ref, primary_key: ColList::from_iter(primary_key), indexes: indexes.into_values().map(Into::into).collect(), @@ -729,6 +742,7 @@ impl From for TableDef { is_public, product_type_ref, return_columns, + accessor_name, .. } = def; Self { @@ -743,6 +757,7 @@ impl From for TableDef { table_type: TableType::User, table_access: if is_public { Public } else { Private }, is_event: false, + accessor_name, } } } @@ -816,10 +831,15 @@ pub struct IndexDef { /// generated by the system using the same algorithm as V9 and earlier. pub name: RawIdentifier, + /// It will be same as `name` for V9. + /// In V10, this index name used inside module. + pub accessor_name: RawIdentifier, + /// codegen_name is the name of the index to be used for client code generation. /// /// In V9 and earlier, this could be passed by the user, as `accessor` macro in bindings. - /// In V10, this will be always be `name`. + /// In V10, this will be always be `name`, It is redundant to have both `name` and + /// `codegen_name` in V10, but we keep it because migration code for V9 uses this field. /// /// In V9, this may be set to `None` if this is an auto-generated index for which the user /// has not supplied a name. In this case, no client code generation for this index @@ -1000,6 +1020,10 @@ pub struct ColumnDef { /// NOT within the containing `ModuleDef`. pub name: Identifier, + /// For V9, it is same as `name`. + /// for V10, this is the name of column used inside module. + pub accessor_name: Identifier, + /// The ID of this column. pub col_id: ColId, @@ -1029,6 +1053,7 @@ impl From for ColumnDef { ty, ty_for_generate, view_name: table_name, + accessor_name, } = def; Self { name, @@ -1037,6 +1062,7 @@ impl From for ColumnDef { ty_for_generate, table_name, default_value: None, + accessor_name, } } } @@ -1048,6 +1074,8 @@ pub struct ViewColumnDef { /// The name of the column. pub name: Identifier, + pub accessor_name: Identifier, + /// The position of this column in the view's return type. pub col_id: ColId, @@ -1069,6 +1097,7 @@ impl From for ViewColumnDef { ty, ty_for_generate, table_name: view_name, + accessor_name, .. }: ColumnDef, ) -> Self { @@ -1078,6 +1107,7 @@ impl From for ViewColumnDef { ty, ty_for_generate, view_name, + accessor_name, } } } @@ -1409,6 +1439,8 @@ pub struct ViewDef { /// The name of the view. This must be unique within the module. pub name: Identifier, + pub accessor_name: Identifier, + /// Is this a public or a private view? /// Currently only public views are supported. /// Private views may be supported in the future. @@ -1832,6 +1864,19 @@ impl ModuleDefLookup for ReducerDef { } } +impl ModuleDefLookup for ProcedureDef { + type Key<'a> = &'a Identifier; + + fn key(&self) -> Self::Key<'_> { + &self.name + } + + fn lookup<'a>(module_def: &'a ModuleDef, key: Self::Key<'_>) -> Option<&'a Self> { + let key = &**key; + module_def.procedures.get(key) + } +} + impl ModuleDefLookup for ViewDef { type Key<'a> = &'a Identifier; diff --git a/crates/schema/src/def/validate/v10.rs b/crates/schema/src/def/validate/v10.rs index d5256750dff..ab7667d18d2 100644 --- a/crates/schema/src/def/validate/v10.rs +++ b/crates/schema/src/def/validate/v10.rs @@ -395,7 +395,7 @@ impl<'a> ModuleValidatorV10<'a> { .combine_errors()?; Ok(TableDef { - name, + name: name.clone(), product_type_ref, primary_key, columns, @@ -406,6 +406,7 @@ impl<'a> ModuleValidatorV10<'a> { table_type, table_access, is_event, + accessor_name: name, }) } @@ -637,7 +638,7 @@ impl<'a> ModuleValidatorV10<'a> { (name_result, return_type_for_generate, return_columns, param_columns).combine_errors()?; Ok(ViewDef { - name: name_result, + name: name_result.clone(), is_anonymous, is_public, params, @@ -651,6 +652,7 @@ impl<'a> ModuleValidatorV10<'a> { product_type_ref, return_columns, param_columns, + accessor_name: name_result, }) } } @@ -870,16 +872,19 @@ mod tests { name: "Apples_count_idx_direct".into(), codegen_name: Some(expect_identifier("Apples_count_idx_direct")), algorithm: DirectAlgorithm { column: 2.into() }.into(), + accessor_name: "Apples_count_idx_direct".into(), }, &IndexDef { name: "Apples_name_count_idx_btree".into(), codegen_name: Some(expect_identifier("Apples_name_count_idx_btree")), algorithm: BTreeAlgorithm { columns: [1, 2].into() }.into(), + accessor_name: "Apples_count_idx_direct".into(), }, &IndexDef { name: "Apples_type_idx_btree".into(), codegen_name: Some(expect_identifier("Apples_type_idx_btree")), algorithm: BTreeAlgorithm { columns: 3.into() }.into(), + accessor_name: "Apples_count_idx_direct".into(), } ] ); diff --git a/crates/schema/src/def/validate/v9.rs b/crates/schema/src/def/validate/v9.rs index d8d5d4a0be6..3cf9e39fe38 100644 --- a/crates/schema/src/def/validate/v9.rs +++ b/crates/schema/src/def/validate/v9.rs @@ -307,7 +307,7 @@ impl ModuleValidatorV9<'_> { .combine_errors()?; Ok(TableDef { - name, + name: name.clone(), product_type_ref, primary_key, columns, @@ -318,6 +318,7 @@ impl ModuleValidatorV9<'_> { table_type, table_access, is_event: false, // V9 does not support event tables + accessor_name: name, }) } @@ -506,7 +507,7 @@ impl ModuleValidatorV9<'_> { (name, return_type_for_generate, return_columns, param_columns).combine_errors()?; Ok(ViewDef { - name, + name: name.clone(), is_anonymous, is_public, params, @@ -520,6 +521,7 @@ impl ModuleValidatorV9<'_> { product_type_ref, return_columns, param_columns, + accessor_name: name, }) } @@ -937,12 +939,13 @@ impl<'a, 'b> TableValidator<'a, 'b> { let (name, ty_for_generate, table_name) = (name, ty_for_generate, table_name).combine_errors()?; Ok(ColumnDef { - name, + name: name.clone(), ty: column.algebraic_type.clone(), ty_for_generate, col_id, table_name, default_value: None, // filled in later + accessor_name: name.clone(), }) } @@ -1110,9 +1113,10 @@ impl<'a, 'b> TableValidator<'a, 'b> { let (name, codegen_name, algorithm) = (name, codegen_name, algorithm).combine_errors()?; Ok(IndexDef { - name, + name: name.clone(), algorithm, codegen_name, + accessor_name: name, }) } @@ -1611,16 +1615,19 @@ mod tests { name: "Apples_count_idx_direct".into(), codegen_name: Some(expect_identifier("Apples_count_direct")), algorithm: DirectAlgorithm { column: 2.into() }.into(), + accessor_name: "Apples_name_count_idx_btree".into(), }, &IndexDef { name: "Apples_name_count_idx_btree".into(), codegen_name: Some(expect_identifier("apples_id")), algorithm: BTreeAlgorithm { columns: [1, 2].into() }.into(), + accessor_name: "Apples_name_count_idx_btree".into(), }, &IndexDef { name: "Apples_type_idx_btree".into(), codegen_name: Some(expect_identifier("Apples_type_btree")), algorithm: BTreeAlgorithm { columns: 3.into() }.into(), + accessor_name: "Apples_name_count_idx_btree".into(), } ] ); diff --git a/crates/schema/src/schema.rs b/crates/schema/src/schema.rs index dc2e0d76986..8e6bed74cd4 100644 --- a/crates/schema/src/schema.rs +++ b/crates/schema/src/schema.rs @@ -141,6 +141,14 @@ impl TableOrViewSchema { pub fn get_column_by_name(&self, col_name: &str) -> Option<&ColumnSchema> { self.public_columns().iter().find(|x| &*x.col_name == col_name) } + + /// Check if the `col_name` exists on this [`TableOrViewSchema`], prioritizing alias over canonical name. + pub fn get_column_by_name_or_alias(&self, col_name: &str) -> Option<&ColumnSchema> { + self.public_columns() + .iter() + .find(|col| col.alias.as_deref().is_some_and(|alias| alias == col_name)) + .or_else(|| self.get_column_by_name(col_name)) + } } /// A data structure representing the schema of a database table. @@ -155,6 +163,8 @@ pub struct TableSchema { /// The name of the table. pub table_name: TableName, + pub alias: Option, + /// Is this the backing table of a view? pub view_info: Option, @@ -216,6 +226,7 @@ impl TableSchema { schedule: Option, primary_key: Option, is_event: bool, + alias: Option, ) -> Self { Self { row_type: columns_to_row_type(&columns), @@ -231,6 +242,7 @@ impl TableSchema { schedule, primary_key, is_event, + alias, } } @@ -251,6 +263,7 @@ impl TableSchema { .map(Identifier::new_assume_valid) .unwrap_or_else(|| Identifier::for_test(format!("col{col_pos}"))), col_type: element.algebraic_type.clone(), + alias: None, }) .collect(); @@ -267,6 +280,7 @@ impl TableSchema { None, None, false, + None, ) } @@ -426,6 +440,14 @@ impl TableSchema { self.columns.iter().find(|x| &*x.col_name == col_name) } + /// Check if the `col_name` exists on this [TableSchema], prioritizing alias over canonical name. + pub fn get_column_by_name_or_alias(&self, col_name: &str) -> Option<&ColumnSchema> { + self.columns + .iter() + .find(|col| col.alias.as_deref().is_some_and(|alias| alias == col_name)) + .or_else(|| self.get_column_by_name(col_name)) + } + /// Check if the `col_name` exist on this [TableSchema] /// /// Warning: It ignores the `table_name` @@ -436,6 +458,22 @@ impl TableSchema { .map(|x| x.into()) } + /// Check if the `col_name` exists on this [TableSchema], prioritizing alias over canonical name. + /// + /// Warning: It ignores the `table_name`. + pub fn get_column_id_by_name_or_alias(&self, col_name: &str) -> Option { + self.columns + .iter() + .position(|col| col.alias.as_deref().is_some_and(|alias| alias == col_name)) + .or_else(|| self.get_column_id_by_name(col_name).map(|id| id.idx())) + .map(Into::into) + } + + /// Check whether `name` matches table alias or canonical table name. + pub fn matches_name_or_alias(&self, name: &str) -> bool { + self.alias.as_deref().is_some_and(|alias| alias == name) || self.table_name.as_ref() == name + } + /// Retrieve the column ids for this index id pub fn col_list_for_index_id(&self, index_id: IndexId) -> ColList { self.indexes @@ -760,6 +798,7 @@ impl TableSchema { None, None, false, + None, ) } @@ -805,6 +844,7 @@ impl TableSchema { is_anonymous, param_columns, return_columns, + accessor_name, .. } = view_def; @@ -822,6 +862,7 @@ impl TableSchema { col_pos: columns.len().into(), col_name: Identifier::new_assume_valid(name.into()), col_type, + alias: None, }); }; @@ -849,6 +890,7 @@ impl TableSchema { table_id: TableId::SENTINEL, index_name: RawIdentifier::new(index_name), index_algorithm: IndexAlgorithm::BTree(col_list.into()), + alias: None, } }; @@ -883,6 +925,7 @@ impl TableSchema { None, None, false, + Some(accessor_name.clone()), ) } } @@ -913,6 +956,8 @@ impl Schema for TableSchema { table_type, table_access, is_event, + accessor_name, + .. } = def; let columns = column_schemas_from_defs(module_def, columns, table_id); @@ -951,6 +996,7 @@ impl Schema for TableSchema { schedule, *primary_key, *is_event, + Some(accessor_name.clone()), ) } @@ -1068,6 +1114,8 @@ pub struct ColumnSchema { pub col_pos: ColId, /// The name of the column. Unique within the table. pub col_name: Identifier, + + pub alias: Option, /// The type of the column. This will never contain any `AlgebraicTypeRef`s, /// that is, it will be resolved. pub col_type: AlgebraicType, @@ -1080,6 +1128,7 @@ impl spacetimedb_memory_usage::MemoryUsage for ColumnSchema { col_pos, col_name, col_type, + .. } = self; table_id.heap_usage() + col_pos.heap_usage() + col_name.heap_usage() + col_type.heap_usage() } @@ -1093,6 +1142,7 @@ impl ColumnSchema { col_pos: pos.into(), col_name: Identifier::for_test(name), col_type: ty, + alias: None, } } @@ -1105,6 +1155,7 @@ impl ColumnSchema { col_pos: def.col_id, col_name: def.name.clone(), col_type, + alias: Some(def.accessor_name.clone()), } } } @@ -1130,6 +1181,7 @@ impl Schema for ColumnSchema { col_pos, col_name: def.name.clone(), col_type, + alias: Some(def.accessor_name.clone()), } } @@ -1337,6 +1389,8 @@ pub struct IndexSchema { /// The name of the index. This should not be assumed to follow any particular format. /// Unique within the database. pub index_name: RawIdentifier, + + pub alias: Option, /// The data for the schema. pub index_algorithm: IndexAlgorithm, } @@ -1348,6 +1402,7 @@ impl spacetimedb_memory_usage::MemoryUsage for IndexSchema { table_id, index_name, index_algorithm, + alias: _, } = self; index_id.heap_usage() + table_id.heap_usage() + index_name.heap_usage() + index_algorithm.heap_usage() } @@ -1360,6 +1415,7 @@ impl IndexSchema { table_id: TableId::SENTINEL, index_name: RawIdentifier::new(name.as_ref()), index_algorithm: algo.into(), + alias: None, } } } @@ -1378,6 +1434,7 @@ impl Schema for IndexSchema { table_id: parent_id, index_name: def.name.clone(), index_algorithm, + alias: Some(def.accessor_name.clone()), } } diff --git a/crates/smoketests/modules/sql-format/src/lib.rs b/crates/smoketests/modules/sql-format/src/lib.rs index dc06aaf59cb..b00d643394e 100644 --- a/crates/smoketests/modules/sql-format/src/lib.rs +++ b/crates/smoketests/modules/sql-format/src/lib.rs @@ -1,5 +1,7 @@ use spacetimedb::sats::{i256, u256}; -use spacetimedb::{ConnectionId, Identity, ReducerContext, Table, Timestamp, TimeDuration, SpacetimeType, Uuid}; +use spacetimedb::{ + ConnectionId, Identity, Query, ReducerContext, SpacetimeType, Table, TimeDuration, Timestamp, Uuid, ViewContext, +}; #[derive(Copy, Clone)] #[spacetimedb::table(accessor = t_ints)] @@ -44,13 +46,13 @@ pub struct TOthers { identity: Identity, connection_id: ConnectionId, timestamp: Timestamp, - duration: TimeDuration, + duration: TimeDuration, uuid: Uuid, } #[spacetimedb::table(accessor = t_others_tuple)] pub struct TOthersTuple { - tuple: TOthers + tuple: TOthers, } #[derive(SpacetimeType, Debug, Clone, Copy)] @@ -101,7 +103,7 @@ pub fn test(ctx: &ReducerContext) { f32: 594806.58906, f64: -3454353.345389043278459, str: "This is spacetimedb".to_string(), - bytes: vec!(1, 2, 3, 4, 5, 6, 7), + bytes: vec![1, 2, 3, 4, 5, 6, 7], identity: Identity::ONE, connection_id: ConnectionId::ZERO, timestamp: Timestamp::UNIX_EPOCH, @@ -120,3 +122,24 @@ pub fn test(ctx: &ReducerContext) { ctx.db.t_enums().insert(tuple.clone()); ctx.db.t_enums_tuple().insert(TEnumsTuple { tuple }); } + +#[spacetimedb::table(accessor = accessor_table, name = "canonical_table", public)] +pub struct AccessorRow { + #[primary_key] + id: u32, + #[sats(name = "canonical_value")] + accessor_value: u32, +} + +#[spacetimedb::reducer(init)] +pub fn init(ctx: &ReducerContext) { + ctx.db.accessor_table().insert(AccessorRow { + id: 1, + accessor_value: 7, + }); +} + +#[spacetimedb::view(accessor = accessor_filtered, name = "canonical_filtered", public)] +fn accessor_filtered(ctx: &ViewContext) -> impl Query { + ctx.from.accessor_table().r#where(|r| r.accessor_value.eq(7)) +} diff --git a/crates/smoketests/tests/sql.rs b/crates/smoketests/tests/sql.rs index ca8b1318f31..c0cd1f6de3f 100644 --- a/crates/smoketests/tests/sql.rs +++ b/crates/smoketests/tests/sql.rs @@ -63,3 +63,72 @@ fn test_sql_format() { (bool_opt = (some = true), bool_result = (ok = false), action = (Active = ()))"#, ); } + +#[test] +fn test_sql_resolves_accessor_and_canonical_names_for_table() { + let test = Smoketest::builder().precompiled_module("sql-format").build(); + + test.assert_sql( + "SELECT * FROM accessor_table", + r#" id | accessor_value +----+---------------- + 1 | 7"#, + ); + + test.assert_sql( + "SELECT * FROM canonical_table", + r#" id | accessor_value +----+---------------- + 1 | 7"#, + ); +} + +#[test] +fn test_sql_resolves_accessor_and_canonical_names_for_view() { + let test = Smoketest::builder().precompiled_module("sql-format").build(); + + test.assert_sql( + "SELECT * FROM accessor_filtered", + r#" id | accessor_value +----+---------------- + 1 | 7"#, + ); + + test.assert_sql( + "SELECT * FROM canonical_filtered", + r#" id | accessor_value +----+---------------- + 1 | 7"#, + ); +} + +#[test] +fn test_sql_resolves_accessor_and_canonical_names_for_column() { + let test = Smoketest::builder().precompiled_module("sql-format").build(); + + test.assert_sql( + "SELECT accessor_value FROM accessor_table", + r#" accessor_value +---------------- + 7"#, + ); + + test.assert_sql( + "SELECT canonical_value FROM accessor_table", + r#" canonical_value +---------------- + 7"#, + ); +} + +#[test] +fn test_query_builder_resolves_accessor_and_canonical_names() { + let test = Smoketest::builder().precompiled_module("sql-format").build(); + + test.assert_sql( + "SELECT * FROM accessor_filtered", + r#" id | accessor_value +----+---------------- + 1 | 7"#, + ); +}