use crate::def::error::{DefType, SchemaError};
use crate::relation::{combine_constraints, Column, DbTable, FieldName, Header};
use crate::table_name::TableName;
use core::mem;
use itertools::Itertools;
use spacetimedb_lib::db::auth::{StAccess, StTableType};
use spacetimedb_lib::db::raw_def::v9::RawSql;
use spacetimedb_lib::db::raw_def::{generate_cols_name, RawConstraintDefV8};
use spacetimedb_primitives::*;
use spacetimedb_sats::product_value::InvalidFieldError;
use spacetimedb_sats::raw_identifier::RawIdentifier;
use spacetimedb_sats::{AlgebraicType, ProductType, ProductTypeElement, WithTypespace};
use std::collections::BTreeMap;
use std::sync::Arc;
use crate::def::{
ColumnDef, ConstraintData, ConstraintDef, IndexAlgorithm, IndexDef, ModuleDef, ModuleDefLookup,
RawModuleDefVersion, ScheduleDef, SequenceDef, TableDef, UniqueConstraintData, ViewColumnDef, ViewDef,
};
use crate::identifier::Identifier;
pub trait Schema: Sized {
type Def: ModuleDefLookup;
type Id;
type ParentId;
fn from_module_def(module_def: &ModuleDef, def: &Self::Def, parent_id: Self::ParentId, id: Self::Id) -> Self;
fn check_compatible(&self, module_def: &ModuleDef, def: &Self::Def) -> Result<(), anyhow::Error>;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ViewDefInfo {
pub view_id: ViewId,
pub has_args: bool,
pub is_anonymous: bool,
}
impl ViewDefInfo {
pub fn num_private_cols(&self) -> usize {
(if self.is_anonymous { 0 } else { 1 }) + (if self.has_args { 1 } else { 0 })
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TableOrViewSchema {
pub table_id: TableId,
pub view_info: Option<ViewDefInfo>,
pub table_name: TableName,
pub table_access: StAccess,
inner: Arc<TableSchema>,
}
impl From<Arc<TableSchema>> for TableOrViewSchema {
fn from(inner: Arc<TableSchema>) -> Self {
Self {
table_id: inner.table_id,
view_info: inner.view_info,
table_name: inner.table_name.clone(),
table_access: inner.table_access,
inner,
}
}
}
impl TableOrViewSchema {
pub fn is_view(&self) -> bool {
self.view_info.is_some()
}
pub fn is_anonymous_view(&self) -> bool {
self.view_info.as_ref().is_some_and(|view_info| view_info.is_anonymous)
}
pub fn inner(&self) -> Arc<TableSchema> {
self.inner.clone()
}
pub fn public_columns(&self) -> &[ColumnSchema] {
match self.view_info {
Some(ViewDefInfo {
has_args: true,
is_anonymous: false,
..
}) => &self.inner.columns[2..],
Some(ViewDefInfo {
has_args: true,
is_anonymous: true,
..
}) => &self.inner.columns[1..],
Some(ViewDefInfo {
has_args: false,
is_anonymous: false,
..
}) => &self.inner.columns[1..],
Some(ViewDefInfo {
has_args: false,
is_anonymous: true,
..
})
| None => &self.inner.columns,
}
}
pub fn get_column_by_name(&self, col_name: &str) -> Option<&ColumnSchema> {
self.public_columns().iter().find(|x| &*x.col_name == col_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))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TableSchema {
pub table_id: TableId,
pub table_name: TableName,
pub alias: Option<Identifier>,
pub view_info: Option<ViewDefInfo>,
pub columns: Vec<ColumnSchema>,
pub primary_key: Option<ColId>,
pub indexes: Vec<IndexSchema>,
pub constraints: Vec<ConstraintSchema>,
pub sequences: Vec<SequenceSchema>,
pub table_type: StTableType,
pub table_access: StAccess,
pub schedule: Option<ScheduleSchema>,
pub is_event: bool,
pub row_type: ProductType,
}
pub fn columns_to_row_type(columns: &[ColumnSchema]) -> ProductType {
ProductType::new(columns.iter().map(ProductTypeElement::from).collect())
}
impl TableSchema {
#[allow(clippy::too_many_arguments)]
pub fn new(
table_id: TableId,
table_name: TableName,
view_info: Option<ViewDefInfo>,
columns: Vec<ColumnSchema>,
indexes: Vec<IndexSchema>,
constraints: Vec<ConstraintSchema>,
sequences: Vec<SequenceSchema>,
table_type: StTableType,
table_access: StAccess,
schedule: Option<ScheduleSchema>,
primary_key: Option<ColId>,
is_event: bool,
alias: Option<Identifier>,
) -> Self {
Self {
row_type: columns_to_row_type(&columns),
table_id,
table_name,
view_info,
columns,
indexes,
constraints,
sequences,
table_type,
table_access,
schedule,
primary_key,
is_event,
alias,
}
}
#[cfg(any(test, feature = "test"))]
pub fn from_product_type(ty: ProductType) -> TableSchema {
let columns = ty
.elements
.iter()
.enumerate()
.map(|(col_pos, element)| ColumnSchema {
table_id: TableId::SENTINEL,
col_pos: ColId(col_pos as _),
col_name: element
.name
.clone()
.map(Identifier::new_assume_valid)
.unwrap_or_else(|| Identifier::for_test(format!("col{col_pos}"))),
col_type: element.algebraic_type.clone(),
alias: None,
})
.collect();
TableSchema::new(
TableId::SENTINEL,
TableName::for_test("TestTable"),
None,
columns,
vec![],
vec![],
vec![],
StTableType::User,
StAccess::Public,
None,
None,
false,
None,
)
}
pub fn is_view(&self) -> bool {
self.view_info.is_some()
}
pub fn is_anonymous_view(&self) -> bool {
self.view_info.as_ref().is_some_and(|view_info| view_info.is_anonymous)
}
pub fn num_private_cols(&self) -> usize {
self.view_info
.as_ref()
.map(|view_info| view_info.num_private_cols())
.unwrap_or_default()
}
pub fn update_table_id(&mut self, id: TableId) {
self.table_id = id;
self.columns.iter_mut().for_each(|c| c.table_id = id);
self.indexes.iter_mut().for_each(|i| i.table_id = id);
self.constraints.iter_mut().for_each(|c| c.table_id = id);
self.sequences.iter_mut().for_each(|s| s.table_id = id);
if let Some(s) = self.schedule.as_mut() {
s.table_id = id;
}
}
pub fn reset(&mut self) {
self.update_table_id(TableId::SENTINEL);
self.indexes.iter_mut().for_each(|i| i.index_id = IndexId::SENTINEL);
self.sequences
.iter_mut()
.for_each(|i| i.sequence_id = SequenceId::SENTINEL);
self.constraints
.iter_mut()
.for_each(|i| i.constraint_id = ConstraintId::SENTINEL);
self.row_type = columns_to_row_type(&self.columns);
}
pub fn into_columns(self) -> Vec<ColumnSchema> {
self.columns
}
pub fn columns(&self) -> &[ColumnSchema] {
&self.columns
}
pub fn num_cols(&self) -> usize {
self.columns.len()
}
pub fn take_adjacent_schemas(&mut self) -> (Vec<IndexSchema>, Vec<SequenceSchema>, Vec<ConstraintSchema>) {
(
mem::take(&mut self.indexes),
mem::take(&mut self.sequences),
mem::take(&mut self.constraints),
)
}
pub fn update_sequence(&mut self, of: SequenceSchema) {
if let Some(x) = self.sequences.iter_mut().find(|x| x.sequence_id == of.sequence_id) {
*x = of;
} else {
self.sequences.push(of);
}
}
pub fn remove_sequence(&mut self, sequence_id: SequenceId) -> Option<SequenceSchema> {
find_remove(&mut self.sequences, |x| x.sequence_id == sequence_id)
}
pub fn update_index(&mut self, of: IndexSchema) {
if let Some(x) = self.indexes.iter_mut().find(|x| x.index_id == of.index_id) {
*x = of;
} else {
self.indexes.push(of);
}
}
pub fn remove_index(&mut self, index_id: IndexId) -> Option<IndexSchema> {
find_remove(&mut self.indexes, |x| x.index_id == index_id)
}
pub fn update_constraint(&mut self, of: ConstraintSchema) {
if let Some(x) = self
.constraints
.iter_mut()
.find(|x| x.constraint_id == of.constraint_id)
{
*x = of;
} else {
self.constraints.push(of);
}
}
pub fn remove_constraint(&mut self, constraint_id: ConstraintId) -> Option<ConstraintSchema> {
find_remove(&mut self.constraints, |x| x.constraint_id == constraint_id)
}
pub fn generate_cols_name(&self, columns: &ColList) -> String {
generate_cols_name(columns, |p| self.get_column(p.idx()).map(|c| &*c.col_name))
}
pub fn get_column_by_field(&self, field: FieldName) -> Option<&ColumnSchema> {
self.get_column(field.col.idx())
}
pub fn get_columns<'a>(
&'a self,
columns: &'a ColList,
) -> impl 'a + Iterator<Item = (ColId, Option<&'a ColumnSchema>)> {
columns.iter().map(|col| (col, self.columns.get(col.idx())))
}
pub fn get_column(&self, pos: usize) -> Option<&ColumnSchema> {
self.columns.get(pos)
}
pub fn get_column_by_name(&self, col_name: &str) -> Option<&ColumnSchema> {
self.columns.iter().find(|x| &*x.col_name == col_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))
}
pub fn get_column_id_by_name(&self, col_name: &str) -> Option<ColId> {
self.columns
.iter()
.position(|x| &*x.col_name == col_name)
.map(|x| x.into())
}
pub fn get_column_id_by_name_or_alias(&self, col_name: &str) -> Option<ColId> {
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)
}
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
}
pub fn col_list_for_index_id(&self, index_id: IndexId) -> ColList {
self.indexes
.iter()
.find(|schema| schema.index_id == index_id)
.map(|schema| schema.index_algorithm.columns())
.map(|cols| ColList::from_iter(cols.iter()))
.unwrap_or_else(ColList::empty)
}
pub fn is_unique(&self, cols: &impl PartialEq<ColList>) -> bool {
self.constraints
.iter()
.filter_map(|cs| cs.data.unique_columns())
.any(|unique_cols| *cols == **unique_cols)
}
pub fn project(&self, indexes: impl Iterator<Item = ColId>) -> Result<Vec<&ColumnSchema>, InvalidFieldError> {
indexes
.map(|index| self.get_column(index.0 as usize).ok_or_else(|| index.into()))
.collect()
}
pub fn project_not_empty(&self, indexes: ColList) -> Result<Vec<&ColumnSchema>, InvalidFieldError> {
self.project(indexes.iter())
}
pub fn get_row_type(&self) -> &ProductType {
&self.row_type
}
pub fn into_row_type(self) -> ProductType {
self.row_type
}
fn backcompat_constraints_iter(&self) -> impl Iterator<Item = (ColList, Constraints)> + '_ {
self.constraints
.iter()
.map(|x| -> (ColList, Constraints) {
match &x.data {
ConstraintData::Unique(unique) => (unique.columns.clone().into(), Constraints::unique()),
}
})
.chain(self.indexes.iter().map(|x| match &x.index_algorithm {
IndexAlgorithm::BTree(btree) => (btree.columns.clone(), Constraints::indexed()),
IndexAlgorithm::Hash(hash) => (hash.columns.clone(), Constraints::indexed()),
IndexAlgorithm::Direct(direct) => (direct.column.into(), Constraints::indexed()),
}))
.chain(
self.sequences
.iter()
.map(|x| (col_list![x.col_pos], Constraints::auto_inc())),
)
.chain(
self.primary_key
.iter()
.map(|x| (col_list![*x], Constraints::primary_key())),
)
}
pub fn backcompat_constraints(&self) -> BTreeMap<ColList, Constraints> {
combine_constraints(self.backcompat_constraints_iter())
}
pub fn backcompat_column_constraints(&self) -> BTreeMap<ColList, Constraints> {
let mut result = self.backcompat_constraints();
for col in &self.columns {
result.entry(col_list![col.col_pos]).or_insert(Constraints::unset());
}
result
}
pub fn pk(&self) -> Option<&ColumnSchema> {
self.primary_key.and_then(|pk| self.get_column(pk.0 as usize))
}
pub fn validated(self) -> Result<Self, Vec<SchemaError>> {
let mut errors = Vec::new();
let columns_not_found = self
.sequences
.iter()
.map(|x| (DefType::Sequence, x.sequence_name.clone(), ColList::new(x.col_pos)))
.chain(self.indexes.iter().map(|x| {
let cols = x.index_algorithm.columns().to_owned();
(DefType::Index, x.index_name.clone(), cols)
}))
.chain(self.constraints.iter().map(|x| {
(
DefType::Constraint,
x.constraint_name.clone(),
match &x.data {
ConstraintData::Unique(unique) => unique.columns.clone().into(),
},
)
}))
.filter_map(|(ty, name, cols)| {
let mut not_found_iter = self
.get_columns(&cols)
.filter(|(_, x)| x.is_none())
.map(|(col, _)| col)
.peekable();
if not_found_iter.peek().is_none() {
None
} else {
Some(SchemaError::ColumnsNotFound {
name,
table: self.table_name.clone(),
columns: not_found_iter.collect(),
ty,
})
}
});
errors.extend(columns_not_found);
errors.extend(self.columns.iter().filter_map(|x| {
if x.col_name.is_empty() {
Some(SchemaError::EmptyName {
table: self.table_name.clone(),
ty: DefType::Column,
id: x.col_pos.0 as _,
})
} else {
None
}
}));
errors.extend(self.indexes.iter().filter_map(|x| {
if x.index_name.is_empty() {
Some(SchemaError::EmptyName {
table: self.table_name.clone(),
ty: DefType::Index,
id: x.index_id.0,
})
} else {
None
}
}));
errors.extend(self.constraints.iter().filter_map(|x| {
if x.constraint_name.is_empty() {
Some(SchemaError::EmptyName {
table: self.table_name.clone(),
ty: DefType::Constraint,
id: x.constraint_id.0,
})
} else {
None
}
}));
errors.extend(self.sequences.iter().filter_map(|x| {
if x.sequence_name.is_empty() {
Some(SchemaError::EmptyName {
table: self.table_name.clone(),
ty: DefType::Sequence,
id: x.sequence_id.0,
})
} else {
None
}
}));
if let Some(err) = self
.sequences
.iter()
.group_by(|&seq| seq.col_pos)
.into_iter()
.find_map(|(key, group)| {
let count = group.count();
if count > 1 {
Some(SchemaError::OneAutoInc {
table: self.table_name.clone(),
field: self.columns[key.idx()].col_name.clone(),
})
} else {
None
}
})
{
errors.push(err);
}
if errors.is_empty() {
Ok(self)
} else {
Err(errors)
}
}
pub fn janky_fix_column_defs(&mut self, module_def: &ModuleDef) {
let table_name = self.table_name.clone().into();
for col in &mut self.columns {
let def: &ColumnDef = module_def.lookup((&table_name, &col.col_name)).unwrap();
col.col_type = def.ty.clone();
}
let table_def: &TableDef = module_def.expect_lookup(&table_name);
self.row_type = module_def.typespace()[table_def.product_type_ref]
.as_product()
.unwrap()
.clone();
}
pub fn normalize(&mut self) {
self.indexes.sort_by(|a, b| a.index_name.cmp(&b.index_name));
self.constraints
.sort_by(|a, b| a.constraint_name.cmp(&b.constraint_name));
self.sequences.sort_by(|a, b| a.sequence_name.cmp(&b.sequence_name));
}
}
fn find_remove<T>(vec: &mut Vec<T>, predicate: impl Fn(&T) -> bool) -> Option<T> {
let pos = vec.iter().position(predicate)?;
Some(vec.remove(pos))
}
macro_rules! ensure_eq {
($a:expr, $b:expr, $msg:expr) => {
if $a != $b {
anyhow::bail!(
"{0}: expected {1} == {2}:\n {1}: {3:?}\n {2}: {4:?}",
$msg,
stringify!($a),
stringify!($b),
$a,
$b
);
}
};
}
pub fn column_schemas_from_defs(module_def: &ModuleDef, columns: &[ColumnDef], table_id: TableId) -> Vec<ColumnSchema> {
columns
.iter()
.enumerate()
.map(|(col_pos, def)| ColumnSchema::from_module_def(module_def, def, (), (table_id, col_pos.into())))
.collect()
}
impl TableSchema {
pub fn from_view_def_for_codegen(module_def: &ModuleDef, view_def: &ViewDef) -> Self {
module_def.expect_contains(view_def);
let ViewDef {
name,
is_public,
is_anonymous,
primary_key,
param_columns,
return_columns,
..
} = view_def;
let columns = return_columns
.iter()
.map(|def| ColumnSchema::from_view_column_def(module_def, def))
.enumerate()
.map(|(i, schema)| (ColId::from(i), schema))
.map(|(col_pos, schema)| ColumnSchema { col_pos, ..schema })
.collect();
let view_primary_key = (module_def.raw_module_def_version() == RawModuleDefVersion::V10)
.then_some(*primary_key)
.flatten();
let table_access = if *is_public {
StAccess::Public
} else {
StAccess::Private
};
let view_info = ViewDefInfo {
view_id: ViewId::SENTINEL,
has_args: !param_columns.is_empty(),
is_anonymous: *is_anonymous,
};
TableSchema::new(
TableId::SENTINEL,
TableName::new(name.clone()),
Some(view_info),
columns,
vec![],
vec![],
vec![],
StTableType::User,
table_access,
None,
view_primary_key,
false,
None,
)
}
pub fn from_view_def_for_datastore(module_def: &ModuleDef, view_def: &ViewDef) -> Self {
module_def.expect_contains(view_def);
let ViewDef {
name,
is_public,
is_anonymous,
primary_key,
param_columns,
return_columns,
accessor_name,
..
} = view_def;
let n = return_columns.len() + 2;
let mut columns = Vec::with_capacity(n);
let mut meta_cols = 0;
let mut push_column = |name: &'static str, col_type| {
meta_cols += 1;
columns.push(ColumnSchema {
table_id: TableId::SENTINEL,
col_pos: columns.len().into(),
col_name: Identifier::new_assume_valid(name.into()),
col_type,
alias: None,
});
};
if !is_anonymous {
push_column("sender", AlgebraicType::identity());
}
if !param_columns.is_empty() {
push_column("arg_id", AlgebraicType::U64);
}
columns.extend(
return_columns
.iter()
.map(|def| ColumnSchema::from_view_column_def(module_def, def))
.enumerate()
.map(|(i, schema)| (ColId::from(meta_cols + i), schema))
.map(|(col_pos, schema)| ColumnSchema { col_pos, ..schema }),
);
let make_index_name = |col_list: &ColList| {
let cols_name = generate_cols_name(col_list, |col| columns.get(col.idx()).map(|col| &*col.col_name));
RawIdentifier::new(format!("{name}_{cols_name}_idx_btree"))
};
let make_constraint_name = |col_list: &ColList| {
let cols_name = generate_cols_name(col_list, |col| columns.get(col.idx()).map(|col| &*col.col_name));
RawIdentifier::new(format!("{name}_{cols_name}_key"))
};
let mut indexes = match meta_cols {
1 => vec![IndexSchema {
index_id: IndexId::SENTINEL,
table_id: TableId::SENTINEL,
index_name: make_index_name(&col_list![0]),
index_algorithm: IndexAlgorithm::BTree(col_list![0].into()),
alias: None,
}],
2 => vec![IndexSchema {
index_id: IndexId::SENTINEL,
table_id: TableId::SENTINEL,
index_name: make_index_name(&col_list![0, 1]),
index_algorithm: IndexAlgorithm::BTree(col_list![0, 1].into()),
alias: None,
}],
_ => vec![],
};
let mut constraints = vec![];
let view_primary_key = (module_def.raw_module_def_version() == RawModuleDefVersion::V10)
.then_some(primary_key.map(|pk| ColId::from(meta_cols + pk.idx())))
.flatten();
if *is_anonymous {
if let Some(pk_col) = view_primary_key {
let cols = col_list![pk_col];
constraints.push(ConstraintSchema {
table_id: TableId::SENTINEL,
constraint_id: ConstraintId::SENTINEL,
constraint_name: make_constraint_name(&cols),
data: ConstraintData::Unique(UniqueConstraintData {
columns: ColSet::from(cols.clone()),
}),
});
indexes.push(IndexSchema {
index_id: IndexId::SENTINEL,
table_id: TableId::SENTINEL,
index_name: make_index_name(&cols),
index_algorithm: IndexAlgorithm::BTree(cols.into()),
alias: None,
});
}
} else if let Some(pk_col) = view_primary_key {
let cols = col_list![ColId(0), pk_col];
indexes.push(IndexSchema {
index_id: IndexId::SENTINEL,
table_id: TableId::SENTINEL,
index_name: make_index_name(&cols),
index_algorithm: IndexAlgorithm::BTree(cols.into()),
alias: None,
});
}
let table_access = if *is_public {
StAccess::Public
} else {
StAccess::Private
};
let view_info = ViewDefInfo {
view_id: ViewId::SENTINEL,
has_args: !param_columns.is_empty(),
is_anonymous: *is_anonymous,
};
TableSchema::new(
TableId::SENTINEL,
TableName::new(name.clone()),
Some(view_info),
columns,
indexes,
constraints,
vec![],
StTableType::User,
table_access,
None,
if *is_anonymous { view_primary_key } else { None },
false,
Some(accessor_name.clone()),
)
}
}
impl Schema for TableSchema {
type Def = TableDef;
type Id = TableId;
type ParentId = ();
fn from_module_def(
module_def: &ModuleDef,
def: &Self::Def,
_parent_id: Self::ParentId,
table_id: Self::Id,
) -> Self {
module_def.expect_contains(def);
let TableDef {
name,
product_type_ref: _,
primary_key,
columns,
indexes,
constraints,
sequences,
schedule,
table_type,
table_access,
is_event,
accessor_name,
..
} = def;
let columns = column_schemas_from_defs(module_def, columns, table_id);
let indexes = indexes
.values()
.map(|def| IndexSchema::from_module_def(module_def, def, table_id, IndexId::SENTINEL))
.collect();
let sequences = sequences
.values()
.map(|def| SequenceSchema::from_module_def(module_def, def, table_id, SequenceId::SENTINEL))
.collect();
let constraints = constraints
.values()
.map(|def| ConstraintSchema::from_module_def(module_def, def, table_id, ConstraintId::SENTINEL))
.collect();
let schedule = schedule
.as_ref()
.map(|schedule| ScheduleSchema::from_module_def(module_def, schedule, table_id, ScheduleId::SENTINEL));
TableSchema::new(
table_id,
TableName::new(name.clone()),
None,
columns,
indexes,
constraints,
sequences,
(*table_type).into(),
(*table_access).into(),
schedule,
*primary_key,
*is_event,
Some(accessor_name.clone()),
)
}
fn check_compatible(&self, module_def: &ModuleDef, def: &Self::Def) -> Result<(), anyhow::Error> {
ensure_eq!(&self.table_name[..], &def.name[..], "Table name mismatch");
ensure_eq!(self.primary_key, def.primary_key, "Primary key mismatch");
let def_table_access: StAccess = (def.table_access).into();
ensure_eq!(self.table_access, def_table_access, "Table access mismatch");
let def_table_type: StTableType = (def.table_type).into();
ensure_eq!(self.table_type, def_table_type, "Table type mismatch");
for col in &self.columns {
let col_def = def
.columns
.get(col.col_pos.0 as usize)
.ok_or_else(|| anyhow::anyhow!("Column {} not found in definition", col.col_pos.0))?;
col.check_compatible(module_def, col_def)?;
}
ensure_eq!(self.columns.len(), def.columns.len(), "Column count mismatch");
for index in &self.indexes {
let index_def = def
.indexes
.get(&index.index_name)
.ok_or_else(|| anyhow::anyhow!("Index {} not found in definition", index.index_id.0))?;
index.check_compatible(module_def, index_def)?;
}
ensure_eq!(self.indexes.len(), def.indexes.len(), "Index count mismatch");
for constraint in &self.constraints {
let constraint_def = def
.constraints
.get(&constraint.constraint_name)
.ok_or_else(|| anyhow::anyhow!("Constraint {} not found in definition", constraint.constraint_id.0))?;
constraint.check_compatible(module_def, constraint_def)?;
}
ensure_eq!(
self.constraints.len(),
def.constraints.len(),
"Constraint count mismatch"
);
for sequence in &self.sequences {
let sequence_def = def
.sequences
.get(&sequence.sequence_name)
.ok_or_else(|| anyhow::anyhow!("Sequence {} not found in definition", sequence.sequence_id.0))?;
sequence.check_compatible(module_def, sequence_def)?;
}
ensure_eq!(self.sequences.len(), def.sequences.len(), "Sequence count mismatch");
if let Some(schedule) = &self.schedule {
let schedule_def = def
.schedule
.as_ref()
.ok_or_else(|| anyhow::anyhow!("Schedule not found in definition"))?;
schedule.check_compatible(module_def, schedule_def)?;
}
ensure_eq!(
self.schedule.is_some(),
def.schedule.is_some(),
"Schedule presence mismatch"
);
Ok(())
}
}
impl From<&TableSchema> for ProductType {
fn from(value: &TableSchema) -> Self {
value.row_type.clone()
}
}
impl From<&TableSchema> for DbTable {
fn from(value: &TableSchema) -> Self {
DbTable::new(
Arc::new(value.into()),
value.table_id,
value.table_type,
value.table_access,
)
}
}
impl From<&TableSchema> for Header {
fn from(value: &TableSchema) -> Self {
let fields = value
.columns
.iter()
.map(|x| Column::new(FieldName::new(value.table_id, x.col_pos), x.col_type.clone()))
.collect();
Header::new(
value.table_id,
value.table_name.clone(),
fields,
value.backcompat_constraints(),
)
}
}
impl From<TableSchema> for Header {
fn from(schema: TableSchema) -> Self {
Header::from(&schema)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Ord, PartialOrd)]
pub struct ColumnSchema {
pub table_id: TableId,
pub col_pos: ColId,
pub col_name: Identifier,
pub alias: Option<Identifier>,
pub col_type: AlgebraicType,
}
impl spacetimedb_memory_usage::MemoryUsage for ColumnSchema {
fn heap_usage(&self) -> usize {
let Self {
table_id,
col_pos,
col_name,
col_type,
..
} = self;
table_id.heap_usage() + col_pos.heap_usage() + col_name.heap_usage() + col_type.heap_usage()
}
}
impl ColumnSchema {
#[cfg(any(test, feature = "test"))]
pub fn for_test(pos: impl Into<ColId>, name: impl AsRef<str>, ty: AlgebraicType) -> Self {
Self {
table_id: TableId::SENTINEL,
col_pos: pos.into(),
col_name: Identifier::for_test(name),
col_type: ty,
alias: None,
}
}
fn from_view_column_def(module_def: &ModuleDef, def: &ViewColumnDef) -> Self {
let col_type = WithTypespace::new(module_def.typespace(), &def.ty)
.resolve_refs()
.expect("validated module should have all types resolve");
ColumnSchema {
table_id: TableId::SENTINEL,
col_pos: def.col_id,
col_name: def.name.clone(),
col_type,
alias: Some(def.accessor_name.clone()),
}
}
}
impl Schema for ColumnSchema {
type Def = ColumnDef;
type ParentId = ();
type Id = (TableId, ColId);
fn from_module_def(
module_def: &ModuleDef,
def: &ColumnDef,
_parent_id: (),
(table_id, col_pos): (TableId, ColId),
) -> Self {
let col_type = WithTypespace::new(module_def.typespace(), &def.ty)
.resolve_refs()
.expect("validated module should have all types resolve");
ColumnSchema {
table_id,
col_pos,
col_name: def.name.clone(),
col_type,
alias: Some(def.accessor_name.clone()),
}
}
fn check_compatible(&self, module_def: &ModuleDef, def: &Self::Def) -> Result<(), anyhow::Error> {
ensure_eq!(&self.col_name[..], &def.name[..], "Column name mismatch");
let resolved_def_ty = WithTypespace::new(module_def.typespace(), &def.ty).resolve_refs()?;
ensure_eq!(self.col_type, resolved_def_ty, "Column type mismatch");
ensure_eq!(self.col_pos, def.col_id, "Column ID mismatch");
Ok(())
}
}
impl From<&ColumnSchema> for ProductTypeElement {
fn from(value: &ColumnSchema) -> Self {
Self {
name: Some(value.col_name.clone().into()),
algebraic_type: value.col_type.clone(),
}
}
}
impl From<ColumnSchema> for Column {
fn from(schema: ColumnSchema) -> Self {
Column {
field: FieldName {
table: schema.table_id,
col: schema.col_pos,
},
algebraic_type: schema.col_type,
}
}
}
#[derive(Debug, Clone)]
pub struct ColumnSchemaRef<'a> {
pub column: &'a ColumnSchema,
pub table_name: &'a RawIdentifier,
}
impl From<ColumnSchemaRef<'_>> for ProductTypeElement {
fn from(value: ColumnSchemaRef) -> Self {
ProductTypeElement::new(
value.column.col_type.clone(),
Some(value.column.col_name.clone().into()),
)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SequenceSchema {
pub sequence_id: SequenceId,
pub sequence_name: RawIdentifier,
pub table_id: TableId,
pub col_pos: ColId,
pub increment: i128,
pub start: i128,
pub min_value: i128,
pub max_value: i128,
}
impl spacetimedb_memory_usage::MemoryUsage for SequenceSchema {
fn heap_usage(&self) -> usize {
let Self {
sequence_id,
sequence_name,
table_id,
col_pos,
increment,
start,
min_value,
max_value,
} = self;
sequence_id.heap_usage()
+ sequence_name.heap_usage()
+ table_id.heap_usage()
+ col_pos.heap_usage()
+ increment.heap_usage()
+ start.heap_usage()
+ min_value.heap_usage()
+ max_value.heap_usage()
}
}
impl Schema for SequenceSchema {
type Def = SequenceDef;
type Id = SequenceId;
type ParentId = TableId;
fn from_module_def(module_def: &ModuleDef, def: &Self::Def, parent_id: Self::ParentId, id: Self::Id) -> Self {
module_def.expect_contains(def);
SequenceSchema {
sequence_id: id,
sequence_name: def.name.clone(),
table_id: parent_id,
col_pos: def.column,
increment: def.increment,
start: def.start.unwrap_or(1),
min_value: def.min_value.unwrap_or(1),
max_value: def.max_value.unwrap_or(i128::MAX),
}
}
fn check_compatible(&self, _module_def: &ModuleDef, def: &Self::Def) -> Result<(), anyhow::Error> {
ensure_eq!(&self.sequence_name[..], &def.name[..], "Sequence name mismatch");
ensure_eq!(self.col_pos, def.column, "Sequence column mismatch");
ensure_eq!(self.increment, def.increment, "Sequence increment mismatch");
if let Some(start) = &def.start {
ensure_eq!(self.start, *start, "Sequence start mismatch");
}
if let Some(min_value) = &def.min_value {
ensure_eq!(self.min_value, *min_value, "Sequence min_value mismatch");
}
if let Some(max_value) = &def.max_value {
ensure_eq!(self.max_value, *max_value, "Sequence max_value mismatch");
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ScheduleSchema {
pub table_id: TableId,
pub schedule_id: ScheduleId,
pub schedule_name: Identifier,
pub function_name: Identifier,
pub at_column: ColId,
}
impl ScheduleSchema {
#[cfg(any(test, feature = "test"))]
pub fn for_test(name: impl AsRef<str>, function: impl AsRef<str>, at: impl Into<ColId>) -> Self {
Self {
table_id: TableId::SENTINEL,
schedule_id: ScheduleId::SENTINEL,
schedule_name: Identifier::for_test(name.as_ref()),
function_name: Identifier::for_test(function.as_ref()),
at_column: at.into(),
}
}
}
impl Schema for ScheduleSchema {
type Def = ScheduleDef;
type Id = ScheduleId;
type ParentId = TableId;
fn from_module_def(module_def: &ModuleDef, def: &Self::Def, parent_id: Self::ParentId, id: Self::Id) -> Self {
module_def.expect_contains(def);
ScheduleSchema {
table_id: parent_id,
schedule_id: id,
schedule_name: def.name.clone(),
function_name: def.function_name.clone(),
at_column: def.at_column,
}
}
fn check_compatible(&self, _module_def: &ModuleDef, def: &Self::Def) -> Result<(), anyhow::Error> {
ensure_eq!(&self.schedule_name[..], &def.name[..], "Schedule name mismatch");
ensure_eq!(
&self.function_name[..],
&def.function_name[..],
"Schedule function name mismatch"
);
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct IndexSchema {
pub index_id: IndexId,
pub table_id: TableId,
pub index_name: RawIdentifier,
pub alias: Option<RawIdentifier>,
pub index_algorithm: IndexAlgorithm,
}
impl spacetimedb_memory_usage::MemoryUsage for IndexSchema {
fn heap_usage(&self) -> usize {
let Self {
index_id,
table_id,
index_name,
index_algorithm,
alias: _,
} = self;
index_id.heap_usage() + table_id.heap_usage() + index_name.heap_usage() + index_algorithm.heap_usage()
}
}
impl IndexSchema {
pub fn for_test(name: impl AsRef<str>, algo: impl Into<IndexAlgorithm>) -> Self {
Self {
index_id: IndexId::SENTINEL,
table_id: TableId::SENTINEL,
index_name: RawIdentifier::new(name.as_ref()),
index_algorithm: algo.into(),
alias: None,
}
}
}
impl Schema for IndexSchema {
type Def = IndexDef;
type Id = IndexId;
type ParentId = TableId;
fn from_module_def(module_def: &ModuleDef, def: &Self::Def, parent_id: Self::ParentId, id: Self::Id) -> Self {
module_def.expect_contains(def);
let index_algorithm = def.algorithm.clone();
IndexSchema {
index_id: id,
table_id: parent_id,
index_name: def.name.clone(),
index_algorithm,
alias: Some(def.source_name.clone()),
}
}
fn check_compatible(&self, _module_def: &ModuleDef, def: &Self::Def) -> Result<(), anyhow::Error> {
ensure_eq!(&self.index_name[..], &def.name[..], "Index name mismatch");
ensure_eq!(&self.index_algorithm, &def.algorithm, "Index algorithm mismatch");
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ConstraintSchema {
pub table_id: TableId,
pub constraint_id: ConstraintId,
pub constraint_name: RawIdentifier,
pub data: ConstraintData, }
impl spacetimedb_memory_usage::MemoryUsage for ConstraintSchema {
fn heap_usage(&self) -> usize {
let Self {
table_id,
constraint_id,
constraint_name,
data,
} = self;
table_id.heap_usage() + constraint_id.heap_usage() + constraint_name.heap_usage() + data.heap_usage()
}
}
impl ConstraintSchema {
pub fn unique_for_test(name: impl AsRef<str>, cols: impl Into<ColSet>) -> Self {
Self {
table_id: TableId::SENTINEL,
constraint_id: ConstraintId::SENTINEL,
constraint_name: RawIdentifier::new(name.as_ref()),
data: ConstraintData::Unique(UniqueConstraintData { columns: cols.into() }),
}
}
#[deprecated(note = "Use TableSchema::from_module_def instead")]
pub fn from_def(table_id: TableId, constraint: RawConstraintDefV8) -> Option<Self> {
if constraint.constraints.has_unique() {
Some(ConstraintSchema {
constraint_id: ConstraintId::SENTINEL, constraint_name: RawIdentifier::new(constraint.constraint_name.trim()),
table_id,
data: ConstraintData::Unique(UniqueConstraintData {
columns: constraint.columns.into(),
}),
})
} else {
None
}
}
}
impl Schema for ConstraintSchema {
type Def = ConstraintDef;
type Id = ConstraintId;
type ParentId = TableId;
fn from_module_def(module_def: &ModuleDef, def: &Self::Def, parent_id: Self::ParentId, id: Self::Id) -> Self {
module_def.expect_contains(def);
ConstraintSchema {
constraint_id: id,
constraint_name: def.name.clone(),
table_id: parent_id,
data: def.data.clone(),
}
}
fn check_compatible(&self, _module_def: &ModuleDef, def: &Self::Def) -> Result<(), anyhow::Error> {
ensure_eq!(&self.constraint_name[..], &def.name[..], "Constraint name mismatch");
ensure_eq!(&self.data, &def.data, "Constraint data mismatch");
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RowLevelSecuritySchema {
pub table_id: TableId,
pub sql: RawSql,
}