Skip to main content

spacetimedb_schema/
schema.rs

1//! Schema data structures.
2//! These are used at runtime by the vm to store the schema of the database.
3//! They are mirrored in the system tables -- see `spacetimedb_core::db::datastore::system_tables`.
4//! Types in this file are not public ABI or API and may be changed at any time; it's the system tables that cannot.
5
6// TODO(1.0): change all the `RawIdentifier`s in this file to `Identifier`.
7// This doesn't affect the ABI so can wait until 1.0.
8
9use crate::def::error::{DefType, SchemaError};
10use crate::relation::{combine_constraints, Column, DbTable, FieldName, Header};
11use crate::table_name::TableName;
12use core::mem;
13use itertools::Itertools;
14use spacetimedb_lib::db::auth::{StAccess, StTableType};
15use spacetimedb_lib::db::raw_def::v9::RawSql;
16use spacetimedb_lib::db::raw_def::{generate_cols_name, RawConstraintDefV8};
17use spacetimedb_primitives::*;
18use spacetimedb_sats::product_value::InvalidFieldError;
19use spacetimedb_sats::raw_identifier::RawIdentifier;
20use spacetimedb_sats::{AlgebraicType, ProductType, ProductTypeElement, WithTypespace};
21use std::collections::BTreeMap;
22use std::sync::Arc;
23
24use crate::def::{
25    ColumnDef, ConstraintData, ConstraintDef, IndexAlgorithm, IndexDef, ModuleDef, ModuleDefLookup,
26    RawModuleDefVersion, ScheduleDef, SequenceDef, TableDef, UniqueConstraintData, ViewColumnDef, ViewDef,
27};
28use crate::identifier::Identifier;
29
30/// Helper trait documenting allowing schema entities to be built from a validated `ModuleDef`.
31pub trait Schema: Sized {
32    /// The `Def` type corresponding to this schema type.
33    type Def: ModuleDefLookup;
34    /// The `Id` type corresponding to this schema type.
35    type Id;
36    /// The `Id` type corresponding to the parent of this schema type.
37    /// Set to `()` if there is no parent.
38    type ParentId;
39
40    /// Construct a schema entity from a validated `ModuleDef`.
41    /// Panics if `module_def` does not contain `def`.
42    ///
43    /// If this schema entity contains children (e.g. if it is a table schema), they should be constructed with
44    /// IDs set to `ChildId::SENTINEL`.
45    ///
46    /// If this schema entity contains `AlgebraicType`s, they should be fully resolved by this function (via
47    /// `WithTypespace::resolve_refs`). This means they will no longer contain references to any typespace (and be non-recursive).
48    /// This is necessary because the database does not currently attempt to handle typespaces / recursive types.
49    fn from_module_def(module_def: &ModuleDef, def: &Self::Def, parent_id: Self::ParentId, id: Self::Id) -> Self;
50
51    /// Check that a schema entity is compatible with a definition.
52    fn check_compatible(&self, module_def: &ModuleDef, def: &Self::Def) -> Result<(), anyhow::Error>;
53}
54
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56pub struct ViewDefInfo {
57    pub view_id: ViewId,
58    pub has_args: bool,
59    pub is_anonymous: bool,
60}
61
62impl ViewDefInfo {
63    pub fn num_private_cols(&self) -> usize {
64        (if self.is_anonymous { 0 } else { 1 }) + (if self.has_args { 1 } else { 0 })
65    }
66}
67
68/// A wrapper around a [`TableSchema`] for views.
69#[derive(Debug, Clone, PartialEq, Eq)]
70pub struct TableOrViewSchema {
71    pub table_id: TableId,
72    pub view_info: Option<ViewDefInfo>,
73    pub table_name: TableName,
74    pub table_access: StAccess,
75    inner: Arc<TableSchema>,
76}
77
78impl From<Arc<TableSchema>> for TableOrViewSchema {
79    fn from(inner: Arc<TableSchema>) -> Self {
80        Self {
81            table_id: inner.table_id,
82            view_info: inner.view_info,
83            table_name: inner.table_name.clone(),
84            table_access: inner.table_access,
85            inner,
86        }
87    }
88}
89
90impl TableOrViewSchema {
91    /// Is this schema that of a view?
92    pub fn is_view(&self) -> bool {
93        self.view_info.is_some()
94    }
95
96    /// Is this schema that of an anonymous view?
97    pub fn is_anonymous_view(&self) -> bool {
98        self.view_info.as_ref().is_some_and(|view_info| view_info.is_anonymous)
99    }
100
101    /// Returns the [`TableSchema`] of the underlying datastore table.
102    /// For views, this schema will include the internal `sender` and `arg_id` columns.
103    pub fn inner(&self) -> Arc<TableSchema> {
104        self.inner.clone()
105    }
106
107    /// Returns the public columns of this table.
108    ///
109    /// The [`ColId`]s in this list do not necessarily correspond to their position in this list.
110    /// Rather they correspond to the position of the column in the physical datastore table.
111    /// This is important since this method may not return all columns recorded in the datastore.
112    /// For views in particular it will not include the internal `sender` and `arg_id` columns.
113    /// Hence columns in this list should be looked up by their [`ColId`] - not their position.
114    pub fn public_columns(&self) -> &[ColumnSchema] {
115        match self.view_info {
116            Some(ViewDefInfo {
117                has_args: true,
118                is_anonymous: false,
119                ..
120            }) => &self.inner.columns[2..],
121            Some(ViewDefInfo {
122                has_args: true,
123                is_anonymous: true,
124                ..
125            }) => &self.inner.columns[1..],
126            Some(ViewDefInfo {
127                has_args: false,
128                is_anonymous: false,
129                ..
130            }) => &self.inner.columns[1..],
131            Some(ViewDefInfo {
132                has_args: false,
133                is_anonymous: true,
134                ..
135            })
136            | None => &self.inner.columns,
137        }
138    }
139
140    /// Check if the `col_name` exist on this [`TableOrViewSchema`]
141    pub fn get_column_by_name(&self, col_name: &str) -> Option<&ColumnSchema> {
142        self.public_columns().iter().find(|x| &*x.col_name == col_name)
143    }
144
145    /// Check if the `col_name` exists on this [`TableOrViewSchema`], prioritizing alias over canonical name.
146    pub fn get_column_by_name_or_alias(&self, col_name: &str) -> Option<&ColumnSchema> {
147        self.public_columns()
148            .iter()
149            .find(|col| col.alias.as_deref().is_some_and(|alias| alias == col_name))
150            .or_else(|| self.get_column_by_name(col_name))
151    }
152}
153
154/// A data structure representing the schema of a database table.
155///
156/// This struct holds information about the table, including its identifier,
157/// name, columns, indexes, constraints, sequences, type, and access rights.
158#[derive(Debug, Clone, PartialEq, Eq)]
159pub struct TableSchema {
160    /// The unique identifier of the table within the database.
161    pub table_id: TableId,
162
163    /// The name of the table.
164    pub table_name: TableName,
165
166    pub alias: Option<Identifier>,
167
168    /// Is this the backing table of a view?
169    pub view_info: Option<ViewDefInfo>,
170
171    /// The columns of the table.
172    /// The ordering of the columns is significant. Columns are frequently identified by `ColId`, that is, position in this list.
173    pub columns: Vec<ColumnSchema>,
174
175    /// The primary key of the table, if present. Must refer to a valid column.
176    ///
177    /// Currently, there must be a unique constraint and an index corresponding to the primary key.
178    /// Eventually, we may remove the requirement for an index.
179    ///
180    /// The database engine does not actually care about this, but client code generation does.
181    pub primary_key: Option<ColId>,
182
183    /// The indexes on the table.
184    pub indexes: Vec<IndexSchema>,
185
186    /// The constraints on the table.
187    pub constraints: Vec<ConstraintSchema>,
188
189    /// The sequences on the table.
190    pub sequences: Vec<SequenceSchema>,
191
192    /// Whether the table was created by a user or by the system.
193    pub table_type: StTableType,
194
195    /// The visibility of the table.
196    pub table_access: StAccess,
197
198    /// The schedule for the table, if present.
199    pub schedule: Option<ScheduleSchema>,
200
201    /// Whether this is an event table.
202    pub is_event: bool,
203
204    /// Cache for `row_type_for_table` in the data store.
205    pub row_type: ProductType,
206}
207
208/// Converts a list of columns to a table's row type.
209pub fn columns_to_row_type(columns: &[ColumnSchema]) -> ProductType {
210    ProductType::new(columns.iter().map(ProductTypeElement::from).collect())
211}
212
213impl TableSchema {
214    /// Create a table schema.
215    #[allow(clippy::too_many_arguments)]
216    pub fn new(
217        table_id: TableId,
218        table_name: TableName,
219        view_info: Option<ViewDefInfo>,
220        columns: Vec<ColumnSchema>,
221        indexes: Vec<IndexSchema>,
222        constraints: Vec<ConstraintSchema>,
223        sequences: Vec<SequenceSchema>,
224        table_type: StTableType,
225        table_access: StAccess,
226        schedule: Option<ScheduleSchema>,
227        primary_key: Option<ColId>,
228        is_event: bool,
229        alias: Option<Identifier>,
230    ) -> Self {
231        Self {
232            row_type: columns_to_row_type(&columns),
233            table_id,
234            table_name,
235            view_info,
236            columns,
237            indexes,
238            constraints,
239            sequences,
240            table_type,
241            table_access,
242            schedule,
243            primary_key,
244            is_event,
245            alias,
246        }
247    }
248
249    /// Create a `TableSchema` corresponding to a product type.
250    /// For use in tests.
251    #[cfg(any(test, feature = "test"))]
252    pub fn from_product_type(ty: ProductType) -> TableSchema {
253        let columns = ty
254            .elements
255            .iter()
256            .enumerate()
257            .map(|(col_pos, element)| ColumnSchema {
258                table_id: TableId::SENTINEL,
259                col_pos: ColId(col_pos as _),
260                col_name: element
261                    .name
262                    .clone()
263                    .map(Identifier::new_assume_valid)
264                    .unwrap_or_else(|| Identifier::for_test(format!("col{col_pos}"))),
265                col_type: element.algebraic_type.clone(),
266                alias: None,
267            })
268            .collect();
269
270        TableSchema::new(
271            TableId::SENTINEL,
272            TableName::for_test("TestTable"),
273            None,
274            columns,
275            vec![],
276            vec![],
277            vec![],
278            StTableType::User,
279            StAccess::Public,
280            None,
281            None,
282            false,
283            None,
284        )
285    }
286
287    /// Is this the backing table for a view?
288    pub fn is_view(&self) -> bool {
289        self.view_info.is_some()
290    }
291
292    /// Is this the backing table for an anonymous view?
293    pub fn is_anonymous_view(&self) -> bool {
294        self.view_info.as_ref().is_some_and(|view_info| view_info.is_anonymous)
295    }
296
297    /// How many private columns does this table have?
298    /// Will only be non-zero in the case of views.
299    pub fn num_private_cols(&self) -> usize {
300        self.view_info
301            .as_ref()
302            .map(|view_info| view_info.num_private_cols())
303            .unwrap_or_default()
304    }
305
306    /// Update the table id of this schema.
307    /// For use by the core database engine after assigning a table id.
308    pub fn update_table_id(&mut self, id: TableId) {
309        self.table_id = id;
310        self.columns.iter_mut().for_each(|c| c.table_id = id);
311        self.indexes.iter_mut().for_each(|i| i.table_id = id);
312        self.constraints.iter_mut().for_each(|c| c.table_id = id);
313        self.sequences.iter_mut().for_each(|s| s.table_id = id);
314        if let Some(s) = self.schedule.as_mut() {
315            s.table_id = id;
316        }
317    }
318
319    /// Reset all the ids in this schema to sentinel values.
320    /// It is useful when cloning a schema to create a new table.
321    pub fn reset(&mut self) {
322        self.update_table_id(TableId::SENTINEL);
323        self.indexes.iter_mut().for_each(|i| i.index_id = IndexId::SENTINEL);
324        self.sequences
325            .iter_mut()
326            .for_each(|i| i.sequence_id = SequenceId::SENTINEL);
327        self.constraints
328            .iter_mut()
329            .for_each(|i| i.constraint_id = ConstraintId::SENTINEL);
330        self.row_type = columns_to_row_type(&self.columns);
331    }
332
333    /// Convert a table schema into a list of columns.
334    pub fn into_columns(self) -> Vec<ColumnSchema> {
335        self.columns
336    }
337
338    /// Get the columns of the table. Only immutable access to the columns is provided.
339    /// The ordering of the columns is significant. Columns are frequently identified by `ColId`, that is, position in this list.
340    pub fn columns(&self) -> &[ColumnSchema] {
341        &self.columns
342    }
343
344    /// How many columns does this table have?
345    pub fn num_cols(&self) -> usize {
346        self.columns.len()
347    }
348
349    /// Extracts all the [Self::indexes], [Self::sequences], and [Self::constraints].
350    pub fn take_adjacent_schemas(&mut self) -> (Vec<IndexSchema>, Vec<SequenceSchema>, Vec<ConstraintSchema>) {
351        (
352            mem::take(&mut self.indexes),
353            mem::take(&mut self.sequences),
354            mem::take(&mut self.constraints),
355        )
356    }
357
358    // Crud operation on adjacent schemas
359
360    /// Add OR replace the [SequenceSchema]
361    pub fn update_sequence(&mut self, of: SequenceSchema) {
362        if let Some(x) = self.sequences.iter_mut().find(|x| x.sequence_id == of.sequence_id) {
363            *x = of;
364        } else {
365            self.sequences.push(of);
366        }
367    }
368
369    /// Removes the given `sequence_id`
370    pub fn remove_sequence(&mut self, sequence_id: SequenceId) -> Option<SequenceSchema> {
371        find_remove(&mut self.sequences, |x| x.sequence_id == sequence_id)
372    }
373
374    /// Add OR replace the [IndexSchema]
375    pub fn update_index(&mut self, of: IndexSchema) {
376        if let Some(x) = self.indexes.iter_mut().find(|x| x.index_id == of.index_id) {
377            *x = of;
378        } else {
379            self.indexes.push(of);
380        }
381    }
382
383    /// Removes the given `index_id`
384    pub fn remove_index(&mut self, index_id: IndexId) -> Option<IndexSchema> {
385        find_remove(&mut self.indexes, |x| x.index_id == index_id)
386    }
387
388    /// Add OR replace the [ConstraintSchema]
389    pub fn update_constraint(&mut self, of: ConstraintSchema) {
390        if let Some(x) = self
391            .constraints
392            .iter_mut()
393            .find(|x| x.constraint_id == of.constraint_id)
394        {
395            *x = of;
396        } else {
397            self.constraints.push(of);
398        }
399    }
400
401    /// Removes the given `index_id`
402    pub fn remove_constraint(&mut self, constraint_id: ConstraintId) -> Option<ConstraintSchema> {
403        find_remove(&mut self.constraints, |x| x.constraint_id == constraint_id)
404    }
405
406    /// Concatenate the column names from the `columns`
407    ///
408    /// WARNING: If the `ColId` not exist, is skipped.
409    /// TODO(Tyler): This should return an error and not allow this to be constructed
410    /// if there is an invalid `ColId`
411    pub fn generate_cols_name(&self, columns: &ColList) -> String {
412        generate_cols_name(columns, |p| self.get_column(p.idx()).map(|c| &*c.col_name))
413    }
414
415    /// Check if the specified `field` exists in this [TableSchema].
416    ///
417    /// # Warning
418    ///
419    /// This function ignores the `table_id` when searching for a column.
420    pub fn get_column_by_field(&self, field: FieldName) -> Option<&ColumnSchema> {
421        self.get_column(field.col.idx())
422    }
423
424    /// Look up a list of columns by their positions in the table.
425    /// Invalid column positions are permitted.
426    pub fn get_columns<'a>(
427        &'a self,
428        columns: &'a ColList,
429    ) -> impl 'a + Iterator<Item = (ColId, Option<&'a ColumnSchema>)> {
430        columns.iter().map(|col| (col, self.columns.get(col.idx())))
431    }
432
433    /// Get a reference to a column by its position (`pos`) in the table.
434    pub fn get_column(&self, pos: usize) -> Option<&ColumnSchema> {
435        self.columns.get(pos)
436    }
437
438    /// Check if the `col_name` exist on this [TableSchema]
439    pub fn get_column_by_name(&self, col_name: &str) -> Option<&ColumnSchema> {
440        self.columns.iter().find(|x| &*x.col_name == col_name)
441    }
442
443    /// Check if the `col_name` exists on this [TableSchema], prioritizing alias over canonical name.
444    pub fn get_column_by_name_or_alias(&self, col_name: &str) -> Option<&ColumnSchema> {
445        self.columns
446            .iter()
447            .find(|col| col.alias.as_deref().is_some_and(|alias| alias == col_name))
448            .or_else(|| self.get_column_by_name(col_name))
449    }
450
451    /// Check if the `col_name` exist on this [TableSchema]
452    ///
453    /// Warning: It ignores the `table_name`
454    pub fn get_column_id_by_name(&self, col_name: &str) -> Option<ColId> {
455        self.columns
456            .iter()
457            .position(|x| &*x.col_name == col_name)
458            .map(|x| x.into())
459    }
460
461    /// Check if the `col_name` exists on this [TableSchema], prioritizing alias over canonical name.
462    ///
463    /// Warning: It ignores the `table_name`.
464    pub fn get_column_id_by_name_or_alias(&self, col_name: &str) -> Option<ColId> {
465        self.columns
466            .iter()
467            .position(|col| col.alias.as_deref().is_some_and(|alias| alias == col_name))
468            .or_else(|| self.get_column_id_by_name(col_name).map(|id| id.idx()))
469            .map(Into::into)
470    }
471
472    /// Check whether `name` matches table alias or canonical table name.
473    pub fn matches_name_or_alias(&self, name: &str) -> bool {
474        self.alias.as_deref().is_some_and(|alias| alias == name) || self.table_name.as_ref() == name
475    }
476
477    /// Retrieve the column ids for this index id
478    pub fn col_list_for_index_id(&self, index_id: IndexId) -> ColList {
479        self.indexes
480            .iter()
481            .find(|schema| schema.index_id == index_id)
482            .map(|schema| schema.index_algorithm.columns())
483            .map(|cols| ColList::from_iter(cols.iter()))
484            .unwrap_or_else(ColList::empty)
485    }
486
487    /// Is there a unique constraint for this set of columns?
488    pub fn is_unique(&self, cols: &impl PartialEq<ColList>) -> bool {
489        self.constraints
490            .iter()
491            .filter_map(|cs| cs.data.unique_columns())
492            .any(|unique_cols| *cols == **unique_cols)
493    }
494
495    /// Project the fields from the supplied `indexes`.
496    pub fn project(&self, indexes: impl Iterator<Item = ColId>) -> Result<Vec<&ColumnSchema>, InvalidFieldError> {
497        indexes
498            .map(|index| self.get_column(index.0 as usize).ok_or_else(|| index.into()))
499            .collect()
500    }
501
502    /// Utility for project the fields from the supplied `indexes` that is a [ColList],
503    /// used for when the list of field indexes have at least one value.
504    pub fn project_not_empty(&self, indexes: ColList) -> Result<Vec<&ColumnSchema>, InvalidFieldError> {
505        self.project(indexes.iter())
506    }
507
508    /// IMPORTANT: Is required to have this cached to avoid a perf drop on datastore operations
509    pub fn get_row_type(&self) -> &ProductType {
510        &self.row_type
511    }
512
513    /// Utility to avoid cloning in `row_type_for_table`
514    pub fn into_row_type(self) -> ProductType {
515        self.row_type
516    }
517
518    /// Iterate over the constraints on sets of columns on this table.
519    fn backcompat_constraints_iter(&self) -> impl Iterator<Item = (ColList, Constraints)> + '_ {
520        self.constraints
521            .iter()
522            .map(|x| -> (ColList, Constraints) {
523                match &x.data {
524                    ConstraintData::Unique(unique) => (unique.columns.clone().into(), Constraints::unique()),
525                }
526            })
527            .chain(self.indexes.iter().map(|x| match &x.index_algorithm {
528                IndexAlgorithm::BTree(btree) => (btree.columns.clone(), Constraints::indexed()),
529                IndexAlgorithm::Hash(hash) => (hash.columns.clone(), Constraints::indexed()),
530                IndexAlgorithm::Direct(direct) => (direct.column.into(), Constraints::indexed()),
531            }))
532            .chain(
533                self.sequences
534                    .iter()
535                    .map(|x| (col_list![x.col_pos], Constraints::auto_inc())),
536            )
537            .chain(
538                self.primary_key
539                    .iter()
540                    .map(|x| (col_list![*x], Constraints::primary_key())),
541            )
542    }
543
544    /// Get backwards-compatible constraints for this table.
545    ///
546    /// This is closer to how `TableSchema` used to work.
547    pub fn backcompat_constraints(&self) -> BTreeMap<ColList, Constraints> {
548        combine_constraints(self.backcompat_constraints_iter())
549    }
550
551    /// Get backwards-compatible constraints for this table.
552    ///
553    /// Resolves the constraints per each column. If the column don't have one, auto-generate [Constraints::unset()].
554    /// This guarantee all columns can be queried for it constraints.
555    pub fn backcompat_column_constraints(&self) -> BTreeMap<ColList, Constraints> {
556        let mut result = self.backcompat_constraints();
557        for col in &self.columns {
558            result.entry(col_list![col.col_pos]).or_insert(Constraints::unset());
559        }
560        result
561    }
562
563    /// Get the column corresponding to the primary key, if any.
564    pub fn pk(&self) -> Option<&ColumnSchema> {
565        self.primary_key.and_then(|pk| self.get_column(pk.0 as usize))
566    }
567
568    /// Verify the definitions of this schema are valid:
569    /// - Check all names are not empty
570    /// - All columns exists
571    /// - Only 1 PK
572    /// - Only 1 sequence per column
573    /// - Only Btree Indexes
574    ///
575    /// Deprecated. This will eventually be replaced by the `schema` crate.
576    pub fn validated(self) -> Result<Self, Vec<SchemaError>> {
577        let mut errors = Vec::new();
578
579        let columns_not_found = self
580            .sequences
581            .iter()
582            .map(|x| (DefType::Sequence, x.sequence_name.clone(), ColList::new(x.col_pos)))
583            .chain(self.indexes.iter().map(|x| {
584                let cols = x.index_algorithm.columns().to_owned();
585                (DefType::Index, x.index_name.clone(), cols)
586            }))
587            .chain(self.constraints.iter().map(|x| {
588                (
589                    DefType::Constraint,
590                    x.constraint_name.clone(),
591                    match &x.data {
592                        ConstraintData::Unique(unique) => unique.columns.clone().into(),
593                    },
594                )
595            }))
596            .filter_map(|(ty, name, cols)| {
597                let mut not_found_iter = self
598                    .get_columns(&cols)
599                    .filter(|(_, x)| x.is_none())
600                    .map(|(col, _)| col)
601                    .peekable();
602
603                if not_found_iter.peek().is_none() {
604                    None
605                } else {
606                    Some(SchemaError::ColumnsNotFound {
607                        name,
608                        table: self.table_name.clone(),
609                        columns: not_found_iter.collect(),
610                        ty,
611                    })
612                }
613            });
614
615        errors.extend(columns_not_found);
616
617        errors.extend(self.columns.iter().filter_map(|x| {
618            if x.col_name.is_empty() {
619                Some(SchemaError::EmptyName {
620                    table: self.table_name.clone(),
621                    ty: DefType::Column,
622                    id: x.col_pos.0 as _,
623                })
624            } else {
625                None
626            }
627        }));
628
629        errors.extend(self.indexes.iter().filter_map(|x| {
630            if x.index_name.is_empty() {
631                Some(SchemaError::EmptyName {
632                    table: self.table_name.clone(),
633                    ty: DefType::Index,
634                    id: x.index_id.0,
635                })
636            } else {
637                None
638            }
639        }));
640        errors.extend(self.constraints.iter().filter_map(|x| {
641            if x.constraint_name.is_empty() {
642                Some(SchemaError::EmptyName {
643                    table: self.table_name.clone(),
644                    ty: DefType::Constraint,
645                    id: x.constraint_id.0,
646                })
647            } else {
648                None
649            }
650        }));
651
652        errors.extend(self.sequences.iter().filter_map(|x| {
653            if x.sequence_name.is_empty() {
654                Some(SchemaError::EmptyName {
655                    table: self.table_name.clone(),
656                    ty: DefType::Sequence,
657                    id: x.sequence_id.0,
658                })
659            } else {
660                None
661            }
662        }));
663
664        // Verify we don't have more than 1 auto_inc for the same column
665        if let Some(err) = self
666            .sequences
667            .iter()
668            .group_by(|&seq| seq.col_pos)
669            .into_iter()
670            .find_map(|(key, group)| {
671                let count = group.count();
672                if count > 1 {
673                    Some(SchemaError::OneAutoInc {
674                        table: self.table_name.clone(),
675                        field: self.columns[key.idx()].col_name.clone(),
676                    })
677                } else {
678                    None
679                }
680            })
681        {
682            errors.push(err);
683        }
684
685        if errors.is_empty() {
686            Ok(self)
687        } else {
688            Err(errors)
689        }
690    }
691
692    /// The C# and Rust SDKs are inconsistent about whether v8 column defs store resolved or unresolved algebraic types.
693    /// This method works around this problem by copying the column types from the module def into the table schema.
694    /// It can be removed once v8 is removed, since v9 will reject modules with an inconsistency like this.
695    pub fn janky_fix_column_defs(&mut self, module_def: &ModuleDef) {
696        let table_name = self.table_name.clone().into();
697        for col in &mut self.columns {
698            let def: &ColumnDef = module_def.lookup((&table_name, &col.col_name)).unwrap();
699            col.col_type = def.ty.clone();
700        }
701        let table_def: &TableDef = module_def.expect_lookup(&table_name);
702        self.row_type = module_def.typespace()[table_def.product_type_ref]
703            .as_product()
704            .unwrap()
705            .clone();
706    }
707
708    /// Normalize a `TableSchema`.
709    /// The result is semantically equivalent, but may have reordered indexes, constraints, or sequences.
710    /// Columns will not be reordered.
711    pub fn normalize(&mut self) {
712        self.indexes.sort_by(|a, b| a.index_name.cmp(&b.index_name));
713        self.constraints
714            .sort_by(|a, b| a.constraint_name.cmp(&b.constraint_name));
715        self.sequences.sort_by(|a, b| a.sequence_name.cmp(&b.sequence_name));
716    }
717}
718
719/// Removes and returns the first element satisfying `predicate` in `vec`.
720fn find_remove<T>(vec: &mut Vec<T>, predicate: impl Fn(&T) -> bool) -> Option<T> {
721    let pos = vec.iter().position(predicate)?;
722    Some(vec.remove(pos))
723}
724
725/// Like `assert_eq!` for `anyhow`, but `$msg` is just a string, not a format string.
726macro_rules! ensure_eq {
727    ($a:expr, $b:expr, $msg:expr) => {
728        if $a != $b {
729            anyhow::bail!(
730                "{0}: expected {1} == {2}:\n   {1}: {3:?}\n   {2}: {4:?}",
731                $msg,
732                stringify!($a),
733                stringify!($b),
734                $a,
735                $b
736            );
737        }
738    };
739}
740
741/// Returns the list of [`ColumnSchema`]s for a certain list of [`ColumnDef`]s.
742pub fn column_schemas_from_defs(module_def: &ModuleDef, columns: &[ColumnDef], table_id: TableId) -> Vec<ColumnSchema> {
743    columns
744        .iter()
745        .enumerate()
746        .map(|(col_pos, def)| ColumnSchema::from_module_def(module_def, def, (), (table_id, col_pos.into())))
747        .collect()
748}
749
750impl TableSchema {
751    /// Generates a [`TableSchema`] for the purpose of client codegen.
752    ///
753    /// This is the schema defined in the module.
754    /// It does not have any internal columns like the schema for the datastore.
755    /// See [`Self::from_view_def_for_datastore`] for more details.
756    pub fn from_view_def_for_codegen(module_def: &ModuleDef, view_def: &ViewDef) -> Self {
757        module_def.expect_contains(view_def);
758
759        let ViewDef {
760            name,
761            is_public,
762            is_anonymous,
763            primary_key,
764            param_columns,
765            return_columns,
766            ..
767        } = view_def;
768
769        let columns = return_columns
770            .iter()
771            .map(|def| ColumnSchema::from_view_column_def(module_def, def))
772            .enumerate()
773            .map(|(i, schema)| (ColId::from(i), schema))
774            .map(|(col_pos, schema)| ColumnSchema { col_pos, ..schema })
775            .collect();
776        let view_primary_key = (module_def.raw_module_def_version() == RawModuleDefVersion::V10)
777            .then_some(*primary_key)
778            .flatten();
779
780        let table_access = if *is_public {
781            StAccess::Public
782        } else {
783            StAccess::Private
784        };
785
786        let view_info = ViewDefInfo {
787            view_id: ViewId::SENTINEL,
788            has_args: !param_columns.is_empty(),
789            is_anonymous: *is_anonymous,
790        };
791
792        TableSchema::new(
793            TableId::SENTINEL,
794            TableName::new(name.clone()),
795            Some(view_info),
796            columns,
797            vec![],
798            vec![],
799            vec![],
800            StTableType::User,
801            table_access,
802            None,
803            view_primary_key,
804            false,
805            None,
806        )
807    }
808
809    /// Generate a [`TableSchema`] for the purpose of materializing in the datastore.
810    ///
811    /// Note, every view is materialized by default. For example:
812    /// ```rust,ignore
813    /// #[table]
814    /// pub struct MyTable {
815    ///     a: u32,
816    ///     b: u32,
817    /// }
818    ///
819    /// #[view(accessor = my_view, public)]
820    /// fn my_view(ctx: &ViewContext, x: u32, y: u32) -> Vec<MyTable> { ... }
821    ///
822    /// #[view(accessor = my_anonymous_view, public)]
823    /// fn my_anonymous_view(ctx: &AnonymousViewContext, x: u32, y: u32) -> Vec<MyTable> { ... }
824    /// ```
825    ///
826    /// The above views are materialized with the following schema:
827    ///
828    /// my_view:
829    ///
830    /// | sender         | arg_id | a   | b   |
831    /// |----------------|--------|-----|-----|
832    /// | (some = 0x...) | u64    | u32 | u32 |
833    ///
834    /// my_anonymous_view:
835    ///
836    /// | sender      | arg_id | a   | b   |
837    /// |-------------|--------|-----|-----|
838    /// | (none = ()) | u64    | u32 | u32 |
839    ///
840    /// Note, `sender` and `arg_id` are internal columns not defined by the module,
841    /// where `arg_id` is a foreign key into `st_view_arg`.
842    pub fn from_view_def_for_datastore(module_def: &ModuleDef, view_def: &ViewDef) -> Self {
843        module_def.expect_contains(view_def);
844
845        let ViewDef {
846            name,
847            is_public,
848            is_anonymous,
849            primary_key,
850            param_columns,
851            return_columns,
852            accessor_name,
853            ..
854        } = view_def;
855
856        let n = return_columns.len() + 2;
857        let mut columns = Vec::with_capacity(n);
858        let mut meta_cols = 0;
859
860        let mut push_column = |name: &'static str, col_type| {
861            meta_cols += 1;
862            columns.push(ColumnSchema {
863                table_id: TableId::SENTINEL,
864                col_pos: columns.len().into(),
865                col_name: Identifier::new_assume_valid(name.into()),
866                col_type,
867                alias: None,
868            });
869        };
870
871        if !is_anonymous {
872            push_column("sender", AlgebraicType::identity());
873        }
874
875        if !param_columns.is_empty() {
876            push_column("arg_id", AlgebraicType::U64);
877        }
878
879        columns.extend(
880            return_columns
881                .iter()
882                .map(|def| ColumnSchema::from_view_column_def(module_def, def))
883                .enumerate()
884                .map(|(i, schema)| (ColId::from(meta_cols + i), schema))
885                .map(|(col_pos, schema)| ColumnSchema { col_pos, ..schema }),
886        );
887
888        let make_index_name = |col_list: &ColList| {
889            let cols_name = generate_cols_name(col_list, |col| columns.get(col.idx()).map(|col| &*col.col_name));
890            RawIdentifier::new(format!("{name}_{cols_name}_idx_btree"))
891        };
892
893        let make_constraint_name = |col_list: &ColList| {
894            let cols_name = generate_cols_name(col_list, |col| columns.get(col.idx()).map(|col| &*col.col_name));
895            RawIdentifier::new(format!("{name}_{cols_name}_key"))
896        };
897
898        let mut indexes = match meta_cols {
899            1 => vec![IndexSchema {
900                index_id: IndexId::SENTINEL,
901                table_id: TableId::SENTINEL,
902                index_name: make_index_name(&col_list![0]),
903                index_algorithm: IndexAlgorithm::BTree(col_list![0].into()),
904                alias: None,
905            }],
906            2 => vec![IndexSchema {
907                index_id: IndexId::SENTINEL,
908                table_id: TableId::SENTINEL,
909                index_name: make_index_name(&col_list![0, 1]),
910                index_algorithm: IndexAlgorithm::BTree(col_list![0, 1].into()),
911                alias: None,
912            }],
913            _ => vec![],
914        };
915
916        let mut constraints = vec![];
917        let view_primary_key = (module_def.raw_module_def_version() == RawModuleDefVersion::V10)
918            .then_some(primary_key.map(|pk| ColId::from(meta_cols + pk.idx())))
919            .flatten();
920
921        if *is_anonymous {
922            if let Some(pk_col) = view_primary_key {
923                let cols = col_list![pk_col];
924                constraints.push(ConstraintSchema {
925                    table_id: TableId::SENTINEL,
926                    constraint_id: ConstraintId::SENTINEL,
927                    constraint_name: make_constraint_name(&cols),
928                    data: ConstraintData::Unique(UniqueConstraintData {
929                        columns: ColSet::from(cols.clone()),
930                    }),
931                });
932                indexes.push(IndexSchema {
933                    index_id: IndexId::SENTINEL,
934                    table_id: TableId::SENTINEL,
935                    index_name: make_index_name(&cols),
936                    index_algorithm: IndexAlgorithm::BTree(cols.into()),
937                    alias: None,
938                });
939            }
940        } else if let Some(pk_col) = view_primary_key {
941            let cols = col_list![ColId(0), pk_col];
942            indexes.push(IndexSchema {
943                index_id: IndexId::SENTINEL,
944                table_id: TableId::SENTINEL,
945                index_name: make_index_name(&cols),
946                index_algorithm: IndexAlgorithm::BTree(cols.into()),
947                alias: None,
948            });
949        }
950
951        let table_access = if *is_public {
952            StAccess::Public
953        } else {
954            StAccess::Private
955        };
956
957        let view_info = ViewDefInfo {
958            view_id: ViewId::SENTINEL,
959            has_args: !param_columns.is_empty(),
960            is_anonymous: *is_anonymous,
961        };
962
963        TableSchema::new(
964            TableId::SENTINEL,
965            TableName::new(name.clone()),
966            Some(view_info),
967            columns,
968            indexes,
969            constraints,
970            vec![],
971            StTableType::User,
972            table_access,
973            None,
974            if *is_anonymous { view_primary_key } else { None },
975            false,
976            Some(accessor_name.clone()),
977        )
978    }
979}
980
981impl Schema for TableSchema {
982    type Def = TableDef;
983    type Id = TableId;
984    type ParentId = ();
985
986    // N.B. This implementation gives all children ID 0 (the auto-inc sentinel value.)
987    fn from_module_def(
988        module_def: &ModuleDef,
989        def: &Self::Def,
990        _parent_id: Self::ParentId,
991        table_id: Self::Id,
992    ) -> Self {
993        module_def.expect_contains(def);
994
995        let TableDef {
996            name,
997            product_type_ref: _,
998            primary_key,
999            columns,
1000            indexes,
1001            constraints,
1002            sequences,
1003            schedule,
1004            table_type,
1005            table_access,
1006            is_event,
1007            accessor_name,
1008            ..
1009        } = def;
1010
1011        let columns = column_schemas_from_defs(module_def, columns, table_id);
1012
1013        // note: these Ids are fixed up somewhere else, so we can just use 0 here...
1014        // but it would be nice to pass the correct values into this method.
1015        let indexes = indexes
1016            .values()
1017            .map(|def| IndexSchema::from_module_def(module_def, def, table_id, IndexId::SENTINEL))
1018            .collect();
1019
1020        let sequences = sequences
1021            .values()
1022            .map(|def| SequenceSchema::from_module_def(module_def, def, table_id, SequenceId::SENTINEL))
1023            .collect();
1024
1025        let constraints = constraints
1026            .values()
1027            .map(|def| ConstraintSchema::from_module_def(module_def, def, table_id, ConstraintId::SENTINEL))
1028            .collect();
1029
1030        let schedule = schedule
1031            .as_ref()
1032            .map(|schedule| ScheduleSchema::from_module_def(module_def, schedule, table_id, ScheduleId::SENTINEL));
1033
1034        TableSchema::new(
1035            table_id,
1036            TableName::new(name.clone()),
1037            None,
1038            columns,
1039            indexes,
1040            constraints,
1041            sequences,
1042            (*table_type).into(),
1043            (*table_access).into(),
1044            schedule,
1045            *primary_key,
1046            *is_event,
1047            Some(accessor_name.clone()),
1048        )
1049    }
1050
1051    fn check_compatible(&self, module_def: &ModuleDef, def: &Self::Def) -> Result<(), anyhow::Error> {
1052        ensure_eq!(&self.table_name[..], &def.name[..], "Table name mismatch");
1053        ensure_eq!(self.primary_key, def.primary_key, "Primary key mismatch");
1054        let def_table_access: StAccess = (def.table_access).into();
1055        ensure_eq!(self.table_access, def_table_access, "Table access mismatch");
1056        let def_table_type: StTableType = (def.table_type).into();
1057        ensure_eq!(self.table_type, def_table_type, "Table type mismatch");
1058
1059        for col in &self.columns {
1060            let col_def = def
1061                .columns
1062                .get(col.col_pos.0 as usize)
1063                .ok_or_else(|| anyhow::anyhow!("Column {} not found in definition", col.col_pos.0))?;
1064            col.check_compatible(module_def, col_def)?;
1065        }
1066        ensure_eq!(self.columns.len(), def.columns.len(), "Column count mismatch");
1067
1068        for index in &self.indexes {
1069            let index_def = def
1070                .indexes
1071                .get(&index.index_name)
1072                .ok_or_else(|| anyhow::anyhow!("Index {} not found in definition", index.index_id.0))?;
1073            index.check_compatible(module_def, index_def)?;
1074        }
1075        ensure_eq!(self.indexes.len(), def.indexes.len(), "Index count mismatch");
1076
1077        for constraint in &self.constraints {
1078            let constraint_def = def
1079                .constraints
1080                .get(&constraint.constraint_name)
1081                .ok_or_else(|| anyhow::anyhow!("Constraint {} not found in definition", constraint.constraint_id.0))?;
1082            constraint.check_compatible(module_def, constraint_def)?;
1083        }
1084        ensure_eq!(
1085            self.constraints.len(),
1086            def.constraints.len(),
1087            "Constraint count mismatch"
1088        );
1089
1090        for sequence in &self.sequences {
1091            let sequence_def = def
1092                .sequences
1093                .get(&sequence.sequence_name)
1094                .ok_or_else(|| anyhow::anyhow!("Sequence {} not found in definition", sequence.sequence_id.0))?;
1095            sequence.check_compatible(module_def, sequence_def)?;
1096        }
1097        ensure_eq!(self.sequences.len(), def.sequences.len(), "Sequence count mismatch");
1098
1099        if let Some(schedule) = &self.schedule {
1100            let schedule_def = def
1101                .schedule
1102                .as_ref()
1103                .ok_or_else(|| anyhow::anyhow!("Schedule not found in definition"))?;
1104            schedule.check_compatible(module_def, schedule_def)?;
1105        }
1106        ensure_eq!(
1107            self.schedule.is_some(),
1108            def.schedule.is_some(),
1109            "Schedule presence mismatch"
1110        );
1111        Ok(())
1112    }
1113}
1114
1115impl From<&TableSchema> for ProductType {
1116    fn from(value: &TableSchema) -> Self {
1117        value.row_type.clone()
1118    }
1119}
1120
1121impl From<&TableSchema> for DbTable {
1122    fn from(value: &TableSchema) -> Self {
1123        DbTable::new(
1124            Arc::new(value.into()),
1125            value.table_id,
1126            value.table_type,
1127            value.table_access,
1128        )
1129    }
1130}
1131
1132impl From<&TableSchema> for Header {
1133    fn from(value: &TableSchema) -> Self {
1134        let fields = value
1135            .columns
1136            .iter()
1137            .map(|x| Column::new(FieldName::new(value.table_id, x.col_pos), x.col_type.clone()))
1138            .collect();
1139
1140        Header::new(
1141            value.table_id,
1142            value.table_name.clone(),
1143            fields,
1144            value.backcompat_constraints(),
1145        )
1146    }
1147}
1148
1149impl From<TableSchema> for Header {
1150    fn from(schema: TableSchema) -> Self {
1151        // TODO: optimize.
1152        Header::from(&schema)
1153    }
1154}
1155
1156/// A struct representing the schema of a database column.
1157#[derive(Debug, Clone, PartialEq, Eq, Ord, PartialOrd)]
1158pub struct ColumnSchema {
1159    /// The ID of the table this column is attached to.
1160    pub table_id: TableId,
1161    /// The position of the column within the table.
1162    pub col_pos: ColId,
1163    /// The name of the column. Unique within the table.
1164    pub col_name: Identifier,
1165
1166    pub alias: Option<Identifier>,
1167    /// The type of the column. This will never contain any `AlgebraicTypeRef`s,
1168    /// that is, it will be resolved.
1169    pub col_type: AlgebraicType,
1170}
1171
1172impl spacetimedb_memory_usage::MemoryUsage for ColumnSchema {
1173    fn heap_usage(&self) -> usize {
1174        let Self {
1175            table_id,
1176            col_pos,
1177            col_name,
1178            col_type,
1179            ..
1180        } = self;
1181        table_id.heap_usage() + col_pos.heap_usage() + col_name.heap_usage() + col_type.heap_usage()
1182    }
1183}
1184
1185impl ColumnSchema {
1186    #[cfg(any(test, feature = "test"))]
1187    pub fn for_test(pos: impl Into<ColId>, name: impl AsRef<str>, ty: AlgebraicType) -> Self {
1188        Self {
1189            table_id: TableId::SENTINEL,
1190            col_pos: pos.into(),
1191            col_name: Identifier::for_test(name),
1192            col_type: ty,
1193            alias: None,
1194        }
1195    }
1196
1197    fn from_view_column_def(module_def: &ModuleDef, def: &ViewColumnDef) -> Self {
1198        let col_type = WithTypespace::new(module_def.typespace(), &def.ty)
1199            .resolve_refs()
1200            .expect("validated module should have all types resolve");
1201        ColumnSchema {
1202            table_id: TableId::SENTINEL,
1203            col_pos: def.col_id,
1204            col_name: def.name.clone(),
1205            col_type,
1206            alias: Some(def.accessor_name.clone()),
1207        }
1208    }
1209}
1210
1211impl Schema for ColumnSchema {
1212    type Def = ColumnDef;
1213    type ParentId = ();
1214    // This is not like the other ID types: it's a tuple of the table ID and the column position.
1215    // A `ColId` alone does NOT suffice to identify a column!
1216    type Id = (TableId, ColId);
1217
1218    fn from_module_def(
1219        module_def: &ModuleDef,
1220        def: &ColumnDef,
1221        _parent_id: (),
1222        (table_id, col_pos): (TableId, ColId),
1223    ) -> Self {
1224        let col_type = WithTypespace::new(module_def.typespace(), &def.ty)
1225            .resolve_refs()
1226            .expect("validated module should have all types resolve");
1227        ColumnSchema {
1228            table_id,
1229            col_pos,
1230            col_name: def.name.clone(),
1231            col_type,
1232            alias: Some(def.accessor_name.clone()),
1233        }
1234    }
1235
1236    fn check_compatible(&self, module_def: &ModuleDef, def: &Self::Def) -> Result<(), anyhow::Error> {
1237        ensure_eq!(&self.col_name[..], &def.name[..], "Column name mismatch");
1238        let resolved_def_ty = WithTypespace::new(module_def.typespace(), &def.ty).resolve_refs()?;
1239        ensure_eq!(self.col_type, resolved_def_ty, "Column type mismatch");
1240        ensure_eq!(self.col_pos, def.col_id, "Column ID mismatch");
1241        Ok(())
1242    }
1243}
1244
1245impl From<&ColumnSchema> for ProductTypeElement {
1246    fn from(value: &ColumnSchema) -> Self {
1247        Self {
1248            name: Some(value.col_name.clone().into()),
1249            algebraic_type: value.col_type.clone(),
1250        }
1251    }
1252}
1253
1254impl From<ColumnSchema> for Column {
1255    fn from(schema: ColumnSchema) -> Self {
1256        Column {
1257            field: FieldName {
1258                table: schema.table_id,
1259                col: schema.col_pos,
1260            },
1261            algebraic_type: schema.col_type,
1262        }
1263    }
1264}
1265
1266/// Contextualizes a reference to a [ColumnSchema] with the name of the table the column is attached to.
1267#[derive(Debug, Clone)]
1268pub struct ColumnSchemaRef<'a> {
1269    /// The column we are referring to.
1270    pub column: &'a ColumnSchema,
1271    /// The name of the table the column is attached to.
1272    pub table_name: &'a RawIdentifier,
1273}
1274
1275impl From<ColumnSchemaRef<'_>> for ProductTypeElement {
1276    fn from(value: ColumnSchemaRef) -> Self {
1277        ProductTypeElement::new(
1278            value.column.col_type.clone(),
1279            Some(value.column.col_name.clone().into()),
1280        )
1281    }
1282}
1283
1284/// Represents a schema definition for a database sequence.
1285#[derive(Debug, Clone, PartialEq, Eq)]
1286pub struct SequenceSchema {
1287    /// The unique identifier for the sequence within a database.
1288    pub sequence_id: SequenceId,
1289    /// The name of the sequence.
1290    /// Deprecated. In the future, sequences will be identified by col_pos.
1291    pub sequence_name: RawIdentifier,
1292    /// The ID of the table associated with the sequence.
1293    pub table_id: TableId,
1294    /// The position of the column associated with this sequence.
1295    pub col_pos: ColId,
1296    /// The increment value for the sequence.
1297    pub increment: i128,
1298    /// The initial value to be returned by this sequence.
1299    pub start: i128,
1300    /// The minimum value for the sequence.
1301    pub min_value: i128,
1302    /// The maximum value for the sequence.
1303    pub max_value: i128,
1304}
1305
1306impl spacetimedb_memory_usage::MemoryUsage for SequenceSchema {
1307    fn heap_usage(&self) -> usize {
1308        let Self {
1309            sequence_id,
1310            sequence_name,
1311            table_id,
1312            col_pos,
1313            increment,
1314            start,
1315            min_value,
1316            max_value,
1317        } = self;
1318        sequence_id.heap_usage()
1319            + sequence_name.heap_usage()
1320            + table_id.heap_usage()
1321            + col_pos.heap_usage()
1322            + increment.heap_usage()
1323            + start.heap_usage()
1324            + min_value.heap_usage()
1325            + max_value.heap_usage()
1326    }
1327}
1328
1329impl Schema for SequenceSchema {
1330    type Def = SequenceDef;
1331    type Id = SequenceId;
1332    type ParentId = TableId;
1333
1334    fn from_module_def(module_def: &ModuleDef, def: &Self::Def, parent_id: Self::ParentId, id: Self::Id) -> Self {
1335        module_def.expect_contains(def);
1336
1337        SequenceSchema {
1338            sequence_id: id,
1339            sequence_name: def.name.clone(),
1340            table_id: parent_id,
1341            col_pos: def.column,
1342            increment: def.increment,
1343            start: def.start.unwrap_or(1),
1344            min_value: def.min_value.unwrap_or(1),
1345            max_value: def.max_value.unwrap_or(i128::MAX),
1346            // allocated: 0, // TODO: information not available in the `Def`s anymore, which is correct, but this may need to be overridden later.
1347        }
1348    }
1349
1350    fn check_compatible(&self, _module_def: &ModuleDef, def: &Self::Def) -> Result<(), anyhow::Error> {
1351        ensure_eq!(&self.sequence_name[..], &def.name[..], "Sequence name mismatch");
1352        ensure_eq!(self.col_pos, def.column, "Sequence column mismatch");
1353        ensure_eq!(self.increment, def.increment, "Sequence increment mismatch");
1354        if let Some(start) = &def.start {
1355            ensure_eq!(self.start, *start, "Sequence start mismatch");
1356        }
1357        if let Some(min_value) = &def.min_value {
1358            ensure_eq!(self.min_value, *min_value, "Sequence min_value mismatch");
1359        }
1360        if let Some(max_value) = &def.max_value {
1361            ensure_eq!(self.max_value, *max_value, "Sequence max_value mismatch");
1362        }
1363        Ok(())
1364    }
1365}
1366
1367/// Marks a table as a timer table for a scheduled reducer.
1368#[derive(Debug, Clone, PartialEq, Eq)]
1369pub struct ScheduleSchema {
1370    /// The identifier of the table.
1371    pub table_id: TableId,
1372
1373    /// The identifier of the schedule.
1374    pub schedule_id: ScheduleId,
1375
1376    /// The name of the schedule.
1377    pub schedule_name: Identifier,
1378
1379    /// The name of the reducer or procedure to call.
1380    pub function_name: Identifier,
1381
1382    /// The column containing the `ScheduleAt` enum.
1383    pub at_column: ColId,
1384}
1385
1386impl ScheduleSchema {
1387    #[cfg(any(test, feature = "test"))]
1388    pub fn for_test(name: impl AsRef<str>, function: impl AsRef<str>, at: impl Into<ColId>) -> Self {
1389        Self {
1390            table_id: TableId::SENTINEL,
1391            schedule_id: ScheduleId::SENTINEL,
1392            schedule_name: Identifier::for_test(name.as_ref()),
1393            function_name: Identifier::for_test(function.as_ref()),
1394            at_column: at.into(),
1395        }
1396    }
1397}
1398
1399impl Schema for ScheduleSchema {
1400    type Def = ScheduleDef;
1401
1402    type Id = ScheduleId;
1403
1404    type ParentId = TableId;
1405
1406    fn from_module_def(module_def: &ModuleDef, def: &Self::Def, parent_id: Self::ParentId, id: Self::Id) -> Self {
1407        module_def.expect_contains(def);
1408
1409        ScheduleSchema {
1410            table_id: parent_id,
1411            schedule_id: id,
1412            schedule_name: def.name.clone(),
1413            function_name: def.function_name.clone(),
1414            at_column: def.at_column,
1415            // Ignore def.at_column and id_column. Those are recovered at runtime.
1416        }
1417    }
1418
1419    fn check_compatible(&self, _module_def: &ModuleDef, def: &Self::Def) -> Result<(), anyhow::Error> {
1420        ensure_eq!(&self.schedule_name[..], &def.name[..], "Schedule name mismatch");
1421        ensure_eq!(
1422            &self.function_name[..],
1423            &def.function_name[..],
1424            "Schedule function name mismatch"
1425        );
1426        Ok(())
1427    }
1428}
1429
1430/// A struct representing the schema of a database index.
1431#[derive(Debug, Clone, PartialEq, Eq)]
1432pub struct IndexSchema {
1433    /// The unique ID of the index within the schema.
1434    pub index_id: IndexId,
1435    /// The ID of the table associated with the index.
1436    pub table_id: TableId,
1437    /// The name of the index. This should not be assumed to follow any particular format.
1438    /// Unique within the database.
1439    pub index_name: RawIdentifier,
1440
1441    pub alias: Option<RawIdentifier>,
1442    /// The data for the schema.
1443    pub index_algorithm: IndexAlgorithm,
1444}
1445
1446impl spacetimedb_memory_usage::MemoryUsage for IndexSchema {
1447    fn heap_usage(&self) -> usize {
1448        let Self {
1449            index_id,
1450            table_id,
1451            index_name,
1452            index_algorithm,
1453            alias: _,
1454        } = self;
1455        index_id.heap_usage() + table_id.heap_usage() + index_name.heap_usage() + index_algorithm.heap_usage()
1456    }
1457}
1458
1459impl IndexSchema {
1460    pub fn for_test(name: impl AsRef<str>, algo: impl Into<IndexAlgorithm>) -> Self {
1461        Self {
1462            index_id: IndexId::SENTINEL,
1463            table_id: TableId::SENTINEL,
1464            index_name: RawIdentifier::new(name.as_ref()),
1465            index_algorithm: algo.into(),
1466            alias: None,
1467        }
1468    }
1469}
1470
1471impl Schema for IndexSchema {
1472    type Def = IndexDef;
1473    type Id = IndexId;
1474    type ParentId = TableId;
1475
1476    fn from_module_def(module_def: &ModuleDef, def: &Self::Def, parent_id: Self::ParentId, id: Self::Id) -> Self {
1477        module_def.expect_contains(def);
1478
1479        let index_algorithm = def.algorithm.clone();
1480        IndexSchema {
1481            index_id: id,
1482            table_id: parent_id,
1483            index_name: def.name.clone(),
1484            index_algorithm,
1485            alias: Some(def.source_name.clone()),
1486        }
1487    }
1488
1489    fn check_compatible(&self, _module_def: &ModuleDef, def: &Self::Def) -> Result<(), anyhow::Error> {
1490        ensure_eq!(&self.index_name[..], &def.name[..], "Index name mismatch");
1491        ensure_eq!(&self.index_algorithm, &def.algorithm, "Index algorithm mismatch");
1492        Ok(())
1493    }
1494}
1495
1496/// A struct representing the schema of a database constraint.
1497///
1498/// This struct holds information about a database constraint, including its unique identifier,
1499/// name, the table it belongs to, and the columns it is associated with.
1500#[derive(Debug, Clone, PartialEq, Eq)]
1501pub struct ConstraintSchema {
1502    /// The ID of the table the constraint applies to.
1503    pub table_id: TableId,
1504    /// The unique ID of the constraint within the database.
1505    pub constraint_id: ConstraintId,
1506    /// The name of the constraint.
1507    pub constraint_name: RawIdentifier,
1508    /// The data for the constraint.
1509    pub data: ConstraintData, // this reuses the type from Def, which is fine, neither of `schema` nor `def` are ABI modules.
1510}
1511
1512impl spacetimedb_memory_usage::MemoryUsage for ConstraintSchema {
1513    fn heap_usage(&self) -> usize {
1514        let Self {
1515            table_id,
1516            constraint_id,
1517            constraint_name,
1518            data,
1519        } = self;
1520        table_id.heap_usage() + constraint_id.heap_usage() + constraint_name.heap_usage() + data.heap_usage()
1521    }
1522}
1523
1524impl ConstraintSchema {
1525    pub fn unique_for_test(name: impl AsRef<str>, cols: impl Into<ColSet>) -> Self {
1526        Self {
1527            table_id: TableId::SENTINEL,
1528            constraint_id: ConstraintId::SENTINEL,
1529            constraint_name: RawIdentifier::new(name.as_ref()),
1530            data: ConstraintData::Unique(UniqueConstraintData { columns: cols.into() }),
1531        }
1532    }
1533
1534    /// Constructs a `ConstraintSchema` from a given `ConstraintDef` and table identifier.
1535    ///
1536    /// # Parameters
1537    ///
1538    /// * `table_id`: Identifier of the table to which the constraint belongs.
1539    /// * `constraint`: The `ConstraintDef` containing constraint information.
1540    #[deprecated(note = "Use TableSchema::from_module_def instead")]
1541    pub fn from_def(table_id: TableId, constraint: RawConstraintDefV8) -> Option<Self> {
1542        if constraint.constraints.has_unique() {
1543            Some(ConstraintSchema {
1544                constraint_id: ConstraintId::SENTINEL, // Set to 0 as it may be assigned later.
1545                constraint_name: RawIdentifier::new(constraint.constraint_name.trim()),
1546                table_id,
1547                data: ConstraintData::Unique(UniqueConstraintData {
1548                    columns: constraint.columns.into(),
1549                }),
1550            })
1551        } else {
1552            None
1553        }
1554    }
1555}
1556
1557impl Schema for ConstraintSchema {
1558    type Def = ConstraintDef;
1559    type Id = ConstraintId;
1560    type ParentId = TableId;
1561
1562    fn from_module_def(module_def: &ModuleDef, def: &Self::Def, parent_id: Self::ParentId, id: Self::Id) -> Self {
1563        module_def.expect_contains(def);
1564
1565        ConstraintSchema {
1566            constraint_id: id,
1567            constraint_name: def.name.clone(),
1568            table_id: parent_id,
1569            data: def.data.clone(),
1570        }
1571    }
1572
1573    fn check_compatible(&self, _module_def: &ModuleDef, def: &Self::Def) -> Result<(), anyhow::Error> {
1574        ensure_eq!(&self.constraint_name[..], &def.name[..], "Constraint name mismatch");
1575        ensure_eq!(&self.data, &def.data, "Constraint data mismatch");
1576        Ok(())
1577    }
1578}
1579
1580/// A struct representing the schema of a row-level security policy.
1581#[derive(Debug, Clone, PartialEq, Eq)]
1582pub struct RowLevelSecuritySchema {
1583    pub table_id: TableId,
1584    pub sql: RawSql,
1585}