From b44ce1955669ca9c6d12b13dac962588de11f57a Mon Sep 17 00:00:00 2001 From: Shubham Mishra Date: Thu, 22 Jan 2026 15:30:39 +0530 Subject: [PATCH 01/11] RaeModuleDefV10 def --- crates/lib/src/db/raw_def.rs | 2 + crates/lib/src/db/raw_def/v10.rs | 941 +++++++++++++++ crates/schema/src/def.rs | 20 + crates/schema/src/def/validate.rs | 1 + crates/schema/src/def/validate/common.rs | 0 crates/schema/src/def/validate/v10.rs | 1397 ++++++++++++++++++++++ crates/schema/src/def/validate/v9.rs | 389 +++--- crates/schema/src/error.rs | 4 + 8 files changed, 2596 insertions(+), 158 deletions(-) create mode 100644 crates/lib/src/db/raw_def/v10.rs create mode 100644 crates/schema/src/def/validate/common.rs create mode 100644 crates/schema/src/def/validate/v10.rs diff --git a/crates/lib/src/db/raw_def.rs b/crates/lib/src/db/raw_def.rs index 8759e0316af..84cb66a6e98 100644 --- a/crates/lib/src/db/raw_def.rs +++ b/crates/lib/src/db/raw_def.rs @@ -14,3 +14,5 @@ pub mod v8; pub use v8::*; pub mod v9; + +pub mod v10; \ No newline at end of file diff --git a/crates/lib/src/db/raw_def/v10.rs b/crates/lib/src/db/raw_def/v10.rs new file mode 100644 index 00000000000..fd51f649aa2 --- /dev/null +++ b/crates/lib/src/db/raw_def/v10.rs @@ -0,0 +1,941 @@ +//! ABI Version 10 of the raw module definitions. +//! +//! This is a refactored version of V9 with a section-based structure. +//! V10 moves schedules, lifecycle reducers, and default values out of their V9 locations +//! into dedicated sections for cleaner organization. +//! It allows easier future extensibility to add new kinds of definitions. + +use std::any::TypeId; +use std::collections::{btree_map, BTreeMap}; + +use spacetimedb_primitives::{ColId, ColList}; +use spacetimedb_sats::typespace::TypespaceBuilder; +use spacetimedb_sats::{AlgebraicType, AlgebraicTypeRef, AlgebraicValue, ProductType, SpacetimeType, Typespace}; + +use crate::db::raw_def::v9::{ + sats_name_to_scoped_name, Lifecycle, RawIdentifier, RawIndexAlgorithm, TableAccess, TableType, +}; + +// Type aliases for consistency with V9 +pub type RawIndexDefV10 = super::v9::RawIndexDefV9; +pub type RawViewDefV10 = super::v9::RawViewDefV9; +pub type RawConstraintDefV10 = super::v9::RawConstraintDefV9; +pub type RawConstraintDataV10 = super::v9::RawConstraintDataV9; +pub type RawSequenceDefV10 = super::v9::RawSequenceDefV9; +pub type RawTypeDefV10 = super::v9::RawTypeDefV9; +pub type RawScopedTypeNameV10 = super::v9::RawScopedTypeNameV9; + +/// A possibly-invalid raw module definition. +/// +/// ABI Version 10. +/// +/// These "raw definitions" may contain invalid data, and are validated by the `validate` module +/// into a proper `spacetimedb_schema::ModuleDef`, or a collection of errors. +/// +/// The module definition maintains the same logical global namespace as V9, mapping `Identifier`s to: +/// +/// - database-level objects: +/// - logical schema objects: tables, constraints, sequence definitions +/// - physical schema objects: indexes +/// - module-level objects: reducers, procedures, schedule definitions +/// - binding-level objects: type aliases +/// +/// All of these types of objects must have unique names within the module. +/// The exception is columns, which need unique names only within a table. +#[derive(Default, Debug, Clone, SpacetimeType)] +#[sats(crate = crate)] +#[cfg_attr(feature = "test", derive(PartialEq, Eq, PartialOrd, Ord))] +pub struct RawModuleDefV10 { + /// The sections comprising this module definition. + /// + /// Sections can appear in any order and are optional. + pub sections: Vec, +} + +/// A section of a V10 module definition. +/// +/// New variants MUST be added to the END of this enum, to maintain ABI compatibility. +#[derive(Debug, Clone, SpacetimeType)] +#[sats(crate = crate)] +#[cfg_attr(feature = "test", derive(PartialEq, Eq, PartialOrd, Ord))] +#[non_exhaustive] +pub enum RawModuleDefV10Section { + /// The `Typespace` used by the module. + /// + /// `AlgebraicTypeRef`s in other sections refer to this typespace. + /// See [`crate::db::raw_def::v9::RawModuleDefV9::typespace`] for validation requirements. + Typespace(Typespace), + + /// Type definitions exported by the module. + Types(Vec), + + /// Table definitions. + Tables(Vec), + + /// Reducer definitions. + Reducers(Vec), + + /// Procedure definitions. + Procedures(Vec), + + /// View definitions. + Views(Vec), + + /// Schedule definitions. + /// + /// Unlike V9 where schedules were embedded in table definitions, + /// V10 stores them in a dedicated section. + Schedules(Vec), + + /// Lifecycle reducer assignments. + /// + /// Unlike V9 where lifecycle was a field on reducers, + /// V10 stores lifecycle-to-reducer mappings separately. + LifeCycleReducers(Vec), +} + +/// The definition of a database table. +/// +/// This struct holds information about the table, including its name, columns, indexes, +/// constraints, sequences, type, and access rights. +/// +/// Validation rules are the same as V9, except: +/// - Default values are stored inline rather than in `MiscModuleExport` +/// - Schedules are stored in a separate section rather than embedded here +#[derive(Debug, Clone, SpacetimeType)] +#[sats(crate = crate)] +#[cfg_attr(feature = "test", derive(PartialEq, Eq, PartialOrd, Ord))] +pub struct RawTableDefV10 { + /// The name of the table. + /// Unique within a module, acts as the table's identifier. + /// Must be a valid `spacetimedb_schema::identifier::Identifier`. + pub name: RawIdentifier, + + /// A reference to a `ProductType` containing the columns of this table. + /// This is the single source of truth for the table's columns. + /// All elements of the `ProductType` must have names. + /// + /// Like all types in the module, this must have the [default element ordering](crate::db::default_element_ordering), + /// UNLESS a custom ordering is declared via a `RawTypeDefV10` for this type. + pub product_type_ref: AlgebraicTypeRef, + + /// The primary key of the table, if present. Must refer to a valid column. + /// + /// Currently, there must be a unique constraint and an index corresponding to the primary key. + /// Eventually, we may remove the requirement for an index. + /// + /// The database engine does not actually care about this, but client code generation does. + /// + /// A list of length 0 means no primary key. Currently, a list of length >1 is not supported. + pub primary_key: ColList, + + /// The indices of the table. + pub indexes: Vec, + + /// Any unique constraints on the table. + pub constraints: Vec, + + /// The sequences for the table. + pub sequences: Vec, + + /// Whether this is a system- or user-created table. + pub table_type: TableType, + + /// Whether this table is public or private. + pub table_access: TableAccess, + + /// Default values for columns in this table. + pub default_values: Vec, +} + +/// Marks a particular table column as having a particular default value. +#[derive(Debug, Clone, SpacetimeType)] +#[sats(crate = crate)] +#[cfg_attr(feature = "test", derive(PartialEq, Eq, PartialOrd, Ord))] +pub struct RawColumnDefaultValueV10 { + /// Identifies which column has the default value. + pub col_id: ColId, + + /// A BSATN-encoded [`AlgebraicValue`] valid at the column's type. + /// (We cannot use `AlgebraicValue` directly as it isn't `SpacetimeType`.) + pub value: Box<[u8]>, +} + +/// A reducer definition. +#[derive(Debug, Clone, SpacetimeType)] +#[sats(crate = crate)] +#[cfg_attr(feature = "test", derive(PartialEq, Eq, PartialOrd, Ord))] +pub struct RawReducerDefV10 { + /// The name of the reducer. + pub name: RawIdentifier, + + /// The types and optional names of the parameters, in order. + /// This `ProductType` need not be registered in the typespace. + pub params: ProductType, + + /// Whether this reducer is callable from clients or is internal-only. + pub visibility: FunctionVisibility, +} + +/// The visibility of a function (reducer or procedure). +#[derive(Debug, Copy, Clone, SpacetimeType)] +#[sats(crate = crate)] +#[cfg_attr(feature = "test", derive(PartialEq, Eq, PartialOrd, Ord))] +pub enum FunctionVisibility { + /// Internal-only, not callable from clients. + /// Typically used for lifecycle reducers and scheduled functions. + Internal, + + /// Callable from client code. + ClientCallable, +} + +/// A schedule definition. +#[derive(Debug, Clone, SpacetimeType)] +#[sats(crate = crate)] +#[cfg_attr(feature = "test", derive(PartialEq, Eq, PartialOrd, Ord))] +pub struct RawScheduleDefV10 { + /// In the future, the user may FOR SOME REASON want to override this. + /// Even though there is ABSOLUTELY NO REASON TO. + /// If `None`, a nicely-formatted unique default will be chosen. + pub name: Option>, + + /// The name of the table containing the schedule. + pub table_name: RawIdentifier, + + /// The column of the `scheduled_at` field in the table. + pub schedule_at_col: ColId, + + /// The name of the reducer or procedure to call. + pub function_name: RawIdentifier, +} + +/// A lifecycle reducer assignment. +#[derive(Debug, Clone, SpacetimeType)] +#[sats(crate = crate)] +#[cfg_attr(feature = "test", derive(PartialEq, Eq, PartialOrd, Ord))] +pub struct RawLifeCycleReducerDefV10 { + /// Which lifecycle event this reducer handles. + pub lifecycle_spec: Lifecycle, + + /// The name of the reducer to call for this lifecycle event. + pub function_name: RawIdentifier, +} + +/// A procedure definition. +#[derive(Debug, Clone, SpacetimeType)] +#[sats(crate = crate)] +#[cfg_attr(feature = "test", derive(PartialEq, Eq, PartialOrd, Ord))] +pub struct RawProcedureDefV10 { + /// The name of the procedure. + pub name: RawIdentifier, + + /// The types and optional names of the parameters, in order. + /// This `ProductType` need not be registered in the typespace. + pub params: ProductType, + + /// The type of the return value. + /// + /// If this is a user-defined product or sum type, + /// it should be registered in the typespace and indirected through an [`AlgebraicType::Ref`]. + pub return_type: AlgebraicType, + + /// Whether this procedure is callable from clients or is internal-only. + pub visibility: FunctionVisibility, +} + +impl RawModuleDefV10 { + /// Get the types section, if present. + pub fn types(&self) -> Option<&Vec> { + self.sections.iter().find_map(|s| match s { + RawModuleDefV10Section::Types(types) => Some(types), + _ => None, + }) + } + + /// Get the tables section, if present. + pub fn tables(&self) -> Option<&Vec> { + self.sections.iter().find_map(|s| match s { + RawModuleDefV10Section::Tables(tables) => Some(tables), + _ => None, + }) + } + + /// Get the typespace section, if present. + pub fn typespace(&self) -> Option<&Typespace> { + self.sections.iter().find_map(|s| match s { + RawModuleDefV10Section::Typespace(ts) => Some(ts), + _ => None, + }) + } + + /// Get the reducers section, if present. + pub fn reducers(&self) -> Option<&Vec> { + self.sections.iter().find_map(|s| match s { + RawModuleDefV10Section::Reducers(reducers) => Some(reducers), + _ => None, + }) + } + + /// Get the procedures section, if present. + pub fn procedures(&self) -> Option<&Vec> { + self.sections.iter().find_map(|s| match s { + RawModuleDefV10Section::Procedures(procedures) => Some(procedures), + _ => None, + }) + } + + /// Get the views section, if present. + pub fn views(&self) -> Option<&Vec> { + self.sections.iter().find_map(|s| match s { + RawModuleDefV10Section::Views(views) => Some(views), + _ => None, + }) + } + + /// Get the schedules section, if present. + pub fn schedules(&self) -> Option<&Vec> { + self.sections.iter().find_map(|s| match s { + RawModuleDefV10Section::Schedules(schedules) => Some(schedules), + _ => None, + }) + } + + /// Get the lifecycle reducers section, if present. + pub fn lifecycle_reducers(&self) -> Option<&Vec> { + self.sections.iter().find_map(|s| match s { + RawModuleDefV10Section::LifeCycleReducers(lcrs) => Some(lcrs), + _ => None, + }) + } +} + +/// A builder for a [`RawModuleDefV10`]. +#[derive(Default)] +pub struct RawModuleDefV10Builder { + /// The module definition being built. + module: RawModuleDefV10, + + /// The type map from `T: 'static` Rust types to sats types. + type_map: BTreeMap, +} + +impl RawModuleDefV10Builder { + /// Create a new, empty `RawModuleDefV10Builder`. + pub fn new() -> Self { + Default::default() + } + + /// Get mutable access to the typespace section, creating it if missing. + fn typespace_mut(&mut self) -> &mut Typespace { + let idx = self + .module + .sections + .iter() + .position(|s| matches!(s, RawModuleDefV10Section::Typespace(_))) + .unwrap_or_else(|| { + self.module + .sections + .push(RawModuleDefV10Section::Typespace(Typespace::EMPTY.clone())); + self.module.sections.len() - 1 + }); + + match &mut self.module.sections[idx] { + RawModuleDefV10Section::Typespace(ts) => ts, + _ => unreachable!("Just ensured Typespace section exists"), + } + } + + /// Get mutable access to the tables section, creating it if missing. + fn tables_mut(&mut self) -> &mut Vec { + let idx = self + .module + .sections + .iter() + .position(|s| matches!(s, RawModuleDefV10Section::Tables(_))) + .unwrap_or_else(|| { + self.module.sections.push(RawModuleDefV10Section::Tables(Vec::new())); + self.module.sections.len() - 1 + }); + + match &mut self.module.sections[idx] { + RawModuleDefV10Section::Tables(tables) => tables, + _ => unreachable!("Just ensured Tables section exists"), + } + } + + /// Get mutable access to the reducers section, creating it if missing. + fn reducers_mut(&mut self) -> &mut Vec { + let idx = self + .module + .sections + .iter() + .position(|s| matches!(s, RawModuleDefV10Section::Reducers(_))) + .unwrap_or_else(|| { + self.module.sections.push(RawModuleDefV10Section::Reducers(Vec::new())); + self.module.sections.len() - 1 + }); + + match &mut self.module.sections[idx] { + RawModuleDefV10Section::Reducers(reducers) => reducers, + _ => unreachable!("Just ensured Reducers section exists"), + } + } + + /// Get mutable access to the procedures section, creating it if missing. + fn procedures_mut(&mut self) -> &mut Vec { + let idx = self + .module + .sections + .iter() + .position(|s| matches!(s, RawModuleDefV10Section::Procedures(_))) + .unwrap_or_else(|| { + self.module + .sections + .push(RawModuleDefV10Section::Procedures(Vec::new())); + self.module.sections.len() - 1 + }); + + match &mut self.module.sections[idx] { + RawModuleDefV10Section::Procedures(procedures) => procedures, + _ => unreachable!("Just ensured Procedures section exists"), + } + } + + /// Get mutable access to the views section, creating it if missing. + fn views_mut(&mut self) -> &mut Vec { + let idx = self + .module + .sections + .iter() + .position(|s| matches!(s, RawModuleDefV10Section::Views(_))) + .unwrap_or_else(|| { + self.module.sections.push(RawModuleDefV10Section::Views(Vec::new())); + self.module.sections.len() - 1 + }); + + match &mut self.module.sections[idx] { + RawModuleDefV10Section::Views(views) => views, + _ => unreachable!("Just ensured Views section exists"), + } + } + + /// Get mutable access to the schedules section, creating it if missing. + fn schedules_mut(&mut self) -> &mut Vec { + let idx = self + .module + .sections + .iter() + .position(|s| matches!(s, RawModuleDefV10Section::Schedules(_))) + .unwrap_or_else(|| { + self.module.sections.push(RawModuleDefV10Section::Schedules(Vec::new())); + self.module.sections.len() - 1 + }); + + match &mut self.module.sections[idx] { + RawModuleDefV10Section::Schedules(schedules) => schedules, + _ => unreachable!("Just ensured Schedules section exists"), + } + } + + /// Get mutable access to the lifecycle reducers section, creating it if missing. + fn lifecycle_reducers_mut(&mut self) -> &mut Vec { + let idx = self + .module + .sections + .iter() + .position(|s| matches!(s, RawModuleDefV10Section::LifeCycleReducers(_))) + .unwrap_or_else(|| { + self.module + .sections + .push(RawModuleDefV10Section::LifeCycleReducers(Vec::new())); + self.module.sections.len() - 1 + }); + + match &mut self.module.sections[idx] { + RawModuleDefV10Section::LifeCycleReducers(lcrs) => lcrs, + _ => unreachable!("Just ensured LifeCycleReducers section exists"), + } + } + + /// Get mutable access to the types section, creating it if missing. + fn types_mut(&mut self) -> &mut Vec { + let idx = self + .module + .sections + .iter() + .position(|s| matches!(s, RawModuleDefV10Section::Types(_))) + .unwrap_or_else(|| { + self.module.sections.push(RawModuleDefV10Section::Types(Vec::new())); + self.module.sections.len() - 1 + }); + + match &mut self.module.sections[idx] { + RawModuleDefV10Section::Types(types) => types, + _ => unreachable!("Just ensured Types section exists"), + } + } + + /// Add a type to the in-progress module. + /// + /// The returned type must satisfy `AlgebraicType::is_valid_for_client_type_definition` or `AlgebraicType::is_valid_for_client_type_use`. + pub fn add_type(&mut self) -> AlgebraicType { + TypespaceBuilder::add_type::(self) + } + + /// Create a table builder. + /// + /// Does not validate that the product_type_ref is valid; this is left to the module validation code. + pub fn build_table( + &mut self, + name: impl Into, + product_type_ref: AlgebraicTypeRef, + ) -> RawTableDefBuilderV10<'_> { + let name = name.into(); + RawTableDefBuilderV10 { + module: &mut self.module, + table: RawTableDefV10 { + name, + product_type_ref, + indexes: vec![], + constraints: vec![], + sequences: vec![], + primary_key: ColList::empty(), + table_type: TableType::User, + table_access: TableAccess::Public, + default_values: vec![], + }, + } + } + + /// Build a new table with a product type. + /// Adds the type to the module. + pub fn build_table_with_new_type( + &mut self, + table_name: impl Into, + product_type: impl Into, + custom_ordering: bool, + ) -> RawTableDefBuilderV10<'_> { + let table_name = table_name.into(); + + let product_type_ref = self.add_algebraic_type( + [], + table_name.clone(), + AlgebraicType::from(product_type.into()), + custom_ordering, + ); + + self.build_table(table_name, product_type_ref) + } + + /// Build a new table with a product type, for testing. + /// Adds the type to the module. + pub fn build_table_with_new_type_for_tests( + &mut self, + table_name: impl Into, + mut product_type: ProductType, + custom_ordering: bool, + ) -> RawTableDefBuilderV10<'_> { + self.add_expand_product_type_for_tests(&mut 0, &mut product_type); + self.build_table_with_new_type(table_name, product_type, custom_ordering) + } + + fn add_expand_type_for_tests(&mut self, name_gen: &mut usize, ty: &mut AlgebraicType) { + if ty.is_valid_for_client_type_use() { + return; + } + + match ty { + AlgebraicType::Product(prod_ty) => self.add_expand_product_type_for_tests(name_gen, prod_ty), + AlgebraicType::Sum(sum_type) => { + if let Some(wrapped) = sum_type.as_option_mut() { + self.add_expand_type_for_tests(name_gen, wrapped); + } else { + for elem in sum_type.variants.iter_mut() { + self.add_expand_type_for_tests(name_gen, &mut elem.algebraic_type); + } + } + } + AlgebraicType::Array(ty) => { + self.add_expand_type_for_tests(name_gen, &mut ty.elem_ty); + return; + } + _ => return, + } + + // Make the type into a ref. + let name = *name_gen; + let add_ty = core::mem::replace(ty, AlgebraicType::U8); + *ty = AlgebraicType::Ref(self.add_algebraic_type([], format!("gen_{name}"), add_ty, true)); + *name_gen += 1; + } + + fn add_expand_product_type_for_tests(&mut self, name_gen: &mut usize, ty: &mut ProductType) { + for elem in ty.elements.iter_mut() { + self.add_expand_type_for_tests(name_gen, &mut elem.algebraic_type); + } + } + + /// Add a type to the typespace, along with a type alias declaring its name. + /// This method should only be used for `AlgebraicType`s not corresponding to a Rust + /// type that implements `SpacetimeType`. + /// + /// Returns a reference to the newly-added type. + /// + /// NOT idempotent, calling this twice with the same name will cause errors during validation. + /// + /// You must set `custom_ordering` if you're not using the default element ordering. + pub fn add_algebraic_type( + &mut self, + scope: impl IntoIterator, + name: impl Into, + ty: AlgebraicType, + custom_ordering: bool, + ) -> AlgebraicTypeRef { + let ty_ref = self.typespace_mut().add(ty); + let scope = scope.into_iter().collect(); + let name = name.into(); + self.types_mut().push(RawTypeDefV10 { + name: RawScopedTypeNameV10 { name, scope }, + ty: ty_ref, + custom_ordering, + }); + // We don't add a `TypeId` to `self.type_map`, because there may not be a corresponding Rust type! + // e.g. if we are randomly generating types in proptests. + ty_ref + } + + /// Add a reducer to the in-progress module. + /// Accepts a `ProductType` of reducer arguments for convenience. + /// The `ProductType` need not be registered in the typespace. + /// + /// Importantly, if the reducer's first argument is a `ReducerContext`, that + /// information should not be provided to this method. + /// That is an implementation detail handled by the module bindings and can be ignored. + /// As far as the module definition is concerned, the reducer's arguments + /// start with the first non-`ReducerContext` argument. + /// + /// (It is impossible, with the current implementation of `ReducerContext`, to + /// have more than one `ReducerContext` argument, at least in Rust. + /// This is because `SpacetimeType` is not implemented for `ReducerContext`, + /// so it can never act like an ordinary argument.) + pub fn add_reducer(&mut self, name: impl Into, params: ProductType) { + self.reducers_mut().push(RawReducerDefV10 { + name: name.into(), + params, + visibility: FunctionVisibility::ClientCallable, + }); + } + + /// Add a procedure to the in-progress module. + /// + /// Accepts a `ProductType` of arguments. + /// The arguments `ProductType` need not be registered in the typespace. + /// + /// Also accepts an `AlgebraicType` return type. + /// If this is a user-defined product or sum type, + /// it should be registered in the typespace and indirected through an `AlgebraicType::Ref`. + /// + /// The `&mut ProcedureContext` first argument to the procedure should not be included in the `params`. + pub fn add_procedure(&mut self, name: impl Into, params: ProductType, return_type: AlgebraicType) { + self.procedures_mut().push(RawProcedureDefV10 { + name: name.into(), + params, + return_type, + visibility: FunctionVisibility::ClientCallable, + }) + } + + /// Add a view to the in-progress module. + pub fn add_view( + &mut self, + name: impl Into, + index: usize, + is_public: bool, + is_anonymous: bool, + params: ProductType, + return_type: AlgebraicType, + ) { + self.views_mut().push(RawViewDefV10 { + name: name.into(), + index: index as u32, + is_public, + is_anonymous, + params, + return_type, + }); + } + + /// Add a lifecycle reducer assignment to the module. + /// + /// The function must be a previously-added reducer. + pub fn add_lifecycle_reducer( + &mut self, + lifecycle_spec: Lifecycle, + function_name: impl Into, + params: ProductType, + ) { + let function_name = function_name.into(); + self.lifecycle_reducers_mut().push(RawLifeCycleReducerDefV10 { + lifecycle_spec, + function_name: function_name.clone(), + }); + + self.reducers_mut().push(RawReducerDefV10 { + name: function_name, + params, + visibility: FunctionVisibility::Internal, + }); + } + + /// Add a schedule definition to the module. + /// + /// The `function_name` should name a reducer or procedure + /// which accepts one argument, a row of the specified table. + /// + /// The table must have the appropriate columns for a scheduled table. + pub fn add_schedule( + &mut self, + table: impl Into, + column: impl Into, + function: impl Into, + ) { + self.schedules_mut().push(RawScheduleDefV10 { + name: None, + table_name: table.into(), + schedule_at_col: column.into(), + function_name: function.into(), + }); + } + + /// Finish building, consuming the builder and returning the module. + /// The module should be validated before use. + /// + /// This method automatically marks functions used in lifecycle or schedule definitions + /// as `Internal` visibility. + pub fn finish(mut self) -> RawModuleDefV10 { + let internal_functions = self + .module + .lifecycle_reducers() + .cloned() + .into_iter() + .flatten() + .map(|lcr| lcr.function_name.clone()) + .chain( + self.module + .schedules() + .cloned() + .into_iter() + .flatten() + .map(|sched| sched.function_name.clone()), + ); + + for internal_function in internal_functions { + self.reducers_mut() + .iter_mut() + .find(|r| r.name == internal_function) + .map(|r| { + r.visibility = FunctionVisibility::Internal; + }); + + self.procedures_mut() + .iter_mut() + .find(|p| p.name == internal_function) + .map(|p| { + p.visibility = FunctionVisibility::Internal; + }); + } + self.module + } + + #[cfg(test)] + pub(crate) fn tables_mut_for_tests(&mut self) -> &mut Vec { + self.tables_mut() + } +} + +/// Implement TypespaceBuilder for V10 +impl TypespaceBuilder for RawModuleDefV10Builder { + fn add( + &mut self, + typeid: TypeId, + name: Option<&'static str>, + make_ty: impl FnOnce(&mut Self) -> AlgebraicType, + ) -> AlgebraicType { + if let btree_map::Entry::Occupied(o) = self.type_map.entry(typeid) { + AlgebraicType::Ref(*o.get()) + } else { + let slot_ref = { + let ts = self.typespace_mut(); + // Bind a fresh alias to the unit type. + let slot_ref = ts.add(AlgebraicType::unit()); + // Relate `typeid -> fresh alias`. + self.type_map.insert(typeid, slot_ref); + + // Alias provided? Relate `name -> slot_ref`. + if let Some(sats_name) = name { + let name = sats_name_to_scoped_name(sats_name); + + self.types_mut().push(RawTypeDefV10 { + name, + ty: slot_ref, + // TODO(1.0): we need to update the `TypespaceBuilder` trait to include + // a `custom_ordering` parameter. + // For now, we assume all types have custom orderings, since the derive + // macro doesn't know about the default ordering yet. + custom_ordering: true, + }); + } + slot_ref + }; + + // Borrow of `v` has ended here, so we can now convince the borrow checker. + let ty = make_ty(self); + self.typespace_mut()[slot_ref] = ty; + AlgebraicType::Ref(slot_ref) + } + } +} + +/// Builder for a `RawTableDefV10`. +pub struct RawTableDefBuilderV10<'a> { + module: &'a mut RawModuleDefV10, + table: RawTableDefV10, +} + +impl RawTableDefBuilderV10<'_> { + /// Set the table type. + /// + /// This is not about column algebraic types, but about whether the table + /// was created by the system or the user. + pub fn with_type(mut self, table_type: TableType) -> Self { + self.table.table_type = table_type; + self + } + + /// Sets the access rights for the table and return it. + pub fn with_access(mut self, table_access: TableAccess) -> Self { + self.table.table_access = table_access; + self + } + + /// Generates a [RawConstraintDefV9] using the supplied `columns`. + pub fn with_unique_constraint(mut self, columns: impl Into) -> Self { + let columns = columns.into(); + self.table.constraints.push(RawConstraintDefV10 { + name: None, + data: RawConstraintDataV10::Unique(super::v9::RawUniqueConstraintDataV9 { columns }), + }); + + self + } + + /// Adds a primary key to the table. + /// You must also add a unique constraint on the primary key column. + pub fn with_primary_key(mut self, column: impl Into) -> Self { + self.table.primary_key = ColList::new(column.into()); + self + } + + /// Adds a primary key to the table, with corresponding unique constraint and sequence definitions. + /// You will also need to call [`Self::with_index`] to create an index on `column`. + pub fn with_auto_inc_primary_key(self, column: impl Into) -> Self { + let column = column.into(); + self.with_primary_key(column) + .with_unique_constraint(column) + .with_column_sequence(column) + } + + /// Generates a [RawIndexDefV10] using the supplied `columns`. + pub fn with_index(mut self, algorithm: RawIndexAlgorithm, accessor_name: impl Into) -> Self { + let accessor_name = accessor_name.into(); + + self.table.indexes.push(RawIndexDefV10 { + name: None, + accessor_name: Some(accessor_name), + algorithm, + }); + self + } + + /// Generates a [RawIndexDefV10] using the supplied `columns` but with no `accessor_name`. + pub fn with_index_no_accessor_name(mut self, algorithm: RawIndexAlgorithm) -> Self { + self.table.indexes.push(RawIndexDefV10 { + name: None, + accessor_name: None, + algorithm, + }); + self + } + + /// Adds a [RawSequenceDefV10] on the supplied `column`. + pub fn with_column_sequence(mut self, column: impl Into) -> Self { + let column = column.into(); + self.table.sequences.push(RawSequenceDefV10 { + name: None, + column, + start: None, + min_value: None, + max_value: None, + increment: 1, + }); + + self + } + + /// Adds a default value for a column. + pub fn with_default_column_value(mut self, column: impl Into, value: AlgebraicValue) -> Self { + let col_id = column.into(); + self.table.default_values.push(RawColumnDefaultValueV10 { + col_id, + value: spacetimedb_sats::bsatn::to_vec(&value).unwrap().into(), + }); + + self + } + + /// Build the table and add it to the module, returning the `product_type_ref` of the table. + pub fn finish(self) -> AlgebraicTypeRef { + let product_type_ref = self.table.product_type_ref; + + let tables = match self + .module + .sections + .iter_mut() + .find(|s| matches!(s, RawModuleDefV10Section::Tables(_))) + { + Some(RawModuleDefV10Section::Tables(t)) => t, + _ => { + self.module.sections.push(RawModuleDefV10Section::Tables(Vec::new())); + match self.module.sections.last_mut().unwrap() { + RawModuleDefV10Section::Tables(t) => t, + _ => unreachable!(), + } + } + }; + + tables.push(self.table); + product_type_ref + } + + /// Find a column position by its name in the table's product type. + pub fn find_col_pos_by_name(&self, column: impl AsRef) -> Option { + let column = column.as_ref(); + + let typespace = self.module.sections.iter().find_map(|s| { + if let RawModuleDefV10Section::Typespace(ts) = s { + Some(ts) + } else { + None + } + })?; + + typespace + .get(self.table.product_type_ref)? + .as_product()? + .elements + .iter() + .position(|e| e.name().is_some_and(|n| n == column)) + .map(|i| ColId(i as u16)) + } +} \ No newline at end of file diff --git a/crates/schema/src/def.rs b/crates/schema/src/def.rs index c72c534a147..514a864d646 100644 --- a/crates/schema/src/def.rs +++ b/crates/schema/src/def.rs @@ -139,6 +139,17 @@ pub struct ModuleDef { /// /// **Note**: Are only validated syntax-wise. row_level_security_raw: HashMap, + + /// Indicates which raw module definition semantics this module + /// was authored under. + raw_module_def_version: RawModuleDefVersion, +} + +#[derive(Debug, Clone)] +pub enum RawModuleDefVersion { + /// Represents [`RawModuleDefV9`] and earlier. + V9OrEarlier, + V10, } impl ModuleDef { @@ -405,6 +416,7 @@ impl From for RawModuleDefV9 { refmap: _, row_level_security_raw, procedures, + raw_module_def_version: _, } = val; RawModuleDefV9 { @@ -423,6 +435,14 @@ impl From for RawModuleDefV9 { } } +impl TryFrom for ModuleDef { + type Error = ValidationErrors; + + fn try_from(v10_mod: raw_def::v10::RawModuleDefV10) -> Result { + validate::v10::validate(v10_mod) + } +} + /// Implemented by definitions stored in a `ModuleDef`. /// Allows looking definitions up in a `ModuleDef`, and across /// `ModuleDef`s during migrations. diff --git a/crates/schema/src/def/validate.rs b/crates/schema/src/def/validate.rs index 8810fd36ea1..1c9461244da 100644 --- a/crates/schema/src/def/validate.rs +++ b/crates/schema/src/def/validate.rs @@ -2,6 +2,7 @@ use crate::error::ValidationErrors; +pub mod v10; pub mod v8; pub mod v9; diff --git a/crates/schema/src/def/validate/common.rs b/crates/schema/src/def/validate/common.rs new file mode 100644 index 00000000000..e69de29bb2d diff --git a/crates/schema/src/def/validate/v10.rs b/crates/schema/src/def/validate/v10.rs new file mode 100644 index 00000000000..ee5d7aef921 --- /dev/null +++ b/crates/schema/src/def/validate/v10.rs @@ -0,0 +1,1397 @@ +use std::borrow::Cow; + +use spacetimedb_data_structures::map::HashCollectionExt; +use spacetimedb_lib::bsatn::Deserializer; +use spacetimedb_lib::db::raw_def::v10::*; +use spacetimedb_lib::de::DeserializeSeed as _; +use spacetimedb_sats::{Typespace, WithTypespace}; + +use crate::def::validate::v9::{ + check_function_names_are_unique, generate_schedule_name, identifier, CoreValidator, TableValidator, ViewValidator, +}; +use crate::def::*; +use crate::error::ValidationError; +use crate::type_for_generate::ProductTypeDef; +use crate::{def::validate::Result, error::TypeLocation}; + +/// Validate a `RawModuleDefV9` and convert it into a `ModuleDef`, +/// or return a stream of errors if the definition is invalid. +pub fn validate(def: RawModuleDefV10) -> Result { + let typespace = def.typespace().cloned().unwrap_or_else(|| Typespace::EMPTY.clone()); + let known_type_definitions = def.types().into_iter().flatten().map(|def| def.ty); + + let mut validator = ModuleValidatorV10 { + core: CoreValidator { + typespace: &typespace, + stored_in_table_def: Default::default(), + type_namespace: Default::default(), + lifecycle_reducers: Default::default(), + typespace_for_generate: TypespaceForGenerate::builder(&typespace, known_type_definitions), + }, + }; + + // Important general note: + // This file uses the `ErrorStream` combinator to return *multiple errors + // at once* when validating a definition. + // The general pattern is that we use `collect_all_errors` when building + // a collection, and `combine_errors` when we have multiple + // things to validate that are independent of each other. + // We try to avoid using `?` until the end of a function, after we've called + // `combine_errors` or `collect_all_errors` on all the things we need to validate. + // Sometimes it is unavoidable to use `?` early and this should be commented on. + + let reducers = def + .reducers() + .cloned() + .into_iter() + .flatten() + .enumerate() + .map(|(_idx, reducer)| { + validator + .validate_reducer_def(reducer) + .map(|reducer_def| (reducer_def.name.clone(), reducer_def)) + }) + // Collect into a `Vec` first to preserve duplicate names. + // Later on, in `check_function_names_are_unique`, we'll transform this into an `IndexMap`. + .collect_all_errors::>(); + + let procedures = def + .procedures() + .cloned() + .into_iter() + .flatten() + .map(|procedure| { + validator + .validate_procedure_def(procedure) + .map(|procedure_def| (procedure_def.name.clone(), procedure_def)) + }) + // Collect into a `Vec` first to preserve duplicate names. + // Later on, in `check_function_names_are_unique`, we'll transform this into an `IndexMap`. + .collect_all_errors::>(); + + let views = def + .views() + .cloned() + .into_iter() + .flatten() + .map(|view| { + validator + .validate_view_def(view) + .map(|view_def| (view_def.name.clone(), view_def)) + }) + .collect_all_errors(); + + let tables = def + .tables() + .cloned() + .into_iter() + .flatten() + .map(|table| { + validator + .validate_table_def(table) + .map(|table_def| (table_def.name.clone(), table_def)) + }) + .collect_all_errors(); + + let mut refmap = HashMap::default(); + let types = def + .types() + .cloned() + .into_iter() + .flatten() + .map(|ty| { + validator.core.validate_type_def(ty).map(|type_def| { + refmap.insert(type_def.ty, type_def.name.clone()); + (type_def.name.clone(), type_def) + }) + }) + .collect_all_errors::>(); + + // Validate schedules - they need the validated tables to exist first + let schedules = tables + .as_ref() + .ok() + .map(|tables_map| { + def.schedules() + .cloned() + .into_iter() + .flatten() + .map(|schedule| { + validator + .validate_schedule_def(schedule, tables_map) + .map(|schedule_def| (schedule_def.name.clone(), schedule_def)) + }) + .collect_all_errors::>() + }) + .unwrap_or_else(|| Ok(Vec::new())); + + // Validate lifecycle reducers - they reference reducers by name + let lifecycle_validations = reducers + .as_ref() + .ok() + .map(|reducers_vec| { + def.lifecycle_reducers() + .cloned() + .into_iter() + .flatten() + .map(|lifecycle_def| { + // Find the reducer by name + let function_name = identifier(lifecycle_def.function_name.clone())?; + let reducer_id = reducers_vec + .iter() + .position(|(name, _)| name == &function_name) + .map(|pos| ReducerId(pos as u32)) + .ok_or_else(|| ValidationError::LifecycleWithoutReducer { + lifecycle: (lifecycle_def.lifecycle_spec), + })?; + + validator.validate_lifecycle_reducer(lifecycle_def, reducer_id) + }) + .collect_all_errors::>() + }) + .unwrap_or_else(|| Ok(Vec::new())); + + // Combine all validation results + let tables_types_reducers_procedures_views = ( + tables, + types, + reducers, + procedures, + views, + schedules, + lifecycle_validations, + ) + .combine_errors() + .and_then( + |(mut tables, types, reducers, procedures, views, schedules, _lifecycles)| { + let (reducers, procedures, views) = check_function_names_are_unique(reducers, procedures, views)?; + + // Attach schedules to their respective tables + for (_, schedule_def) in schedules { + // Find the table this schedule belongs to + if let Some(table) = tables.values_mut().find(|t| { + // Match by checking if any column matches the schedule's at_column + t.columns.iter().any(|col| col.col_id == schedule_def.at_column) + }) { + // Only one schedule per table is allowed in current design + if table.schedule.is_some() { + return Err(ValidationError::DuplicateSchedule { + table: table.name.clone(), + } + .into()); + } + table.schedule = Some(schedule_def); + } + } + + Ok((tables, types, reducers, procedures, views)) + }, + ); + + let CoreValidator { + stored_in_table_def, + typespace_for_generate, + lifecycle_reducers, + .. + } = validator.core; + + let (tables, types, reducers, procedures, views) = + (tables_types_reducers_procedures_views).map_err(|errors| errors.sort_deduplicate())?; + + let typespace_for_generate = typespace_for_generate.finish(); + + Ok(ModuleDef { + tables, + reducers, + views, + types, + typespace, + typespace_for_generate, + stored_in_table_def, + refmap, + row_level_security_raw: HashMap::new(), + lifecycle_reducers, + procedures, + raw_module_def_version: RawModuleDefVersion::V10, + }) +} + +struct ModuleValidatorV10<'a> { + core: CoreValidator<'a>, +} + +impl<'a> ModuleValidatorV10<'a> { + fn validate_table_def(&mut self, table: RawTableDefV10) -> Result { + let RawTableDefV10 { + name: raw_table_name, + product_type_ref, + primary_key, + indexes, + constraints, + sequences, + table_type, + table_access, + default_values, + } = table; + + let product_type: &ProductType = self + .core + .typespace + .get(product_type_ref) + .and_then(AlgebraicType::as_product) + .ok_or_else(|| { + ValidationErrors::from(ValidationError::InvalidProductTypeRef { + table: raw_table_name.clone(), + ref_: product_type_ref, + }) + })?; + + let mut table_validator = + TableValidator::new(raw_table_name.clone(), product_type_ref, product_type, &mut self.core); + + // Validate columns first + let mut columns: Vec = (0..product_type.elements.len()) + .map(|id| table_validator.validate_column_def(id.into())) + .collect_all_errors()?; + + let indexes = indexes + .into_iter() + .map(|index| { + table_validator + .validate_index_def(index) + .map(|index| (index.name.clone(), index)) + }) + .collect_all_errors::>(); + + let constraints_primary_key = constraints + .into_iter() + .map(|constraint| { + table_validator + .validate_constraint_def(constraint) + .map(|constraint| (constraint.name.clone(), constraint)) + }) + .collect_all_errors() + .and_then(|constraints: StrMap| { + table_validator.validate_primary_key(constraints, primary_key) + }); + + let constraints_backed_by_indices = + if let (Ok((constraints, _)), Ok(indexes)) = (&constraints_primary_key, &indexes) { + constraints + .values() + .filter_map(|c| c.data.unique_columns().map(|cols| (c, cols))) + .filter(|(_, unique_cols)| { + !indexes + .values() + .any(|i| ColSet::from(i.algorithm.columns()) == **unique_cols) + }) + .map(|(c, cols)| { + let constraint = c.name.clone(); + let columns = cols.clone(); + Err(ValidationError::UniqueConstraintWithoutIndex { constraint, columns }.into()) + }) + .collect_all_errors() + } else { + Ok(()) + }; + + let sequences = sequences + .into_iter() + .map(|sequence| { + table_validator + .validate_sequence_def(sequence) + .map(|sequence| (sequence.name.clone(), sequence)) + }) + .collect_all_errors(); + + let name = table_validator + .add_to_global_namespace(raw_table_name.clone()) + .and_then(|name| { + let name = identifier(name)?; + if table_type != TableType::System && name.starts_with("st_") { + Err(ValidationError::TableNameReserved { table: name }.into()) + } else { + Ok(name) + } + }); + + // Validate default values inline and attach them to columns + let validated_defaults: Result> = default_values + .iter() + .map(|cdv| { + let col_id = cdv.col_id; + let Some(col_elem) = product_type.elements.get(col_id.idx()) else { + return Err(ValidationError::ColumnNotFound { + table: raw_table_name.clone(), + def: raw_table_name.clone(), + column: col_id, + } + .into()); + }; + + let mut reader = &cdv.value[..]; + let ty = WithTypespace::new(self.core.typespace, &col_elem.algebraic_type); + let field_value = ty.deserialize(Deserializer::new(&mut reader)).map_err(|decode_error| { + ValidationError::ColumnDefaultValueMalformed { + table: raw_table_name.clone(), + col_id, + err: decode_error, + } + })?; + + Ok((col_id, field_value)) + }) + .collect_all_errors(); + + let validated_defaults = validated_defaults?; + // Attach default values to columns + for column in &mut columns { + if let Some(default_value) = validated_defaults.get(&column.col_id) { + column.default_value = Some(default_value.clone()); + } + } + + let (name, indexes, (constraints, primary_key), (), sequences) = ( + name, + indexes, + constraints_primary_key, + constraints_backed_by_indices, + sequences, + ) + .combine_errors()?; + + Ok(TableDef { + name, + product_type_ref, + primary_key, + columns, + indexes, + constraints, + sequences, + schedule: None, // V10 handles schedules separately + table_type, + table_access, + }) + } + + fn validate_reducer_def(&mut self, reducer_def: RawReducerDefV10) -> Result { + let RawReducerDefV10 { name, params, .. } = reducer_def; + + let params_for_generate = + self.core + .params_for_generate(¶ms, |position, arg_name| TypeLocation::ReducerArg { + reducer_name: (&*name).into(), + position, + arg_name, + }); + + let name_result = identifier(name); + + let (name_result, params_for_generate) = (name_result, params_for_generate).combine_errors()?; + + Ok(ReducerDef { + name: name_result, + params: params.clone(), + params_for_generate: ProductTypeDef { + elements: params_for_generate, + recursive: false, // A ProductTypeDef not stored in a Typespace cannot be recursive. + }, + lifecycle: None, // V10 handles lifecycle separately + }) + } + + fn validate_schedule_def( + &mut self, + schedule: RawScheduleDefV10, + tables: &HashMap, + ) -> Result { + let RawScheduleDefV10 { + name, + table_name, + schedule_at_col, + function_name, + } = schedule; + + let table_ident = identifier(table_name.clone())?; + + // Look up the table to validate the schedule + let table = tables.get(&table_ident).ok_or_else(|| ValidationError::TableNotFound { + table: table_name.clone(), + })?; + + let product_type = self + .core + .typespace + .get(table.product_type_ref) + .and_then(AlgebraicType::as_product) + .ok_or_else(|| ValidationError::InvalidProductTypeRef { + table: table_name.clone(), + ref_: table.product_type_ref, + })?; + + let name = name.unwrap_or_else(|| generate_schedule_name(&table_name)); + self.core.validate_schedule_def( + table_name, + identifier(name)?, + function_name, + product_type, + schedule_at_col, + table.primary_key, + ) + } + + fn validate_lifecycle_reducer( + &mut self, + lifecycle_def: RawLifeCycleReducerDefV10, + reducer_id: ReducerId, + ) -> Result { + let RawLifeCycleReducerDefV10 { + lifecycle_spec, + function_name: _, + } = lifecycle_def; + + self.core.register_lifecycle(lifecycle_spec, reducer_id)?; + Ok(lifecycle_spec) + } + + fn validate_procedure_def(&mut self, procedure_def: RawProcedureDefV10) -> Result { + let RawProcedureDefV10 { + name, + params, + return_type, + .. + } = procedure_def; + + let params_for_generate = + self.core + .params_for_generate(¶ms, |position, arg_name| TypeLocation::ProcedureArg { + procedure_name: Cow::Borrowed(&name), + position, + arg_name, + }); + + let return_type_for_generate = self.core.validate_for_type_use( + &TypeLocation::ProcedureReturn { + procedure_name: Cow::Borrowed(&name), + }, + &return_type, + ); + + let name_result = identifier(name); + + let (name_result, params_for_generate, return_type_for_generate) = + (name_result, params_for_generate, return_type_for_generate).combine_errors()?; + + Ok(ProcedureDef { + name: name_result, + params, + params_for_generate: ProductTypeDef { + elements: params_for_generate, + recursive: false, + }, + return_type, + return_type_for_generate, + }) + } + + fn validate_view_def(&mut self, view_def: RawViewDefV10) -> Result { + let RawViewDefV10 { + name, + is_public, + is_anonymous, + params, + return_type, + index, + } = view_def; + + let invalid_return_type = || { + ValidationErrors::from(ValidationError::InvalidViewReturnType { + view: name.clone(), + ty: return_type.clone().into(), + }) + }; + + let product_type_ref = return_type + .as_option() + .and_then(AlgebraicType::as_ref) + .or_else(|| { + return_type + .as_array() + .map(|array_type| array_type.elem_ty.as_ref()) + .and_then(AlgebraicType::as_ref) + }) + .cloned() + .ok_or_else(invalid_return_type)?; + + let product_type = self + .core + .typespace + .get(product_type_ref) + .and_then(AlgebraicType::as_product) + .ok_or_else(|| { + ValidationErrors::from(ValidationError::InvalidProductTypeRef { + table: name.clone(), + ref_: product_type_ref, + }) + })?; + + let params_for_generate = + self.core + .params_for_generate(¶ms, |position, arg_name| TypeLocation::ViewArg { + view_name: Cow::Borrowed(&name), + position, + arg_name, + })?; + + let return_type_for_generate = self.core.validate_for_type_use( + &TypeLocation::ViewReturn { + view_name: Cow::Borrowed(&name), + }, + &return_type, + ); + + let mut view_validator = ViewValidator::new( + name.clone(), + product_type_ref, + product_type, + ¶ms, + ¶ms_for_generate, + &mut self.core, + ); + + let name_result = view_validator.add_to_global_namespace(name).and_then(identifier); + + let n = product_type.elements.len(); + let return_columns = (0..n) + .map(|id| view_validator.validate_view_column_def(id.into())) + .collect_all_errors(); + + let n = params.elements.len(); + let param_columns = (0..n) + .map(|id| view_validator.validate_param_column_def(id.into())) + .collect_all_errors(); + + let (name_result, return_type_for_generate, return_columns, param_columns) = + (name_result, return_type_for_generate, return_columns, param_columns).combine_errors()?; + + Ok(ViewDef { + name: name_result, + is_anonymous, + is_public, + params, + fn_ptr: index.into(), + params_for_generate: ProductTypeDef { + elements: params_for_generate, + recursive: false, // A `ProductTypeDef` not stored in a `Typespace` cannot be recursive. + }, + return_type, + return_type_for_generate, + product_type_ref, + return_columns, + param_columns, + }) + } + + fn validate_type_def(&mut self, type_def: RawTypeDefV10) -> Result { + self.core.validate_type_def(type_def) + } +} + +#[cfg(test)] +mod tests { + use crate::def::validate::tests::{ + check_product_type, expect_identifier, expect_raw_type_name, expect_resolve, expect_type_name, + }; + use crate::def::{validate::Result, ModuleDef}; + use crate::def::{ + BTreeAlgorithm, ConstraintData, ConstraintDef, DirectAlgorithm, FunctionKind, IndexAlgorithm, IndexDef, + SequenceDef, UniqueConstraintData, + }; + use crate::error::*; + use crate::type_for_generate::ClientCodegenError; + + use itertools::Itertools; + use spacetimedb_data_structures::expect_error_matching; + use spacetimedb_lib::db::raw_def::v10::RawModuleDefV10Builder; + use spacetimedb_lib::db::raw_def::v9::{btree, direct, hash}; + use spacetimedb_lib::db::raw_def::*; + use spacetimedb_lib::ScheduleAt; + use spacetimedb_primitives::{ColId, ColList, ColSet}; + use spacetimedb_sats::{AlgebraicType, AlgebraicTypeRef, AlgebraicValue, ProductType, SumValue}; + use v9::{Lifecycle, TableAccess, TableType}; + + /// This test attempts to exercise every successful path in the validation code. + #[test] + fn valid_definition() { + let mut builder = RawModuleDefV10Builder::new(); + + let product_type = AlgebraicType::product([("a", AlgebraicType::U64), ("b", AlgebraicType::String)]); + let product_type_ref = builder.add_algebraic_type( + ["scope1".into(), "scope2".into()], + "ReferencedProduct", + product_type.clone(), + false, + ); + + let sum_type = AlgebraicType::simple_enum(["Gala", "GrannySmith", "RedDelicious"].into_iter()); + let sum_type_ref = builder.add_algebraic_type([], "ReferencedSum", sum_type.clone(), false); + + let schedule_at_type = builder.add_type::(); + + let red_delicious = AlgebraicValue::Sum(SumValue::new(2, ())); + + builder + .build_table_with_new_type( + "Apples", + ProductType::from([ + ("id", AlgebraicType::U64), + ("name", AlgebraicType::String), + ("count", AlgebraicType::U16), + ("type", sum_type_ref.into()), + ]), + true, + ) + .with_index(btree([1, 2]), "apples_id") + .with_index(direct(2), "Apples_count_direct") + .with_unique_constraint(2) + .with_index(btree(3), "Apples_type_btree") + .with_unique_constraint(3) + .with_default_column_value(2, AlgebraicValue::U16(37)) + .with_default_column_value(3, red_delicious.clone()) + .finish(); + + builder + .build_table_with_new_type( + "Bananas", + ProductType::from([ + ("count", AlgebraicType::U16), + ("id", AlgebraicType::U64), + ("name", AlgebraicType::String), + ( + "optional_product_column", + AlgebraicType::option(product_type_ref.into()), + ), + ]), + false, + ) + .with_column_sequence(0) + .with_unique_constraint(ColId(0)) + .with_primary_key(0) + .with_access(TableAccess::Private) + .with_index(btree(0), "bananas_count") + .with_index(btree([0, 1, 2]), "bananas_count_id_name") + .finish(); + + let deliveries_product_type = builder + .build_table_with_new_type( + "Deliveries", + ProductType::from([ + ("id", AlgebraicType::U64), + ("scheduled_at", schedule_at_type.clone()), + ("scheduled_id", AlgebraicType::U64), + ]), + true, + ) + .with_auto_inc_primary_key(2) + .with_index(btree(2), "scheduled_id_index") + .with_schedule("check_deliveries", 1) + .with_type(TableType::System) + .finish(); + + builder.add_reducer("init", ProductType::unit()); + builder.add_reducer("on_connect", ProductType::unit()); + builder.add_reducer("on_disconnect", ProductType::unit()); + builder.add_reducer("extra_reducer", ProductType::from([("a", AlgebraicType::U64)])); + builder.add_reducer( + "check_deliveries", + ProductType::from([("a", deliveries_product_type.into())])); + + let def: ModuleDef = builder.finish().try_into().unwrap(); + + let apples = expect_identifier("Apples"); + let bananas = expect_identifier("Bananas"); + let deliveries = expect_identifier("Deliveries"); + + assert_eq!(def.tables.len(), 3); + + let apples_def = &def.tables[&apples]; + + assert_eq!(apples_def.name, apples); + assert_eq!(apples_def.table_type, TableType::User); + assert_eq!(apples_def.table_access, TableAccess::Public); + + assert_eq!(apples_def.columns.len(), 4); + assert_eq!(apples_def.columns[0].name, expect_identifier("id")); + assert_eq!(apples_def.columns[0].ty, AlgebraicType::U64); + assert_eq!(apples_def.columns[0].default_value, None); + assert_eq!(apples_def.columns[1].name, expect_identifier("name")); + assert_eq!(apples_def.columns[1].ty, AlgebraicType::String); + assert_eq!(apples_def.columns[1].default_value, None); + assert_eq!(apples_def.columns[2].name, expect_identifier("count")); + assert_eq!(apples_def.columns[2].ty, AlgebraicType::U16); + assert_eq!(apples_def.columns[2].default_value, Some(AlgebraicValue::U16(37))); + assert_eq!(apples_def.columns[3].name, expect_identifier("type")); + assert_eq!(apples_def.columns[3].ty, sum_type_ref.into()); + assert_eq!(apples_def.columns[3].default_value, Some(red_delicious)); + assert_eq!(expect_resolve(&def.typespace, &apples_def.columns[3].ty), sum_type); + + assert_eq!(apples_def.primary_key, None); + + assert_eq!(apples_def.constraints.len(), 2); + let apples_unique_constraint = "Apples_type_key"; + assert_eq!( + apples_def.constraints[apples_unique_constraint].data, + ConstraintData::Unique(UniqueConstraintData { + columns: ColId(3).into() + }) + ); + assert_eq!( + &apples_def.constraints[apples_unique_constraint].name[..], + apples_unique_constraint + ); + + assert_eq!(apples_def.indexes.len(), 3); + assert_eq!( + apples_def + .indexes + .values() + .sorted_by_key(|id| &id.name) + .collect::>(), + [ + &IndexDef { + name: "Apples_count_idx_direct".into(), + accessor_name: Some(expect_identifier("Apples_count_direct")), + algorithm: DirectAlgorithm { column: 2.into() }.into(), + }, + &IndexDef { + name: "Apples_name_count_idx_btree".into(), + accessor_name: Some(expect_identifier("apples_id")), + algorithm: BTreeAlgorithm { columns: [1, 2].into() }.into(), + }, + &IndexDef { + name: "Apples_type_idx_btree".into(), + accessor_name: Some(expect_identifier("Apples_type_btree")), + algorithm: BTreeAlgorithm { columns: 3.into() }.into(), + } + ] + ); + + let bananas_def = &def.tables[&bananas]; + + assert_eq!(bananas_def.name, bananas); + assert_eq!(bananas_def.table_access, TableAccess::Private); + assert_eq!(bananas_def.table_type, TableType::User); + assert_eq!(bananas_def.columns.len(), 4); + assert_eq!(bananas_def.columns[0].name, expect_identifier("count")); + assert_eq!(bananas_def.columns[0].ty, AlgebraicType::U16); + assert_eq!(bananas_def.columns[1].name, expect_identifier("id")); + assert_eq!(bananas_def.columns[1].ty, AlgebraicType::U64); + assert_eq!(bananas_def.columns[2].name, expect_identifier("name")); + assert_eq!(bananas_def.columns[2].ty, AlgebraicType::String); + assert_eq!( + bananas_def.columns[3].name, + expect_identifier("optional_product_column") + ); + assert_eq!( + bananas_def.columns[3].ty, + AlgebraicType::option(product_type_ref.into()) + ); + assert_eq!(bananas_def.primary_key, Some(0.into())); + assert_eq!(bananas_def.indexes.len(), 2); + assert_eq!(bananas_def.constraints.len(), 1); + let (bananas_constraint_name, bananas_constraint) = bananas_def.constraints.iter().next().unwrap(); + assert_eq!(bananas_constraint_name, &bananas_constraint.name); + assert_eq!( + bananas_constraint.data, + ConstraintData::Unique(UniqueConstraintData { + columns: ColId(0).into() + }) + ); + + let delivery_def = &def.tables[&deliveries]; + assert_eq!(delivery_def.name, deliveries); + assert_eq!(delivery_def.table_access, TableAccess::Public); + assert_eq!(delivery_def.table_type, TableType::System); + assert_eq!(delivery_def.columns.len(), 3); + assert_eq!(delivery_def.columns[0].name, expect_identifier("id")); + assert_eq!(delivery_def.columns[0].ty, AlgebraicType::U64); + assert_eq!(delivery_def.columns[1].name, expect_identifier("scheduled_at")); + assert_eq!(delivery_def.columns[1].ty, schedule_at_type); + assert_eq!(delivery_def.columns[2].name, expect_identifier("scheduled_id")); + assert_eq!(delivery_def.columns[2].ty, AlgebraicType::U64); + assert_eq!(delivery_def.schedule.as_ref().unwrap().at_column, 1.into()); + assert_eq!( + &delivery_def.schedule.as_ref().unwrap().function_name[..], + "check_deliveries" + ); + assert_eq!( + delivery_def.schedule.as_ref().unwrap().function_kind, + FunctionKind::Reducer + ); + assert_eq!(delivery_def.primary_key, Some(ColId(2))); + + assert_eq!(def.typespace.get(product_type_ref), Some(&product_type)); + assert_eq!(def.typespace.get(sum_type_ref), Some(&sum_type)); + + check_product_type(&def, apples_def); + check_product_type(&def, bananas_def); + check_product_type(&def, delivery_def); + + let product_type_name = expect_type_name("scope1::scope2::ReferencedProduct"); + let sum_type_name = expect_type_name("ReferencedSum"); + let apples_type_name = expect_type_name("Apples"); + let bananas_type_name = expect_type_name("Bananas"); + let deliveries_type_name = expect_type_name("Deliveries"); + + assert_eq!(def.types[&product_type_name].ty, product_type_ref); + assert_eq!(def.types[&sum_type_name].ty, sum_type_ref); + assert_eq!(def.types[&apples_type_name].ty, apples_def.product_type_ref); + assert_eq!(def.types[&bananas_type_name].ty, bananas_def.product_type_ref); + assert_eq!(def.types[&deliveries_type_name].ty, delivery_def.product_type_ref); + + let init_name = expect_identifier("init"); + assert_eq!(def.reducers[&init_name].name, init_name); + assert_eq!(def.reducers[&init_name].lifecycle, Some(Lifecycle::Init)); + + let on_connect_name = expect_identifier("on_connect"); + assert_eq!(def.reducers[&on_connect_name].name, on_connect_name); + assert_eq!(def.reducers[&on_connect_name].lifecycle, Some(Lifecycle::OnConnect)); + + let on_disconnect_name = expect_identifier("on_disconnect"); + assert_eq!(def.reducers[&on_disconnect_name].name, on_disconnect_name); + assert_eq!( + def.reducers[&on_disconnect_name].lifecycle, + Some(Lifecycle::OnDisconnect) + ); + + let extra_reducer_name = expect_identifier("extra_reducer"); + assert_eq!(def.reducers[&extra_reducer_name].name, extra_reducer_name); + assert_eq!(def.reducers[&extra_reducer_name].lifecycle, None); + assert_eq!( + def.reducers[&extra_reducer_name].params, + ProductType::from([("a", AlgebraicType::U64)]) + ); + + let check_deliveries_name = expect_identifier("check_deliveries"); + assert_eq!(def.reducers[&check_deliveries_name].name, check_deliveries_name); + assert_eq!(def.reducers[&check_deliveries_name].lifecycle, None); + assert_eq!( + def.reducers[&check_deliveries_name].params, + ProductType::from([("a", deliveries_product_type.into())]) + ); + } + + #[test] + fn invalid_product_type_ref() { + let mut builder = RawModuleDefV10Builder::new(); + + // `build_table` does NOT initialize table.product_type_ref, which should result in an error. + builder.build_table("Bananas", AlgebraicTypeRef(1337)).finish(); + + let result: Result = builder.finish().try_into(); + + expect_error_matching!(result, ValidationError::InvalidProductTypeRef { table, ref_ } => { + &table[..] == "Bananas" && ref_ == &AlgebraicTypeRef(1337) + }); + } + + #[test] + fn not_canonically_ordered_columns() { + let mut builder = RawModuleDefV10Builder::new(); + let product_type = ProductType::from([("b", AlgebraicType::U16), ("a", AlgebraicType::U64)]); + builder + .build_table_with_new_type("Bananas", product_type.clone(), false) + .finish(); + let result: Result = builder.finish().try_into(); + + expect_error_matching!(result, ValidationError::TypeHasIncorrectOrdering { type_name, ref_, bad_type } => { + type_name == &expect_raw_type_name("Bananas") && + ref_ == &AlgebraicTypeRef(0) && + bad_type == &product_type.clone().into() + }); + } + + #[test] + fn invalid_table_name() { + let mut builder = RawModuleDefV10Builder::new(); + builder + .build_table_with_new_type( + "", + ProductType::from([("b", AlgebraicType::U16), ("a", AlgebraicType::U64)]), + false, + ) + .finish(); + let result: Result = builder.finish().try_into(); + + expect_error_matching!(result, ValidationError::IdentifierError { error } => { + error == &IdentifierError::Empty {} + }); + } + + #[test] + fn invalid_column_name() { + let mut builder = RawModuleDefV10Builder::new(); + builder + .build_table_with_new_type( + "", + ProductType::from([("b", AlgebraicType::U16), ("a", AlgebraicType::U64)]), + false, + ) + .finish(); + let result: Result = builder.finish().try_into(); + + expect_error_matching!(result, ValidationError::IdentifierError { error } => { + error == &IdentifierError::Empty {} + }); + } + + #[test] + fn invalid_index_column_ref() { + let mut builder = RawModuleDefV10Builder::new(); + builder + .build_table_with_new_type( + "Bananas", + ProductType::from([("b", AlgebraicType::U16), ("a", AlgebraicType::U64)]), + false, + ) + .with_index(btree([0, 55]), "bananas_a_b") + .finish(); + let result: Result = builder.finish().try_into(); + + expect_error_matching!(result, ValidationError::ColumnNotFound { table, def, column } => { + &table[..] == "Bananas" && + &def[..] == "Bananas_b_col_55_idx_btree" && + column == &55.into() + }); + } + + #[test] + fn invalid_unique_constraint_column_ref() { + let mut builder = RawModuleDefV10Builder::new(); + builder + .build_table_with_new_type( + "Bananas", + ProductType::from([("b", AlgebraicType::U16), ("a", AlgebraicType::U64)]), + false, + ) + .with_unique_constraint(ColId(55)) + .finish(); + let result: Result = builder.finish().try_into(); + + expect_error_matching!(result, ValidationError::ColumnNotFound { table, def, column } => { + &table[..] == "Bananas" && + &def[..] == "Bananas_col_55_key" && + column == &55.into() + }); + } + + #[test] + fn invalid_sequence_column_ref() { + // invalid column id + let mut builder = RawModuleDefV10Builder::new(); + builder + .build_table_with_new_type( + "Bananas", + ProductType::from([("b", AlgebraicType::U16), ("a", AlgebraicType::U64)]), + false, + ) + .with_column_sequence(55) + .finish(); + let result: Result = builder.finish().try_into(); + + expect_error_matching!(result, ValidationError::ColumnNotFound { table, def, column } => { + &table[..] == "Bananas" && + &def[..] == "Bananas_col_55_seq" && + column == &55.into() + }); + + // incorrect column type + let mut builder = RawModuleDefV10Builder::new(); + builder + .build_table_with_new_type( + "Bananas", + ProductType::from([("b", AlgebraicType::U16), ("a", AlgebraicType::String)]), + false, + ) + .with_column_sequence(1) + .finish(); + let result: Result = builder.finish().try_into(); + + expect_error_matching!(result, ValidationError::InvalidSequenceColumnType { sequence, column, column_type } => { + &sequence[..] == "Bananas_a_seq" && + column == &RawColumnName::new("Bananas", "a") && + column_type.0 == AlgebraicType::String + }); + } + + #[test] + fn invalid_index_column_duplicates() { + let mut builder = RawModuleDefV10Builder::new(); + builder + .build_table_with_new_type( + "Bananas", + ProductType::from([("b", AlgebraicType::U16), ("a", AlgebraicType::U64)]), + false, + ) + .with_index(btree([0, 0]), "bananas_b_b") + .finish(); + let result: Result = builder.finish().try_into(); + + expect_error_matching!(result, ValidationError::DuplicateColumns{ def, columns } => { + &def[..] == "Bananas_b_b_idx_btree" && columns == &ColList::from_iter([0, 0]) + }); + } + + #[test] + fn invalid_unique_constraint_column_duplicates() { + let mut builder = RawModuleDefV10Builder::new(); + builder + .build_table_with_new_type( + "Bananas", + ProductType::from([("b", AlgebraicType::U16), ("a", AlgebraicType::U64)]), + false, + ) + .with_unique_constraint(ColList::from_iter([1, 1])) + .finish(); + let result: Result = builder.finish().try_into(); + + expect_error_matching!(result, ValidationError::DuplicateColumns{ def, columns } => { + &def[..] == "Bananas_a_a_key" && columns == &ColList::from_iter([1, 1]) + }); + } + + #[test] + fn recursive_ref() { + let recursive_type = AlgebraicType::product([("a", AlgebraicTypeRef(0).into())]); + + let mut builder = RawModuleDefV10Builder::new(); + let ref_ = builder.add_algebraic_type([], "Recursive", recursive_type.clone(), false); + builder.add_reducer("silly", ProductType::from([("a", ref_.into())]), None); + let result: ModuleDef = builder.finish().try_into().unwrap(); + + assert!(result.typespace_for_generate[ref_].is_recursive()); + } + + #[test] + fn out_of_bounds_ref() { + let invalid_type_1 = AlgebraicType::product([("a", AlgebraicTypeRef(31).into())]); + let mut builder = RawModuleDefV10Builder::new(); + let ref_ = builder.add_algebraic_type([], "Invalid", invalid_type_1.clone(), false); + builder.add_reducer("silly", ProductType::from([("a", ref_.into())]), None); + let result: Result = builder.finish().try_into(); + + expect_error_matching!(result, ValidationError::ClientCodegenError { location, error: ClientCodegenError::TypeRefError(_) } => { + location == &TypeLocation::InTypespace { ref_: AlgebraicTypeRef(0) } + }); + } + + #[test] + fn not_valid_for_client_code_generation() { + let inner_type_invalid_for_use = AlgebraicType::product([("b", AlgebraicType::U32)]); + let invalid_type = AlgebraicType::product([("a", inner_type_invalid_for_use.clone())]); + let mut builder = RawModuleDefV10Builder::new(); + let ref_ = builder.add_algebraic_type([], "Invalid", invalid_type.clone(), false); + builder.add_reducer("silly", ProductType::from([("a", ref_.into())]), None); + let result: Result = builder.finish().try_into(); + + expect_error_matching!( + result, + ValidationError::ClientCodegenError { + location, + error: ClientCodegenError::NonSpecialTypeNotAUse { ty } + } => { + location == &TypeLocation::InTypespace { ref_: AlgebraicTypeRef(0) } && + ty.0 == inner_type_invalid_for_use + } + ); + } + + #[test] + fn hash_index_supported() { + let mut builder = RawModuleDefV10Builder::new(); + builder + .build_table_with_new_type( + "Bananas", + ProductType::from([("b", AlgebraicType::U16), ("a", AlgebraicType::U64)]), + true, + ) + .with_index(hash(0), "bananas_b") + .finish(); + let def: ModuleDef = builder.finish().try_into().unwrap(); + let indexes = def.indexes().collect::>(); + assert_eq!(indexes.len(), 1); + assert_eq!(indexes[0].algorithm, IndexAlgorithm::Hash(0.into())); + } + + #[test] + fn unique_constrain_without_index() { + let mut builder = RawModuleDefV10Builder::new(); + builder + .build_table_with_new_type( + "Bananas", + ProductType::from([("b", AlgebraicType::U16), ("a", AlgebraicType::U64)]), + false, + ) + .with_unique_constraint(1) + .finish(); + let result: Result = builder.finish().try_into(); + + expect_error_matching!( + result, + ValidationError::UniqueConstraintWithoutIndex { constraint, columns } => { + &**constraint == "Bananas_a_key" && *columns == ColSet::from(1) + } + ); + } + + #[test] + fn direct_index_only_u8_to_u64() { + let mut builder = RawModuleDefV10Builder::new(); + builder + .build_table_with_new_type( + "Bananas", + ProductType::from([("b", AlgebraicType::I32), ("a", AlgebraicType::U64)]), + false, + ) + .with_index(direct(0), "bananas_b") + .finish(); + let result: Result = builder.finish().try_into(); + + expect_error_matching!(result, ValidationError::DirectIndexOnBadType { index, .. } => { + &index[..] == "Bananas_b_idx_direct" + }); + } + + #[test] + fn one_auto_inc() { + let mut builder = RawModuleDefV10Builder::new(); + builder + .build_table_with_new_type( + "Bananas", + ProductType::from([("b", AlgebraicType::U16), ("a", AlgebraicType::U64)]), + false, + ) + .with_column_sequence(1) + .with_column_sequence(1) + .finish(); + let result: Result = builder.finish().try_into(); + + expect_error_matching!(result, ValidationError::OneAutoInc { column } => { + column == &RawColumnName::new("Bananas", "a") + }); + } + + #[test] + fn invalid_primary_key() { + let mut builder = RawModuleDefV10Builder::new(); + builder + .build_table_with_new_type( + "Bananas", + ProductType::from([("b", AlgebraicType::U16), ("a", AlgebraicType::U64)]), + false, + ) + .with_primary_key(44) + .finish(); + let result: Result = builder.finish().try_into(); + + expect_error_matching!(result, ValidationError::ColumnNotFound { table, def, column } => { + &table[..] == "Bananas" && + &def[..] == "Bananas" && + column == &44.into() + }); + } + + #[test] + fn missing_primary_key_unique_constraint() { + let mut builder = RawModuleDefV10Builder::new(); + builder + .build_table_with_new_type( + "Bananas", + ProductType::from([("b", AlgebraicType::U16), ("a", AlgebraicType::U64)]), + false, + ) + .with_primary_key(0) + .finish(); + let result: Result = builder.finish().try_into(); + + expect_error_matching!(result, ValidationError::MissingPrimaryKeyUniqueConstraint { column } => { + column == &RawColumnName::new("Bananas", "b") + }); + } + + #[test] + fn duplicate_type_name() { + let mut builder = RawModuleDefV10Builder::new(); + builder.add_algebraic_type( + ["scope1".into(), "scope2".into()], + "Duplicate", + AlgebraicType::U64, + false, + ); + builder.add_algebraic_type( + ["scope1".into(), "scope2".into()], + "Duplicate", + AlgebraicType::U32, + false, + ); + let result: Result = builder.finish().try_into(); + + expect_error_matching!(result, ValidationError::DuplicateTypeName { name } => { + name == &expect_type_name("scope1::scope2::Duplicate") + }); + } + + #[test] + fn duplicate_lifecycle() { + let mut builder = RawModuleDefV10Builder::new(); + builder.add_reducer("init1", ProductType::unit(), Some(Lifecycle::Init)); + builder.add_reducer("init1", ProductType::unit(), Some(Lifecycle::Init)); + let result: Result = builder.finish().try_into(); + + expect_error_matching!(result, ValidationError::DuplicateLifecycle { lifecycle } => { + lifecycle == &Lifecycle::Init + }); + } + + #[test] + fn missing_scheduled_reducer() { + let mut builder = RawModuleDefV10Builder::new(); + let schedule_at_type = builder.add_type::(); + builder + .build_table_with_new_type( + "Deliveries", + ProductType::from([ + ("id", AlgebraicType::U64), + ("scheduled_at", schedule_at_type.clone()), + ("scheduled_id", AlgebraicType::U64), + ]), + true, + ) + .with_auto_inc_primary_key(2) + .with_index(btree(2), "scheduled_id_index") + .with_schedule("check_deliveries", 1) + .with_type(TableType::System) + .finish(); + let result: Result = builder.finish().try_into(); + + expect_error_matching!(result, ValidationError::MissingScheduledFunction { schedule, function } => { + &schedule[..] == "Deliveries_sched" && + function == &expect_identifier("check_deliveries") + }); + } + + #[test] + fn incorrect_scheduled_reducer_args() { + let mut builder = RawModuleDefV10Builder::new(); + let schedule_at_type = builder.add_type::(); + let deliveries_product_type = builder + .build_table_with_new_type( + "Deliveries", + ProductType::from([ + ("id", AlgebraicType::U64), + ("scheduled_at", schedule_at_type.clone()), + ("scheduled_id", AlgebraicType::U64), + ]), + true, + ) + .with_auto_inc_primary_key(2) + .with_index(direct(2), "scheduled_id_idx") + .with_schedule("check_deliveries", 1) + .with_type(TableType::System) + .finish(); + builder.add_reducer("check_deliveries", ProductType::from([("a", AlgebraicType::U64)])); + let result: Result = builder.finish().try_into(); + + expect_error_matching!(result, ValidationError::IncorrectScheduledFunctionParams { function_name, function_kind, expected, actual } => { + &function_name[..] == "check_deliveries" && + *function_kind == FunctionKind::Reducer && + expected.0 == AlgebraicType::product([AlgebraicType::Ref(deliveries_product_type)]) && + actual.0 == ProductType::from([("a", AlgebraicType::U64)]).into() + }); + } + + #[test] + fn wacky_names() { + let mut builder = RawModuleDefV10Builder::new(); + + let schedule_at_type = builder.add_type::(); + + let deliveries_product_type = builder + .build_table_with_new_type( + "Deliveries", + ProductType::from([ + ("id", AlgebraicType::U64), + ("scheduled_at", schedule_at_type.clone()), + ("scheduled_id", AlgebraicType::U64), + ]), + true, + ) + .with_auto_inc_primary_key(2) + .with_index(direct(2), "scheduled_id_index") + .with_index(btree([0, 2]), "nice_index_name") + .with_type(TableType::System) + .finish(); + + builder.add_schedule("Deliveries", 1, "check_deliveries"); + builder.add_reducer( + "check_deliveries", + ProductType::from([("a", deliveries_product_type.into())]), + ); + + let tables = &mut builder.tables_mut_for_test(); + // Our builder methods ignore the possibility of setting names at the moment. + // But, it could be done in the future for some reason. + // Check if it works. + let mut raw_def = builder.finish(); + raw_def.tables[0].constraints[0].name = Some("wacky.constraint()".into()); + raw_def.tables[0].indexes[0].name = Some("wacky.index()".into()); + raw_def.tables[0].sequences[0].name = Some("wacky.sequence()".into()); + + let def: ModuleDef = raw_def.try_into().unwrap(); + assert!(def.lookup::("wacky.constraint()").is_some()); + assert!(def.lookup::("wacky.index()").is_some()); + assert!(def.lookup::("wacky.sequence()").is_some()); + } + + #[test] + fn duplicate_reducer_names() { + let mut builder = RawModuleDefV10Builder::new(); + + builder.add_reducer("foo", [("i", AlgebraicType::I32)].into()); + builder.add_reducer("foo", [("name", AlgebraicType::String)].into()); + + let result: Result = builder.finish().try_into(); + + expect_error_matching!(result, ValidationError::DuplicateFunctionName { name } => { + &name[..] == "foo" + }); + } + + #[test] + fn duplicate_procedure_names() { + let mut builder = RawModuleDefV10Builder::new(); + + builder.add_procedure("foo", [("i", AlgebraicType::I32)].into(), AlgebraicType::unit()); + builder.add_procedure("foo", [("name", AlgebraicType::String)].into(), AlgebraicType::unit()); + + let result: Result = builder.finish().try_into(); + + expect_error_matching!(result, ValidationError::DuplicateFunctionName { name } => { + &name[..] == "foo" + }); + } + + #[test] + fn duplicate_procedure_and_reducer_name() { + let mut builder = RawModuleDefV10Builder::new(); + + builder.add_reducer("foo", [("i", AlgebraicType::I32)].into()); + builder.add_procedure("foo", [("i", AlgebraicType::I32)].into(), AlgebraicType::unit()); + + let result: Result = builder.finish().try_into(); + + expect_error_matching!(result, ValidationError::DuplicateFunctionName { name } => { + &name[..] == "foo" + }); + } +} diff --git a/crates/schema/src/def/validate/v9.rs b/crates/schema/src/def/validate/v9.rs index 77db786b067..760d657e471 100644 --- a/crates/schema/src/def/validate/v9.rs +++ b/crates/schema/src/def/validate/v9.rs @@ -25,12 +25,14 @@ pub fn validate(def: RawModuleDefV9) -> Result { let known_type_definitions = types.iter().map(|def| def.ty); - let mut validator = ModuleValidator { - typespace: &typespace, - stored_in_table_def: Default::default(), - type_namespace: Default::default(), - lifecycle_reducers: Default::default(), - typespace_for_generate: TypespaceForGenerate::builder(&typespace, known_type_definitions), + let mut validator = ModuleValidatorV9 { + core: CoreValidator { + typespace: &typespace, + stored_in_table_def: Default::default(), + type_namespace: Default::default(), + lifecycle_reducers: Default::default(), + typespace_for_generate: TypespaceForGenerate::builder(&typespace, known_type_definitions), + }, }; // Important general note: @@ -118,7 +120,7 @@ pub fn validate(def: RawModuleDefV9) -> Result { let types = types .into_iter() .map(|ty| { - validator.validate_type_def(ty).map(|type_def| { + validator.core.validate_type_def(ty).map(|type_def| { refmap.insert(type_def.ty, type_def.name.clone()); (type_def.name.clone(), type_def) }) @@ -137,12 +139,12 @@ pub fn validate(def: RawModuleDefV9) -> Result { Ok((tables, types, reducers, procedures, views)) }); - let ModuleValidator { + let CoreValidator { stored_in_table_def, typespace_for_generate, lifecycle_reducers, .. - } = validator; + } = validator.core; let (tables, types, reducers, procedures, views) = (tables_types_reducers_procedures_views).map_err(|errors| errors.sort_deduplicate())?; @@ -161,34 +163,15 @@ pub fn validate(def: RawModuleDefV9) -> Result { row_level_security_raw, lifecycle_reducers, procedures, + raw_module_def_version: RawModuleDefVersion::V9OrEarlier, }) } -/// Collects state used during validation. -struct ModuleValidator<'a> { - /// The typespace of the module. - /// - /// Behind a reference to ensure we don't accidentally mutate it. - typespace: &'a Typespace, - - /// The in-progress typespace used to generate client types. - typespace_for_generate: TypespaceForGenerateBuilder<'a>, - - /// Names we have seen so far. - /// - /// It would be nice if we could have span information here, but currently it isn't passed - /// through the ABI boundary. - /// We could add it as a `MiscModuleExport` later without breaking the ABI. - stored_in_table_def: StrMap, - - /// Module-scoped type names we have seen so far. - type_namespace: HashMap, - - /// Reducers that play special lifecycle roles. - lifecycle_reducers: EnumMap>, +struct ModuleValidatorV9<'a> { + core: CoreValidator<'a>, } -impl ModuleValidator<'_> { +impl ModuleValidatorV9<'_> { fn validate_table_def(&mut self, table: RawTableDefV9) -> Result { let RawTableDefV9 { name: raw_table_name, @@ -205,6 +188,7 @@ impl ModuleValidator<'_> { // We exit early if we don't find the product type ref, // since this breaks all the other checks. let product_type: &ProductType = self + .core .typespace .get(product_type_ref) .and_then(AlgebraicType::as_product) @@ -219,7 +203,7 @@ impl ModuleValidator<'_> { raw_name: raw_table_name.clone(), product_type_ref, product_type, - module_validator: self, + module_validator: &mut self.core, has_sequence: Default::default(), }; @@ -340,35 +324,6 @@ impl ModuleValidator<'_> { }) } - fn params_for_generate<'a>( - &mut self, - params: &'a ProductType, - make_type_location: impl Fn(usize, Option>) -> TypeLocation<'a>, - ) -> Result> { - params - .elements - .iter() - .enumerate() - .map(|(position, param)| { - // Note: this does not allocate, since `TypeLocation` is defined using `Cow`. - // We only allocate if an error is returned. - let location = make_type_location(position, param.name().map(Into::into)); - let param_name = param - .name() - .ok_or_else(|| { - ValidationError::ClientCodegenError { - location: location.clone().make_static(), - error: ClientCodegenError::NamelessReducerParam, - } - .into() - }) - .and_then(|s| identifier(s.into())); - let ty_use = self.validate_for_type_use(&location, ¶m.algebraic_type); - (param_name, ty_use).combine_errors() - }) - .collect_all_errors() - } - /// Validate a reducer definition. fn validate_reducer_def(&mut self, reducer_def: RawReducerDefV9, reducer_id: ReducerId) -> Result { let RawReducerDefV9 { @@ -378,18 +333,19 @@ impl ModuleValidator<'_> { } = reducer_def; let params_for_generate: Result<_> = - self.params_for_generate(¶ms, |position, arg_name| TypeLocation::ReducerArg { - reducer_name: (&*name).into(), - position, - arg_name, - }); + self.core + .params_for_generate(¶ms, |position, arg_name| TypeLocation::ReducerArg { + reducer_name: (&*name).into(), + position, + arg_name, + }); // Reducers share the "function namespace" with procedures. // Uniqueness is validated in a later pass, in `check_function_names_are_unique`. let name = identifier(name.clone()); let lifecycle = lifecycle - .map(|lifecycle| match &mut self.lifecycle_reducers[lifecycle] { + .map(|lifecycle| match &mut self.core.lifecycle_reducers[lifecycle] { x @ None => { *x = Some(reducer_id); Ok(lifecycle) @@ -416,13 +372,15 @@ impl ModuleValidator<'_> { return_type, } = procedure_def; - let params_for_generate = self.params_for_generate(¶ms, |position, arg_name| TypeLocation::ProcedureArg { - procedure_name: Cow::Borrowed(&name), - position, - arg_name, - }); + let params_for_generate = + self.core + .params_for_generate(¶ms, |position, arg_name| TypeLocation::ProcedureArg { + procedure_name: Cow::Borrowed(&name), + position, + arg_name, + }); - let return_type_for_generate = self.validate_for_type_use( + let return_type_for_generate = self.core.validate_for_type_use( &TypeLocation::ProcedureReturn { procedure_name: Cow::Borrowed(&name), }, @@ -483,6 +441,7 @@ impl ModuleValidator<'_> { .ok_or_else(invalid_return_type)?; let product_type = self + .core .typespace .get(product_type_ref) .and_then(AlgebraicType::as_product) @@ -493,13 +452,15 @@ impl ModuleValidator<'_> { }) })?; - let params_for_generate = self.params_for_generate(¶ms, |position, arg_name| TypeLocation::ViewArg { - view_name: Cow::Borrowed(&name), - position, - arg_name, - })?; + let params_for_generate = + self.core + .params_for_generate(¶ms, |position, arg_name| TypeLocation::ViewArg { + view_name: Cow::Borrowed(&name), + position, + arg_name, + })?; - let return_type_for_generate = self.validate_for_type_use( + let return_type_for_generate = self.core.validate_for_type_use( &TypeLocation::ViewReturn { view_name: Cow::Borrowed(&name), }, @@ -512,7 +473,7 @@ impl ModuleValidator<'_> { product_type, ¶ms, ¶ms_for_generate, - self, + &mut self.core, ); // Views have the same interface as tables and therefore must be registered in the global namespace. @@ -579,7 +540,7 @@ impl ModuleValidator<'_> { // First time the type of the default value is known, so decode it. let mut reader = &cdv.value[..]; - let ty = WithTypespace::new(self.typespace, &col.ty); + let ty = WithTypespace::new(self.core.typespace, &col.ty); let field_value: Result = ty.deserialize(Deserializer::new(&mut reader)).map_err(|decode_error| { ValidationError::ColumnDefaultValueMalformed { @@ -592,9 +553,80 @@ impl ModuleValidator<'_> { field_value } +} + +/// Collects state used during validation. +pub(crate) struct CoreValidator<'a> { + /// The typespace of the module. + /// + /// Behind a reference to ensure we don't accidentally mutate it. + pub(crate) typespace: &'a Typespace, + + /// The in-progress typespace used to generate client types. + pub(crate) typespace_for_generate: TypespaceForGenerateBuilder<'a>, + + /// Names we have seen so far. + /// + /// It would be nice if we could have span information here, but currently it isn't passed + /// through the ABI boundary. + /// We could add it as a `MiscModuleExport` later without breaking the ABI. + pub(crate) stored_in_table_def: StrMap, + + /// Module-scoped type names we have seen so far. + pub(crate) type_namespace: HashMap, + + /// Reducers that play special lifecycle roles. + pub(crate) lifecycle_reducers: EnumMap>, +} + +impl CoreValidator<'_> { + pub(crate) fn params_for_generate<'a>( + &mut self, + params: &'a ProductType, + make_type_location: impl Fn(usize, Option>) -> TypeLocation<'a>, + ) -> Result> { + params + .elements + .iter() + .enumerate() + .map(|(position, param)| { + // Note: this does not allocate, since `TypeLocation` is defined using `Cow`. + // We only allocate if an error is returned. + let location = make_type_location(position, param.name().map(Into::into)); + let param_name = param + .name() + .ok_or_else(|| { + ValidationError::ClientCodegenError { + location: location.clone().make_static(), + error: ClientCodegenError::NamelessReducerParam, + } + .into() + }) + .and_then(|s| identifier(s.into())); + let ty_use = self.validate_for_type_use(&location, ¶m.algebraic_type); + (param_name, ty_use).combine_errors() + }) + .collect_all_errors() + } + + /// Add a name to the global namespace. + /// + /// If it has already been added, return an error. + /// + /// This is not used for all `Def` types. + pub(crate) fn add_to_global_namespace(&mut self, raw_name: Box, ident: Identifier) -> Result> { + // This may report the table_name as invalid multiple times, but this will be removed + // when we sort and deduplicate the error stream. + if self.stored_in_table_def.contains_key(&raw_name) { + Err(ValidationError::DuplicateName { name: raw_name }.into()) + } else { + self.stored_in_table_def.insert(raw_name.clone(), ident); + Ok(raw_name) + } + } /// Validate a type definition. - fn validate_type_def(&mut self, type_def: RawTypeDefV9) -> Result { + pub(crate) fn validate_type_def(&mut self, type_def: RawTypeDefV9) -> Result { let RawTypeDefV9 { name, ty, @@ -672,7 +704,11 @@ impl ModuleValidator<'_> { } /// Validates that a type can be used to generate a client type use. - fn validate_for_type_use(&mut self, location: &TypeLocation, ty: &AlgebraicType) -> Result { + pub(crate) fn validate_for_type_use( + &mut self, + location: &TypeLocation, + ty: &AlgebraicType, + ) -> Result { self.typespace_for_generate.parse_use(ty).map_err(|err| { ErrorStream::expect_nonempty(err.into_iter().map(|error| ValidationError::ClientCodegenError { location: location.clone().make_static(), @@ -682,7 +718,7 @@ impl ModuleValidator<'_> { } /// Validates that a type can be used to generate a client type definition. - fn validate_for_type_definition(&mut self, ref_: AlgebraicTypeRef) -> Result<()> { + pub(crate) fn validate_for_type_definition(&mut self, ref_: AlgebraicTypeRef) -> Result<()> { self.typespace_for_generate.add_definition(ref_).map_err(|err| { ErrorStream::expect_nonempty(err.into_iter().map(|error| ValidationError::ClientCodegenError { location: TypeLocation::InTypespace { ref_ }, @@ -690,6 +726,65 @@ impl ModuleValidator<'_> { })) }) } + + pub(crate) fn register_lifecycle(&mut self, lifecycle: Lifecycle, reducer_id: ReducerId) -> Result<()> { + match &mut self.lifecycle_reducers[lifecycle] { + x @ None => { + *x = Some(reducer_id); + Ok(()) + } + Some(_) => Err(ValidationError::DuplicateLifecycle { lifecycle }.into()), + } + } + + pub(crate) fn validate_schedule_def( + &mut self, + table_name: Box, + name: Identifier, + function_name: Box, + product_type: &ProductType, + schedule_at_col: ColId, + primary_key: Option, + ) -> Result { + let at_column = product_type + .elements + .get(schedule_at_col.idx()) + .is_some_and(|ty| ty.algebraic_type.is_schedule_at()) + .then_some(schedule_at_col); + + let id_column = primary_key.filter(|pk| { + product_type + .elements + .get(pk.idx()) + .is_some_and(|ty| ty.algebraic_type == AlgebraicType::U64) + }); + + // Error if either column is missing. + let at_id = at_column.zip(id_column).ok_or_else(|| { + ValidationError::ScheduledIncorrectColumns { + table: table_name.clone(), + columns: product_type.clone(), + } + .into() + }); + + let name = self.add_to_global_namespace(table_name, name); + let function_name = identifier(function_name); + + let (name, (at_column, id_column), function_name) = (name, at_id, function_name).combine_errors()?; + + Ok(ScheduleDef { + name, + at_column, + id_column, + function_name, + + // Fill this in as a placeholder now. + // It will be populated with the correct `FunctionKind` later, + // in `check_scheduled_functions_exist`. + function_kind: FunctionKind::Unknown, + }) + } } /// A partially validated view. @@ -697,20 +792,20 @@ impl ModuleValidator<'_> { /// This is just a small wrapper around [`TableValidator`] so that we can: /// 1. Validate column defs /// 2. Insert view names into the global namespace. -struct ViewValidator<'a, 'b> { +pub(crate) struct ViewValidator<'a, 'b> { inner: TableValidator<'a, 'b>, params: &'a ProductType, params_for_generate: &'a [(Identifier, AlgebraicTypeUse)], } impl<'a, 'b> ViewValidator<'a, 'b> { - fn new( + pub(crate) fn new( raw_name: Box, product_type_ref: AlgebraicTypeRef, product_type: &'a ProductType, params: &'a ProductType, params_for_generate: &'a [(Identifier, AlgebraicTypeUse)], - module_validator: &'a mut ModuleValidator<'b>, + module_validator: &'a mut CoreValidator<'b>, ) -> Self { Self { inner: TableValidator { @@ -725,7 +820,7 @@ impl<'a, 'b> ViewValidator<'a, 'b> { } } - fn validate_param_column_def(&mut self, col_id: ColId) -> Result { + pub(crate) fn validate_param_column_def(&mut self, col_id: ColId) -> Result { let column = &self .params .elements @@ -763,30 +858,44 @@ impl<'a, 'b> ViewValidator<'a, 'b> { }) } - fn validate_view_column_def(&mut self, col_id: ColId) -> Result { + pub(crate) fn validate_view_column_def(&mut self, col_id: ColId) -> Result { self.inner.validate_column_def(col_id).map(ViewColumnDef::from) } - fn add_to_global_namespace(&mut self, name: Box) -> Result> { + pub(crate) fn add_to_global_namespace(&mut self, name: Box) -> Result> { self.inner.add_to_global_namespace(name) } } /// A partially validated table. -struct TableValidator<'a, 'b> { - module_validator: &'a mut ModuleValidator<'b>, +pub(crate) struct TableValidator<'a, 'b> { + module_validator: &'a mut CoreValidator<'b>, raw_name: Box, product_type_ref: AlgebraicTypeRef, product_type: &'a ProductType, has_sequence: HashSet, } -impl TableValidator<'_, '_> { +impl<'a, 'b> TableValidator<'a, 'b> { + pub(crate) fn new( + raw_name: Box, + product_type_ref: AlgebraicTypeRef, + product_type: &'a ProductType, + module_validator: &'a mut CoreValidator<'b>, + ) -> Self { + Self { + raw_name, + product_type_ref, + product_type, + module_validator, + has_sequence: Default::default(), + } + } /// Validate a column. /// /// Note that this accepts a `ProductTypeElement` rather than a `ColumnDef`, /// because all information about columns is stored in the `Typespace` in ABI version 9. - fn validate_column_def(&mut self, col_id: ColId) -> Result { + pub(crate) fn validate_column_def(&mut self, col_id: ColId) -> Result { let column = &self .product_type .elements @@ -830,7 +939,7 @@ impl TableValidator<'_, '_> { }) } - fn validate_primary_key( + pub(crate) fn validate_primary_key( &mut self, validated_constraints: StrMap, primary_key: ColList, @@ -862,7 +971,7 @@ impl TableValidator<'_, '_> { Ok((validated_constraints, pk)) } - fn validate_sequence_def(&mut self, sequence: RawSequenceDefV9) -> Result { + pub(crate) fn validate_sequence_def(&mut self, sequence: RawSequenceDefV9) -> Result { let RawSequenceDefV9 { column, min_value, @@ -897,7 +1006,7 @@ impl TableValidator<'_, '_> { /// Compare two `Option` values, returning `true` if `lo <= hi`, /// or if either is `None`. - fn le(lo: Option, hi: Option) -> bool { + pub(crate) fn le(lo: Option, hi: Option) -> bool { match (lo, hi) { (Some(lo), Some(hi)) => lo <= hi, _ => true, @@ -932,7 +1041,7 @@ impl TableValidator<'_, '_> { } /// Validate an index definition. - fn validate_index_def(&mut self, index: RawIndexDefV9) -> Result { + pub(crate) fn validate_index_def(&mut self, index: RawIndexDefV9) -> Result { let RawIndexDefV9 { name, algorithm, @@ -984,7 +1093,7 @@ impl TableValidator<'_, '_> { } /// Validate a unique constraint definition. - fn validate_constraint_def(&mut self, constraint: RawConstraintDefV9) -> Result { + pub(crate) fn validate_constraint_def(&mut self, constraint: RawConstraintDefV9) -> Result { let RawConstraintDefV9 { name, data } = constraint; if let RawConstraintDataV9::Unique(RawUniqueConstraintDataV9 { columns }) = data { @@ -1006,7 +1115,11 @@ impl TableValidator<'_, '_> { } /// Validate a schedule definition. - fn validate_schedule_def(&mut self, schedule: RawScheduleDefV9, primary_key: Option) -> Result { + pub(crate) fn validate_schedule_def( + &mut self, + schedule: RawScheduleDefV9, + primary_key: Option, + ) -> Result { let RawScheduleDefV9 { // Despite the field name, a `RawScheduleDefV9` may refer to either a reducer or a function. reducer_name: function_name, @@ -1014,48 +1127,16 @@ impl TableValidator<'_, '_> { name, } = schedule; - let name = name.unwrap_or_else(|| generate_schedule_name(&self.raw_name)); - - // Find the appropriate columns. - let at_column = self - .product_type - .elements - .get(scheduled_at_column.idx()) - .is_some_and(|ty| ty.algebraic_type.is_schedule_at()) - .then_some(scheduled_at_column); - - let id_column = primary_key.filter(|pk| { - self.product_type - .elements - .get(pk.idx()) - .is_some_and(|ty| ty.algebraic_type == AlgebraicType::U64) - }); - - // Error if either column is missing. - let at_id = at_column.zip(id_column).ok_or_else(|| { - ValidationError::ScheduledIncorrectColumns { - table: self.raw_name.clone(), - columns: self.product_type.clone(), - } - .into() - }); - - let name = self.add_to_global_namespace(name); - let function_name = identifier(function_name); - - let (name, (at_column, id_column), function_name) = (name, at_id, function_name).combine_errors()?; + let name = identifier(name.unwrap_or_else(|| generate_schedule_name(&self.raw_name.clone())))?; - Ok(ScheduleDef { + self.module_validator.validate_schedule_def( + self.raw_name.clone(), name, - at_column, - id_column, function_name, - - // Fill this in as a placeholder now. - // It will be populated with the correct `FunctionKind` later, - // in `check_scheduled_functions_exist`. - function_kind: FunctionKind::Unknown, - }) + self.product_type, + scheduled_at_column, + primary_key, + ) } /// Validate `name` as an `Identifier` and add it to the global namespace, registering the corresponding `Def` as being stored in a particular `TableDef`. @@ -1063,24 +1144,16 @@ impl TableValidator<'_, '_> { /// If it has already been added, return an error. /// /// This is not used for all `Def` types. - fn add_to_global_namespace(&mut self, name: Box) -> Result> { - let table_name = identifier(self.raw_name.clone())?; - + pub(crate) fn add_to_global_namespace(&mut self, name: Box) -> Result> { + let ident = identifier(name.clone())?; // This may report the table_name as invalid multiple times, but this will be removed // when we sort and deduplicate the error stream. - if self.module_validator.stored_in_table_def.contains_key(&name) { - Err(ValidationError::DuplicateName { name }.into()) - } else { - self.module_validator - .stored_in_table_def - .insert(name.clone(), table_name); - Ok(name) - } + self.module_validator.add_to_global_namespace(name, ident) } /// Validate a `ColId` for this table, returning it unmodified if valid. /// `def_name` is the name of the definition being validated and is used in errors. - pub fn validate_col_id(&self, def_name: &str, col_id: ColId) -> Result { + pub(crate) fn validate_col_id(&self, def_name: &str, col_id: ColId) -> Result { if self.product_type.elements.get(col_id.idx()).is_some() { Ok(col_id) } else { @@ -1095,7 +1168,7 @@ impl TableValidator<'_, '_> { /// Validate a `ColList` for this table, returning it unmodified if valid. /// `def_name` is the name of the definition being validated and is used in errors. - pub fn validate_col_ids(&self, def_name: &str, ids: ColList) -> Result { + pub(crate) fn validate_col_ids(&self, def_name: &str, ids: ColList) -> Result { let mut collected: Vec = ids .iter() .map(|column| self.validate_col_id(def_name, column)) @@ -1120,7 +1193,7 @@ impl TableValidator<'_, '_> { /// /// (It's generally preferable to avoid integer names, since types using the default /// ordering are implicitly shuffled!) - pub fn raw_column_name(&self, col_id: ColId) -> RawColumnName { + pub(crate) fn raw_column_name(&self, col_id: ColId) -> RawColumnName { let column: Box = self .product_type .elements @@ -1190,7 +1263,7 @@ pub fn generate_unique_constraint_name( /// Helper to create an `Identifier` from a `str` with the appropriate error type. /// TODO: memoize this. -fn identifier(name: Box) -> Result { +pub(crate) fn identifier(name: Box) -> Result { Identifier::new(name).map_err(|error| ValidationError::IdentifierError { error }.into()) } @@ -1246,7 +1319,7 @@ fn check_scheduled_functions_exist( /// then re-organize the reducers and procedures into [`IndexMap`]s /// for storage in the [`ModuleDef`]. #[allow(clippy::type_complexity)] -fn check_function_names_are_unique( +pub(crate) fn check_function_names_are_unique( reducers: Vec<(Identifier, ReducerDef)>, procedures: Vec<(Identifier, ProcedureDef)>, views: Vec<(Identifier, ViewDef)>, @@ -1292,7 +1365,7 @@ fn check_function_names_are_unique( fn check_non_procedure_misc_exports( misc_exports: Vec, - validator: &ModuleValidator, + validator: &ModuleValidatorV9, tables: &mut IdentifierMap, ) -> Result<()> { misc_exports @@ -1309,7 +1382,7 @@ fn check_non_procedure_misc_exports( fn process_column_default_value( cdv: &RawColumnDefaultValueV9, - validator: &ModuleValidator, + validator: &ModuleValidatorV9, tables: &mut IdentifierMap, ) -> Result<()> { // Validate the default value diff --git a/crates/schema/src/error.rs b/crates/schema/src/error.rs index e40bae898d5..0b0b148af6e 100644 --- a/crates/schema/src/error.rs +++ b/crates/schema/src/error.rs @@ -136,6 +136,10 @@ pub enum ValidationError { TableNotFound { table: RawIdentifier }, #[error("Name {name} is used for multiple reducers, procedures and/or views")] DuplicateFunctionName { name: Identifier }, + #[error("lifecycle event {lifecycle:?} without reducer")] + LifecycleWithoutReducer { lifecycle: Lifecycle }, + #[error("table {table} is assigned in multiple schedules")] + DuplicateSchedule { table: Identifier }, } /// A wrapper around an `AlgebraicType` that implements `fmt::Display`. From 2a93fce9e819127fb7d75ddbb609f3a2497abc0f Mon Sep 17 00:00:00 2001 From: Shubham Mishra Date: Thu, 22 Jan 2026 19:13:52 +0530 Subject: [PATCH 02/11] tests --- crates/lib/src/db/raw_def/v10.rs | 39 +++---- crates/schema/src/def.rs | 1 + crates/schema/src/def/validate/v10.rs | 156 ++++++++++++++++---------- crates/schema/src/def/validate/v9.rs | 20 ++-- crates/schema/src/error.rs | 7 ++ 5 files changed, 130 insertions(+), 93 deletions(-) diff --git a/crates/lib/src/db/raw_def/v10.rs b/crates/lib/src/db/raw_def/v10.rs index fd51f649aa2..b0c1b84a4fc 100644 --- a/crates/lib/src/db/raw_def/v10.rs +++ b/crates/lib/src/db/raw_def/v10.rs @@ -92,6 +92,7 @@ pub enum RawModuleDefV10Section { /// Unlike V9 where lifecycle was a field on reducers, /// V10 stores lifecycle-to-reducer mappings separately. LifeCycleReducers(Vec), + //TODO: Add section for Event tables, and Case conversion before exposing this from module } /// The definition of a database table. @@ -308,6 +309,16 @@ impl RawModuleDefV10 { _ => None, }) } + + pub fn tables_mut_for_tests(&mut self) -> &mut Vec { + self.sections + .iter_mut() + .find_map(|s| match s { + RawModuleDefV10Section::Tables(tables) => Some(tables), + _ => None, + }) + .expect("Tables section must exist for tests") + } } /// A builder for a [`RawModuleDefV10`]. @@ -346,24 +357,6 @@ impl RawModuleDefV10Builder { } } - /// Get mutable access to the tables section, creating it if missing. - fn tables_mut(&mut self) -> &mut Vec { - let idx = self - .module - .sections - .iter() - .position(|s| matches!(s, RawModuleDefV10Section::Tables(_))) - .unwrap_or_else(|| { - self.module.sections.push(RawModuleDefV10Section::Tables(Vec::new())); - self.module.sections.len() - 1 - }); - - match &mut self.module.sections[idx] { - RawModuleDefV10Section::Tables(tables) => tables, - _ => unreachable!("Just ensured Tables section exists"), - } - } - /// Get mutable access to the reducers section, creating it if missing. fn reducers_mut(&mut self) -> &mut Vec { let idx = self @@ -747,11 +740,6 @@ impl RawModuleDefV10Builder { } self.module } - - #[cfg(test)] - pub(crate) fn tables_mut_for_tests(&mut self) -> &mut Vec { - self.tables_mut() - } } /// Implement TypespaceBuilder for V10 @@ -907,7 +895,7 @@ impl RawTableDefBuilderV10<'_> { Some(RawModuleDefV10Section::Tables(t)) => t, _ => { self.module.sections.push(RawModuleDefV10Section::Tables(Vec::new())); - match self.module.sections.last_mut().unwrap() { + match self.module.sections.last_mut().expect("Just pushed Tables section") { RawModuleDefV10Section::Tables(t) => t, _ => unreachable!(), } @@ -938,4 +926,5 @@ impl RawTableDefBuilderV10<'_> { .position(|e| e.name().is_some_and(|n| n == column)) .map(|i| ColId(i as u16)) } -} \ No newline at end of file +} + diff --git a/crates/schema/src/def.rs b/crates/schema/src/def.rs index 514a864d646..bdb0ae775ee 100644 --- a/crates/schema/src/def.rs +++ b/crates/schema/src/def.rs @@ -142,6 +142,7 @@ pub struct ModuleDef { /// Indicates which raw module definition semantics this module /// was authored under. + #[allow(unused)] raw_module_def_version: RawModuleDefVersion, } diff --git a/crates/schema/src/def/validate/v10.rs b/crates/schema/src/def/validate/v10.rs index ee5d7aef921..5c57aa9b817 100644 --- a/crates/schema/src/def/validate/v10.rs +++ b/crates/schema/src/def/validate/v10.rs @@ -7,7 +7,8 @@ use spacetimedb_lib::de::DeserializeSeed as _; use spacetimedb_sats::{Typespace, WithTypespace}; use crate::def::validate::v9::{ - check_function_names_are_unique, generate_schedule_name, identifier, CoreValidator, TableValidator, ViewValidator, + check_function_names_are_unique, check_scheduled_functions_exist, generate_schedule_name, identifier, + CoreValidator, TableValidator, ViewValidator, }; use crate::def::*; use crate::error::ValidationError; @@ -116,11 +117,7 @@ pub fn validate(def: RawModuleDefV10) -> Result { .cloned() .into_iter() .flatten() - .map(|schedule| { - validator - .validate_schedule_def(schedule, tables_map) - .map(|schedule_def| (schedule_def.name.clone(), schedule_def)) - }) + .map(|schedule| validator.validate_schedule_def(schedule, tables_map)) .collect_all_errors::>() }) .unwrap_or_else(|| Ok(Vec::new())); @@ -135,22 +132,25 @@ pub fn validate(def: RawModuleDefV10) -> Result { .into_iter() .flatten() .map(|lifecycle_def| { - // Find the reducer by name let function_name = identifier(lifecycle_def.function_name.clone())?; - let reducer_id = reducers_vec + + let (pos, _) = reducers_vec .iter() - .position(|(name, _)| name == &function_name) - .map(|pos| ReducerId(pos as u32)) + .enumerate() + .find(|(_, (_, r))| r.name == function_name) .ok_or_else(|| ValidationError::LifecycleWithoutReducer { - lifecycle: (lifecycle_def.lifecycle_spec), + lifecycle: lifecycle_def.lifecycle_spec, })?; - validator.validate_lifecycle_reducer(lifecycle_def, reducer_id) + let reducer_id = ReducerId(pos as u32); + + validator.validate_lifecycle_reducer(lifecycle_def.clone(), reducer_id)?; + + Ok((reducer_id, lifecycle_def.lifecycle_spec)) }) .collect_all_errors::>() }) .unwrap_or_else(|| Ok(Vec::new())); - // Combine all validation results let tables_types_reducers_procedures_views = ( tables, @@ -163,26 +163,16 @@ pub fn validate(def: RawModuleDefV10) -> Result { ) .combine_errors() .and_then( - |(mut tables, types, reducers, procedures, views, schedules, _lifecycles)| { - let (reducers, procedures, views) = check_function_names_are_unique(reducers, procedures, views)?; + |(mut tables, types, reducers, procedures, views, schedules, lifecycles)| { + let (mut reducers, procedures, views) = check_function_names_are_unique(reducers, procedures, views)?; + + // Attach lifecycles to their respective reducers + attach_lifecycles_to_reducers(&mut reducers, lifecycles)?; // Attach schedules to their respective tables - for (_, schedule_def) in schedules { - // Find the table this schedule belongs to - if let Some(table) = tables.values_mut().find(|t| { - // Match by checking if any column matches the schedule's at_column - t.columns.iter().any(|col| col.col_id == schedule_def.at_column) - }) { - // Only one schedule per table is allowed in current design - if table.schedule.is_some() { - return Err(ValidationError::DuplicateSchedule { - table: table.name.clone(), - } - .into()); - } - table.schedule = Some(schedule_def); - } - } + attach_schedules_to_tables(&mut tables, schedules)?; + + check_scheduled_functions_exist(&mut tables, &reducers, &procedures)?; Ok((tables, types, reducers, procedures, views)) }, @@ -404,7 +394,7 @@ impl<'a> ModuleValidatorV10<'a> { &mut self, schedule: RawScheduleDefV10, tables: &HashMap, - ) -> Result { + ) -> Result<(ScheduleDef, Box)> { let RawScheduleDefV10 { name, table_name, @@ -430,14 +420,16 @@ impl<'a> ModuleValidatorV10<'a> { })?; let name = name.unwrap_or_else(|| generate_schedule_name(&table_name)); - self.core.validate_schedule_def( - table_name, - identifier(name)?, - function_name, - product_type, - schedule_at_col, - table.primary_key, - ) + self.core + .validate_schedule_def( + table_name.clone(), + identifier(name)?, + function_name, + product_type, + schedule_at_col, + table.primary_key, + ) + .map(|schedule_def| (schedule_def, table_name)) } fn validate_lifecycle_reducer( @@ -591,10 +583,55 @@ impl<'a> ModuleValidatorV10<'a> { param_columns, }) } +} + +fn attach_lifecycles_to_reducers( + reducers: &mut IndexMap, + lifecycles: Vec<(ReducerId, Lifecycle)>, +) -> Result<()> { + for lifecycle in lifecycles { + let (reducer_id, lifecycle) = lifecycle; + let reducer = reducers + .values_mut() + .nth(reducer_id.idx()) + .ok_or_else(|| ValidationError::LifecycleWithoutReducer { lifecycle })?; + + // Enforce invariant: only one lifecycle per reducer + if reducer.lifecycle.is_some() { + return Err(ValidationError::DuplicateLifecycle { lifecycle }.into()); + } + + reducer.lifecycle = Some(lifecycle); + } + + Ok(()) +} + +fn attach_schedules_to_tables( + tables: &mut HashMap, + schedules: Vec<(ScheduleDef, Box)>, +) -> Result<()> { + for schedule in schedules { + let (schedule, table_name) = schedule; + let table = tables.values_mut().find(|t| *t.name == *table_name).ok_or_else(|| { + ValidationError::MissingScheduleTable { + table_name: table_name.clone(), + schedule_name: schedule.name.clone(), + } + })?; + + // Enforce invariant: only one schedule per table + if table.schedule.is_some() { + return Err(ValidationError::DuplicateSchedule { + table: table.name.clone(), + } + .into()); + } - fn validate_type_def(&mut self, type_def: RawTypeDefV10) -> Result { - self.core.validate_type_def(type_def) + table.schedule = Some(schedule); } + + Ok(()) } #[cfg(test)] @@ -694,17 +731,18 @@ mod tests { ) .with_auto_inc_primary_key(2) .with_index(btree(2), "scheduled_id_index") - .with_schedule("check_deliveries", 1) .with_type(TableType::System) .finish(); - builder.add_reducer("init", ProductType::unit()); - builder.add_reducer("on_connect", ProductType::unit()); - builder.add_reducer("on_disconnect", ProductType::unit()); + builder.add_lifecycle_reducer(Lifecycle::Init, "init", ProductType::unit()); + builder.add_lifecycle_reducer(Lifecycle::OnConnect, "on_connect", ProductType::unit()); + builder.add_lifecycle_reducer(Lifecycle::OnDisconnect, "on_disconnect", ProductType::unit()); builder.add_reducer("extra_reducer", ProductType::from([("a", AlgebraicType::U64)])); builder.add_reducer( "check_deliveries", - ProductType::from([("a", deliveries_product_type.into())])); + ProductType::from([("a", deliveries_product_type.into())]), + ); + builder.add_schedule("Deliveries", 1, "check_deliveries"); let def: ModuleDef = builder.finish().try_into().unwrap(); @@ -1066,7 +1104,7 @@ mod tests { let mut builder = RawModuleDefV10Builder::new(); let ref_ = builder.add_algebraic_type([], "Recursive", recursive_type.clone(), false); - builder.add_reducer("silly", ProductType::from([("a", ref_.into())]), None); + builder.add_reducer("silly", ProductType::from([("a", ref_.into())])); let result: ModuleDef = builder.finish().try_into().unwrap(); assert!(result.typespace_for_generate[ref_].is_recursive()); @@ -1077,7 +1115,7 @@ mod tests { let invalid_type_1 = AlgebraicType::product([("a", AlgebraicTypeRef(31).into())]); let mut builder = RawModuleDefV10Builder::new(); let ref_ = builder.add_algebraic_type([], "Invalid", invalid_type_1.clone(), false); - builder.add_reducer("silly", ProductType::from([("a", ref_.into())]), None); + builder.add_reducer("silly", ProductType::from([("a", ref_.into())])); let result: Result = builder.finish().try_into(); expect_error_matching!(result, ValidationError::ClientCodegenError { location, error: ClientCodegenError::TypeRefError(_) } => { @@ -1091,7 +1129,7 @@ mod tests { let invalid_type = AlgebraicType::product([("a", inner_type_invalid_for_use.clone())]); let mut builder = RawModuleDefV10Builder::new(); let ref_ = builder.add_algebraic_type([], "Invalid", invalid_type.clone(), false); - builder.add_reducer("silly", ProductType::from([("a", ref_.into())]), None); + builder.add_reducer("silly", ProductType::from([("a", ref_.into())])); let result: Result = builder.finish().try_into(); expect_error_matching!( @@ -1244,8 +1282,8 @@ mod tests { #[test] fn duplicate_lifecycle() { let mut builder = RawModuleDefV10Builder::new(); - builder.add_reducer("init1", ProductType::unit(), Some(Lifecycle::Init)); - builder.add_reducer("init1", ProductType::unit(), Some(Lifecycle::Init)); + builder.add_lifecycle_reducer(Lifecycle::Init, "init1", ProductType::unit()); + builder.add_lifecycle_reducer(Lifecycle::Init, "init1", ProductType::unit()); let result: Result = builder.finish().try_into(); expect_error_matching!(result, ValidationError::DuplicateLifecycle { lifecycle } => { @@ -1269,9 +1307,10 @@ mod tests { ) .with_auto_inc_primary_key(2) .with_index(btree(2), "scheduled_id_index") - .with_schedule("check_deliveries", 1) .with_type(TableType::System) .finish(); + + builder.add_schedule("Deliveries", 1, "check_deliveries"); let result: Result = builder.finish().try_into(); expect_error_matching!(result, ValidationError::MissingScheduledFunction { schedule, function } => { @@ -1296,9 +1335,10 @@ mod tests { ) .with_auto_inc_primary_key(2) .with_index(direct(2), "scheduled_id_idx") - .with_schedule("check_deliveries", 1) .with_type(TableType::System) .finish(); + + builder.add_schedule("Deliveries", 1, "check_deliveries"); builder.add_reducer("check_deliveries", ProductType::from([("a", AlgebraicType::U64)])); let result: Result = builder.finish().try_into(); @@ -1338,14 +1378,14 @@ mod tests { ProductType::from([("a", deliveries_product_type.into())]), ); - let tables = &mut builder.tables_mut_for_test(); // Our builder methods ignore the possibility of setting names at the moment. // But, it could be done in the future for some reason. // Check if it works. let mut raw_def = builder.finish(); - raw_def.tables[0].constraints[0].name = Some("wacky.constraint()".into()); - raw_def.tables[0].indexes[0].name = Some("wacky.index()".into()); - raw_def.tables[0].sequences[0].name = Some("wacky.sequence()".into()); + let tables = raw_def.tables_mut_for_tests(); + tables[0].constraints[0].name = Some("wacky.constraint()".into()); + tables[0].indexes[0].name = Some("wacky.index()".into()); + tables[0].sequences[0].name = Some("wacky.sequence()".into()); let def: ModuleDef = raw_def.try_into().unwrap(); assert!(def.lookup::("wacky.constraint()").is_some()); diff --git a/crates/schema/src/def/validate/v9.rs b/crates/schema/src/def/validate/v9.rs index 760d657e471..32af62964ce 100644 --- a/crates/schema/src/def/validate/v9.rs +++ b/crates/schema/src/def/validate/v9.rs @@ -614,14 +614,14 @@ impl CoreValidator<'_> { /// If it has already been added, return an error. /// /// This is not used for all `Def` types. - pub(crate) fn add_to_global_namespace(&mut self, raw_name: Box, ident: Identifier) -> Result> { + pub(crate) fn add_to_global_namespace(&mut self, name: Box, ident: Identifier) -> Result> { // This may report the table_name as invalid multiple times, but this will be removed // when we sort and deduplicate the error stream. - if self.stored_in_table_def.contains_key(&raw_name) { - Err(ValidationError::DuplicateName { name: raw_name }.into()) + if self.stored_in_table_def.contains_key(&name) { + Err(ValidationError::DuplicateName { name }.into()) } else { - self.stored_in_table_def.insert(raw_name.clone(), ident); - Ok(raw_name) + self.stored_in_table_def.insert(name.clone(), ident); + Ok(name) } } @@ -767,8 +767,8 @@ impl CoreValidator<'_> { } .into() }); - - let name = self.add_to_global_namespace(table_name, name); + let table_name = identifier(table_name)?; + let name = self.add_to_global_namespace(name.into(), table_name); let function_name = identifier(function_name); let (name, (at_column, id_column), function_name) = (name, at_id, function_name).combine_errors()?; @@ -1145,10 +1145,10 @@ impl<'a, 'b> TableValidator<'a, 'b> { /// /// This is not used for all `Def` types. pub(crate) fn add_to_global_namespace(&mut self, name: Box) -> Result> { - let ident = identifier(name.clone())?; + let table_name = identifier(self.raw_name.clone())?; // This may report the table_name as invalid multiple times, but this will be removed // when we sort and deduplicate the error stream. - self.module_validator.add_to_global_namespace(name, ident) + self.module_validator.add_to_global_namespace(name, table_name) } /// Validate a `ColId` for this table, returning it unmodified if valid. @@ -1270,7 +1270,7 @@ pub(crate) fn identifier(name: Box) -> Result { /// Check that every [`ScheduleDef`]'s `function_name` refers to a real reducer or procedure /// and that the function's arguments are appropriate for the table, /// then record the scheduled function's [`FunctionKind`] in the [`ScheduleDef`]. -fn check_scheduled_functions_exist( +pub(crate) fn check_scheduled_functions_exist( tables: &mut IdentifierMap, reducers: &IndexMap, procedures: &IndexMap, diff --git a/crates/schema/src/error.rs b/crates/schema/src/error.rs index 0b0b148af6e..d9bc7aaaa74 100644 --- a/crates/schema/src/error.rs +++ b/crates/schema/src/error.rs @@ -138,8 +138,15 @@ pub enum ValidationError { DuplicateFunctionName { name: Identifier }, #[error("lifecycle event {lifecycle:?} without reducer")] LifecycleWithoutReducer { lifecycle: Lifecycle }, + #[error("lifecycle event {lifecycle:?} assigned multiple reducers")] + DuplicateLifeCycle { lifecycle: Lifecycle }, #[error("table {table} is assigned in multiple schedules")] DuplicateSchedule { table: Identifier }, + #[error("table {} corresponding to schedule {} not found", table_name, schedule_name)] + MissingScheduleTable { + table_name: Box, + schedule_name: Box, + }, } /// A wrapper around an `AlgebraicType` that implements `fmt::Display`. From 24507077f1f5bcf423f3ce1d42a49371f92cc32b Mon Sep 17 00:00:00 2001 From: Shubham Mishra Date: Thu, 22 Jan 2026 19:16:38 +0530 Subject: [PATCH 03/11] clippy --- crates/lib/src/db/raw_def/v10.rs | 20 +++++++------------- crates/schema/src/def/validate/v10.rs | 3 +-- 2 files changed, 8 insertions(+), 15 deletions(-) diff --git a/crates/lib/src/db/raw_def/v10.rs b/crates/lib/src/db/raw_def/v10.rs index b0c1b84a4fc..470707e40b1 100644 --- a/crates/lib/src/db/raw_def/v10.rs +++ b/crates/lib/src/db/raw_def/v10.rs @@ -724,19 +724,13 @@ impl RawModuleDefV10Builder { ); for internal_function in internal_functions { - self.reducers_mut() - .iter_mut() - .find(|r| r.name == internal_function) - .map(|r| { - r.visibility = FunctionVisibility::Internal; - }); - - self.procedures_mut() - .iter_mut() - .find(|p| p.name == internal_function) - .map(|p| { - p.visibility = FunctionVisibility::Internal; - }); + if let Some(r) = self.reducers_mut().iter_mut().find(|r| r.name == internal_function) { + r.visibility = FunctionVisibility::Internal; + } + + if let Some(p) = self.procedures_mut().iter_mut().find(|p| p.name == internal_function) { + p.visibility = FunctionVisibility::Internal; + } } self.module } diff --git a/crates/schema/src/def/validate/v10.rs b/crates/schema/src/def/validate/v10.rs index 5c57aa9b817..5b57b4f6372 100644 --- a/crates/schema/src/def/validate/v10.rs +++ b/crates/schema/src/def/validate/v10.rs @@ -46,8 +46,7 @@ pub fn validate(def: RawModuleDefV10) -> Result { .cloned() .into_iter() .flatten() - .enumerate() - .map(|(_idx, reducer)| { + .map(|reducer| { validator .validate_reducer_def(reducer) .map(|reducer_def| (reducer_def.name.clone(), reducer_def)) From 6dcb5bce962d5869490fee39855734337af005d9 Mon Sep 17 00:00:00 2001 From: Shubham Mishra Date: Thu, 22 Jan 2026 19:48:39 +0530 Subject: [PATCH 04/11] visibility in module_def --- crates/lib/src/db/raw_def/v10.rs | 2 +- crates/schema/src/def.rs | 27 +++++++++++++++++++++++++++ crates/schema/src/def/validate/v10.rs | 24 ++++++++++++++++++++---- crates/schema/src/def/validate/v9.rs | 2 ++ 4 files changed, 50 insertions(+), 5 deletions(-) diff --git a/crates/lib/src/db/raw_def/v10.rs b/crates/lib/src/db/raw_def/v10.rs index 470707e40b1..1c21e60c28e 100644 --- a/crates/lib/src/db/raw_def/v10.rs +++ b/crates/lib/src/db/raw_def/v10.rs @@ -704,7 +704,7 @@ impl RawModuleDefV10Builder { /// Finish building, consuming the builder and returning the module. /// The module should be validated before use. /// - /// This method automatically marks functions used in lifecycle or schedule definitions + /// This method automatically marks functions used in lifecycle or schedule functions /// as `Internal` visibility. pub fn finish(mut self) -> RawModuleDefV10 { let internal_functions = self diff --git a/crates/schema/src/def.rs b/crates/schema/src/def.rs index bdb0ae775ee..9a0cc437fdd 100644 --- a/crates/schema/src/def.rs +++ b/crates/schema/src/def.rs @@ -1288,6 +1288,27 @@ impl From for RawMiscModuleExportV9 { } } +/// The visibility of a function (reducer or procedure). +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum FunctionVisibility { + /// Internal-only, not callable from clients. + /// Typically used for lifecycle reducers and scheduled functions. + Internal, + + /// Callable from client code. + ClientCallable, +} + +use spacetimedb_lib::db::raw_def::v10::FunctionVisibility as RawFunctionVisibility; +impl From for FunctionVisibility { + fn from(val: RawFunctionVisibility) -> Self { + match val { + RawFunctionVisibility::Internal => FunctionVisibility::Internal, + RawFunctionVisibility::ClientCallable => FunctionVisibility::ClientCallable, + } + } +} + /// A reducer exported by the module. #[derive(Debug, Clone, Eq, PartialEq)] #[non_exhaustive] @@ -1307,6 +1328,9 @@ pub struct ReducerDef { /// The special role of this reducer in the module lifecycle, if any. pub lifecycle: Option, + + /// The visibility of this reducer. + pub visibility: FunctionVisibility, } impl From for RawReducerDefV9 { @@ -1348,6 +1372,9 @@ pub struct ProcedureDef { /// If this is a non-special compound type, it should be registered in the module's `TypespaceForGenerate` /// and indirected through an [`AlgebraicTypeUse::Ref`]. pub return_type_for_generate: AlgebraicTypeUse, + + /// The visibility of this procedure. + pub visibility: FunctionVisibility, } impl From for RawProcedureDefV9 { diff --git a/crates/schema/src/def/validate/v10.rs b/crates/schema/src/def/validate/v10.rs index 5b57b4f6372..7ff26131249 100644 --- a/crates/schema/src/def/validate/v10.rs +++ b/crates/schema/src/def/validate/v10.rs @@ -364,7 +364,11 @@ impl<'a> ModuleValidatorV10<'a> { } fn validate_reducer_def(&mut self, reducer_def: RawReducerDefV10) -> Result { - let RawReducerDefV10 { name, params, .. } = reducer_def; + let RawReducerDefV10 { + name, + params, + visibility, + } = reducer_def; let params_for_generate = self.core @@ -386,6 +390,7 @@ impl<'a> ModuleValidatorV10<'a> { recursive: false, // A ProductTypeDef not stored in a Typespace cannot be recursive. }, lifecycle: None, // V10 handles lifecycle separately + visibility: visibility.into(), }) } @@ -450,7 +455,7 @@ impl<'a> ModuleValidatorV10<'a> { name, params, return_type, - .. + visibility, } = procedure_def; let params_for_generate = @@ -482,6 +487,7 @@ impl<'a> ModuleValidatorV10<'a> { }, return_type, return_type_for_generate, + visibility: visibility.into(), }) } @@ -640,8 +646,8 @@ mod tests { }; use crate::def::{validate::Result, ModuleDef}; use crate::def::{ - BTreeAlgorithm, ConstraintData, ConstraintDef, DirectAlgorithm, FunctionKind, IndexAlgorithm, IndexDef, - SequenceDef, UniqueConstraintData, + BTreeAlgorithm, ConstraintData, ConstraintDef, DirectAlgorithm, FunctionKind, FunctionVisibility, + IndexAlgorithm, IndexDef, SequenceDef, UniqueConstraintData, }; use crate::error::*; use crate::type_for_generate::ClientCodegenError; @@ -916,6 +922,16 @@ mod tests { def.reducers[&check_deliveries_name].params, ProductType::from([("a", deliveries_product_type.into())]) ); + + assert_eq!( + def.reducers[&check_deliveries_name].visibility, + FunctionVisibility::Internal, + ); + assert_eq!(def.reducers[&init_name].visibility, FunctionVisibility::Internal); + assert_eq!( + def.reducers[&extra_reducer_name].visibility, + FunctionVisibility::ClientCallable + ); } #[test] diff --git a/crates/schema/src/def/validate/v9.rs b/crates/schema/src/def/validate/v9.rs index 32af62964ce..e06d2ae3249 100644 --- a/crates/schema/src/def/validate/v9.rs +++ b/crates/schema/src/def/validate/v9.rs @@ -362,6 +362,7 @@ impl ModuleValidatorV9<'_> { recursive: false, // A ProductTypeDef not stored in a Typespace cannot be recursive. }, lifecycle, + visibility: FunctionVisibility::ClientCallable, }) } @@ -403,6 +404,7 @@ impl ModuleValidatorV9<'_> { }, return_type, return_type_for_generate, + visibility: FunctionVisibility::ClientCallable, }) } From c61915affacda5f71e1adf97db8007a4f0589c6d Mon Sep 17 00:00:00 2001 From: Shubham Mishra Date: Thu, 22 Jan 2026 20:04:28 +0530 Subject: [PATCH 05/11] remove empty file --- crates/schema/src/def.rs | 1 + crates/schema/src/def/validate/common.rs | 0 2 files changed, 1 insertion(+) delete mode 100644 crates/schema/src/def/validate/common.rs diff --git a/crates/schema/src/def.rs b/crates/schema/src/def.rs index 9a0cc437fdd..84a84d66307 100644 --- a/crates/schema/src/def.rs +++ b/crates/schema/src/def.rs @@ -150,6 +150,7 @@ pub struct ModuleDef { pub enum RawModuleDefVersion { /// Represents [`RawModuleDefV9`] and earlier. V9OrEarlier, + /// Represents [`RawModuleDefV10`]. V10, } diff --git a/crates/schema/src/def/validate/common.rs b/crates/schema/src/def/validate/common.rs deleted file mode 100644 index e69de29bb2d..00000000000 From 55a53f4f291306349c2d1fdc1adb4cf2be570add Mon Sep 17 00:00:00 2001 From: Shubham Mishra Date: Thu, 22 Jan 2026 20:08:45 +0530 Subject: [PATCH 06/11] fmt --- crates/lib/src/db/raw_def.rs | 2 +- crates/lib/src/db/raw_def/v10.rs | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/lib/src/db/raw_def.rs b/crates/lib/src/db/raw_def.rs index 84cb66a6e98..a29161403a5 100644 --- a/crates/lib/src/db/raw_def.rs +++ b/crates/lib/src/db/raw_def.rs @@ -15,4 +15,4 @@ pub use v8::*; pub mod v9; -pub mod v10; \ No newline at end of file +pub mod v10; diff --git a/crates/lib/src/db/raw_def/v10.rs b/crates/lib/src/db/raw_def/v10.rs index 1c21e60c28e..b920c79b22b 100644 --- a/crates/lib/src/db/raw_def/v10.rs +++ b/crates/lib/src/db/raw_def/v10.rs @@ -921,4 +921,3 @@ impl RawTableDefBuilderV10<'_> { .map(|i| ColId(i as u16)) } } - From 08d26c6ede4dd0b85e85ecafa4a42fbd333e1b76 Mon Sep 17 00:00:00 2001 From: Shubham Mishra Date: Thu, 22 Jan 2026 20:13:28 +0530 Subject: [PATCH 07/11] lint --- crates/lib/src/db/raw_def/v10.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/lib/src/db/raw_def/v10.rs b/crates/lib/src/db/raw_def/v10.rs index b920c79b22b..22c66309e01 100644 --- a/crates/lib/src/db/raw_def/v10.rs +++ b/crates/lib/src/db/raw_def/v10.rs @@ -801,7 +801,7 @@ impl RawTableDefBuilderV10<'_> { self } - /// Generates a [RawConstraintDefV9] using the supplied `columns`. + /// Generates a `RawConstraintDefV10` using the supplied `columns`. pub fn with_unique_constraint(mut self, columns: impl Into) -> Self { let columns = columns.into(); self.table.constraints.push(RawConstraintDefV10 { From 5ce1291f09c3ec5b576502499fed8445d7a6fcf7 Mon Sep 17 00:00:00 2001 From: Shubham Mishra Date: Fri, 23 Jan 2026 20:42:46 +0530 Subject: [PATCH 08/11] test UI --- crates/bindings/tests/ui/tables.stderr | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/bindings/tests/ui/tables.stderr b/crates/bindings/tests/ui/tables.stderr index 7696526d4b0..ce4b00109e7 100644 --- a/crates/bindings/tests/ui/tables.stderr +++ b/crates/bindings/tests/ui/tables.stderr @@ -127,13 +127,13 @@ error[E0277]: `&'a Alpha` cannot appear as an argument to an index filtering ope = note: The allowed set of types are limited to integers, bool, strings, `Identity`, `ConnectionId`, `Hash` and no-payload enums which derive `SpacetimeType`, = help: the following other types implement trait `FilterableValue`: &ConnectionId + &FunctionVisibility &Identity &Lifecycle &TableAccess &TableType &bool ðnum::int::I256 - ðnum::uint::U256 and $N others note: required by a bound in `UniqueColumn::::ColType, Col>::find` --> src/table.rs @@ -154,13 +154,13 @@ error[E0277]: the trait bound `Alpha: IndexScanRangeBounds<(Alpha,), SingleBound | = help: the following other types implement trait `FilterableValue`: &ConnectionId + &FunctionVisibility &Identity &Lifecycle &TableAccess &TableType &bool ðnum::int::I256 - ðnum::uint::U256 and $N others = note: required for `Alpha` to implement `IndexScanRangeBounds<(Alpha,), SingleBound>` note: required by a bound in `RangedIndex::::filter` From d743fa7f15428944bbade8d8bf34f2f96a8a76b9 Mon Sep 17 00:00:00 2001 From: Shubham Mishra Date: Tue, 27 Jan 2026 21:25:55 +0530 Subject: [PATCH 09/11] rename name field to source_name --- crates/lib/src/db/raw_def/v10.rs | 260 ++++++++++++++++++++++---- crates/lib/src/db/raw_def/v9.rs | 56 ++++++ crates/schema/src/def/validate/v10.rs | 39 ++-- 3 files changed, 295 insertions(+), 60 deletions(-) diff --git a/crates/lib/src/db/raw_def/v10.rs b/crates/lib/src/db/raw_def/v10.rs index 22c66309e01..f80d228659d 100644 --- a/crates/lib/src/db/raw_def/v10.rs +++ b/crates/lib/src/db/raw_def/v10.rs @@ -5,25 +5,16 @@ //! into dedicated sections for cleaner organization. //! It allows easier future extensibility to add new kinds of definitions. +use core::fmt; use std::any::TypeId; use std::collections::{btree_map, BTreeMap}; +use itertools::Itertools as _; use spacetimedb_primitives::{ColId, ColList}; use spacetimedb_sats::typespace::TypespaceBuilder; use spacetimedb_sats::{AlgebraicType, AlgebraicTypeRef, AlgebraicValue, ProductType, SpacetimeType, Typespace}; -use crate::db::raw_def::v9::{ - sats_name_to_scoped_name, Lifecycle, RawIdentifier, RawIndexAlgorithm, TableAccess, TableType, -}; - -// Type aliases for consistency with V9 -pub type RawIndexDefV10 = super::v9::RawIndexDefV9; -pub type RawViewDefV10 = super::v9::RawViewDefV9; -pub type RawConstraintDefV10 = super::v9::RawConstraintDefV9; -pub type RawConstraintDataV10 = super::v9::RawConstraintDataV9; -pub type RawSequenceDefV10 = super::v9::RawSequenceDefV9; -pub type RawTypeDefV10 = super::v9::RawTypeDefV9; -pub type RawScopedTypeNameV10 = super::v9::RawScopedTypeNameV9; +use crate::db::raw_def::v9::{Lifecycle, RawIdentifier, RawIndexAlgorithm, TableAccess, TableType}; /// A possibly-invalid raw module definition. /// @@ -110,7 +101,7 @@ pub struct RawTableDefV10 { /// The name of the table. /// Unique within a module, acts as the table's identifier. /// Must be a valid `spacetimedb_schema::identifier::Identifier`. - pub name: RawIdentifier, + pub source_name: RawIdentifier, /// A reference to a `ProductType` containing the columns of this table. /// This is the single source of truth for the table's columns. @@ -168,7 +159,7 @@ pub struct RawColumnDefaultValueV10 { #[cfg_attr(feature = "test", derive(PartialEq, Eq, PartialOrd, Ord))] pub struct RawReducerDefV10 { /// The name of the reducer. - pub name: RawIdentifier, + pub source_name: RawIdentifier, /// The types and optional names of the parameters, in order. /// This `ProductType` need not be registered in the typespace. @@ -199,7 +190,7 @@ pub struct RawScheduleDefV10 { /// In the future, the user may FOR SOME REASON want to override this. /// Even though there is ABSOLUTELY NO REASON TO. /// If `None`, a nicely-formatted unique default will be chosen. - pub name: Option>, + pub source_name: Option>, /// The name of the table containing the schedule. pub table_name: RawIdentifier, @@ -229,7 +220,7 @@ pub struct RawLifeCycleReducerDefV10 { #[cfg_attr(feature = "test", derive(PartialEq, Eq, PartialOrd, Ord))] pub struct RawProcedureDefV10 { /// The name of the procedure. - pub name: RawIdentifier, + pub source_name: RawIdentifier, /// The types and optional names of the parameters, in order. /// This `ProductType` need not be registered in the typespace. @@ -245,6 +236,166 @@ pub struct RawProcedureDefV10 { pub visibility: FunctionVisibility, } +/// A sequence definition for a database table column. +#[derive(Debug, Clone, SpacetimeType)] +#[sats(crate = crate)] +#[cfg_attr(feature = "test", derive(PartialEq, Eq, PartialOrd, Ord))] +pub struct RawSequenceDefV10 { + /// In the future, the user may FOR SOME REASON want to override this. + /// Even though there is ABSOLUTELY NO REASON TO. + /// If `None`, a nicely-formatted unique default will be chosen. + pub source_name: Option>, + + /// The position of the column associated with this sequence. + /// This refers to a column in the same `RawTableDef` that contains this `RawSequenceDef`. + /// The column must have integral type. + /// This must be the unique `RawSequenceDef` for this column. + pub column: ColId, + + /// The value to start assigning to this column. + /// Will be incremented by 1 for each new row. + /// If not present, an arbitrary start point may be selected. + pub start: Option, + + /// The minimum allowed value in this column. + /// If not present, no minimum. + pub min_value: Option, + + /// The maximum allowed value in this column. + /// If not present, no maximum. + pub max_value: Option, + + /// The increment used when updating the SequenceDef. + pub increment: i128, +} + +/// The definition of a database index. +#[derive(Debug, Clone, SpacetimeType)] +#[sats(crate = crate)] +#[cfg_attr(feature = "test", derive(PartialEq, Eq, PartialOrd, Ord))] +pub struct RawIndexDefV10 { + /// In the future, the user may FOR SOME REASON want to override this. + /// Even though there is ABSOLUTELY NO REASON TO. + pub source_name: Option>, + + /// Accessor name for the index used in client codegen. + /// + /// This is set the user and should not be assumed to follow + /// any particular format. + /// + /// May be set to `None` if this is an auto-generated index for which the user + /// has not supplied a name. In this case, no client code generation for this index + /// will be performed. + /// + /// This name is not visible in the system tables, it is only used for client codegen. + pub accessor_name: Option, + + /// The algorithm parameters for the index. + pub algorithm: RawIndexAlgorithm, +} + +/// A constraint definition attached to a table. +#[derive(Debug, Clone, SpacetimeType)] +#[sats(crate = crate)] +#[cfg_attr(feature = "test", derive(PartialEq, Eq, PartialOrd, Ord))] +pub struct RawConstraintDefV10 { + /// In the future, the user may FOR SOME REASON want to override this. + /// Even though there is ABSOLUTELY NO REASON TO. + pub source_name: Option>, + + /// The data for the constraint. + pub data: RawConstraintDataV10, +} + +type RawConstraintDataV10 = crate::db::raw_def::v9::RawConstraintDataV9; +type RawUniqueConstraintDataV10 = crate::db::raw_def::v9::RawUniqueConstraintDataV9; + +/// A type declaration. +/// +/// Exactly of these must be attached to every `Product` and `Sum` type used by a module. +#[derive(Debug, Clone, SpacetimeType)] +#[sats(crate = crate)] +#[cfg_attr(feature = "test", derive(PartialEq, Eq, PartialOrd, Ord))] +pub struct RawTypeDefV10 { + /// The name of the type declaration. + pub source_name: RawScopedTypeNameV10, + + /// The type to which the declaration refers. + /// This must point to an `AlgebraicType::Product` or an `AlgebraicType::Sum` in the module's typespace. + pub ty: AlgebraicTypeRef, + + /// Whether this type has a custom ordering. + pub custom_ordering: bool, +} + +/// A scoped type name, in the form `scope0::scope1::...::scopeN::name`. +/// +/// These are the names that will be used *in client code generation*, NOT the names used for types +/// in the module source code. +#[derive(Clone, SpacetimeType, PartialEq, Eq, PartialOrd, Ord)] +#[sats(crate = crate)] +pub struct RawScopedTypeNameV10 { + /// The scope for this type. + /// + /// Empty unless a sats `name` attribute is used, e.g. + /// `#[sats(name = "namespace.name")]` in Rust. + pub scope: Box<[RawIdentifier]>, + + /// The name of the type. This must be unique within the module. + /// + /// Eventually, we may add more information to this, such as generic arguments. + pub source_name: RawIdentifier, +} + +impl fmt::Debug for RawScopedTypeNameV10 { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + for module in self.scope.iter() { + fmt::Debug::fmt(module, f)?; + f.write_str("::")?; + } + fmt::Debug::fmt(&self.source_name, f)?; + Ok(()) + } +} + +/// A view definition. +#[derive(Debug, Clone, SpacetimeType)] +#[sats(crate = crate)] +#[cfg_attr(feature = "test", derive(PartialEq, Eq, PartialOrd, Ord))] +pub struct RawViewDefV10 { + /// The name of the view function as defined in the module + pub source_name: RawIdentifier, + + /// The index of the view in the module's list of views. + pub index: u32, + + /// Is this a public or a private view? + /// Currently only public views are supported. + /// Private views may be supported in the future. + pub is_public: bool, + + /// Is this view anonymous? + /// An anonymous view does not know who called it. + /// Specifically, it is a view that has an `AnonymousViewContext` as its first argument. + /// This type does not have access to the `Identity` of the caller. + pub is_anonymous: bool, + + /// The types and optional names of the parameters, in order. + /// This `ProductType` need not be registered in the typespace. + pub params: ProductType, + + /// The return type of the view. + /// Either `T`, `Option`, or `Vec` where `T` is a `SpacetimeType`. + /// + /// More strictly `T` must be a SATS `ProductType`, + /// however this will be validated by the server on publish. + /// + /// This is the single source of truth for the views's columns. + /// All elements of the inner `ProductType` must have names. + /// This again will be validated by the server on publish. + pub return_type: AlgebraicType, +} + impl RawModuleDefV10 { /// Get the types section, if present. pub fn types(&self) -> Option<&Vec> { @@ -481,14 +632,14 @@ impl RawModuleDefV10Builder { /// Does not validate that the product_type_ref is valid; this is left to the module validation code. pub fn build_table( &mut self, - name: impl Into, + source_name: impl Into, product_type_ref: AlgebraicTypeRef, ) -> RawTableDefBuilderV10<'_> { - let name = name.into(); + let source_name = source_name.into(); RawTableDefBuilderV10 { module: &mut self.module, table: RawTableDefV10 { - name, + source_name, product_type_ref, indexes: vec![], constraints: vec![], @@ -581,15 +732,15 @@ impl RawModuleDefV10Builder { pub fn add_algebraic_type( &mut self, scope: impl IntoIterator, - name: impl Into, + source_name: impl Into, ty: AlgebraicType, custom_ordering: bool, ) -> AlgebraicTypeRef { let ty_ref = self.typespace_mut().add(ty); let scope = scope.into_iter().collect(); - let name = name.into(); + let source_name = source_name.into(); self.types_mut().push(RawTypeDefV10 { - name: RawScopedTypeNameV10 { name, scope }, + source_name: RawScopedTypeNameV10 { source_name, scope }, ty: ty_ref, custom_ordering, }); @@ -612,9 +763,9 @@ impl RawModuleDefV10Builder { /// have more than one `ReducerContext` argument, at least in Rust. /// This is because `SpacetimeType` is not implemented for `ReducerContext`, /// so it can never act like an ordinary argument.) - pub fn add_reducer(&mut self, name: impl Into, params: ProductType) { + pub fn add_reducer(&mut self, source_name: impl Into, params: ProductType) { self.reducers_mut().push(RawReducerDefV10 { - name: name.into(), + source_name: source_name.into(), params, visibility: FunctionVisibility::ClientCallable, }); @@ -630,9 +781,14 @@ impl RawModuleDefV10Builder { /// it should be registered in the typespace and indirected through an `AlgebraicType::Ref`. /// /// The `&mut ProcedureContext` first argument to the procedure should not be included in the `params`. - pub fn add_procedure(&mut self, name: impl Into, params: ProductType, return_type: AlgebraicType) { + pub fn add_procedure( + &mut self, + source_name: impl Into, + params: ProductType, + return_type: AlgebraicType, + ) { self.procedures_mut().push(RawProcedureDefV10 { - name: name.into(), + source_name: source_name.into(), params, return_type, visibility: FunctionVisibility::ClientCallable, @@ -642,7 +798,7 @@ impl RawModuleDefV10Builder { /// Add a view to the in-progress module. pub fn add_view( &mut self, - name: impl Into, + source_name: impl Into, index: usize, is_public: bool, is_anonymous: bool, @@ -650,7 +806,7 @@ impl RawModuleDefV10Builder { return_type: AlgebraicType, ) { self.views_mut().push(RawViewDefV10 { - name: name.into(), + source_name: source_name.into(), index: index as u32, is_public, is_anonymous, @@ -675,7 +831,7 @@ impl RawModuleDefV10Builder { }); self.reducers_mut().push(RawReducerDefV10 { - name: function_name, + source_name: function_name, params, visibility: FunctionVisibility::Internal, }); @@ -694,7 +850,7 @@ impl RawModuleDefV10Builder { function: impl Into, ) { self.schedules_mut().push(RawScheduleDefV10 { - name: None, + source_name: None, table_name: table.into(), schedule_at_col: column.into(), function_name: function.into(), @@ -724,11 +880,19 @@ impl RawModuleDefV10Builder { ); for internal_function in internal_functions { - if let Some(r) = self.reducers_mut().iter_mut().find(|r| r.name == internal_function) { + if let Some(r) = self + .reducers_mut() + .iter_mut() + .find(|r| r.source_name == internal_function) + { r.visibility = FunctionVisibility::Internal; } - if let Some(p) = self.procedures_mut().iter_mut().find(|p| p.name == internal_function) { + if let Some(p) = self + .procedures_mut() + .iter_mut() + .find(|p| p.source_name == internal_function) + { p.visibility = FunctionVisibility::Internal; } } @@ -741,7 +905,7 @@ impl TypespaceBuilder for RawModuleDefV10Builder { fn add( &mut self, typeid: TypeId, - name: Option<&'static str>, + source_name: Option<&'static str>, make_ty: impl FnOnce(&mut Self) -> AlgebraicType, ) -> AlgebraicType { if let btree_map::Entry::Occupied(o) = self.type_map.entry(typeid) { @@ -755,11 +919,11 @@ impl TypespaceBuilder for RawModuleDefV10Builder { self.type_map.insert(typeid, slot_ref); // Alias provided? Relate `name -> slot_ref`. - if let Some(sats_name) = name { - let name = sats_name_to_scoped_name(sats_name); + if let Some(sats_name) = source_name { + let source_name = sats_name_to_scoped_name_v10(sats_name); self.types_mut().push(RawTypeDefV10 { - name, + source_name, ty: slot_ref, // TODO(1.0): we need to update the `TypespaceBuilder` trait to include // a `custom_ordering` parameter. @@ -779,6 +943,20 @@ impl TypespaceBuilder for RawModuleDefV10Builder { } } +/// Convert a string from a sats type-name annotation like `#[sats(name = "namespace.name")]` to a `RawScopedTypeNameV9`. +/// We split the input on the strings `"::"` and `"."` to split up module paths. +/// +pub fn sats_name_to_scoped_name_v10(sats_name: &str) -> RawScopedTypeNameV10 { + // We can't use `&[char]: Pattern` for `split` here because "::" is not a char :/ + let mut scope: Vec = sats_name.split("::").flat_map(|s| s.split('.')).map_into().collect(); + // Unwrapping to "" will result in a validation error down the line, which is exactly what we want. + let source_name = scope.pop().unwrap_or_default(); + RawScopedTypeNameV10 { + scope: scope.into(), + source_name, + } +} + /// Builder for a `RawTableDefV10`. pub struct RawTableDefBuilderV10<'a> { module: &'a mut RawModuleDefV10, @@ -805,8 +983,8 @@ impl RawTableDefBuilderV10<'_> { pub fn with_unique_constraint(mut self, columns: impl Into) -> Self { let columns = columns.into(); self.table.constraints.push(RawConstraintDefV10 { - name: None, - data: RawConstraintDataV10::Unique(super::v9::RawUniqueConstraintDataV9 { columns }), + source_name: None, + data: RawConstraintDataV10::Unique(RawUniqueConstraintDataV10 { columns }), }); self @@ -833,7 +1011,7 @@ impl RawTableDefBuilderV10<'_> { let accessor_name = accessor_name.into(); self.table.indexes.push(RawIndexDefV10 { - name: None, + source_name: None, accessor_name: Some(accessor_name), algorithm, }); @@ -843,7 +1021,7 @@ impl RawTableDefBuilderV10<'_> { /// Generates a [RawIndexDefV10] using the supplied `columns` but with no `accessor_name`. pub fn with_index_no_accessor_name(mut self, algorithm: RawIndexAlgorithm) -> Self { self.table.indexes.push(RawIndexDefV10 { - name: None, + source_name: None, accessor_name: None, algorithm, }); @@ -854,7 +1032,7 @@ impl RawTableDefBuilderV10<'_> { pub fn with_column_sequence(mut self, column: impl Into) -> Self { let column = column.into(); self.table.sequences.push(RawSequenceDefV10 { - name: None, + source_name: None, column, start: None, min_value: None, diff --git a/crates/lib/src/db/raw_def/v9.rs b/crates/lib/src/db/raw_def/v9.rs index 26d7bbcd8d8..7c7efcb49a5 100644 --- a/crates/lib/src/db/raw_def/v9.rs +++ b/crates/lib/src/db/raw_def/v9.rs @@ -22,6 +22,11 @@ use spacetimedb_sats::Typespace; use crate::db::auth::StAccess; use crate::db::auth::StTableType; +use crate::db::raw_def::v10::RawConstraintDefV10; +use crate::db::raw_def::v10::RawIndexDefV10; +use crate::db::raw_def::v10::RawScopedTypeNameV10; +use crate::db::raw_def::v10::RawSequenceDefV10; +use crate::db::raw_def::v10::RawTypeDefV10; /// A not-yet-validated identifier. pub type RawIdentifier = Box; @@ -1045,3 +1050,54 @@ impl Drop for RawTableDefBuilder<'_> { self.module_def.tables.push(self.table.clone()); } } + +impl From for RawTypeDefV9 { + fn from(raw: RawTypeDefV10) -> Self { + RawTypeDefV9 { + name: raw.source_name.into(), + ty: raw.ty, + custom_ordering: raw.custom_ordering, + } + } +} + +impl From for RawScopedTypeNameV9 { + fn from(raw: RawScopedTypeNameV10) -> Self { + RawScopedTypeNameV9 { + scope: raw.scope, + name: raw.source_name, + } + } +} + +impl From for RawIndexDefV9 { + fn from(raw: RawIndexDefV10) -> Self { + RawIndexDefV9 { + name: raw.source_name, + accessor_name: raw.accessor_name, + algorithm: raw.algorithm, + } + } +} + +impl From for RawConstraintDefV9 { + fn from(raw: RawConstraintDefV10) -> Self { + RawConstraintDefV9 { + name: raw.source_name, + data: raw.data, + } + } +} + +impl From for RawSequenceDefV9 { + fn from(raw: RawSequenceDefV10) -> Self { + RawSequenceDefV9 { + name: raw.source_name, + column: raw.column, + start: raw.start, + min_value: raw.min_value, + max_value: raw.max_value, + increment: raw.increment, + } + } +} diff --git a/crates/schema/src/def/validate/v10.rs b/crates/schema/src/def/validate/v10.rs index 7ff26131249..d0165697740 100644 --- a/crates/schema/src/def/validate/v10.rs +++ b/crates/schema/src/def/validate/v10.rs @@ -100,7 +100,7 @@ pub fn validate(def: RawModuleDefV10) -> Result { .into_iter() .flatten() .map(|ty| { - validator.core.validate_type_def(ty).map(|type_def| { + validator.core.validate_type_def(ty.into()).map(|type_def| { refmap.insert(type_def.ty, type_def.name.clone()); (type_def.name.clone(), type_def) }) @@ -212,7 +212,7 @@ struct ModuleValidatorV10<'a> { impl<'a> ModuleValidatorV10<'a> { fn validate_table_def(&mut self, table: RawTableDefV10) -> Result { let RawTableDefV10 { - name: raw_table_name, + source_name: raw_table_name, product_type_ref, primary_key, indexes, @@ -247,7 +247,7 @@ impl<'a> ModuleValidatorV10<'a> { .into_iter() .map(|index| { table_validator - .validate_index_def(index) + .validate_index_def(index.into()) .map(|index| (index.name.clone(), index)) }) .collect_all_errors::>(); @@ -256,7 +256,7 @@ impl<'a> ModuleValidatorV10<'a> { .into_iter() .map(|constraint| { table_validator - .validate_constraint_def(constraint) + .validate_constraint_def(constraint.into()) .map(|constraint| (constraint.name.clone(), constraint)) }) .collect_all_errors() @@ -288,7 +288,7 @@ impl<'a> ModuleValidatorV10<'a> { .into_iter() .map(|sequence| { table_validator - .validate_sequence_def(sequence) + .validate_sequence_def(sequence.into()) .map(|sequence| (sequence.name.clone(), sequence)) }) .collect_all_errors(); @@ -365,7 +365,7 @@ impl<'a> ModuleValidatorV10<'a> { fn validate_reducer_def(&mut self, reducer_def: RawReducerDefV10) -> Result { let RawReducerDefV10 { - name, + source_name, params, visibility, } = reducer_def; @@ -373,12 +373,12 @@ impl<'a> ModuleValidatorV10<'a> { let params_for_generate = self.core .params_for_generate(¶ms, |position, arg_name| TypeLocation::ReducerArg { - reducer_name: (&*name).into(), + reducer_name: (&*source_name).into(), position, arg_name, }); - let name_result = identifier(name); + let name_result = identifier(source_name); let (name_result, params_for_generate) = (name_result, params_for_generate).combine_errors()?; @@ -400,7 +400,7 @@ impl<'a> ModuleValidatorV10<'a> { tables: &HashMap, ) -> Result<(ScheduleDef, Box)> { let RawScheduleDefV10 { - name, + source_name, table_name, schedule_at_col, function_name, @@ -423,11 +423,11 @@ impl<'a> ModuleValidatorV10<'a> { ref_: table.product_type_ref, })?; - let name = name.unwrap_or_else(|| generate_schedule_name(&table_name)); + let source_name = source_name.unwrap_or_else(|| generate_schedule_name(&table_name)); self.core .validate_schedule_def( table_name.clone(), - identifier(name)?, + identifier(source_name)?, function_name, product_type, schedule_at_col, @@ -452,7 +452,7 @@ impl<'a> ModuleValidatorV10<'a> { fn validate_procedure_def(&mut self, procedure_def: RawProcedureDefV10) -> Result { let RawProcedureDefV10 { - name, + source_name, params, return_type, visibility, @@ -461,19 +461,19 @@ impl<'a> ModuleValidatorV10<'a> { let params_for_generate = self.core .params_for_generate(¶ms, |position, arg_name| TypeLocation::ProcedureArg { - procedure_name: Cow::Borrowed(&name), + procedure_name: Cow::Borrowed(&source_name), position, arg_name, }); let return_type_for_generate = self.core.validate_for_type_use( &TypeLocation::ProcedureReturn { - procedure_name: Cow::Borrowed(&name), + procedure_name: Cow::Borrowed(&source_name), }, &return_type, ); - let name_result = identifier(name); + let name_result = identifier(source_name); let (name_result, params_for_generate, return_type_for_generate) = (name_result, params_for_generate, return_type_for_generate).combine_errors()?; @@ -493,13 +493,14 @@ impl<'a> ModuleValidatorV10<'a> { fn validate_view_def(&mut self, view_def: RawViewDefV10) -> Result { let RawViewDefV10 { - name, + source_name, is_public, is_anonymous, params, return_type, index, } = view_def; + let name = source_name; let invalid_return_type = || { ValidationErrors::from(ValidationError::InvalidViewReturnType { @@ -1398,9 +1399,9 @@ mod tests { // Check if it works. let mut raw_def = builder.finish(); let tables = raw_def.tables_mut_for_tests(); - tables[0].constraints[0].name = Some("wacky.constraint()".into()); - tables[0].indexes[0].name = Some("wacky.index()".into()); - tables[0].sequences[0].name = Some("wacky.sequence()".into()); + tables[0].constraints[0].source_name = Some("wacky.constraint()".into()); + tables[0].indexes[0].source_name = Some("wacky.index()".into()); + tables[0].sequences[0].source_name = Some("wacky.sequence()".into()); let def: ModuleDef = raw_def.try_into().unwrap(); assert!(def.lookup::("wacky.constraint()").is_some()); From 24f7042c793e8591fa7d490346e184037ebf2743 Mon Sep 17 00:00:00 2001 From: Shubham Mishra Date: Wed, 28 Jan 2026 01:21:07 +0530 Subject: [PATCH 10/11] return type for reducer --- crates/lib/src/db/raw_def/v10.rs | 18 ++++++++++++++++++ crates/schema/src/def.rs | 6 ++++++ crates/schema/src/def/validate/v10.rs | 21 +++++++++++++++++++-- crates/schema/src/def/validate/v9.rs | 3 +++ crates/schema/src/error.rs | 6 ++++++ 5 files changed, 52 insertions(+), 2 deletions(-) diff --git a/crates/lib/src/db/raw_def/v10.rs b/crates/lib/src/db/raw_def/v10.rs index f80d228659d..e1e95c7f0c0 100644 --- a/crates/lib/src/db/raw_def/v10.rs +++ b/crates/lib/src/db/raw_def/v10.rs @@ -167,6 +167,12 @@ pub struct RawReducerDefV10 { /// Whether this reducer is callable from clients or is internal-only. pub visibility: FunctionVisibility, + + /// The type of the `Ok` return value. + pub ok_return_type: AlgebraicType, + + /// The type of the `Err` return value. + pub err_return_type: AlgebraicType, } /// The visibility of a function (reducer or procedure). @@ -768,6 +774,8 @@ impl RawModuleDefV10Builder { source_name: source_name.into(), params, visibility: FunctionVisibility::ClientCallable, + ok_return_type: reducer_default_ok_return_type(), + err_return_type: reducer_default_err_return_type(), }); } @@ -834,6 +842,8 @@ impl RawModuleDefV10Builder { source_name: function_name, params, visibility: FunctionVisibility::Internal, + ok_return_type: reducer_default_ok_return_type(), + err_return_type: reducer_default_err_return_type(), }); } @@ -943,6 +953,14 @@ impl TypespaceBuilder for RawModuleDefV10Builder { } } +pub fn reducer_default_ok_return_type() -> AlgebraicType { + AlgebraicType::unit() +} + +pub fn reducer_default_err_return_type() -> AlgebraicType { + AlgebraicType::String +} + /// Convert a string from a sats type-name annotation like `#[sats(name = "namespace.name")]` to a `RawScopedTypeNameV9`. /// We split the input on the strings `"::"` and `"."` to split up module paths. /// diff --git a/crates/schema/src/def.rs b/crates/schema/src/def.rs index 84a84d66307..55305f00bb0 100644 --- a/crates/schema/src/def.rs +++ b/crates/schema/src/def.rs @@ -1332,6 +1332,12 @@ pub struct ReducerDef { /// The visibility of this reducer. pub visibility: FunctionVisibility, + + /// The return type of the reducer on success. + pub ok_return_type: AlgebraicType, + + /// The return type of the reducer on error. + pub err_return_type: AlgebraicType, } impl From for RawReducerDefV9 { diff --git a/crates/schema/src/def/validate/v10.rs b/crates/schema/src/def/validate/v10.rs index d0165697740..63681a4d2f5 100644 --- a/crates/schema/src/def/validate/v10.rs +++ b/crates/schema/src/def/validate/v10.rs @@ -368,6 +368,8 @@ impl<'a> ModuleValidatorV10<'a> { source_name, params, visibility, + ok_return_type, + err_return_type, } = reducer_def; let params_for_generate = @@ -378,9 +380,22 @@ impl<'a> ModuleValidatorV10<'a> { arg_name, }); - let name_result = identifier(source_name); + let name_result = identifier(source_name.clone()); + + let return_res: Result<_> = (ok_return_type.is_unit() && err_return_type.is_string()) + .then_some((ok_return_type.clone(), err_return_type.clone())) + .ok_or_else(move || { + ValidationError::InvalidReducerReturnType { + reducer_name: source_name.clone(), + ok_type: ok_return_type.into(), + err_type: err_return_type.into(), + } + .into() + }); - let (name_result, params_for_generate) = (name_result, params_for_generate).combine_errors()?; + let (name_result, params_for_generate, return_res) = + (name_result, params_for_generate, return_res).combine_errors()?; + let (ok_return_type, err_return_type) = return_res; Ok(ReducerDef { name: name_result, @@ -391,6 +406,8 @@ impl<'a> ModuleValidatorV10<'a> { }, lifecycle: None, // V10 handles lifecycle separately visibility: visibility.into(), + ok_return_type, + err_return_type, }) } diff --git a/crates/schema/src/def/validate/v9.rs b/crates/schema/src/def/validate/v9.rs index e06d2ae3249..0574c699904 100644 --- a/crates/schema/src/def/validate/v9.rs +++ b/crates/schema/src/def/validate/v9.rs @@ -5,6 +5,7 @@ use crate::{def::validate::Result, error::TypeLocation}; use spacetimedb_data_structures::error_stream::{CollectAllErrors, CombineErrors}; use spacetimedb_data_structures::map::HashSet; use spacetimedb_lib::db::default_element_ordering::{product_type_has_default_ordering, sum_type_has_default_ordering}; +use spacetimedb_lib::db::raw_def::v10::{reducer_default_err_return_type, reducer_default_ok_return_type}; use spacetimedb_lib::db::raw_def::v9::RawViewDefV9; use spacetimedb_lib::ProductType; use spacetimedb_primitives::col_list; @@ -363,6 +364,8 @@ impl ModuleValidatorV9<'_> { }, lifecycle, visibility: FunctionVisibility::ClientCallable, + ok_return_type: reducer_default_ok_return_type(), + err_return_type: reducer_default_err_return_type(), }) } diff --git a/crates/schema/src/error.rs b/crates/schema/src/error.rs index d9bc7aaaa74..cc57f0e4d3b 100644 --- a/crates/schema/src/error.rs +++ b/crates/schema/src/error.rs @@ -147,6 +147,12 @@ pub enum ValidationError { table_name: Box, schedule_name: Box, }, + #[error("reducer {reducer_name} has invalid return type: found Result<{ok_type}, {err_type}>")] + InvalidReducerReturnType { + reducer_name: RawIdentifier, + ok_type: PrettyAlgebraicType, + err_type: PrettyAlgebraicType, + }, } /// A wrapper around an `AlgebraicType` that implements `fmt::Display`. From 309d7059708f0842767e6ce99afa152e6079f479 Mon Sep 17 00:00:00 2001 From: Shubham Mishra Date: Wed, 28 Jan 2026 17:09:00 +0530 Subject: [PATCH 11/11] fmt --- crates/schema/src/def/validate/v10.rs | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/crates/schema/src/def/validate/v10.rs b/crates/schema/src/def/validate/v10.rs index d2184627ee3..af0ea1d6673 100644 --- a/crates/schema/src/def/validate/v10.rs +++ b/crates/schema/src/def/validate/v10.rs @@ -46,10 +46,7 @@ pub fn validate(def: RawModuleDefV10) -> Result { .cloned() .into_iter() .flatten() - .map(|reducer| { - validator - .validate_reducer_def(reducer) - }) + .map(|reducer| validator.validate_reducer_def(reducer)) // Collect into a `Vec` first to preserve duplicate names. // Later on, in `check_function_names_are_unique`, we'll transform this into an `IndexMap`. .collect_all_errors::>(); @@ -407,7 +404,8 @@ impl<'a> ModuleValidatorV10<'a> { visibility: visibility.into(), ok_return_type, err_return_type, - }).map(|reducer_def| (name_result, reducer_def)) + }) + .map(|reducer_def| (name_result, reducer_def)) } fn validate_schedule_def( @@ -910,22 +908,22 @@ mod tests { assert_eq!(def.types[&deliveries_type_name].ty, delivery_def.product_type_ref); let init_name = expect_identifier("init"); - assert_eq!(def.reducers[&init_name].name, init_name); + assert_eq!(&*def.reducers[&init_name].name, &*init_name); assert_eq!(def.reducers[&init_name].lifecycle, Some(Lifecycle::Init)); let on_connect_name = expect_identifier("on_connect"); - assert_eq!(def.reducers[&on_connect_name].name, on_connect_name); + assert_eq!(&*def.reducers[&on_connect_name].name, &*on_connect_name); assert_eq!(def.reducers[&on_connect_name].lifecycle, Some(Lifecycle::OnConnect)); let on_disconnect_name = expect_identifier("on_disconnect"); - assert_eq!(def.reducers[&on_disconnect_name].name, on_disconnect_name); + assert_eq!(&*def.reducers[&on_disconnect_name].name, &*on_disconnect_name); assert_eq!( def.reducers[&on_disconnect_name].lifecycle, Some(Lifecycle::OnDisconnect) ); let extra_reducer_name = expect_identifier("extra_reducer"); - assert_eq!(def.reducers[&extra_reducer_name].name, extra_reducer_name); + assert_eq!(&*def.reducers[&extra_reducer_name].name, &*extra_reducer_name); assert_eq!(def.reducers[&extra_reducer_name].lifecycle, None); assert_eq!( def.reducers[&extra_reducer_name].params, @@ -933,7 +931,7 @@ mod tests { ); let check_deliveries_name = expect_identifier("check_deliveries"); - assert_eq!(def.reducers[&check_deliveries_name].name, check_deliveries_name); + assert_eq!(&*def.reducers[&check_deliveries_name].name, &*check_deliveries_name); assert_eq!(def.reducers[&check_deliveries_name].lifecycle, None); assert_eq!( def.reducers[&check_deliveries_name].params,