From 423796c8e1f59e9717efb661b1f228f6ffcee42d Mon Sep 17 00:00:00 2001 From: Phoebe Goldman Date: Thu, 9 Oct 2025 11:36:47 -0400 Subject: [PATCH 01/25] ModuleDef and schema changes to support procedures This commit extends various schema and schema-adjacent structures to describe procedures, a new kind of database function which are allowed to perform side effects. This includes extending `RawModuleDefV9` with a way to register `RawProcedureDefV9`s in the `misc_exports`, preserving compatibility with modules that predate procedures. The module validation path is reorganized somewhat to validate various properties related to procedures while preserving code clarity and maintainability. Additionally, the `ArgsTuple` machinery for ser/de-ing reducer arguments using the argument type as a seed is extended to also support procedure arguments. All of this is currently unused. --- crates/core/src/host/mod.rs | 33 ++- .../src/locking_tx_datastore/datastore.rs | 2 +- .../src/locking_tx_datastore/mut_tx.rs | 2 +- crates/datastore/src/system_tables.rs | 7 +- crates/lib/src/db/raw_def/v9.rs | 74 +++++- crates/primitives/src/ids.rs | 32 +++ crates/primitives/src/lib.rs | 2 +- crates/schema/src/auto_migrate/formatter.rs | 11 +- .../src/auto_migrate/termcolor_formatter.rs | 4 +- crates/schema/src/def.rs | 121 ++++++++- crates/schema/src/def/deserialize.rs | 117 ++++++--- crates/schema/src/def/validate/v8.rs | 4 +- crates/schema/src/def/validate/v9.rs | 244 +++++++++++++----- crates/schema/src/error.rs | 49 +++- crates/schema/src/schema.rs | 14 +- 15 files changed, 571 insertions(+), 145 deletions(-) diff --git a/crates/core/src/host/mod.rs b/crates/core/src/host/mod.rs index 05538542bbe..66d58ea8d3e 100644 --- a/crates/core/src/host/mod.rs +++ b/crates/core/src/host/mod.rs @@ -6,9 +6,8 @@ use enum_map::Enum; use once_cell::sync::OnceCell; use spacetimedb_lib::bsatn; use spacetimedb_lib::de::serde::SeedWrapper; -use spacetimedb_lib::de::DeserializeSeed; use spacetimedb_lib::ProductValue; -use spacetimedb_schema::def::deserialize::ReducerArgsDeserializeSeed; +use spacetimedb_schema::def::deserialize::{ArgsSeed, ProcedureArgsDeserializeSeed, ReducerArgsDeserializeSeed}; mod disk_storage; mod host_controller; @@ -31,6 +30,9 @@ pub use host_controller::{ pub use module_host::{ModuleHost, NoSuchModule, ReducerCallError, UpdateDatabaseResult}; pub use scheduler::Scheduler; +/// Encoded arguments to a database function. +/// +/// Despite the name, this may be arguments to either a reducer or a procedure. #[derive(Debug)] pub enum ReducerArgs { Json(ByteString), @@ -39,13 +41,23 @@ pub enum ReducerArgs { } impl ReducerArgs { + #[allow(unused)] + fn into_tuple_for_procedure( + self, + seed: ProcedureArgsDeserializeSeed, + ) -> Result { + self._into_tuple(seed).map_err(|err| InvalidProcedureArguments { + err, + procedure: (*seed.inner_def().name).into(), + }) + } fn into_tuple(self, seed: ReducerArgsDeserializeSeed) -> Result { self._into_tuple(seed).map_err(|err| InvalidReducerArguments { err, - reducer: (*seed.reducer_def().name).into(), + reducer: (*seed.inner_def().name).into(), }) } - fn _into_tuple(self, seed: ReducerArgsDeserializeSeed) -> anyhow::Result { + fn _into_tuple(self, seed: impl ArgsSeed) -> anyhow::Result { Ok(match self { ReducerArgs::Json(json) => ArgsTuple { tuple: from_json_seed(&json, SeedWrapper(seed))?, @@ -58,10 +70,7 @@ impl ReducerArgs { json: OnceCell::new(), }, ReducerArgs::Nullary => { - anyhow::ensure!( - seed.reducer_def().params.elements.is_empty(), - "failed to typecheck args" - ); + anyhow::ensure!(seed.params().elements.is_empty(), "failed to typecheck args"); ArgsTuple::nullary() } }) @@ -114,6 +123,14 @@ pub struct InvalidReducerArguments { reducer: Box, } +#[derive(thiserror::Error, Debug)] +#[error("invalid arguments for procedure {procedure}: {err}")] +pub struct InvalidProcedureArguments { + #[source] + err: anyhow::Error, + procedure: Box, +} + fn from_json_seed<'de, T: serde::de::DeserializeSeed<'de>>(s: &'de str, seed: T) -> anyhow::Result { let mut de = serde_json::Deserializer::from_str(s); let mut track = serde_path_to_error::Track::new(); diff --git a/crates/datastore/src/locking_tx_datastore/datastore.rs b/crates/datastore/src/locking_tx_datastore/datastore.rs index 6ebaa2fc275..99de3312237 100644 --- a/crates/datastore/src/locking_tx_datastore/datastore.rs +++ b/crates/datastore/src/locking_tx_datastore/datastore.rs @@ -3313,7 +3313,7 @@ mod tests { table_id: TableId::SENTINEL, schedule_id: ScheduleId::SENTINEL, schedule_name: "schedule".into(), - reducer_name: "reducer".into(), + function_name: "reducer".into(), at_column: 1.into(), }; let sum_ty = AlgebraicType::sum([("foo", AlgebraicType::Bool), ("bar", AlgebraicType::U16)]); diff --git a/crates/datastore/src/locking_tx_datastore/mut_tx.rs b/crates/datastore/src/locking_tx_datastore/mut_tx.rs index 06889c0ed5c..aee33e34d41 100644 --- a/crates/datastore/src/locking_tx_datastore/mut_tx.rs +++ b/crates/datastore/src/locking_tx_datastore/mut_tx.rs @@ -244,7 +244,7 @@ impl MutTxId { table_id: schedule.table_id, schedule_id: schedule.schedule_id, schedule_name: schedule.schedule_name, - reducer_name: schedule.reducer_name, + reducer_name: schedule.function_name, at_column: schedule.at_column, }; let id = self diff --git a/crates/datastore/src/system_tables.rs b/crates/datastore/src/system_tables.rs index 1ecd69df51f..f5515e379b8 100644 --- a/crates/datastore/src/system_tables.rs +++ b/crates/datastore/src/system_tables.rs @@ -1232,6 +1232,11 @@ impl TryFrom> for StVarRow { pub struct StScheduledRow { pub(crate) schedule_id: ScheduleId, pub(crate) table_id: TableId, + /// The name of the reducer or procedure which will run when this table's rows reach their execution time. + /// + /// Note that, despite the column name, this may refer to either a reducer or a procedure. + /// We cannot change the schema of existing system tables, + /// so we are unable to rename this column. pub(crate) reducer_name: Box, pub(crate) schedule_name: Box, pub(crate) at_column: ColId, @@ -1254,7 +1259,7 @@ impl From for ScheduleSchema { fn from(row: StScheduledRow) -> Self { Self { table_id: row.table_id, - reducer_name: row.reducer_name, + function_name: row.reducer_name, schedule_id: row.schedule_id, schedule_name: row.schedule_name, at_column: row.at_column, diff --git a/crates/lib/src/db/raw_def/v9.rs b/crates/lib/src/db/raw_def/v9.rs index 56518336e5d..c9f886a3a34 100644 --- a/crates/lib/src/db/raw_def/v9.rs +++ b/crates/lib/src/db/raw_def/v9.rs @@ -82,9 +82,16 @@ pub struct RawModuleDefV9 { pub types: Vec, /// Miscellaneous additional module exports. + /// + /// The enum `RawMiscModuleExportV9` can have new variants added + /// without breaking existing compiled modules. + /// As such, this acts as a sort of dumping ground for any exports added after we defined `RawModuleDefV9`. + /// + /// If/when we define `RawModuleDefV10`, these should be moved out of `misc_exports` and into their own fields, + /// and the new `misc_exports` should once again be initially empty. pub misc_exports: Vec, - /// Low level security definitions. + /// Row level security definitions. /// /// Each definition must have a unique name. pub row_level_security: Vec, @@ -294,7 +301,7 @@ pub fn direct(col: impl Into) -> RawIndexAlgorithm { RawIndexAlgorithm::Direct { column: col.into() } } -/// Marks a table as a timer table for a scheduled reducer. +/// Marks a table as a timer table for a scheduled reducer or procedure. /// /// The table must have columns: /// - `scheduled_id` of type `u64`. @@ -307,7 +314,9 @@ pub struct RawScheduleDefV9 { /// Even though there is ABSOLUTELY NO REASON TO. pub name: Option>, - /// The name of the reducer to call. + /// The name of the reducer or procedure to call. + /// + /// Despite the field name here, this may be either a reducer or a procedure. pub reducer_name: RawIdentifier, /// The column of the `scheduled_at` field of this scheduled table. @@ -358,12 +367,18 @@ pub struct RawRowLevelSecurityDefV9 { } /// A miscellaneous module export. +/// +/// All of the variants here were added after the format of [`RawModuleDefV9`] was already stabilized. +/// If/when we define `RawModuleDefV10`, these should allbe moved out of `misc_exports` and into their own fields. #[derive(Debug, Clone, SpacetimeType)] #[sats(crate = crate)] #[cfg_attr(feature = "test", derive(PartialEq, Eq, PartialOrd, Ord))] #[non_exhaustive] pub enum RawMiscModuleExportV9 { + /// A default value for a column added during a supervised automigration. ColumnDefaultValue(RawColumnDefaultValueV9), + /// A procedure definition. + Procedure(RawProcedureDefV9), } /// Marks a particular table's column as having a particular default. @@ -459,6 +474,27 @@ pub enum Lifecycle { OnDisconnect, } +/// A procedure definition. +/// +/// Will be wrapped in [`RawMiscModuleExportV9`] and included in the [`RawModuleDefV9`]'s `misc_exports` vec. +#[derive(Debug, Clone, SpacetimeType)] +#[sats(crate = crate)] +#[cfg_attr(feature = "test", derive(PartialEq, Eq, PartialOrd, Ord))] +pub struct RawProcedureDefV9 { + /// 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, +} + /// A builder for a [`RawModuleDefV9`]. #[derive(Default)] pub struct RawModuleDefV9Builder { @@ -631,6 +667,31 @@ impl RawModuleDefV9Builder { }); } + /// 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: spacetimedb_sats::ProductType, + return_type: spacetimedb_sats::AlgebraicType, + ) { + self.module + .misc_exports + .push(RawMiscModuleExportV9::Procedure(RawProcedureDefV9 { + name: name.into(), + params, + return_type, + })) + } + /// Add a row-level security policy to the module. /// /// The `sql` expression should be a valid SQL expression that will be used to filter rows. @@ -796,13 +857,16 @@ impl RawTableDefBuilder<'_> { /// Adds a schedule definition to the table. /// + /// The `function_name` should name a reducer or procedure + /// which accepts one argument, a row of this table. + /// /// The table must have the appropriate columns for a scheduled table. pub fn with_schedule( mut self, - reducer_name: impl Into, + function_name: impl Into, scheduled_at_column: impl Into, ) -> Self { - let reducer_name = reducer_name.into(); + let reducer_name = function_name.into(); let scheduled_at_column = scheduled_at_column.into(); self.table.schedule = Some(RawScheduleDefV9 { name: None, diff --git a/crates/primitives/src/ids.rs b/crates/primitives/src/ids.rs index fc9a9b69e61..afe5325aba1 100644 --- a/crates/primitives/src/ids.rs +++ b/crates/primitives/src/ids.rs @@ -116,3 +116,35 @@ system_id! { // This is never stored in a system table, but is useful to have defined here. pub struct ReducerId(pub u32); } + +system_id! { + /// The index of a procedure as defined in a module's procedure list. + // This is never stored in a system table, but is useful to have defined here. + pub struct ProcedureId(pub u32); +} + +/// An id for a function exported from a module, which may be a reducer or a procedure. +// This is never stored in a system table, +// but is useful to have defined here to provide a shared language for downstream crates. +#[derive(Clone, Copy, Debug)] +pub enum FunctionId { + Reducer(ReducerId), + Procedure(ProcedureId), +} + +impl FunctionId { + pub fn as_reducer(self) -> Option { + if let Self::Reducer(id) = self { + Some(id) + } else { + None + } + } + pub fn as_procedure(self) -> Option { + if let Self::Procedure(id) = self { + Some(id) + } else { + None + } + } +} diff --git a/crates/primitives/src/lib.rs b/crates/primitives/src/lib.rs index 7ae37765514..d88e541f195 100644 --- a/crates/primitives/src/lib.rs +++ b/crates/primitives/src/lib.rs @@ -7,7 +7,7 @@ mod ids; pub use attr::{AttributeKind, ColumnAttribute, ConstraintKind, Constraints}; pub use col_list::{ColList, ColOrCols, ColSet}; -pub use ids::{ColId, ConstraintId, IndexId, ReducerId, ScheduleId, SequenceId, TableId}; +pub use ids::{ColId, ConstraintId, FunctionId, IndexId, ProcedureId, ReducerId, ScheduleId, SequenceId, TableId}; /// The minimum size of a chunk yielded by a wasm abi RowIter. pub const ROW_ITER_CHUNK_SIZE: usize = 32 * 1024; diff --git a/crates/schema/src/auto_migrate/formatter.rs b/crates/schema/src/auto_migrate/formatter.rs index 54b4ed87ce3..abdf443a9df 100644 --- a/crates/schema/src/auto_migrate/formatter.rs +++ b/crates/schema/src/auto_migrate/formatter.rs @@ -5,7 +5,7 @@ use std::io; use super::{AutoMigratePlan, IndexAlgorithm, ModuleDefLookup, TableDef}; use crate::{ auto_migrate::AutoMigrateStep, - def::{ConstraintData, ModuleDef, ScheduleDef}, + def::{ConstraintData, FunctionKind, ModuleDef, ScheduleDef}, identifier::Identifier, }; use itertools::Itertools; @@ -188,7 +188,8 @@ pub struct AccessChangeInfo { #[derive(Debug, Clone, PartialEq)] pub struct ScheduleInfo { pub table_name: String, - pub reducer_name: Identifier, + pub function_name: Identifier, + pub function_kind: FunctionKind, } #[derive(Debug, Clone, PartialEq)] @@ -314,7 +315,8 @@ fn extract_table_info( let schedule = table_def.schedule.as_ref().map(|schedule| ScheduleInfo { table_name: table_def.name.to_string().clone(), - reducer_name: schedule.reducer_name.clone(), + function_name: schedule.function_name.clone(), + function_kind: schedule.function_kind, }); Ok(TableInfo { @@ -438,7 +440,8 @@ fn extract_schedule_info( Ok(ScheduleInfo { table_name: schedule_def.name.to_string().clone(), - reducer_name: schedule_def.reducer_name.clone(), + function_name: schedule_def.function_name.clone(), + function_kind: schedule_def.function_kind, }) } diff --git a/crates/schema/src/auto_migrate/termcolor_formatter.rs b/crates/schema/src/auto_migrate/termcolor_formatter.rs index 85648e3c244..c5c05f7993a 100644 --- a/crates/schema/src/auto_migrate/termcolor_formatter.rs +++ b/crates/schema/src/auto_migrate/termcolor_formatter.rs @@ -219,7 +219,7 @@ impl MigrationFormatter for TermColorFormatter { if let Some(s) = &table.schedule { self.write_colored_line("Schedule:", Some(self.colors.section_header), true)?; self.indent(); - self.write_bullet(&format!("Calls reducer: {}", s.reducer_name))?; + self.write_bullet(&format!("Calls {}: {}", s.function_kind, s.function_name))?; self.dedent(); } @@ -276,7 +276,7 @@ impl MigrationFormatter for TermColorFormatter { self.buffer.write_all(b" schedule for table ")?; self.write_colored(&s.table_name, Some(self.colors.table_name), true)?; self.buffer - .write_all(format!(" calling reducer {}\n", s.reducer_name).as_bytes()) + .write_all(format!(" calling {} {}\n", s.function_kind, s.function_name).as_bytes()) } fn format_rls(&mut self, r: &RlsInfo, action: Action) -> io::Result<()> { diff --git a/crates/schema/src/def.rs b/crates/schema/src/def.rs index 1461dcca076..c5342f9d23a 100644 --- a/crates/schema/src/def.rs +++ b/crates/schema/src/def.rs @@ -33,12 +33,12 @@ use spacetimedb_data_structures::map::HashMap; use spacetimedb_lib::db::raw_def; use spacetimedb_lib::db::raw_def::v9::{ Lifecycle, RawColumnDefaultValueV9, RawConstraintDataV9, RawConstraintDefV9, RawIdentifier, RawIndexAlgorithm, - RawIndexDefV9, RawMiscModuleExportV9, RawModuleDefV9, RawReducerDefV9, RawRowLevelSecurityDefV9, RawScheduleDefV9, - RawScopedTypeNameV9, RawSequenceDefV9, RawSql, RawTableDefV9, RawTypeDefV9, RawUniqueConstraintDataV9, TableAccess, - TableType, + RawIndexDefV9, RawMiscModuleExportV9, RawModuleDefV9, RawProcedureDefV9, RawReducerDefV9, RawRowLevelSecurityDefV9, + RawScheduleDefV9, RawScopedTypeNameV9, RawSequenceDefV9, RawSql, RawTableDefV9, RawTypeDefV9, + RawUniqueConstraintDataV9, TableAccess, TableType, }; use spacetimedb_lib::{ProductType, RawModuleDef}; -use spacetimedb_primitives::{ColId, ColList, ColOrCols, ColSet, ReducerId, TableId}; +use spacetimedb_primitives::{ColId, ColList, ColOrCols, ColSet, ProcedureId, ReducerId, TableId}; use spacetimedb_sats::{AlgebraicType, AlgebraicValue}; use spacetimedb_sats::{AlgebraicTypeRef, Typespace}; @@ -103,6 +103,12 @@ pub struct ModuleDef { /// and must be preserved for future calls to `__call_reducer__`. reducers: IndexMap, + /// The procedures of the module definition. + /// + /// Like `reducers`, this uses [`IndexMap`] to preserve order + /// so that `__call_procedure__` receives stable integer IDs. + procedures: IndexMap, + /// A map from lifecycle reducer kind to reducer id. lifecycle_reducers: EnumMap>, @@ -161,6 +167,11 @@ impl ModuleDef { self.reducers.values() } + /// The procedures of the module definition. + pub fn procedures(&self) -> impl Iterator { + self.procedures.values() + } + /// The type definitions of the module definition. pub fn types(&self) -> impl Iterator { self.types.values() @@ -243,6 +254,23 @@ impl ModuleDef { self.reducers.get_index(id.idx()).map(|(_, def)| def) } + /// Convenience method to look up a procedure, possibly by a string, returning its id as well. + pub fn procedure_full>( + &self, + name: &K, + ) -> Option<(ProcedureId, &ProcedureDef)> { + // If the string IS a valid identifier, we can just look it up. + self.procedures.get_full(name).map(|(idx, _, def)| (idx.into(), def)) + } + + pub fn procedure_by_id(&self, id: ProcedureId) -> &ProcedureDef { + &self.procedures[id.idx()] + } + + pub fn get_procedure_by_id(&self, id: ProcedureId) -> Option<&ProcedureDef> { + self.procedures.get_index(id.idx()).map(|(_, def)| def) + } + /// Looks up a lifecycle reducer defined in the module. pub fn lifecycle_reducer(&self, lifecycle: Lifecycle) -> Option<(ReducerId, &ReducerDef)> { self.lifecycle_reducers[lifecycle].map(|i| (i, &self.reducers[i.idx()])) @@ -342,13 +370,18 @@ impl From for RawModuleDefV9 { typespace_for_generate: _, refmap: _, row_level_security_raw, + procedures, } = val; RawModuleDefV9 { tables: to_raw(tables), reducers: reducers.into_iter().map(|(_, def)| def.into()).collect(), types: to_raw(types), - misc_exports: vec![], + // TODO: Do we need to include default values here? + misc_exports: procedures + .into_iter() + .map(|(_, def)| RawMiscModuleExportV9::Procedure(def.into())) + .collect(), typespace, row_level_security: row_level_security_raw.into_iter().map(|(_, def)| def).collect(), } @@ -745,7 +778,30 @@ impl From for RawRowLevelSecurityDefV9 { } } -/// Marks a table as a timer table for a scheduled reducer. +#[derive(Copy, Clone, Eq, PartialEq, Debug, Ord, PartialOrd)] +pub enum FunctionKind { + /// Functions which have not yet been determined to be reducers or procedures. + /// + /// Used as a placeholder during module validation, + /// when pre-processing [`ScheduleDef`]s prior to validating their scheduled functions. + /// Will never appear in a fully-validated [`ModuleDef`], + /// and should not be placed in errors either. + Unknown, + Reducer, + Procedure, +} + +impl fmt::Display for FunctionKind { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str(match self { + FunctionKind::Unknown => "exported function", + FunctionKind::Reducer => "reducer", + FunctionKind::Procedure => "procedure", + }) + } +} + +/// Marks a table as a timer table for a scheduled reducer or procedure. #[derive(Debug, Clone, Eq, PartialEq)] #[non_exhaustive] pub struct ScheduleDef { @@ -762,16 +818,18 @@ pub struct ScheduleDef { /// Must be named `scheduled_id` and be of type `u64`. pub id_column: ColId, - /// The name of the reducer to call. Not yet an `Identifier` because - /// reducer names are not currently validated. - pub reducer_name: Identifier, + /// The name of the reducer or procedure to call. + pub function_name: Identifier, + + /// Whether the `function_name` refers to a reducer or a procedure. + pub function_kind: FunctionKind, } impl From for RawScheduleDefV9 { fn from(val: ScheduleDef) -> Self { RawScheduleDefV9 { name: Some(val.name), - reducer_name: val.reducer_name.into(), + reducer_name: val.function_name.into(), scheduled_at_column: val.at_column, } } @@ -902,7 +960,7 @@ impl From for RawScopedTypeNameV9 { #[derive(Debug, Clone, Eq, PartialEq)] #[non_exhaustive] pub struct ReducerDef { - /// The name of the reducer. This must be unique within the module. + /// The name of the reducer. This must be unique within the module's set of reducers and procedures. pub name: Identifier, /// The parameters of the reducer. @@ -929,6 +987,47 @@ impl From for RawReducerDefV9 { } } +#[derive(Debug, Clone, Eq, PartialEq)] +#[non_exhaustive] +pub struct ProcedureDef { + /// The name of the procedure. + /// + /// This must be unique within the module's set of reducers and procedures. + pub name: Identifier, + + /// The parameters of the procedure. + /// + /// This `ProductType` need not be registered in the module's `Typespace`. + pub params: ProductType, + + /// The parameters of the procedure, formatted for client codegen. + /// + /// This `ProductType` need not be registered in the module's `TypespaceForGenerate`. + pub params_for_generate: ProductTypeDef, + + /// The return type of the procedure. + /// + /// If this is a non-special compound type, it should be registered in the module's `Typespace` + /// and indirected through an [`AlgebraicType::Ref`]. + pub return_type: AlgebraicType, + + /// The return type of the procedure. + /// + /// 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, +} + +impl From for RawProcedureDefV9 { + fn from(val: ProcedureDef) -> Self { + RawProcedureDefV9 { + name: val.name.into(), + params: val.params, + return_type: val.return_type, + } + } +} + impl ModuleDefLookup for TableDef { type Key<'a> = &'a Identifier; diff --git a/crates/schema/src/def/deserialize.rs b/crates/schema/src/def/deserialize.rs index a5a14240584..02d3768ab38 100644 --- a/crates/schema/src/def/deserialize.rs +++ b/crates/schema/src/def/deserialize.rs @@ -1,46 +1,93 @@ //! Helpers to allow deserializing data using a ReducerDef. -use crate::def::ReducerDef; -use spacetimedb_lib::sats::{self, de, ProductValue}; - -/// Wrapper around a `ReducerDef` that allows deserializing to a `ProductValue` at the type -/// of the reducer's parameter `ProductType`. -#[derive(Clone, Copy)] -pub struct ReducerArgsDeserializeSeed<'a>(pub sats::WithTypespace<'a, ReducerDef>); - -impl<'a> ReducerArgsDeserializeSeed<'a> { - /// Get the reducer def of this seed. - pub fn reducer_def(&self) -> &'a ReducerDef { - self.0.ty() - } +use crate::def::{ProcedureDef, ReducerDef}; +use spacetimedb_lib::{ + sats::{self, de, impl_serialize, ser, ProductValue}, + ProductType, +}; + +pub trait ArgsSeed: for<'de> de::DeserializeSeed<'de, Output = ProductValue> { + fn params(&self) -> &ProductType; } -impl<'de> de::DeserializeSeed<'de> for ReducerArgsDeserializeSeed<'_> { - type Output = ProductValue; +/// Define `struct_name` as a newtype wrapper around [`WithTypespace`] of `inner_ty`, +/// and implement [`de::DeserializeSeed`] and [`de::ProductVisitor`] for that newtype. +/// +/// `ReducerArgs` (defined in the spacetimedb_core crate) will use this type +/// to deserialize the arguments to a reducer or procedure +/// at the appropriate type for that specific function, which is known only at runtime. +macro_rules! define_args_deserialize_seed { + ($struct_vis:vis struct $struct_name:ident($field_vis:vis $inner_ty:ty)) => { + #[doc = concat!( + "Wrapper around a [`", + stringify!($inner_ty), + "`] that allows deserializing to a [`ProductValue`] at the type of the def's parameter `ProductType`." + )] + #[derive(Clone, Copy)] + $struct_vis struct $struct_name<'a>($field_vis sats::WithTypespace<'a, $inner_ty> ); - fn deserialize>(self, deserializer: D) -> Result { - deserializer.deserialize_product(self) - } -} + impl<'a> $struct_name<'a> { + #[doc = concat!( + "Get the inner [`", + stringify!($inner_ty), + "`] of this seed." + )] + $struct_vis fn inner_def(&self) -> &'a $inner_ty { + self.0.ty() + } + } -impl<'de> de::ProductVisitor<'de> for ReducerArgsDeserializeSeed<'_> { - type Output = ProductValue; + impl<'de> de::DeserializeSeed<'de> for $struct_name<'_> { + type Output = ProductValue; - fn product_name(&self) -> Option<&str> { - Some(&self.0.ty().name) - } - fn product_len(&self) -> usize { - self.0.ty().params.elements.len() - } - fn product_kind(&self) -> de::ProductKind { - de::ProductKind::ReducerArgs - } + fn deserialize>(self, deserializer: D) -> Result { + deserializer.deserialize_product(self) + } + } - fn visit_seq_product>(self, tup: A) -> Result { - de::visit_seq_product(self.0.map(|r| &*r.params.elements), &self, tup) - } + impl<'de> de::ProductVisitor<'de> for $struct_name<'_> { + type Output = ProductValue; - fn visit_named_product>(self, tup: A) -> Result { - de::visit_named_product(self.0.map(|r| &*r.params.elements), &self, tup) + fn product_name(&self) -> Option<&str> { + Some(&self.0.ty().name) + } + fn product_len(&self) -> usize { + self.0.ty().params.elements.len() + } + fn product_kind(&self) -> de::ProductKind { + de::ProductKind::ReducerArgs + } + + fn visit_seq_product>(self, tup: A) -> Result { + de::visit_seq_product(self.0.map(|r| &*r.params.elements), &self, tup) + } + + fn visit_named_product>(self, tup: A) -> Result { + de::visit_named_product(self.0.map(|r| &*r.params.elements), &self, tup) + } + } + + impl<'a> ArgsSeed for $struct_name<'a> { + fn params(&self) -> &ProductType { + &self.0.ty().params + } + } } } + +define_args_deserialize_seed!(pub struct ReducerArgsDeserializeSeed(pub ReducerDef)); +define_args_deserialize_seed!(pub struct ProcedureArgsDeserializeSeed(pub ProcedureDef)); + +pub struct ReducerArgsWithSchema<'a> { + value: &'a ProductValue, + ty: sats::WithTypespace<'a, ReducerDef>, +} +impl_serialize!([] ReducerArgsWithSchema<'_>, (self, ser) => { + use itertools::Itertools; + use ser::SerializeSeqProduct; + let mut seq = ser.serialize_seq_product(self.value.elements.len())?; + for (value, elem) in self.value.elements.iter().zip_eq(&*self.ty.ty().params.elements) { + seq.serialize_element(&self.ty.with(&elem.algebraic_type).with_value(value))?; + } + seq.end() +}); diff --git a/crates/schema/src/def/validate/v8.rs b/crates/schema/src/def/validate/v8.rs index 89a853a995e..347e1f47e07 100644 --- a/crates/schema/src/def/validate/v8.rs +++ b/crates/schema/src/def/validate/v8.rs @@ -57,6 +57,8 @@ fn upgrade_module(def: RawModuleDefV8, extra_errors: &mut Vec) tables, reducers, types, + // V8 module defs don't have procedures or column default values, + // which are all we use the `misc_exports` for at this time (pgoldman 2025-10-09). misc_exports: Default::default(), row_level_security: vec![], // v8 doesn't have row-level security } @@ -526,7 +528,7 @@ mod tests { 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().reducer_name[..], + &delivery_def.schedule.as_ref().unwrap().function_name[..], "check_deliveries" ); assert_eq!(delivery_def.primary_key, Some(ColId(2))); diff --git a/crates/schema/src/def/validate/v9.rs b/crates/schema/src/def/validate/v9.rs index 6adbbf01b9a..20bbea6c278 100644 --- a/crates/schema/src/def/validate/v9.rs +++ b/crates/schema/src/def/validate/v9.rs @@ -8,6 +8,7 @@ use spacetimedb_lib::db::default_element_ordering::{product_type_has_default_ord use spacetimedb_lib::ProductType; use spacetimedb_primitives::col_list; use spacetimedb_sats::{bsatn::de::Deserializer, de::DeserializeSeed, WithTypespace}; +use std::borrow::Cow; /// Validate a `RawModuleDefV9` and convert it into a `ModuleDef`, /// or return a stream of errors if the definition is invalid. @@ -51,6 +52,28 @@ pub fn validate(def: RawModuleDefV9) -> Result { }) .collect_all_errors(); + let (procedures, non_procedure_misc_exports) = + misc_exports + .into_iter() + .partition::, _>(|misc_export| { + matches!(misc_export, RawMiscModuleExportV9::Procedure(_)) + }); + + let procedures = procedures + .into_iter() + .map(|procedure| { + let RawMiscModuleExportV9::Procedure(procedure) = procedure else { + unreachable!("Already partitioned procedures separate from other `RawMiscModuleExportV9` variants"); + }; + procedure + }) + .map(|procedure| { + validator + .validate_procedure_def(procedure) + .map(|procedure_def| (procedure_def.name.clone(), procedure_def)) + }) + .collect_all_errors(); + let tables = tables .into_iter() .map(|table| { @@ -76,15 +99,19 @@ pub fn validate(def: RawModuleDefV9) -> Result { }) .collect_all_errors::>(); - let tables_types_reducers = (tables, types, reducers) - .combine_errors() - .and_then(|(mut tables, types, reducers)| { - let sched_exists = check_scheduled_reducers_exist(&tables, &reducers); - let default_values_work = proccess_misc_exports(misc_exports, &validator, &mut tables); - (sched_exists, default_values_work).combine_errors()?; - - Ok((tables, types, reducers)) - }); + let tables_types_reducers_procedures = + (tables, types, reducers, procedures) + .combine_errors() + .and_then(|(mut tables, types, reducers, procedures)| { + ( + check_scheduled_functions_exist(&mut tables, &reducers, &procedures), + check_non_procedure_misc_exports(non_procedure_misc_exports, &validator, &mut tables), + check_function_names_are_unique(&reducers, &procedures), + ) + .combine_errors()?; + + Ok((tables, types, reducers, procedures)) + }); let ModuleValidator { stored_in_table_def, @@ -93,7 +120,8 @@ pub fn validate(def: RawModuleDefV9) -> Result { .. } = validator; - let (tables, types, reducers) = (tables_types_reducers).map_err(|errors| errors.sort_deduplicate())?; + let (tables, types, reducers, procedures) = + (tables_types_reducers_procedures).map_err(|errors| errors.sort_deduplicate())?; let typespace_for_generate = typespace_for_generate.finish(); @@ -107,6 +135,7 @@ pub fn validate(def: RawModuleDefV9) -> Result { refmap, row_level_security_raw, lifecycle_reducers, + procedures, }) } @@ -286,26 +315,19 @@ impl ModuleValidator<'_> { }) } - /// Validate a reducer definition. - fn validate_reducer_def(&mut self, reducer_def: RawReducerDefV9, reducer_id: ReducerId) -> Result { - let RawReducerDefV9 { - name, - params, - lifecycle, - } = reducer_def; - - let params_for_generate: Result<_> = params + 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 = TypeLocation::ReducerArg { - reducer_name: (&*name).into(), - position, - arg_name: param.name().map(Into::into), - }; + let location = make_type_location(position, param.name().map(Into::into)); let param_name = param .name() .ok_or_else(|| { @@ -319,10 +341,27 @@ impl ModuleValidator<'_> { let ty_use = self.validate_for_type_use(&location, ¶m.algebraic_type); (param_name, ty_use).combine_errors() }) - .collect_all_errors(); + .collect_all_errors() + } - // reducers don't live in the global namespace. - let name = identifier(name); + /// Validate a reducer definition. + fn validate_reducer_def(&mut self, reducer_def: RawReducerDefV9, reducer_id: ReducerId) -> Result { + let RawReducerDefV9 { + name, + params, + lifecycle, + } = reducer_def; + + let params_for_generate: Result<_> = + self.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] { @@ -333,9 +372,7 @@ impl ModuleValidator<'_> { Some(_) => Err(ValidationError::DuplicateLifecycle { lifecycle }.into()), }) .transpose(); - let (name, params_for_generate, lifecycle) = (name, params_for_generate, lifecycle).combine_errors()?; - Ok(ReducerDef { name, params: params.clone(), @@ -347,6 +384,45 @@ impl ModuleValidator<'_> { }) } + fn validate_procedure_def(&mut self, procedure_def: RawProcedureDefV9) -> Result { + let RawProcedureDefV9 { + name, + params, + 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 return_type_for_generate = self.validate_for_type_use( + &TypeLocation::ProcedureReturn { + procedure_name: Cow::Borrowed(&name), + }, + &return_type, + ); + + // Procedures share the "function namespace" with reducers. + // Uniqueness is validated in a later pass, in `check_function_names_are_unique`. + let name = identifier(name); + + let (name, params_for_generate, return_type_for_generate) = + (name, params_for_generate, return_type_for_generate).combine_errors()?; + + Ok(ProcedureDef { + name, + params, + 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, + }) + } + fn validate_column_default_value( &self, tables: &HashMap, @@ -717,7 +793,8 @@ impl TableValidator<'_, '_> { /// Validate a schedule definition. fn validate_schedule_def(&mut self, schedule: RawScheduleDefV9, primary_key: Option) -> Result { let RawScheduleDefV9 { - reducer_name, + // Despite the field name, a `RawScheduleDefV9` may refer to either a reducer or a function. + reducer_name: function_name, scheduled_at_column, name, } = schedule; @@ -749,15 +826,20 @@ impl TableValidator<'_, '_> { }); let name = self.add_to_global_namespace(name); - let reducer_name = identifier(reducer_name); + let function_name = identifier(function_name); - let (name, (at_column, id_column), reducer_name) = (name, at_id, reducer_name).combine_errors()?; + let (name, (at_column, id_column), function_name) = (name, at_id, function_name).combine_errors()?; Ok(ScheduleDef { name, at_column, id_column, - reducer_name, + 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, }) } @@ -897,32 +979,44 @@ fn identifier(name: Box) -> Result { Identifier::new(name).map_err(|error| ValidationError::IdentifierError { error }.into()) } -fn check_scheduled_reducers_exist( - tables: &IdentifierMap, +/// 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( + tables: &mut IdentifierMap, reducers: &IndexMap, + procedures: &IndexMap, ) -> Result<()> { + let validate_params = + |params_from_function: &ProductType, table_row_type_ref: AlgebraicTypeRef, function_name: &str| { + if params_from_function.elements.len() == 1 + && params_from_function.elements[0].algebraic_type == table_row_type_ref.into() + { + Ok(()) + } else { + Err(ValidationError::IncorrectScheduledFunctionParams { + function_name: function_name.into(), + function_kind: FunctionKind::Reducer, + expected: AlgebraicType::product([AlgebraicType::Ref(table_row_type_ref)]).into(), + actual: params_from_function.clone().into(), + }) + } + }; + tables - .values() + .values_mut() .map(|table| -> Result<()> { - if let Some(schedule) = &table.schedule { - let reducer = reducers.get(&schedule.reducer_name); - if let Some(reducer) = reducer { - if reducer.params.elements.len() == 1 - && reducer.params.elements[0].algebraic_type == table.product_type_ref.into() - { - Ok(()) - } else { - Err(ValidationError::IncorrectScheduledReducerParams { - reducer: (&*schedule.reducer_name).into(), - expected: AlgebraicType::product([AlgebraicType::Ref(table.product_type_ref)]).into(), - actual: reducer.params.clone().into(), - } - .into()) - } + if let Some(schedule) = &mut table.schedule { + if let Some(reducer) = reducers.get(&schedule.function_name) { + schedule.function_kind = FunctionKind::Reducer; + validate_params(&reducer.params, table.product_type_ref, &reducer.name).map_err(Into::into) + } else if let Some(procedure) = procedures.get(&schedule.function_name) { + schedule.function_kind = FunctionKind::Procedure; + validate_params(&procedure.params, table.product_type_ref, &procedure.name).map_err(Into::into) } else { - Err(ValidationError::MissingScheduledReducer { + Err(ValidationError::MissingScheduledFunction { schedule: schedule.name.clone(), - reducer: schedule.reducer_name.clone(), + function: schedule.function_name.clone(), } .into()) } @@ -933,7 +1027,24 @@ fn check_scheduled_reducers_exist( .collect_all_errors() } -fn proccess_misc_exports( +fn check_function_names_are_unique( + reducers: &IndexMap, + procedures: &IndexMap, +) -> Result<()> { + let names = reducers.keys().collect::>(); + procedures + .keys() + .map(|name| -> Result<()> { + if names.contains(name) { + Err(ValidationError::DuplicateFunctionName { name: name.clone() }.into()) + } else { + Ok(()) + } + }) + .collect_all_errors() +} + +fn check_non_procedure_misc_exports( misc_exports: Vec, validator: &ModuleValidator, tables: &mut IdentifierMap, @@ -942,6 +1053,9 @@ fn proccess_misc_exports( .into_iter() .map(|export| match export { RawMiscModuleExportV9::ColumnDefaultValue(cdv) => process_column_default_value(&cdv, validator, tables), + RawMiscModuleExportV9::Procedure(_proc) => { + unreachable!("Procedure defs should already have been sorted out of `misc_exports`") + } _ => unimplemented!("unknown misc export"), }) .collect_all_errors::<()>() @@ -993,7 +1107,8 @@ mod tests { }; use crate::def::{validate::Result, ModuleDef}; use crate::def::{ - BTreeAlgorithm, ConstraintData, ConstraintDef, DirectAlgorithm, IndexDef, SequenceDef, UniqueConstraintData, + BTreeAlgorithm, ConstraintData, ConstraintDef, DirectAlgorithm, FunctionKind, IndexDef, SequenceDef, + UniqueConstraintData, }; use crate::error::*; use crate::type_for_generate::ClientCodegenError; @@ -1210,9 +1325,13 @@ mod tests { 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().reducer_name[..], + &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)); @@ -1660,9 +1779,9 @@ mod tests { .finish(); let result: Result = builder.finish().try_into(); - expect_error_matching!(result, ValidationError::MissingScheduledReducer { schedule, reducer } => { + expect_error_matching!(result, ValidationError::MissingScheduledFunction { schedule, function } => { &schedule[..] == "Deliveries_sched" && - reducer == &expect_identifier("check_deliveries") + function == &expect_identifier("check_deliveries") }); } @@ -1688,10 +1807,11 @@ mod tests { builder.add_reducer("check_deliveries", ProductType::from([("a", AlgebraicType::U64)]), None); let result: Result = builder.finish().try_into(); - expect_error_matching!(result, ValidationError::IncorrectScheduledReducerParams { reducer, expected, actual } => { - &reducer[..] == "check_deliveries" && - expected.0 == AlgebraicType::product([AlgebraicType::Ref(deliveries_product_type)]) && - actual.0 == ProductType::from([("a", AlgebraicType::U64)]).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() }); } diff --git a/crates/schema/src/error.rs b/crates/schema/src/error.rs index e7526ad095d..f7d05c8febf 100644 --- a/crates/schema/src/error.rs +++ b/crates/schema/src/error.rs @@ -7,7 +7,7 @@ use spacetimedb_sats::{bsatn::DecodeError, AlgebraicType, AlgebraicTypeRef}; use std::borrow::Cow; use std::fmt; -use crate::def::ScopedTypeName; +use crate::def::{FunctionKind, ScopedTypeName}; use crate::identifier::Identifier; use crate::type_for_generate::ClientCodegenError; @@ -108,11 +108,12 @@ pub enum ValidationError { MissingPrimaryKeyUniqueConstraint { column: RawColumnName }, #[error("Table {table} should have a type definition for its product_type_element, but does not")] TableTypeNameMismatch { table: Identifier }, - #[error("Schedule {schedule} refers to a scheduled reducer {reducer} that does not exist")] - MissingScheduledReducer { schedule: Box, reducer: Identifier }, - #[error("Scheduled reducer {reducer} expected to have type {expected}, but has type {actual}")] - IncorrectScheduledReducerParams { - reducer: RawIdentifier, + #[error("Schedule {schedule} refers to a scheduled reducer or procedure {function} that does not exist")] + MissingScheduledFunction { schedule: Box, function: Identifier }, + #[error("Scheduled {function_kind} {function_name} expected to have type {expected}, but has type {actual}")] + IncorrectScheduledFunctionParams { + function_name: RawIdentifier, + function_kind: FunctionKind, expected: PrettyAlgebraicType, actual: PrettyAlgebraicType, }, @@ -130,6 +131,8 @@ pub enum ValidationError { MultipleColumnDefaultValues { table: RawIdentifier, col_id: ColId }, #[error("Table {table} not found")] TableNotFound { table: RawIdentifier }, + #[error("Name {name} is used for multiple reducers and/or procedures")] + DuplicateFunctionName { name: Identifier }, } /// A wrapper around an `AlgebraicType` that implements `fmt::Display`. @@ -173,6 +176,14 @@ pub enum TypeLocation<'a> { position: usize, arg_name: Option>, }, + /// A procedure argument. + ProcedureArg { + procedure_name: Cow<'a, str>, + position: usize, + arg_name: Option>, + }, + /// A procedure return type. + ProcedureReturn { procedure_name: Cow<'a, str> }, /// A type in the typespace. InTypespace { /// The reference to the type within the typespace. @@ -193,6 +204,18 @@ impl TypeLocation<'_> { position, arg_name: arg_name.map(|s| s.to_string().into()), }, + TypeLocation::ProcedureArg { + procedure_name, + position, + arg_name, + } => TypeLocation::ProcedureArg { + procedure_name: procedure_name.to_string().into(), + position, + arg_name: arg_name.map(|s| s.to_string().into()), + }, + Self::ProcedureReturn { procedure_name } => TypeLocation::ProcedureReturn { + procedure_name: procedure_name.to_string().into(), + }, // needed to convince rustc this is allowed. TypeLocation::InTypespace { ref_ } => TypeLocation::InTypespace { ref_ }, } @@ -213,6 +236,20 @@ impl fmt::Display for TypeLocation<'_> { } Ok(()) } + TypeLocation::ProcedureArg { + procedure_name, + position, + arg_name, + } => { + write!(f, "procedure `{procedure_name}` argument {position}")?; + if let Some(arg_name) = arg_name { + write!(f, " (`{arg_name}`)")?; + } + Ok(()) + } + TypeLocation::ProcedureReturn { procedure_name } => { + write!(f, "procedure `{procedure_name}` return value") + } TypeLocation::InTypespace { ref_ } => { write!(f, "typespace ref `{ref_}`") } diff --git a/crates/schema/src/schema.rs b/crates/schema/src/schema.rs index f9d104795f2..6d284729f5a 100644 --- a/crates/schema/src/schema.rs +++ b/crates/schema/src/schema.rs @@ -922,20 +922,20 @@ pub struct ScheduleSchema { /// The name of the schedule. pub schedule_name: Box, - /// The name of the reducer to call. - pub reducer_name: Box, + /// The name of the reducer or procedure to call. + pub function_name: Box, /// The column containing the `ScheduleAt` enum. pub at_column: ColId, } impl ScheduleSchema { - pub fn for_test(name: impl Into>, reducer: impl Into>, at: impl Into) -> Self { + pub fn for_test(name: impl Into>, function: impl Into>, at: impl Into) -> Self { Self { table_id: TableId::SENTINEL, schedule_id: ScheduleId::SENTINEL, schedule_name: name.into(), - reducer_name: reducer.into(), + function_name: function.into(), at_column: at.into(), } } @@ -955,7 +955,7 @@ impl Schema for ScheduleSchema { table_id: parent_id, schedule_id: id, schedule_name: (*def.name).into(), - reducer_name: (*def.reducer_name).into(), + function_name: (*def.function_name).into(), at_column: def.at_column, // Ignore def.at_column and id_column. Those are recovered at runtime. } @@ -964,8 +964,8 @@ impl Schema for ScheduleSchema { fn check_compatible(&self, _module_def: &ModuleDef, def: &Self::Def) -> Result<(), anyhow::Error> { ensure_eq!(&self.schedule_name[..], &def.name[..], "Schedule name mismatch"); ensure_eq!( - &self.reducer_name[..], - &def.reducer_name[..], + &self.function_name[..], + &def.function_name[..], "Schedule reducer name mismatch" ); Ok(()) From acbe18a5cf255efc2800be6971d4a69df04f90b5 Mon Sep 17 00:00:00 2001 From: Phoebe Goldman Date: Thu, 9 Oct 2025 12:50:03 -0400 Subject: [PATCH 02/25] Regen C# moduledef bindings --- .../Runtime/Internal/Autogen/RawMiscModuleExportV9.g.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bindings-csharp/Runtime/Internal/Autogen/RawMiscModuleExportV9.g.cs b/crates/bindings-csharp/Runtime/Internal/Autogen/RawMiscModuleExportV9.g.cs index b2887f2b49e..45b5cf690cd 100644 --- a/crates/bindings-csharp/Runtime/Internal/Autogen/RawMiscModuleExportV9.g.cs +++ b/crates/bindings-csharp/Runtime/Internal/Autogen/RawMiscModuleExportV9.g.cs @@ -10,6 +10,6 @@ namespace SpacetimeDB.Internal [SpacetimeDB.Type] public partial record RawMiscModuleExportV9 : SpacetimeDB.TaggedEnum<( RawColumnDefaultValueV9 ColumnDefaultValue, - SpacetimeDB.Unit _Reserved + RawProcedureDefV9 Procedure )>; } From 4375cfeebe95265057aac7afda77bf0b9d7f2429 Mon Sep 17 00:00:00 2001 From: Phoebe Goldman Date: Thu, 9 Oct 2025 13:04:35 -0400 Subject: [PATCH 03/25] Also commit newly-created autogen file... --- .../Internal/Autogen/RawProcedureDefV9.g.cs | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 crates/bindings-csharp/Runtime/Internal/Autogen/RawProcedureDefV9.g.cs diff --git a/crates/bindings-csharp/Runtime/Internal/Autogen/RawProcedureDefV9.g.cs b/crates/bindings-csharp/Runtime/Internal/Autogen/RawProcedureDefV9.g.cs new file mode 100644 index 00000000000..b286db08c11 --- /dev/null +++ b/crates/bindings-csharp/Runtime/Internal/Autogen/RawProcedureDefV9.g.cs @@ -0,0 +1,41 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#nullable enable + +using System; +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace SpacetimeDB.Internal +{ + [SpacetimeDB.Type] + [DataContract] + public sealed partial class RawProcedureDefV9 + { + [DataMember(Name = "name")] + public string Name; + [DataMember(Name = "params")] + public List Params; + [DataMember(Name = "return_type")] + public SpacetimeDB.BSATN.AlgebraicType ReturnType; + + public RawProcedureDefV9( + string Name, + List Params, + SpacetimeDB.BSATN.AlgebraicType ReturnType + ) + { + this.Name = Name; + this.Params = Params; + this.ReturnType = ReturnType; + } + + public RawProcedureDefV9() + { + this.Name = ""; + this.Params = new(); + this.ReturnType = null!; + } + } +} From 6c06ce154a240bc18d4219399f4eb35c9180da70 Mon Sep 17 00:00:00 2001 From: Phoebe Goldman Date: Wed, 15 Oct 2025 11:36:49 -0400 Subject: [PATCH 04/25] ArgsSeed: structured type generics, not macros --- crates/core/src/host/mod.rs | 17 ++-- crates/core/src/host/module_host.rs | 6 +- crates/schema/src/def.rs | 6 +- crates/schema/src/def/deserialize.rs | 138 ++++++++++++++------------- 4 files changed, 89 insertions(+), 78 deletions(-) diff --git a/crates/core/src/host/mod.rs b/crates/core/src/host/mod.rs index 66d58ea8d3e..c5ea9bb73ca 100644 --- a/crates/core/src/host/mod.rs +++ b/crates/core/src/host/mod.rs @@ -5,9 +5,12 @@ use derive_more::Display; use enum_map::Enum; use once_cell::sync::OnceCell; use spacetimedb_lib::bsatn; -use spacetimedb_lib::de::serde::SeedWrapper; +use spacetimedb_lib::de::{serde::SeedWrapper, DeserializeSeed}; use spacetimedb_lib::ProductValue; -use spacetimedb_schema::def::deserialize::{ArgsSeed, ProcedureArgsDeserializeSeed, ReducerArgsDeserializeSeed}; +use spacetimedb_schema::def::{ + deserialize::{ArgsSeed, FunctionDef}, + ProcedureDef, ReducerDef, +}; mod disk_storage; mod host_controller; @@ -44,20 +47,20 @@ impl ReducerArgs { #[allow(unused)] fn into_tuple_for_procedure( self, - seed: ProcedureArgsDeserializeSeed, + seed: ArgsSeed<'_, ProcedureDef>, ) -> Result { self._into_tuple(seed).map_err(|err| InvalidProcedureArguments { err, - procedure: (*seed.inner_def().name).into(), + procedure: (seed.name()).into(), }) } - fn into_tuple(self, seed: ReducerArgsDeserializeSeed) -> Result { + fn into_tuple(self, seed: ArgsSeed<'_, ReducerDef>) -> Result { self._into_tuple(seed).map_err(|err| InvalidReducerArguments { err, - reducer: (*seed.inner_def().name).into(), + reducer: (seed.name()).into(), }) } - fn _into_tuple(self, seed: impl ArgsSeed) -> anyhow::Result { + fn _into_tuple(self, seed: ArgsSeed<'_, Def>) -> anyhow::Result { Ok(match self { ReducerArgs::Json(json) => ArgsTuple { tuple: from_json_seed(&json, SeedWrapper(seed))?, diff --git a/crates/core/src/host/module_host.rs b/crates/core/src/host/module_host.rs index 8d03f012f05..683321f6073 100644 --- a/crates/core/src/host/module_host.rs +++ b/crates/core/src/host/module_host.rs @@ -47,7 +47,7 @@ use spacetimedb_primitives::TableId; use spacetimedb_query::compile_subscription; use spacetimedb_sats::ProductValue; use spacetimedb_schema::auto_migrate::{AutoMigrateError, MigrationPolicy}; -use spacetimedb_schema::def::deserialize::ReducerArgsDeserializeSeed; +use spacetimedb_schema::def::deserialize::ArgsSeed; use spacetimedb_schema::def::{ModuleDef, ReducerDef, TableDef}; use spacetimedb_schema::schema::{Schema, TableSchema}; use spacetimedb_vm::relation::RelValue; @@ -1090,7 +1090,7 @@ impl ModuleHost { reducer_def: &ReducerDef, args: ReducerArgs, ) -> Result { - let reducer_seed = ReducerArgsDeserializeSeed(self.info.module_def.typespace().with_type(reducer_def)); + let reducer_seed = ArgsSeed(self.info.module_def.typespace().with_type(reducer_def)); let args = args.into_tuple(reducer_seed)?; let caller_connection_id = caller_connection_id.unwrap_or(ConnectionId::ZERO); @@ -1125,7 +1125,7 @@ impl ModuleHost { args: ReducerArgs, module_instance: &mut Instance, ) -> Result { - let reducer_seed = ReducerArgsDeserializeSeed(self.info.module_def.typespace().with_type(reducer_def)); + let reducer_seed = ArgsSeed(self.info.module_def.typespace().with_type(reducer_def)); let args = args.into_tuple(reducer_seed)?; let caller_connection_id = caller_connection_id.unwrap_or(ConnectionId::ZERO); diff --git a/crates/schema/src/def.rs b/crates/schema/src/def.rs index 112597943e9..efdb035c876 100644 --- a/crates/schema/src/def.rs +++ b/crates/schema/src/def.rs @@ -23,7 +23,7 @@ use crate::error::{IdentifierError, ValidationErrors}; use crate::identifier::Identifier; use crate::schema::{Schema, TableSchema}; use crate::type_for_generate::{AlgebraicTypeUse, ProductTypeDef, TypespaceForGenerate}; -use deserialize::ReducerArgsDeserializeSeed; +use deserialize::ArgsSeed; use enum_map::EnumMap; use hashbrown::Equivalent; use indexmap::IndexMap; @@ -281,9 +281,9 @@ impl ModuleDef { pub fn reducer_arg_deserialize_seed>( &self, name: &K, - ) -> Option<(ReducerId, ReducerArgsDeserializeSeed<'_>)> { + ) -> Option<(ReducerId, ArgsSeed<'_, ReducerDef>)> { let (id, reducer) = self.reducer_full(name)?; - Some((id, ReducerArgsDeserializeSeed(self.typespace.with_type(reducer)))) + Some((id, ArgsSeed(self.typespace.with_type(reducer)))) } /// Look up the name corresponding to an `AlgebraicTypeRef`. diff --git a/crates/schema/src/def/deserialize.rs b/crates/schema/src/def/deserialize.rs index 02d3768ab38..683ed7fb662 100644 --- a/crates/schema/src/def/deserialize.rs +++ b/crates/schema/src/def/deserialize.rs @@ -6,77 +6,85 @@ use spacetimedb_lib::{ ProductType, }; -pub trait ArgsSeed: for<'de> de::DeserializeSeed<'de, Output = ProductValue> { +/// Wrapper around a function def that allows deserializing to a [`ProductValue`] at the type of the def's parameter [`ProductType`]. +/// +/// Sensible instantiations for `Def` are [`ProcedureDef`] and [`ReducerDef`]. +pub struct ArgsSeed<'a, Def>(pub sats::WithTypespace<'a, Def>); + +// Manual impls of traits rather than derives, +// 'cause derives are always constrained on all type parameters, +// even though `ArgsSeed: Copy` in our case. +impl Clone for ArgsSeed<'_, Def> { + fn clone(&self) -> Self { + Self(self.0) + } +} +impl Copy for ArgsSeed<'_, Def> {} + +pub trait FunctionDef { fn params(&self) -> &ProductType; + fn name(&self) -> &str; } -/// Define `struct_name` as a newtype wrapper around [`WithTypespace`] of `inner_ty`, -/// and implement [`de::DeserializeSeed`] and [`de::ProductVisitor`] for that newtype. -/// -/// `ReducerArgs` (defined in the spacetimedb_core crate) will use this type -/// to deserialize the arguments to a reducer or procedure -/// at the appropriate type for that specific function, which is known only at runtime. -macro_rules! define_args_deserialize_seed { - ($struct_vis:vis struct $struct_name:ident($field_vis:vis $inner_ty:ty)) => { - #[doc = concat!( - "Wrapper around a [`", - stringify!($inner_ty), - "`] that allows deserializing to a [`ProductValue`] at the type of the def's parameter `ProductType`." - )] - #[derive(Clone, Copy)] - $struct_vis struct $struct_name<'a>($field_vis sats::WithTypespace<'a, $inner_ty> ); - - impl<'a> $struct_name<'a> { - #[doc = concat!( - "Get the inner [`", - stringify!($inner_ty), - "`] of this seed." - )] - $struct_vis fn inner_def(&self) -> &'a $inner_ty { - self.0.ty() - } - } - - impl<'de> de::DeserializeSeed<'de> for $struct_name<'_> { - type Output = ProductValue; - - fn deserialize>(self, deserializer: D) -> Result { - deserializer.deserialize_product(self) - } - } - - impl<'de> de::ProductVisitor<'de> for $struct_name<'_> { - type Output = ProductValue; - - fn product_name(&self) -> Option<&str> { - Some(&self.0.ty().name) - } - fn product_len(&self) -> usize { - self.0.ty().params.elements.len() - } - fn product_kind(&self) -> de::ProductKind { - de::ProductKind::ReducerArgs - } - - fn visit_seq_product>(self, tup: A) -> Result { - de::visit_seq_product(self.0.map(|r| &*r.params.elements), &self, tup) - } - - fn visit_named_product>(self, tup: A) -> Result { - de::visit_named_product(self.0.map(|r| &*r.params.elements), &self, tup) - } - } - - impl<'a> ArgsSeed for $struct_name<'a> { - fn params(&self) -> &ProductType { - &self.0.ty().params - } - } +impl FunctionDef for ReducerDef { + fn params(&self) -> &ProductType { + &self.params + } + fn name(&self) -> &str { + &self.name + } +} + +impl FunctionDef for ProcedureDef { + fn params(&self) -> &ProductType { + &self.params + } + fn name(&self) -> &str { + &self.name + } +} + +impl ArgsSeed<'_, Def> { + pub fn name(&self) -> &str { + self.0.ty().name() + } + + pub fn params(&self) -> &ProductType { + self.0.ty().params() } } -define_args_deserialize_seed!(pub struct ReducerArgsDeserializeSeed(pub ReducerDef)); -define_args_deserialize_seed!(pub struct ProcedureArgsDeserializeSeed(pub ProcedureDef)); +impl<'de, Def: FunctionDef> de::DeserializeSeed<'de> for ArgsSeed<'_, Def> { + type Output = ProductValue; + + fn deserialize>(self, deserializer: D) -> Result { + deserializer.deserialize_product(self) + } +} + +impl<'de, Def: FunctionDef> de::ProductVisitor<'de> for ArgsSeed<'_, Def> { + type Output = ProductValue; + + fn product_name(&self) -> Option<&str> { + Some(self.0.ty().name()) + } + + fn product_len(&self) -> usize { + self.0.ty().params().elements.len() + } + + fn product_kind(&self) -> de::ProductKind { + de::ProductKind::ReducerArgs + } + + fn visit_seq_product>(self, tup: A) -> Result { + de::visit_seq_product(self.0.map(|r| &*r.params().elements), &self, tup) + } + + fn visit_named_product>(self, tup: A) -> Result { + de::visit_named_product(self.0.map(|r| &*r.params().elements), &self, tup) + } +} pub struct ReducerArgsWithSchema<'a> { value: &'a ProductValue, From 05ffe4022403132ae5af3ab8de8525b6a4393696 Mon Sep 17 00:00:00 2001 From: Phoebe Goldman Date: Wed, 15 Oct 2025 11:39:45 -0400 Subject: [PATCH 05/25] Rename `ReducerArgs` to `FunctionArgs` --- crates/client-api/src/routes/database.rs | 4 ++-- crates/core/src/client/client_connection.rs | 4 ++-- crates/core/src/client/message_handlers.rs | 6 +++--- crates/core/src/host/mod.rs | 12 ++++++------ crates/core/src/host/module_host.rs | 14 ++++++++------ crates/core/src/host/scheduler.rs | 10 +++++----- crates/core/src/host/v8/syscall.rs | 2 +- crates/core/src/host/wasmtime/wasm_instance_env.rs | 2 +- crates/testing/src/modules.rs | 8 ++++---- 9 files changed, 32 insertions(+), 30 deletions(-) diff --git a/crates/client-api/src/routes/database.rs b/crates/client-api/src/routes/database.rs index ea056f57b2d..b79c4499daa 100644 --- a/crates/client-api/src/routes/database.rs +++ b/crates/client-api/src/routes/database.rs @@ -23,7 +23,7 @@ use spacetimedb::host::module_host::ClientConnectedError; use spacetimedb::host::ReducerCallError; use spacetimedb::host::ReducerOutcome; use spacetimedb::host::UpdateDatabaseResult; -use spacetimedb::host::{MigratePlanResult, ReducerArgs}; +use spacetimedb::host::{FunctionArgs, MigratePlanResult}; use spacetimedb::identity::Identity; use spacetimedb::messages::control_db::{Database, HostType}; use spacetimedb_client_api_messages::name::{ @@ -61,7 +61,7 @@ pub async fn call( } let caller_identity = auth.claims.identity; - let args = ReducerArgs::Json(body); + let args = FunctionArgs::Json(body); let db_identity = name_or_identity.resolve(&worker_ctx).await?; let database = worker_ctx_find_database(&worker_ctx, &db_identity) diff --git a/crates/core/src/client/client_connection.rs b/crates/core/src/client/client_connection.rs index 85d43482d16..ccc63497bc2 100644 --- a/crates/core/src/client/client_connection.rs +++ b/crates/core/src/client/client_connection.rs @@ -12,7 +12,7 @@ use super::{message_handlers, ClientActorId, MessageHandleError}; use crate::db::relational_db::RelationalDB; use crate::error::DBError; use crate::host::module_host::ClientConnectedError; -use crate::host::{ModuleHost, NoSuchModule, ReducerArgs, ReducerCallError, ReducerCallResult}; +use crate::host::{FunctionArgs, ModuleHost, NoSuchModule, ReducerCallError, ReducerCallResult}; use crate::messages::websocket::Subscribe; use crate::util::asyncify; use crate::util::prometheus_handle::IntGaugeExt; @@ -809,7 +809,7 @@ impl ClientConnection { pub async fn call_reducer( &self, reducer: &str, - args: ReducerArgs, + args: FunctionArgs, request_id: RequestId, timer: Instant, flags: CallReducerFlags, diff --git a/crates/core/src/client/message_handlers.rs b/crates/core/src/client/message_handlers.rs index e2077d948e4..90829afa9e5 100644 --- a/crates/core/src/client/message_handlers.rs +++ b/crates/core/src/client/message_handlers.rs @@ -2,7 +2,7 @@ use super::messages::{SubscriptionUpdateMessage, SwitchedServerMessage, ToProtoc use super::{ClientConnection, DataMessage, Protocol}; use crate::energy::EnergyQuanta; use crate::host::module_host::{EventStatus, ModuleEvent, ModuleFunctionCall}; -use crate::host::{ReducerArgs, ReducerId}; +use crate::host::{FunctionArgs, ReducerId}; use crate::identity::Identity; use crate::messages::websocket::{CallReducer, ClientMessage, OneOffQuery}; use crate::worker_metrics::WORKER_METRICS; @@ -36,14 +36,14 @@ pub async fn handle(client: &ClientConnection, message: DataMessage, timer: Inst let DeserializeWrapper(message) = serde_json::from_str::>>>(&text)?; message.map_args(|s| { - ReducerArgs::Json(match s { + FunctionArgs::Json(match s { Cow::Borrowed(s) => text.slice_ref(s), Cow::Owned(string) => string.into(), }) }) } DataMessage::Binary(message_buf) => bsatn::from_slice::>(&message_buf)? - .map_args(|b| ReducerArgs::Bsatn(message_buf.slice_ref(b))), + .map_args(|b| FunctionArgs::Bsatn(message_buf.slice_ref(b))), }; let module = client.module(); diff --git a/crates/core/src/host/mod.rs b/crates/core/src/host/mod.rs index c5ea9bb73ca..ab1733101fa 100644 --- a/crates/core/src/host/mod.rs +++ b/crates/core/src/host/mod.rs @@ -35,15 +35,15 @@ pub use scheduler::Scheduler; /// Encoded arguments to a database function. /// -/// Despite the name, this may be arguments to either a reducer or a procedure. +/// A database function is either a reducer or a procedure. #[derive(Debug)] -pub enum ReducerArgs { +pub enum FunctionArgs { Json(ByteString), Bsatn(Bytes), Nullary, } -impl ReducerArgs { +impl FunctionArgs { #[allow(unused)] fn into_tuple_for_procedure( self, @@ -62,17 +62,17 @@ impl ReducerArgs { } fn _into_tuple(self, seed: ArgsSeed<'_, Def>) -> anyhow::Result { Ok(match self { - ReducerArgs::Json(json) => ArgsTuple { + FunctionArgs::Json(json) => ArgsTuple { tuple: from_json_seed(&json, SeedWrapper(seed))?, bsatn: OnceCell::new(), json: OnceCell::with_value(json), }, - ReducerArgs::Bsatn(bytes) => ArgsTuple { + FunctionArgs::Bsatn(bytes) => ArgsTuple { tuple: seed.deserialize(bsatn::Deserializer::new(&mut &bytes[..]))?, bsatn: OnceCell::with_value(bytes), json: OnceCell::new(), }, - ReducerArgs::Nullary => { + FunctionArgs::Nullary => { anyhow::ensure!(seed.params().elements.is_empty(), "failed to typecheck args"); ArgsTuple::nullary() } diff --git a/crates/core/src/host/module_host.rs b/crates/core/src/host/module_host.rs index 683321f6073..24bb565566e 100644 --- a/crates/core/src/host/module_host.rs +++ b/crates/core/src/host/module_host.rs @@ -1,4 +1,6 @@ -use super::{ArgsTuple, InvalidReducerArguments, ReducerArgs, ReducerCallResult, ReducerId, ReducerOutcome, Scheduler}; +use super::{ + ArgsTuple, FunctionArgs, InvalidReducerArguments, ReducerCallResult, ReducerId, ReducerOutcome, Scheduler, +}; use crate::client::messages::{OneOffQueryResponseMessage, SerializableMessage}; use crate::client::{ClientActorId, ClientConnectionSender}; use crate::database_logger::{LogLevel, Record}; @@ -875,7 +877,7 @@ impl ModuleHost { None, reducer_id, reducer_def, - ReducerArgs::Nullary, + FunctionArgs::Nullary, inst, )?; @@ -1002,7 +1004,7 @@ impl ModuleHost { None, reducer_id, reducer_def, - ReducerArgs::Nullary, + FunctionArgs::Nullary, inst, ); @@ -1088,7 +1090,7 @@ impl ModuleHost { timer: Option, reducer_id: ReducerId, reducer_def: &ReducerDef, - args: ReducerArgs, + args: FunctionArgs, ) -> Result { let reducer_seed = ArgsSeed(self.info.module_def.typespace().with_type(reducer_def)); let args = args.into_tuple(reducer_seed)?; @@ -1122,7 +1124,7 @@ impl ModuleHost { timer: Option, reducer_id: ReducerId, reducer_def: &ReducerDef, - args: ReducerArgs, + args: FunctionArgs, module_instance: &mut Instance, ) -> Result { let reducer_seed = ArgsSeed(self.info.module_def.typespace().with_type(reducer_def)); @@ -1152,7 +1154,7 @@ impl ModuleHost { request_id: Option, timer: Option, reducer_name: &str, - args: ReducerArgs, + args: FunctionArgs, ) -> Result { let res = async { let (reducer_id, reducer_def) = self diff --git a/crates/core/src/host/scheduler.rs b/crates/core/src/host/scheduler.rs index 6852982109c..6d8fbbb846d 100644 --- a/crates/core/src/host/scheduler.rs +++ b/crates/core/src/host/scheduler.rs @@ -21,7 +21,7 @@ use super::module_host::ModuleEvent; use super::module_host::ModuleFunctionCall; use super::module_host::{CallReducerParams, WeakModuleHost}; use super::module_host::{DatabaseUpdate, EventStatus}; -use super::{ModuleHost, ReducerArgs, ReducerCallError}; +use super::{FunctionArgs, ModuleHost, ReducerCallError}; use spacetimedb_datastore::execution_context::Workload; use spacetimedb_datastore::locking_tx_datastore::MutTxId; use spacetimedb_datastore::system_tables::{StFields, StScheduledFields, ST_SCHEDULED_ID}; @@ -60,7 +60,7 @@ enum SchedulerMessage { }, ScheduleImmediate { reducer_name: String, - args: ReducerArgs, + args: FunctionArgs, }, } @@ -242,7 +242,7 @@ impl Scheduler { Ok(()) } - pub fn volatile_nonatomic_schedule_immediate(&self, reducer_name: String, args: ReducerArgs) { + pub fn volatile_nonatomic_schedule_immediate(&self, reducer_name: String, args: FunctionArgs) { let _ = self.tx.send(MsgOrExit::Msg(SchedulerMessage::ScheduleImmediate { reducer_name, args, @@ -267,7 +267,7 @@ struct SchedulerActor { enum QueueItem { Id { id: ScheduledReducerId, at: Timestamp }, - VolatileNonatomicImmediate { reducer_name: String, args: ReducerArgs }, + VolatileNonatomicImmediate { reducer_name: String, args: FunctionArgs }, } #[cfg(target_pointer_width = "64")] @@ -349,7 +349,7 @@ impl SchedulerActor { .reducer_arg_deserialize_seed(&reducer[..]) .ok_or_else(|| anyhow!("Reducer not found: {}", reducer))?; - let reducer_args = ReducerArgs::Bsatn(bsatn_args.into()).into_tuple(reducer_seed)?; + let reducer_args = FunctionArgs::Bsatn(bsatn_args.into()).into_tuple(reducer_seed)?; // the timestamp we tell the reducer it's running at will be // at least the timestamp it was scheduled to run at. diff --git a/crates/core/src/host/v8/syscall.rs b/crates/core/src/host/v8/syscall.rs index 7d574782ebb..33c84ce0a4d 100644 --- a/crates/core/src/host/v8/syscall.rs +++ b/crates/core/src/host/v8/syscall.rs @@ -939,7 +939,7 @@ fn volatile_nonatomic_schedule_immediate<'scope>( let env = env_on_isolate(scope); env.instance_env .scheduler - .volatile_nonatomic_schedule_immediate(name, crate::host::ReducerArgs::Bsatn(args.into())); + .volatile_nonatomic_schedule_immediate(name, crate::host::FunctionArgs::Bsatn(args.into())); Ok(v8::undefined(scope).into()) } diff --git a/crates/core/src/host/wasmtime/wasm_instance_env.rs b/crates/core/src/host/wasmtime/wasm_instance_env.rs index c41c4737034..f60ac7644f4 100644 --- a/crates/core/src/host/wasmtime/wasm_instance_env.rs +++ b/crates/core/src/host/wasmtime/wasm_instance_env.rs @@ -1006,7 +1006,7 @@ impl WasmInstanceEnv { let args = mem.deref_slice(args, args_len)?; env.instance_env.scheduler.volatile_nonatomic_schedule_immediate( name.to_owned(), - crate::host::ReducerArgs::Bsatn(args.to_vec().into()), + crate::host::FunctionArgs::Bsatn(args.to_vec().into()), ); Ok(()) diff --git a/crates/testing/src/modules.rs b/crates/testing/src/modules.rs index 150171871e9..91d60cc55a8 100644 --- a/crates/testing/src/modules.rs +++ b/crates/testing/src/modules.rs @@ -19,7 +19,7 @@ use tokio::runtime::{Builder, Runtime}; use spacetimedb::client::{ClientActorId, ClientConfig, ClientConnection, DataMessage}; use spacetimedb::database_logger::DatabaseLogger; use spacetimedb::db::{Config, Storage}; -use spacetimedb::host::ReducerArgs; +use spacetimedb::host::FunctionArgs; use spacetimedb::messages::websocket::CallReducerFlags; use spacetimedb_client_api::{ControlStateReadAccess, ControlStateWriteAccess, DatabaseDef, NodeDelegate}; use spacetimedb_lib::{bsatn, sats}; @@ -55,7 +55,7 @@ pub struct ModuleHandle { } impl ModuleHandle { - async fn call_reducer(&self, reducer: &str, args: ReducerArgs) -> anyhow::Result<()> { + async fn call_reducer(&self, reducer: &str, args: FunctionArgs) -> anyhow::Result<()> { let result = self .client .call_reducer(reducer, args, 0, Instant::now(), CallReducerFlags::FullUpdate) @@ -72,12 +72,12 @@ impl ModuleHandle { pub async fn call_reducer_json(&self, reducer: &str, args: &sats::ProductValue) -> anyhow::Result<()> { let args = serde_json::to_string(&args).unwrap(); - self.call_reducer(reducer, ReducerArgs::Json(args.into())).await + self.call_reducer(reducer, FunctionArgs::Json(args.into())).await } pub async fn call_reducer_binary(&self, reducer: &str, args: &sats::ProductValue) -> anyhow::Result<()> { let args = bsatn::to_vec(&args).unwrap(); - self.call_reducer(reducer, ReducerArgs::Bsatn(args.into())).await + self.call_reducer(reducer, FunctionArgs::Bsatn(args.into())).await } pub async fn send(&self, message: impl Into) -> anyhow::Result<()> { From 684d09f4259e64ecb0fd2b812f68160bce90985e Mon Sep 17 00:00:00 2001 From: Phoebe Goldman Date: Wed, 15 Oct 2025 12:14:40 -0400 Subject: [PATCH 06/25] Various style nits from Mazdak's review --- Cargo.lock | 1 + crates/core/src/host/mod.rs | 48 ++++++++++++++--------------- crates/core/src/host/module_host.rs | 21 +++++++------ crates/lib/src/db/raw_def/v9.rs | 2 +- crates/primitives/Cargo.toml | 1 + crates/primitives/src/ids.rs | 21 ++----------- crates/schema/src/def.rs | 2 ++ crates/schema/src/schema.rs | 2 +- 8 files changed, 44 insertions(+), 54 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fa6cb645241..86782bc9496 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6363,6 +6363,7 @@ version = "1.5.0" dependencies = [ "bitflags 2.9.0", "either", + "enum-as-inner", "itertools 0.12.1", "nohash-hasher", "proptest", diff --git a/crates/core/src/host/mod.rs b/crates/core/src/host/mod.rs index ab1733101fa..91943980117 100644 --- a/crates/core/src/host/mod.rs +++ b/crates/core/src/host/mod.rs @@ -7,10 +7,7 @@ use once_cell::sync::OnceCell; use spacetimedb_lib::bsatn; use spacetimedb_lib::de::{serde::SeedWrapper, DeserializeSeed}; use spacetimedb_lib::ProductValue; -use spacetimedb_schema::def::{ - deserialize::{ArgsSeed, FunctionDef}, - ProcedureDef, ReducerDef, -}; +use spacetimedb_schema::def::deserialize::{ArgsSeed, FunctionDef}; mod disk_storage; mod host_controller; @@ -44,20 +41,10 @@ pub enum FunctionArgs { } impl FunctionArgs { - #[allow(unused)] - fn into_tuple_for_procedure( - self, - seed: ArgsSeed<'_, ProcedureDef>, - ) -> Result { - self._into_tuple(seed).map_err(|err| InvalidProcedureArguments { + fn into_tuple(self, seed: ArgsSeed<'_, Def>) -> Result { + self._into_tuple(seed).map_err(|err| InvalidFunctionArguments { err, - procedure: (seed.name()).into(), - }) - } - fn into_tuple(self, seed: ArgsSeed<'_, ReducerDef>) -> Result { - self._into_tuple(seed).map_err(|err| InvalidReducerArguments { - err, - reducer: (seed.name()).into(), + function_name: seed.name().into(), }) } fn _into_tuple(self, seed: ArgsSeed<'_, Def>) -> anyhow::Result { @@ -118,21 +105,32 @@ impl Default for ArgsTuple { // TODO(noa): replace imports from this module with imports straight from primitives. pub use spacetimedb_primitives::ReducerId; +/// Inner error type for [`InvalidReducerArguments`] and [`InvalidProcedureArguments`]. #[derive(thiserror::Error, Debug)] -#[error("invalid arguments for reducer {reducer}: {err}")] -pub struct InvalidReducerArguments { +#[error("invalid arguments for function {function_name}: {err}")] +pub struct InvalidFunctionArguments { #[source] err: anyhow::Error, - reducer: Box, + function_name: Box, } +/// Newtype over [`InvalidFunctionArguments`] which renders with the word "reducer". #[derive(thiserror::Error, Debug)] -#[error("invalid arguments for procedure {procedure}: {err}")] -pub struct InvalidProcedureArguments { +#[error("invalid arguments for reducer {}: {}", .0.function_name, .0.err)] +pub struct InvalidReducerArguments( + #[from] #[source] - err: anyhow::Error, - procedure: Box, -} + InvalidFunctionArguments, +); + +/// Newtype over [`InvalidFunctionArguments`] which renders with the word "procedure". +#[derive(thiserror::Error, Debug)] +#[error("invalid arguments for procedure {}: {}", .0.function_name, .0.err)] +pub struct InvalidProcedureArguments( + #[from] + #[source] + InvalidFunctionArguments, +); fn from_json_seed<'de, T: serde::de::DeserializeSeed<'de>>(s: &'de str, seed: T) -> anyhow::Result { let mut de = serde_json::Deserializer::from_str(s); diff --git a/crates/core/src/host/module_host.rs b/crates/core/src/host/module_host.rs index 24bb565566e..d9eade69aeb 100644 --- a/crates/core/src/host/module_host.rs +++ b/crates/core/src/host/module_host.rs @@ -9,6 +9,7 @@ use crate::energy::EnergyQuanta; use crate::error::DBError; use crate::estimation::estimate_rows_scanned; use crate::hash::Hash; +use crate::host::InvalidFunctionArguments; use crate::identity::Identity; use crate::messages::control_db::{Database, HostType}; use crate::module_host_context::ModuleCreationContext; @@ -972,10 +973,10 @@ impl ModuleHost { log::error!( "`call_identity_disconnected`: fallback transaction to delete from `st_client` failed: {err}" ); - InvalidReducerArguments { + InvalidReducerArguments(InvalidFunctionArguments { err: err.into(), - reducer: reducer_name.into(), - } + function_name: reducer_name.into(), + }) .into() }) }; @@ -1093,7 +1094,7 @@ impl ModuleHost { args: FunctionArgs, ) -> Result { let reducer_seed = ArgsSeed(self.info.module_def.typespace().with_type(reducer_def)); - let args = args.into_tuple(reducer_seed)?; + let args = args.into_tuple(reducer_seed).map_err(InvalidReducerArguments)?; let caller_connection_id = caller_connection_id.unwrap_or(ConnectionId::ZERO); Ok(self @@ -1128,7 +1129,7 @@ impl ModuleHost { module_instance: &mut Instance, ) -> Result { let reducer_seed = ArgsSeed(self.info.module_def.typespace().with_type(reducer_def)); - let args = args.into_tuple(reducer_seed)?; + let args = args.into_tuple(reducer_seed).map_err(InvalidReducerArguments)?; let caller_connection_id = caller_connection_id.unwrap_or(ConnectionId::ZERO); Ok(module_instance.call_reducer( @@ -1233,10 +1234,12 @@ impl ModuleHost { Ok(inst.call_reducer(Some(tx), params)) } Ok(None) => Err(ReducerCallError::ScheduleReducerNotFound), - Err(err) => Err(ReducerCallError::Args(InvalidReducerArguments { - err, - reducer: REDUCER.into(), - })), + Err(err) => Err(ReducerCallError::Args(InvalidReducerArguments( + InvalidFunctionArguments { + err, + function_name: REDUCER.into(), + }, + ))), } }) .await? diff --git a/crates/lib/src/db/raw_def/v9.rs b/crates/lib/src/db/raw_def/v9.rs index 562b84c1da0..58816384383 100644 --- a/crates/lib/src/db/raw_def/v9.rs +++ b/crates/lib/src/db/raw_def/v9.rs @@ -83,7 +83,7 @@ pub struct RawModuleDefV9 { /// Miscellaneous additional module exports. /// - /// The enum `RawMiscModuleExportV9` can have new variants added + /// The enum [`RawMiscModuleExportV9`] can have new variants added /// without breaking existing compiled modules. /// As such, this acts as a sort of dumping ground for any exports added after we defined `RawModuleDefV9`. /// diff --git a/crates/primitives/Cargo.toml b/crates/primitives/Cargo.toml index 8da5f76e113..2715ccb9b6b 100644 --- a/crates/primitives/Cargo.toml +++ b/crates/primitives/Cargo.toml @@ -12,6 +12,7 @@ memory-usage = ["dep:spacetimedb-memory-usage"] [dependencies] bitflags.workspace = true either.workspace = true +enum-as-inner.workspace = true nohash-hasher.workspace = true itertools.workspace = true spacetimedb-memory-usage = { workspace = true, optional = true, default-features = false } diff --git a/crates/primitives/src/ids.rs b/crates/primitives/src/ids.rs index afe5325aba1..77e1352635b 100644 --- a/crates/primitives/src/ids.rs +++ b/crates/primitives/src/ids.rs @@ -2,6 +2,8 @@ use core::fmt; +use enum_as_inner::EnumAsInner; + macro_rules! system_id { ($(#[$($doc_comment:tt)*])* pub struct $name:ident(pub $backing_ty:ty);) => { @@ -126,25 +128,8 @@ system_id! { /// An id for a function exported from a module, which may be a reducer or a procedure. // This is never stored in a system table, // but is useful to have defined here to provide a shared language for downstream crates. -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy, Debug, EnumAsInner)] pub enum FunctionId { Reducer(ReducerId), Procedure(ProcedureId), } - -impl FunctionId { - pub fn as_reducer(self) -> Option { - if let Self::Reducer(id) = self { - Some(id) - } else { - None - } - } - pub fn as_procedure(self) -> Option { - if let Self::Procedure(id) = self { - Some(id) - } else { - None - } - } -} diff --git a/crates/schema/src/def.rs b/crates/schema/src/def.rs index efdb035c876..fb8db9fccf3 100644 --- a/crates/schema/src/def.rs +++ b/crates/schema/src/def.rs @@ -263,10 +263,12 @@ impl ModuleDef { self.procedures.get_full(name).map(|(idx, _, def)| (idx.into(), def)) } + /// Look up a procuedure by its id, panicking if it doesn't exist. pub fn procedure_by_id(&self, id: ProcedureId) -> &ProcedureDef { &self.procedures[id.idx()] } + /// Look up a procuedure by its id, returning `None` if it doesn't exist. pub fn get_procedure_by_id(&self, id: ProcedureId) -> Option<&ProcedureDef> { self.procedures.get_index(id.idx()).map(|(_, def)| def) } diff --git a/crates/schema/src/schema.rs b/crates/schema/src/schema.rs index 6d284729f5a..149cb08eb5b 100644 --- a/crates/schema/src/schema.rs +++ b/crates/schema/src/schema.rs @@ -966,7 +966,7 @@ impl Schema for ScheduleSchema { ensure_eq!( &self.function_name[..], &def.function_name[..], - "Schedule reducer name mismatch" + "Schedule function name mismatch" ); Ok(()) } From 741553fd371e3c4bff5bcb30167870ad6c0cd393 Mon Sep 17 00:00:00 2001 From: Phoebe Goldman Date: Wed, 15 Oct 2025 12:49:56 -0400 Subject: [PATCH 07/25] Clippy --- crates/schema/src/def/deserialize.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/schema/src/def/deserialize.rs b/crates/schema/src/def/deserialize.rs index 683ed7fb662..09b78f3d2d4 100644 --- a/crates/schema/src/def/deserialize.rs +++ b/crates/schema/src/def/deserialize.rs @@ -16,7 +16,7 @@ pub struct ArgsSeed<'a, Def>(pub sats::WithTypespace<'a, Def>); // even though `ArgsSeed: Copy` in our case. impl Clone for ArgsSeed<'_, Def> { fn clone(&self) -> Self { - Self(self.0) + *self } } impl Copy for ArgsSeed<'_, Def> {} From c7daa0c2faf9baaa7250c4bd9b6f6b93031d6e7f Mon Sep 17 00:00:00 2001 From: Phoebe Goldman Date: Wed, 15 Oct 2025 13:04:27 -0400 Subject: [PATCH 08/25] Ensure no two reducers have the same name, add tests Prior to this commit, we never actually checked that reducers had unique names. My new `check_function_names_are_unique` assumed this invariant, but we were silently ignoring duplicate reducer names. With this commit, `check_function_names_are_unique` actually validates uniqueness not only between the two sets, but within them. This commit also adds three tests for the three possible ways to have a duplicate name: two reducers, two procedures, or one of each. --- crates/schema/src/def/validate/v9.rs | 100 +++++++++++++++++++++------ 1 file changed, 80 insertions(+), 20 deletions(-) diff --git a/crates/schema/src/def/validate/v9.rs b/crates/schema/src/def/validate/v9.rs index 20bbea6c278..fa7a8b88067 100644 --- a/crates/schema/src/def/validate/v9.rs +++ b/crates/schema/src/def/validate/v9.rs @@ -50,7 +50,9 @@ pub fn validate(def: RawModuleDefV9) -> Result { .validate_reducer_def(reducer, ReducerId(idx as u32)) .map(|reducer_def| (reducer_def.name.clone(), reducer_def)) }) - .collect_all_errors(); + // 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, non_procedure_misc_exports) = misc_exports @@ -72,7 +74,9 @@ pub fn validate(def: RawModuleDefV9) -> Result { .validate_procedure_def(procedure) .map(|procedure_def| (procedure_def.name.clone(), procedure_def)) }) - .collect_all_errors(); + // 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 tables = tables .into_iter() @@ -103,13 +107,12 @@ pub fn validate(def: RawModuleDefV9) -> Result { (tables, types, reducers, procedures) .combine_errors() .and_then(|(mut tables, types, reducers, procedures)| { - ( - check_scheduled_functions_exist(&mut tables, &reducers, &procedures), + let ((reducers, procedures), ()) = ( + check_function_names_are_unique(reducers, procedures), check_non_procedure_misc_exports(non_procedure_misc_exports, &validator, &mut tables), - check_function_names_are_unique(&reducers, &procedures), ) .combine_errors()?; - + check_scheduled_functions_exist(&mut tables, &reducers, &procedures)?; Ok((tables, types, reducers, procedures)) }); @@ -1027,21 +1030,36 @@ fn check_scheduled_functions_exist( .collect_all_errors() } +/// Check that all function (reducer and procedure) names are unique, +/// then re-organize the reducers and procedures into [`IndexMap`]s +/// for storage in the [`ModuleDef`]. fn check_function_names_are_unique( - reducers: &IndexMap, - procedures: &IndexMap, -) -> Result<()> { - let names = reducers.keys().collect::>(); - procedures - .keys() - .map(|name| -> Result<()> { - if names.contains(name) { - Err(ValidationError::DuplicateFunctionName { name: name.clone() }.into()) - } else { - Ok(()) - } - }) - .collect_all_errors() + reducers: Vec<(Identifier, ReducerDef)>, + procedures: Vec<(Identifier, ProcedureDef)>, +) -> Result<(IndexMap, IndexMap)> { + let mut errors = vec![]; + + let mut reducers_map = IndexMap::with_capacity(reducers.len()); + + for (name, def) in reducers { + if reducers_map.contains_key(&name) { + errors.push(ValidationError::DuplicateFunctionName { name }); + } else { + reducers_map.insert(name, def); + } + } + + let mut procedures_map = IndexMap::with_capacity(procedures.len()); + + for (name, def) in procedures { + if reducers_map.contains_key(&name) || procedures_map.contains_key(&name) { + errors.push(ValidationError::DuplicateFunctionName { name }); + } else { + procedures_map.insert(name, def); + } + } + + ErrorStream::add_extra_errors(Ok((reducers_map, procedures_map)), errors) } fn check_non_procedure_misc_exports( @@ -1857,4 +1875,46 @@ mod tests { assert!(def.lookup::("wacky.index()").is_some()); assert!(def.lookup::("wacky.sequence()").is_some()); } + + #[test] + fn duplicate_reducer_names() { + let mut builder = RawModuleDefV9Builder::new(); + + builder.add_reducer("foo", [("i", AlgebraicType::I32)].into(), None); + builder.add_reducer("foo", [("name", AlgebraicType::String)].into(), None); + + let result: Result = builder.finish().try_into(); + + expect_error_matching!(result, ValidationError::DuplicateFunctionName { name } => { + &name[..] == "foo" + }); + } + + #[test] + fn duplicate_procedure_names() { + let mut builder = RawModuleDefV9Builder::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 = RawModuleDefV9Builder::new(); + + builder.add_reducer("foo", [("i", AlgebraicType::I32)].into(), None); + 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" + }); + } } From 4b57d96317a6d1f2caa867bb58d112b96bb7477d Mon Sep 17 00:00:00 2001 From: Phoebe Goldman Date: Wed, 15 Oct 2025 13:32:45 -0400 Subject: [PATCH 09/25] insta --- .../deps__spacetimedb_bindings_dependencies.snap | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/crates/bindings/tests/snapshots/deps__spacetimedb_bindings_dependencies.snap b/crates/bindings/tests/snapshots/deps__spacetimedb_bindings_dependencies.snap index 6c1e5485c8e..88839cbfdba 100644 --- a/crates/bindings/tests/snapshots/deps__spacetimedb_bindings_dependencies.snap +++ b/crates/bindings/tests/snapshots/deps__spacetimedb_bindings_dependencies.snap @@ -37,6 +37,11 @@ spacetimedb │ ├── spacetimedb_primitives │ │ ├── bitflags │ │ ├── either +│ │ ├── enum_as_inner +│ │ │ ├── heck +│ │ │ ├── proc_macro2 (*) +│ │ │ ├── quote (*) +│ │ │ └── syn (*) │ │ ├── itertools │ │ │ └── either │ │ └── nohash_hasher @@ -45,6 +50,7 @@ spacetimedb │ └── spacetimedb_primitives │ ├── bitflags │ ├── either +│ ├── enum_as_inner (*) │ ├── itertools │ │ └── either │ └── nohash_hasher @@ -64,11 +70,7 @@ spacetimedb │ │ [build-dependencies] │ │ └── autocfg │ ├── derive_more (*) -│ ├── enum_as_inner -│ │ ├── heck -│ │ ├── proc_macro2 (*) -│ │ ├── quote (*) -│ │ └── syn (*) +│ ├── enum_as_inner (*) │ ├── hex │ ├── itertools (*) │ ├── spacetimedb_bindings_macro (*) From 07a984dad79955e0b4058b31327e77de254bc63f Mon Sep 17 00:00:00 2001 From: Zeke Foppa <196249+bfops@users.noreply.github.com> Date: Fri, 10 Oct 2025 07:23:18 -0700 Subject: [PATCH 10/25] CI - Clean up several scripts and call sites related to codegen (#3363) # Description of Changes * Small cleanups in `tools/check-diff.sh` * Use `tools/check-diff.sh` wherever appropriate * Simplify the `sdks/csharp/tools~/gen-*.sh` files after the repo merge # API and ABI breaking changes None. # Expected complexity level and risk 1 # Testing - [x] CI still passes Co-authored-by: Zeke Foppa --- .github/workflows/csharp-test.yml | 22 ++++++------------- .github/workflows/typescript-test.yml | 8 ++----- .../quickstart-chat/client/README.md | 6 ++--- sdks/csharp/tools~/gen-client-api.sh | 2 +- sdks/csharp/tools~/gen-quickstart.sh | 2 +- sdks/csharp/tools~/gen-regression-tests.sh | 2 +- sdks/csharp/tools~/run-regression-tests.sh | 6 ++--- sdks/csharp/tools~/update-against-stdb.sh | 6 ++--- tools/check-diff.sh | 4 ---- 9 files changed, 21 insertions(+), 37 deletions(-) diff --git a/.github/workflows/csharp-test.yml b/.github/workflows/csharp-test.yml index b1f8d834997..baffd85d5f7 100644 --- a/.github/workflows/csharp-test.yml +++ b/.github/workflows/csharp-test.yml @@ -83,15 +83,11 @@ jobs: CARGO_TARGET_DIR: demo/Blackholio/server-rust/target - name: Check quickstart-chat bindings are up to date - working-directory: sdks/csharp/examples~/quickstart-chat + working-directory: sdks/csharp run: | - bash ../../tools~/gen-quickstart.sh "${GITHUB_WORKSPACE}" - # This was copied from tools/check-diff.sh. - # It's required because `spacetime generate` creates lines with the SpacetimeDB commit - # version, which would make this `git diff` check very brittle if included. - PATTERN='^// This was generated using spacetimedb cli version.*' - git diff --exit-code --ignore-matching-lines="$PATTERN" -- . || { - echo "Error: quickstart-chat bindings have changed. Please regenerate the bindings and commit them to this branch." + bash tools~/gen-quickstart.sh + "${GITHUB_WORKSPACE}"/tools/check-diff.sh examples~/quickstart-chat || { + echo 'Error: quickstart-chat bindings have changed. Please run `sdks/csharp/tools~/gen-quickstart.sh`.' exit 1 } @@ -101,12 +97,8 @@ jobs: - name: Check for changes run: | - # This was copied from tools/check-diff.sh. - # It's required because `spacetime generate` creates lines with the SpacetimeDB commit - # version, which would make this `git diff` check very brittle if included. - PATTERN='^// This was generated using spacetimedb cli version.*' - git diff --exit-code --ignore-matching-lines="$PATTERN" -- demo/Blackholio/client-unity/Assets/Scripts/autogen || { - echo "Error: Bindings are dirty. Please generate bindings again and commit them to this branch." + tools/check-diff.sh demo/Blackholio/client-unity/Assets/Scripts/autogen || { + echo 'Error: Bindings are dirty. Please run `demo/Blackholio/server-rust/generate.sh`.' exit 1 } @@ -124,7 +116,7 @@ jobs: disown - name: Run regression tests - run: bash sdks/csharp/tools~/run-regression-tests.sh . + run: bash sdks/csharp/tools~/run-regression-tests.sh - name: Publish unity-tests module to SpacetimeDB working-directory: demo/Blackholio/server-rust diff --git a/.github/workflows/typescript-test.yml b/.github/workflows/typescript-test.yml index 732d5f67778..aea47c7faaa 100644 --- a/.github/workflows/typescript-test.yml +++ b/.github/workflows/typescript-test.yml @@ -102,14 +102,10 @@ jobs: - name: Check for changes working-directory: crates/bindings-typescript run: | - # This was copied from SpacetimeDB/tools/check-diff.sh. - # It's required because `spacetime generate` creates lines with the SpacetimeDB commit - # version, which would make this `git diff` check very brittle if included. - PATTERN='^// This was generated using spacetimedb cli version.*' - if ! git diff --exit-code --ignore-matching-lines="$PATTERN" -- examples/quickstart-chat/src/module_bindings; then + "${GITHUB_WORKSPACE}"/tools/check-diff.sh examples/quickstart-chat/src/module_bindings || { echo "Error: Bindings are dirty. Please generate bindings again and commit them to this branch." exit 1 - fi + } # - name: Start SpacetimeDB # run: | diff --git a/sdks/csharp/examples~/quickstart-chat/client/README.md b/sdks/csharp/examples~/quickstart-chat/client/README.md index 53885f55e46..767c56a06dc 100644 --- a/sdks/csharp/examples~/quickstart-chat/client/README.md +++ b/sdks/csharp/examples~/quickstart-chat/client/README.md @@ -3,8 +3,8 @@ See [SpacetimeDB](https://github.com/clockworklabs/SpacetimeDB)/modules/quicksta ## Regenerating bindings -To regenerate bindings: clone SpacetimeDB next to this repo, then in the root of this repo: +To regenerate bindings: clone SpacetimeDB next to this repo, then in `sdks/csharp` of this repo: ```bash -tools~/gen-quickstart.sh ../SpacetimeDB -``` \ No newline at end of file +tools~/gen-quickstart.sh +``` diff --git a/sdks/csharp/tools~/gen-client-api.sh b/sdks/csharp/tools~/gen-client-api.sh index b3bec5eb4fb..3f01188d1c2 100755 --- a/sdks/csharp/tools~/gen-client-api.sh +++ b/sdks/csharp/tools~/gen-client-api.sh @@ -2,9 +2,9 @@ set -ueo pipefail -STDB_PATH="$1" SDK_PATH="$(dirname "$0")/.." SDK_PATH="$(realpath "$SDK_PATH")" +STDB_PATH="$SDK_PATH/../.." cargo build --manifest-path "$STDB_PATH/crates/standalone/Cargo.toml" diff --git a/sdks/csharp/tools~/gen-quickstart.sh b/sdks/csharp/tools~/gen-quickstart.sh index 3913e4bcfa7..fefb84a5bd1 100755 --- a/sdks/csharp/tools~/gen-quickstart.sh +++ b/sdks/csharp/tools~/gen-quickstart.sh @@ -2,9 +2,9 @@ set -ueo pipefail -STDB_PATH="$1" SDK_PATH="$(dirname "$0")/.." SDK_PATH="$(realpath "$SDK_PATH")" +STDB_PATH="$SDK_PATH/../.." cargo build --manifest-path "$STDB_PATH/crates/standalone/Cargo.toml" cargo run --manifest-path "$STDB_PATH/crates/cli/Cargo.toml" -- generate -y -l csharp -o "$SDK_PATH/examples~/quickstart-chat/client/module_bindings" --project-path "$SDK_PATH/examples~/quickstart-chat/server" diff --git a/sdks/csharp/tools~/gen-regression-tests.sh b/sdks/csharp/tools~/gen-regression-tests.sh index 7a9058efe08..743535ca0b7 100755 --- a/sdks/csharp/tools~/gen-regression-tests.sh +++ b/sdks/csharp/tools~/gen-regression-tests.sh @@ -2,9 +2,9 @@ set -ueo pipefail -STDB_PATH="$1" SDK_PATH="$(dirname "$0")/.." SDK_PATH="$(realpath "$SDK_PATH")" +STDB_PATH="$SDK_PATH/../.." cargo build --manifest-path "$STDB_PATH/crates/standalone/Cargo.toml" cargo run --manifest-path "$STDB_PATH/crates/cli/Cargo.toml" -- generate -y -l csharp -o "$SDK_PATH/examples~/regression-tests/client/module_bindings" --project-path "$SDK_PATH/examples~/regression-tests/server" diff --git a/sdks/csharp/tools~/run-regression-tests.sh b/sdks/csharp/tools~/run-regression-tests.sh index dea10178625..451afaacb73 100644 --- a/sdks/csharp/tools~/run-regression-tests.sh +++ b/sdks/csharp/tools~/run-regression-tests.sh @@ -4,12 +4,12 @@ set -ueo pipefail -STDB_PATH="$1" SDK_PATH="$(dirname "$0")/.." SDK_PATH="$(realpath "$SDK_PATH")" +STDB_PATH="$SDK_PATH/../.." # Regenerate Bindings -"$SDK_PATH/tools~/gen-regression-tests.sh" "$STDB_PATH" +"$SDK_PATH/tools~/gen-regression-tests.sh" # Build and run SpacetimeDB server cargo build --manifest-path "$STDB_PATH/crates/standalone/Cargo.toml" @@ -27,4 +27,4 @@ cargo run --manifest-path "$STDB_PATH/crates/cli/Cargo.toml" call republish-test cd "$SDK_PATH/examples~/regression-tests/client" && dotnet run -c Debug # Run client for republishing module test -cd "$SDK_PATH/examples~/regression-tests/republishing/client" && dotnet run -c Debug \ No newline at end of file +cd "$SDK_PATH/examples~/regression-tests/republishing/client" && dotnet run -c Debug diff --git a/sdks/csharp/tools~/update-against-stdb.sh b/sdks/csharp/tools~/update-against-stdb.sh index 76d082b1058..58c0b8b649a 100644 --- a/sdks/csharp/tools~/update-against-stdb.sh +++ b/sdks/csharp/tools~/update-against-stdb.sh @@ -9,9 +9,9 @@ SDK_PATH="$(dirname "$0")/.." SDK_PATH="$(realpath "$SDK_PATH")" "$SDK_PATH/tools~/write-nuget-config.sh" "$STDB_PATH" -"$SDK_PATH/tools~/gen-client-api.sh" "$STDB_PATH" -"$SDK_PATH/tools~/gen-quickstart.sh" "$STDB_PATH" -"$SDK_PATH/tools~/gen-regression-tests.sh" "$STDB_PATH" +"$SDK_PATH/tools~/gen-client-api.sh" +"$SDK_PATH/tools~/gen-quickstart.sh" +"$SDK_PATH/tools~/gen-regression-tests.sh" dotnet nuget locals all --clear dotnet pack "$STDB_PATH/crates/bindings-csharp" rm -rf "$SDK_PATH/packages" diff --git a/tools/check-diff.sh b/tools/check-diff.sh index e46c2508103..eafd78f41b1 100755 --- a/tools/check-diff.sh +++ b/tools/check-diff.sh @@ -4,13 +4,9 @@ SUBDIR="${1:-.}" # Default to '.' (the whole repo) if no argument given -# We need to figure out the root to make this work when called from a directory within the repo. -GIT_ROOT="$(git rev-parse --show-toplevel)" - # We have a comment in every generated file that has the version and git hash, so these would change with every commit. # We ignore them to avoid having to regen files for every commit unrelated to code gen. PATTERN='^// This was generated using spacetimedb cli version.*' -failed=0 git diff --exit-code --ignore-matching-lines="$PATTERN" -- "$SUBDIR" From 08423104b6a0048f91badb9d68ca6c3baf4a84b7 Mon Sep 17 00:00:00 2001 From: Zeke Foppa <196249+bfops@users.noreply.github.com> Date: Fri, 10 Oct 2025 10:12:33 -0700 Subject: [PATCH 11/25] CI - Check that C# bindings are up to date (#3362) # Description of Changes Check that our generated C# files are up-to-date in our CI. # API and ABI breaking changes None. # Expected complexity level and risk 1 # Testing - [x] CI all passes --------- Co-authored-by: Zeke Foppa --- .github/workflows/csharp-test.yml | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/.github/workflows/csharp-test.yml b/.github/workflows/csharp-test.yml index baffd85d5f7..c6721481fef 100644 --- a/.github/workflows/csharp-test.yml +++ b/.github/workflows/csharp-test.yml @@ -91,6 +91,15 @@ jobs: exit 1 } + - name: Check client-api bindings are up to date + working-directory: sdks/csharp + run: | + bash tools~/gen-client-api.sh + "${GITHUB_WORKSPACE}"/tools/check-diff.sh src/SpacetimeDB/ClientApi || { + echo 'Error: Client API bindings are dirty. Please run `sdks/csharp/tools~/gen-client-api.sh`.' + exit 1 + } + - name: Generate client bindings working-directory: demo/Blackholio/server-rust run: bash ./generate.sh -y @@ -116,7 +125,12 @@ jobs: disown - name: Run regression tests - run: bash sdks/csharp/tools~/run-regression-tests.sh + run: | + bash sdks/csharp/tools~/run-regression-tests.sh + tools/check-diff.sh sdks/csharp/examples~/regression-tests || { + echo 'Error: Bindings are dirty. Please run `sdks/csharp/tools~/gen-regression-tests.sh`.' + exit 1 + } - name: Publish unity-tests module to SpacetimeDB working-directory: demo/Blackholio/server-rust From 85435e60807f05ae20b11dfcc9508d38aedd2413 Mon Sep 17 00:00:00 2001 From: Zeke Foppa <196249+bfops@users.noreply.github.com> Date: Fri, 10 Oct 2025 10:27:39 -0700 Subject: [PATCH 12/25] Misc cleanups in `tools/upgrade-version` (#3370) # Description of Changes * Make sure the user provides at least one of `--rust-and-cli`, `--typescript`, or `--csharp`, since providing none of them is a no-op as of #3308 * Do a semver-parsing of the arg before doing anything, and use that parsed version everywhere * Consolidate some version strings that we were computing in a few places # API and ABI breaking changes None # Expected complexity level and risk 1 # Testing - [x] Running `cargo bump-versions 1.5.0 --typescript --rust-and-cli --csharp` only shows a diff in the change dates --------- Co-authored-by: Zeke Foppa --- tools/upgrade-version/src/main.rs | 68 +++++++++++++++++++------------ 1 file changed, 43 insertions(+), 25 deletions(-) diff --git a/tools/upgrade-version/src/main.rs b/tools/upgrade-version/src/main.rs index 29428070acb..1fa220db7dd 100644 --- a/tools/upgrade-version/src/main.rs +++ b/tools/upgrade-version/src/main.rs @@ -1,7 +1,7 @@ #![allow(clippy::disallowed_macros)] use chrono::{Datelike, Local}; -use clap::{Arg, Command}; +use clap::{Arg, ArgGroup, Command}; use duct::cmd; use regex::Regex; use semver::Version; @@ -90,9 +90,26 @@ fn main() -> anyhow::Result<()> { .action(clap::ArgAction::SetTrue) .help("Also bump versions in C# SDK and templates"), ) + .arg( + Arg::new("all") + .long("all") + .action(clap::ArgAction::SetTrue) + .help("Update all targets (equivalent to --typescript --rust-and-cli --csharp)") + .conflicts_with_all(["typescript", "rust-and-cli", "csharp"]), + ) + .group( + ArgGroup::new("update-targets") + .args(["all", "typescript", "rust-and-cli", "csharp"]) + .required(true) + .multiple(true), + ) .get_matches(); - let version = matches.get_one::("upgrade_version").unwrap(); + let unparsed_version_arg = matches.get_one::("upgrade_version").unwrap(); + let semver = Version::parse(unparsed_version_arg).expect("Invalid semver provided to upgrade-version"); + let full_version = format!("{}.{}.{}", semver.major, semver.minor, semver.patch); + let wildcard_patch = format!("{}.{}.*", semver.major, semver.minor); + if let Some(path) = matches.get_one::("spacetime-path") { env::set_current_dir(path).ok(); } @@ -103,14 +120,14 @@ fn main() -> anyhow::Result<()> { anyhow::bail!("You must execute this binary from inside of the SpacetimeDB directory, or use --spacetime-path"); } - if matches.get_flag("rust-and-cli") { + if matches.get_flag("rust-and-cli") || matches.get_flag("all") { // Use `=` for dependency versions, to avoid issues where Cargo automatically rolls forward to later minor versions. // See https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#default-requirements. - let dep_version = format!("={version}"); + let dep_version = format!("={full_version}"); // root Cargo.toml edit_toml("Cargo.toml", |doc| { - doc["workspace"]["package"]["version"] = toml_edit::value(version); + doc["workspace"]["package"]["version"] = toml_edit::value(full_version.clone()); for (key, dep) in doc["workspace"]["dependencies"] .as_table_like_mut() .expect("workspace.dependencies is not a table") @@ -128,26 +145,19 @@ fn main() -> anyhow::Result<()> { // // Note: This is meaningfully different than setting just major.minor. // See https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#default-requirements. - let v = Version::parse(version).expect("Invalid semver provided to upgrade-version"); - let major_minor = format!("{}.{}.*", v.major, v.minor); - doc["dependencies"]["spacetimedb"] = toml_edit::value(major_minor); + doc["dependencies"]["spacetimedb"] = toml_edit::value(wildcard_patch.clone()); })?; - process_license_file("LICENSE.txt", version); - process_license_file("licenses/BSL.txt", version); + process_license_file("LICENSE.txt", &full_version); + process_license_file("licenses/BSL.txt", &full_version); cmd!("cargo", "check").run().expect("Cargo check failed!"); } - if matches.get_flag("typescript") { - rewrite_json_version_inplace("crates/bindings-typescript/package.json", version)?; + if matches.get_flag("typescript") || matches.get_flag("all") { + rewrite_json_version_inplace("crates/bindings-typescript/package.json", &full_version)?; } - if matches.get_flag("csharp") { - // Compute various version forms - let v = Version::parse(version).expect("Invalid semver provided to upgrade-version"); - let wildcard_patch = format!("{}.{}.*", v.major, v.minor); - let assembly_version = format!("{}.{}.{}", v.major, v.minor, v.patch); - + if matches.get_flag("csharp") || matches.get_flag("all") { // Helpers for XML edits fn rewrite_xml_tag_value(path: &str, tag: &str, new_value: &str) -> anyhow::Result<()> { let contents = fs::read_to_string(path)?; @@ -196,13 +206,13 @@ fn main() -> anyhow::Result<()> { // 1) Client SDK csproj let client_sdk = "sdks/csharp/SpacetimeDB.ClientSDK.csproj"; - rewrite_xml_tag_value(client_sdk, "Version", version)?; - rewrite_xml_tag_value(client_sdk, "AssemblyVersion", &assembly_version)?; + rewrite_xml_tag_value(client_sdk, "Version", &full_version)?; + rewrite_xml_tag_value(client_sdk, "AssemblyVersion", &full_version)?; // Update SpacetimeDB.BSATN.Runtime dependency to major.minor.* rewrite_csproj_package_ref_version(client_sdk, "SpacetimeDB.BSATN.Runtime", &wildcard_patch)?; // Also bump the C# SDK package.json version (preserve formatting) - rewrite_json_version_inplace("sdks/csharp/package.json", version)?; + rewrite_json_version_inplace("sdks/csharp/package.json", &full_version)?; // 2) StdbModule.csproj files: SpacetimeDB.Runtime dependency -> major.minor let stdb_modules: &[&str] = &[ @@ -218,15 +228,23 @@ fn main() -> anyhow::Result<()> { rewrite_xml_tag_value( "crates/bindings-csharp/BSATN.Runtime/BSATN.Runtime.csproj", "Version", - version, + &full_version, + )?; + rewrite_xml_tag_value( + "crates/bindings-csharp/Runtime/Runtime.csproj", + "Version", + &full_version, )?; - rewrite_xml_tag_value("crates/bindings-csharp/Runtime/Runtime.csproj", "Version", version)?; rewrite_xml_tag_value( "crates/bindings-csharp/BSATN.Codegen/BSATN.Codegen.csproj", "Version", - version, + &full_version, + )?; + rewrite_xml_tag_value( + "crates/bindings-csharp/Codegen/Codegen.csproj", + "Version", + &full_version, )?; - rewrite_xml_tag_value("crates/bindings-csharp/Codegen/Codegen.csproj", "Version", version)?; // 4) Template StdbModule._csproj: SpacetimeDB.Runtime dependency -> major.minor.* rewrite_csproj_package_ref_version( From ccf30a33cb43f9fc5847a1b6b5ff09946ea09009 Mon Sep 17 00:00:00 2001 From: Noa Date: Fri, 10 Oct 2025 13:25:26 -0500 Subject: [PATCH 13/25] Noa/ts module host changes (#3388) # Description of Changes Host-side changes extracted from #3327 I added AUTO_INC_OVERFLOW even though we don't currently ever return it, in order to future-proof so it's already there when we start emitting it. Prepublish was failing because it was expecting a wasm module unconditionally, so now it takes ?host_type. I tweaked JS deser to accept null/undefined when the unit type or an option type is expected. I switched to bsatn, because the native sats->js translator wasn't matching what js was expecting. I renamed the sys module: my thinking is that `spacetime:` as a scheme will help disambiguate it, and maybe it could also be used for IMC in the future or something? And I believe we had discussed wanting this to be versioned, similar to wasm imports. Trying to get a borrowed str from deserialize_js doesn't work, because v8 strings don't store utf8. # Testing - [x] All this was done in the course of getting an actual typescript module to successfully publish. --- crates/bindings-csharp/Runtime/Exceptions.cs | 5 + .../bindings-csharp/Runtime/Internal/FFI.cs | 2 + crates/bindings/src/table.rs | 2 +- crates/cli/src/subcommands/publish.rs | 19 ++- crates/client-api/src/routes/database.rs | 18 ++- crates/core/src/host/v8/de.rs | 16 +++ crates/core/src/host/v8/error.rs | 2 +- crates/core/src/host/v8/mod.rs | 48 ++++--- crates/core/src/host/v8/syscall.rs | 10 +- crates/primitives/src/errno.rs | 1 + crates/sats/src/de.rs | 132 +++++++++++++++++- 11 files changed, 219 insertions(+), 36 deletions(-) diff --git a/crates/bindings-csharp/Runtime/Exceptions.cs b/crates/bindings-csharp/Runtime/Exceptions.cs index e591e3a0c00..1488b00d91f 100644 --- a/crates/bindings-csharp/Runtime/Exceptions.cs +++ b/crates/bindings-csharp/Runtime/Exceptions.cs @@ -72,6 +72,11 @@ public class NoSpaceException : StdbException public override string Message => "The provided bytes sink has no more room left"; } +public class AutoIncOverflowException : StdbException +{ + public override string Message => "The auto-increment sequence overflowed"; +} + public class UnknownException : StdbException { private readonly Errno code; diff --git a/crates/bindings-csharp/Runtime/Internal/FFI.cs b/crates/bindings-csharp/Runtime/Internal/FFI.cs index 2c0bf8b925f..dc877ba2d52 100644 --- a/crates/bindings-csharp/Runtime/Internal/FFI.cs +++ b/crates/bindings-csharp/Runtime/Internal/FFI.cs @@ -36,6 +36,7 @@ public enum Errno : short SCHEDULE_AT_DELAY_TOO_LONG = 13, INDEX_NOT_UNIQUE = 14, NO_SUCH_ROW = 15, + AUTO_INC_OVERFLOW = 16, } #pragma warning disable IDE1006 // Naming Styles - Not applicable to FFI stuff. @@ -96,6 +97,7 @@ public static CheckedStatus ConvertToManaged(Errno status) Errno.SCHEDULE_AT_DELAY_TOO_LONG => new ScheduleAtDelayTooLongException(), Errno.INDEX_NOT_UNIQUE => new IndexNotUniqueException(), Errno.NO_SUCH_ROW => new NoSuchRowException(), + Errno.AUTO_INC_OVERFLOW => new AutoIncOverflowException(), _ => new UnknownException(status), }; } diff --git a/crates/bindings/src/table.rs b/crates/bindings/src/table.rs index fa090eb8930..0ca8e174989 100644 --- a/crates/bindings/src/table.rs +++ b/crates/bindings/src/table.rs @@ -1009,7 +1009,7 @@ fn insert(mut row: T::Row, mut buf: IterBuf) -> Result { T::UniqueConstraintViolation::get().map(TryInsertError::UniqueConstraintViolation) } - // sys::Errno::AUTO_INC_OVERFLOW => Tbl::AutoIncOverflow::get().map(TryInsertError::AutoIncOverflow), + sys::Errno::AUTO_INC_OVERFLOW => T::AutoIncOverflow::get().map(TryInsertError::AutoIncOverflow), _ => None, }; err.unwrap_or_else(|| panic!("unexpected insertion error: {e}")) diff --git a/crates/cli/src/subcommands/publish.rs b/crates/cli/src/subcommands/publish.rs index bfaa96b17c6..d1ad3208359 100644 --- a/crates/cli/src/subcommands/publish.rs +++ b/crates/cli/src/subcommands/publish.rs @@ -169,6 +169,7 @@ pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::E &client, &database_host, &domain.to_string(), + host_type, &program_bytes, &auth_header, break_clients_flag, @@ -275,16 +276,27 @@ pub fn pretty_print_style_from_env() -> PrettyPrintStyle { /// Applies pre-publish logic: checking for migration plan, prompting user, and /// modifying the request builder accordingly. +#[allow(clippy::too_many_arguments)] async fn apply_pre_publish_if_needed( mut builder: reqwest::RequestBuilder, client: &reqwest::Client, base_url: &str, domain: &String, + host_type: &str, program_bytes: &[u8], auth_header: &AuthHeader, break_clients_flag: bool, ) -> Result { - if let Some(pre) = call_pre_publish(client, base_url, &domain.to_string(), program_bytes, auth_header).await? { + if let Some(pre) = call_pre_publish( + client, + base_url, + &domain.to_string(), + host_type, + program_bytes, + auth_header, + ) + .await? + { println!("{}", pre.migrate_plan); if pre.break_clients @@ -310,12 +322,15 @@ async fn call_pre_publish( client: &reqwest::Client, database_host: &str, domain: &String, + host_type: &str, program_bytes: &[u8], auth_header: &AuthHeader, ) -> Result, anyhow::Error> { let mut builder = client.post(format!("{database_host}/v1/database/{domain}/pre_publish")); let style = pretty_print_style_from_env(); - builder = builder.query(&[("pretty_print_style", style)]); + builder = builder + .query(&[("pretty_print_style", style)]) + .query(&[("host_type", host_type)]); builder = add_auth_header_opt(builder, auth_header); diff --git a/crates/client-api/src/routes/database.rs b/crates/client-api/src/routes/database.rs index b79c4499daa..fba24452ebe 100644 --- a/crates/client-api/src/routes/database.rs +++ b/crates/client-api/src/routes/database.rs @@ -701,15 +701,29 @@ pub struct PrePublishParams { pub struct PrePublishQueryParams { #[serde(default)] style: PrettyPrintStyle, + #[serde(default)] + host_type: HostType, } pub async fn pre_publish( State(ctx): State, Path(PrePublishParams { name_or_identity }): Path, - Query(PrePublishQueryParams { style }): Query, + Query(PrePublishQueryParams { style, host_type }): Query, Extension(auth): Extension, body: Bytes, ) -> axum::response::Result> { + // Feature gate V8 modules. + // The host must've been compiled with the `unstable` feature. + // TODO(v8): ungate this when V8 is ready to ship. + #[cfg(not(feature = "unstable"))] + if host_type == HostType::Js { + return Err(( + StatusCode::BAD_REQUEST, + "JS host type requires a host with unstable features", + ) + .into()); + } + // User should not be able to print migration plans for a database that they do not own let database_identity = resolve_and_authenticate(&ctx, &name_or_identity, &auth).await?; let style = match style { @@ -723,7 +737,7 @@ pub async fn pre_publish( database_identity, program_bytes: body.into(), num_replicas: None, - host_type: HostType::Wasm, + host_type, }, style, ) diff --git a/crates/core/src/host/v8/de.rs b/crates/core/src/host/v8/de.rs index ac79aaed7fc..d1cf240a460 100644 --- a/crates/core/src/host/v8/de.rs +++ b/crates/core/src/host/v8/de.rs @@ -131,6 +131,11 @@ impl<'de, 'this, 'scope: 'de> de::Deserializer<'de> for Deserializer<'this, 'sco deserialize_primitive!(deserialize_f32, f32); fn deserialize_product>(self, visitor: V) -> Result { + // In `ProductType.serializeValue()` in the TS SDK, null/undefined is accepted for the unit type. + if visitor.product_len() == 0 && self.input.is_null_or_undefined() { + return visitor.visit_seq_product(de::UnitAccess::new()); + } + let object = cast!( self.common.scope, self.input, @@ -149,6 +154,17 @@ impl<'de, 'this, 'scope: 'de> de::Deserializer<'de> for Deserializer<'this, 'sco fn deserialize_sum>(self, visitor: V) -> Result { let scope = &*self.common.scope; + + // In `SumType.serializeValue()` in the TS SDK, option is treated specially - + // null/undefined marks none, any other value `x` is `some(x)`. + if visitor.is_option() { + return if self.input.is_null_or_undefined() { + visitor.visit_sum(de::NoneAccess::new()) + } else { + visitor.visit_sum(de::SomeAccess::new(self)) + }; + } + let sum_name = visitor.sum_name().unwrap_or(""); // We expect a canonical representation of a sum value in JS to be diff --git a/crates/core/src/host/v8/error.rs b/crates/core/src/host/v8/error.rs index 33fedd0115c..cb8bebf4bf4 100644 --- a/crates/core/src/host/v8/error.rs +++ b/crates/core/src/host/v8/error.rs @@ -15,7 +15,7 @@ pub(super) type ValueResult<'scope, T> = Result>; /// /// Newtyped for additional type safety and to track JS exceptions in the type system. #[derive(Debug)] -pub(super) struct ExceptionValue<'scope>(Local<'scope, Value>); +pub(super) struct ExceptionValue<'scope>(pub(super) Local<'scope, Value>); /// Error types that can convert into JS exception values. pub(super) trait IntoException<'scope> { diff --git a/crates/core/src/host/v8/mod.rs b/crates/core/src/host/v8/mod.rs index 7304c10e5f5..16bd261eef3 100644 --- a/crates/core/src/host/v8/mod.rs +++ b/crates/core/src/host/v8/mod.rs @@ -26,7 +26,7 @@ use core::{ptr, str}; use spacetimedb_client_api_messages::energy::ReducerBudget; use spacetimedb_datastore::locking_tx_datastore::MutTxId; use spacetimedb_datastore::traits::Program; -use spacetimedb_lib::{ConnectionId, Identity, RawModuleDef, Timestamp}; +use spacetimedb_lib::{bsatn, ConnectionId, Identity, RawModuleDef, Timestamp}; use spacetimedb_schema::auto_migrate::MigrationPolicy; use std::sync::{Arc, LazyLock}; use std::time::Instant; @@ -415,7 +415,7 @@ fn eval_module<'scope>( script_id: i32, code: &str, resolve_deps: impl MapFnTo>, -) -> ExcResult<(Local<'scope, v8::Module>, Local<'scope, Value>)> { +) -> ExcResult<(Local<'scope, v8::Module>, Local<'scope, v8::Promise>)> { // Get the source map, if any. let source_map_url = find_source_map(code) .map(|sm| sm.into_string(scope)) @@ -454,6 +454,20 @@ fn eval_module<'scope>( // Evaluate the module. let value = module.evaluate(scope).ok_or_else(exception_already_thrown)?; + if module.get_status() == v8::ModuleStatus::Errored { + // If there's an exception while evaluating the code of the module, `evaluate()` won't + // throw, but instead the status will be `Errored` and the exception can be obtained from + // `get_exception()`. + return Err(error::ExceptionValue(module.get_exception()).throw(scope)); + } + + let value = value.cast::(); + if value.state() == v8::PromiseState::Pending { + // If the user were to put top-level `await new Promise((resolve) => { /* do nothing */ })` + // the module value would never actually resolve. For now, reject this entirely. + return Err(error::TypeError("module has top-level await and is pending").throw(scope)); + } + Ok((module, value)) } @@ -461,7 +475,7 @@ fn eval_module<'scope>( fn eval_user_module<'scope>( scope: &PinScope<'scope, '_>, code: &str, -) -> ExcResult<(Local<'scope, v8::Module>, Local<'scope, Value>)> { +) -> ExcResult<(Local<'scope, v8::Module>, Local<'scope, v8::Promise>)> { let name = str_from_ident!(spacetimedb_module).string(scope).into(); eval_module(scope, name, 0, code, resolve_sys_module) } @@ -614,7 +628,7 @@ fn call_call_reducer<'scope>( let sender = serialize_to_js(scope, &sender.to_u256())?; let conn_id: v8::Local<'_, v8::Value> = serialize_to_js(scope, &conn_id.to_u128())?; let timestamp = serialize_to_js(scope, ×tamp)?; - let reducer_args = serialize_to_js(scope, &reducer_args.tuple.elements)?; + let reducer_args = serialize_to_js(scope, reducer_args.get_bsatn())?; let args = &[reducer_id, sender, conn_id, timestamp, reducer_args]; // Get the function on the global proxy object and convert to a function. @@ -677,8 +691,16 @@ fn call_describe_module<'scope>( let raw_mod_js = call_free_fun(scope, fun, &[])?; // Deserialize the raw module. - let raw_mod: RawModuleDef = deserialize_js(scope, raw_mod_js)?; - Ok(raw_mod) + let raw_mod = cast!( + scope, + raw_mod_js, + v8::Uint8Array, + "bytes return from __describe_module__" + ) + .map_err(|e| e.throw(scope))?; + + let bytes = raw_mod.get_contents(&mut []); + bsatn::from_slice::(bytes).map_err(|_e| error::TypeError("invalid bsatn module def").throw(scope)) } #[cfg(test)] @@ -779,19 +801,7 @@ js error Uncaught Error: foobar fn call_describe_module_works() { let code = r#" function __describe_module__() { - return { - "tag": "V9", - "value": { - "typespace": { - "types": [], - }, - "tables": [], - "reducers": [], - "types": [], - "misc_exports": [], - "row_level_security": [], - }, - }; + return new Uint8Array([1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]); } "#; let raw_mod = with_script_catch(code, call_describe_module).map_err(|e| e.to_string()); diff --git a/crates/core/src/host/v8/syscall.rs b/crates/core/src/host/v8/syscall.rs index 33c84ce0a4d..c6184010854 100644 --- a/crates/core/src/host/v8/syscall.rs +++ b/crates/core/src/host/v8/syscall.rs @@ -104,7 +104,7 @@ fn register_sys_module<'scope>(scope: &mut PinScope<'scope, '_>) -> Local<'scope ) } -const SYS_MODULE_NAME: &StringConst = str_from_ident!(spacetimedb_sys); +const SYS_MODULE_NAME: &StringConst = &StringConst::new("spacetime:sys@1.0"); /// The return type of a module -> host syscall. pub(super) type FnRet<'scope> = ExcResult>; @@ -258,8 +258,8 @@ fn with_span<'scope, R>( /// Throws a `TypeError` if: /// - `name` is not `string`. fn table_id_from_name(scope: &mut PinScope<'_, '_>, args: FunctionCallbackArguments<'_>) -> SysCallResult { - let name: &str = deserialize_js(scope, args.get(0))?; - Ok(env_on_isolate(scope).instance_env.table_id_from_name(name)?) + let name: String = deserialize_js(scope, args.get(0))?; + Ok(env_on_isolate(scope).instance_env.table_id_from_name(&name)?) } /// Module ABI that finds the `IndexId` for an index name. @@ -294,8 +294,8 @@ fn table_id_from_name(scope: &mut PinScope<'_, '_>, args: FunctionCallbackArgume /// Throws a `TypeError`: /// - if `name` is not `string`. fn index_id_from_name(scope: &mut PinScope<'_, '_>, args: FunctionCallbackArguments<'_>) -> SysCallResult { - let name: &str = deserialize_js(scope, args.get(0))?; - Ok(env_on_isolate(scope).instance_env.index_id_from_name(name)?) + let name: String = deserialize_js(scope, args.get(0))?; + Ok(env_on_isolate(scope).instance_env.index_id_from_name(&name)?) } /// Module ABI that returns the number of rows currently in table identified by `table_id`. diff --git a/crates/primitives/src/errno.rs b/crates/primitives/src/errno.rs index b00cea7a579..540aab6bdf4 100644 --- a/crates/primitives/src/errno.rs +++ b/crates/primitives/src/errno.rs @@ -22,6 +22,7 @@ macro_rules! errnos { SCHEDULE_AT_DELAY_TOO_LONG(13, "Specified delay in scheduling row was too long"), INDEX_NOT_UNIQUE(14, "The index was not unique"), NO_SUCH_ROW(15, "The row was not found, e.g., in an update call"), + AUTO_INC_OVERFLOW(16, "The auto-increment sequence overflowed"), ); }; } diff --git a/crates/sats/src/de.rs b/crates/sats/src/de.rs index 719e0e01186..5b5b0b118c5 100644 --- a/crates/sats/src/de.rs +++ b/crates/sats/src/de.rs @@ -719,12 +719,7 @@ impl<'de, E: Error> SumAccess<'de> for NoneAccess { impl<'de, E: Error> VariantAccess<'de> for NoneAccess { type Error = E; fn deserialize_seed>(self, seed: T) -> Result { - use crate::algebraic_value::de::*; - seed.deserialize(ValueDeserializer::new(crate::AlgebraicValue::unit())) - .map_err(|err| match err { - ValueDeserializeError::MismatchedType => E::custom("mismatched type"), - ValueDeserializeError::Custom(err) => E::custom(err), - }) + seed.deserialize(UnitAccess::new()) } } @@ -753,3 +748,128 @@ impl<'de, D: Deserializer<'de>> VariantAccess<'de> for SomeAccess { seed.deserialize(self.0) } } + +/// A `Deserializer` that represents a unit value. +// used in the implementation of `VariantAccess for NoneAccess` +pub struct UnitAccess(PhantomData); + +impl UnitAccess { + /// Returns a new [`UnitAccess`]. + pub fn new() -> Self { + Self(PhantomData) + } +} + +impl Default for UnitAccess { + fn default() -> Self { + Self::new() + } +} + +impl<'de, E: Error> SeqProductAccess<'de> for UnitAccess { + type Error = E; + + fn next_element_seed>(&mut self, _seed: T) -> Result, Self::Error> { + Ok(None) + } +} + +impl<'de, E: Error> NamedProductAccess<'de> for UnitAccess { + type Error = E; + + fn get_field_ident>(&mut self, _visitor: V) -> Result, Self::Error> { + Ok(None) + } + + fn get_field_value_seed>(&mut self, _seed: T) -> Result { + unreachable!() + } +} + +impl<'de, E: Error> Deserializer<'de> for UnitAccess { + type Error = E; + + fn deserialize_product>(self, visitor: V) -> Result { + visitor.visit_seq_product(self) + } + + fn deserialize_sum>(self, _visitor: V) -> Result { + Err(E::custom("invalid type")) + } + + fn deserialize_bool(self) -> Result { + Err(E::custom("invalid type")) + } + + fn deserialize_u8(self) -> Result { + Err(E::custom("invalid type")) + } + + fn deserialize_u16(self) -> Result { + Err(E::custom("invalid type")) + } + + fn deserialize_u32(self) -> Result { + Err(E::custom("invalid type")) + } + + fn deserialize_u64(self) -> Result { + Err(E::custom("invalid type")) + } + + fn deserialize_u128(self) -> Result { + Err(E::custom("invalid type")) + } + + fn deserialize_u256(self) -> Result { + Err(E::custom("invalid type")) + } + + fn deserialize_i8(self) -> Result { + Err(E::custom("invalid type")) + } + + fn deserialize_i16(self) -> Result { + Err(E::custom("invalid type")) + } + + fn deserialize_i32(self) -> Result { + Err(E::custom("invalid type")) + } + + fn deserialize_i64(self) -> Result { + Err(E::custom("invalid type")) + } + + fn deserialize_i128(self) -> Result { + Err(E::custom("invalid type")) + } + + fn deserialize_i256(self) -> Result { + Err(E::custom("invalid type")) + } + + fn deserialize_f32(self) -> Result { + Err(E::custom("invalid type")) + } + + fn deserialize_f64(self) -> Result { + Err(E::custom("invalid type")) + } + + fn deserialize_str>(self, _visitor: V) -> Result { + Err(E::custom("invalid type")) + } + + fn deserialize_bytes>(self, _visitor: V) -> Result { + Err(E::custom("invalid type")) + } + + fn deserialize_array_seed, T: DeserializeSeed<'de> + Clone>( + self, + _visitor: V, + _seed: T, + ) -> Result { + Err(E::custom("invalid type")) + } +} From 8987c3d41eeb047f0ee9ab59d5b9938f20f01060 Mon Sep 17 00:00:00 2001 From: Mario Montoya Date: Fri, 10 Oct 2025 13:52:50 -0500 Subject: [PATCH 14/25] Fix python example to not advise to use ~/token.py (#3393) # Description of Changes Python is funny, if a file `token.py` is created and another script run on the same dir, it will cause python to block: ```python python3-3.13.7/lib/python3.13/tokenize.py", line 35, in from token import * File "/Users/mamcx/token.py", line 3, in text = sys.stdin.read() ``` By coincidence the docs on `pg wire` use this name. Changed to one that don't cause the trouble. # Expected complexity level and risk 1 # Testing - [x] Created another script and run it, see it blocks because this... --- docs/docs/sql/pg-wire.md | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/docs/docs/sql/pg-wire.md b/docs/docs/sql/pg-wire.md index f8a5d9bf309..b131bdc1df8 100644 --- a/docs/docs/sql/pg-wire.md +++ b/docs/docs/sql/pg-wire.md @@ -70,20 +70,16 @@ spacetime login show --token To export the token to `PGPASSWORD`: -```python -# token.py -import sys, re - -text = sys.stdin.read() -match = re.search(r"Your auth token \(don't share this!\) is\s+(\S+)", text) -if not match: - sys.exit("No token found") +*For Bash*: -print(f"export PGPASSWORD={match.group(1)}") +```bash +export PGPASSWORD="$(spacetime login show --token | sed -n 's/^Your auth token.*is //p')" ``` -```bash -eval "$(spacetime login show --token | python3 ~/token.py)" +*For PowerShell*: + +```powershell +$env:PGPASSWORD = (spacetime login show --token | Select-String 'Your auth token.*is (.*)' | % { $_.Matches[0].Groups[1].Value }) ``` ## Examples From 5e3e6bbc7e60c3cab7cbfd1db10007ed76812428 Mon Sep 17 00:00:00 2001 From: Tyler Cloutier Date: Tue, 14 Oct 2025 11:31:19 -0400 Subject: [PATCH 15/25] Removed errant console.log/print statements (#3402) # Description of Changes `console.log` debugging statements accidentally made it into the release. # API and ABI breaking changes None # Expected complexity level and risk 1, trivial # Testing - [x] Automated testing only --- crates/bindings-typescript/src/react/useTable.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/crates/bindings-typescript/src/react/useTable.ts b/crates/bindings-typescript/src/react/useTable.ts index 19b4e809b85..714e1b43ca7 100644 --- a/crates/bindings-typescript/src/react/useTable.ts +++ b/crates/bindings-typescript/src/react/useTable.ts @@ -368,9 +368,6 @@ export function useTable< if (whereClause && !evaluate(whereClause, row)) { return; } - if (tableName === 'message') { - console.log('onInsert for messages table:', row); - } callbacks?.onInsert?.(row); if ( ctx.event !== latestTransactionEvent.current || @@ -386,9 +383,6 @@ export function useTable< if (whereClause && !evaluate(whereClause, row)) { return; } - if (tableName === 'message') { - console.log('onDelete for messages table:', row); - } callbacks?.onDelete?.(row); if ( ctx.event !== latestTransactionEvent.current || From 5db32124615f5be769172886b97acb4f78c0c415 Mon Sep 17 00:00:00 2001 From: Jason Larabie Date: Tue, 14 Oct 2025 10:50:29 -0700 Subject: [PATCH 16/25] Fix for Unreal codegen for Optional fields (#3360) # Description of Changes - Fixed logic issue around Option>> applying the wrong types for primitives and dropping Optional - Fixed an issue with enum vs enum variants wrapped in Option<> producing the incorrect Unreal type - Removed unnecessary and incorrect header bindings - Type fix in the tests around the Optional Int32 # API and ABI breaking changes No breaking changes # Expected complexity level and risk 2 - Reworked incorrect optional lookups which can happen recursively # Testing I built out a few simple and complex objects in a Rust module to triple-check possible cases beyond what the test framework calls out. - [x] Tested many combinations from a Rust module to an Unreal project - [x] Ran and updated Unreal test cases as necessary --- crates/codegen/src/unrealcpp.rs | 111 +-- .../ModuleBindings/SpacetimeDBClient.g.cpp | 666 +++++++++--------- .../Tests/SpacetimeFullClientTests.cpp | 6 +- ...=> TestClientOptionalVecOptionalInt32.g.h} | 23 +- .../Reducers/InsertOptionVecOptionI32.g.h | 8 +- .../ModuleBindings/SpacetimeDBClient.g.h | 200 +++--- .../Types/EnumWithPayloadType.g.h | 4 +- .../Types/EveryPrimitiveStructType.g.h | 6 +- .../Types/IndexedTable2Type.g.h | 2 +- .../ModuleBindings/Types/LargeTableType.g.h | 6 +- .../ModuleBindings/Types/OneBoolType.g.h | 2 +- .../ModuleBindings/Types/OneF32Type.g.h | 2 +- .../ModuleBindings/Types/OneF64Type.g.h | 2 +- .../Types/OptionVecOptionI32Type.g.h | 4 +- .../ModuleBindings/Types/PkBoolType.g.h | 2 +- .../ModuleBindings/Types/UniqueBoolType.g.h | 2 +- .../TestClient/Public/Tests/TestHandler.h | 2 +- 17 files changed, 506 insertions(+), 542 deletions(-) rename sdks/unreal/tests/TestClient/Source/TestClient/Public/ModuleBindings/Optionals/{TestClientOptionalVecInt32.g.h => TestClientOptionalVecOptionalInt32.g.h} (57%) diff --git a/crates/codegen/src/unrealcpp.rs b/crates/codegen/src/unrealcpp.rs index 12b745168db..35ec3baecd7 100644 --- a/crates/codegen/src/unrealcpp.rs +++ b/crates/codegen/src/unrealcpp.rs @@ -3123,20 +3123,20 @@ fn collect_optional_types(module: &ModuleDef) -> HashSet { fn get_cpp_type_for_array_element(elem_type_str: &str, _: &ModuleDef, module_name: &str) -> String { match elem_type_str { "Bool" => "bool".to_string(), - "I8" => "int8".to_string(), - "U8" => "uint8".to_string(), - "I16" => "int16".to_string(), - "U16" => "uint16".to_string(), - "I32" => "int32".to_string(), - "U32" => "uint32".to_string(), - "I64" => "int64".to_string(), - "U64" => "uint64".to_string(), - "F32" => "float".to_string(), - "F64" => "double".to_string(), - "I128" => "FSpacetimeDBInt128".to_string(), - "U128" => "FSpacetimeDBUInt128".to_string(), - "I256" => "FSpacetimeDBInt256".to_string(), - "U256" => "FSpacetimeDBUInt256".to_string(), + "I8" | "Int8" => "int8".to_string(), + "U8" | "UInt8" => "uint8".to_string(), + "I16" | "Int16" => "int16".to_string(), + "U16" | "UInt16" => "uint16".to_string(), + "I32" | "Int32" => "int32".to_string(), + "U32" | "UInt32" => "uint32".to_string(), + "I64" | "Int64" => "int64".to_string(), + "U64" | "UInt64" => "uint64".to_string(), + "F32" | "Float" => "float".to_string(), + "F64" | "Double" => "double".to_string(), + "I128" | "Int128" => "FSpacetimeDBInt128".to_string(), + "U128" | "UInt128" => "FSpacetimeDBUInt128".to_string(), + "I256" | "Int256" => "FSpacetimeDBInt256".to_string(), + "U256" | "UInt256" => "FSpacetimeDBUInt256".to_string(), "String" => "FString".to_string(), "Identity" => "FSpacetimeDBIdentity".to_string(), "ConnectionId" => "FSpacetimeDBConnectionId".to_string(), @@ -3159,20 +3159,20 @@ fn get_array_element_type_name(module: &ModuleDef, elem: &AlgebraicTypeUse) -> S match elem { AlgebraicTypeUse::Primitive(p) => match p { PrimitiveType::Bool => "Bool".to_string(), - PrimitiveType::I8 => "I8".to_string(), - PrimitiveType::U8 => "U8".to_string(), - PrimitiveType::I16 => "I16".to_string(), - PrimitiveType::U16 => "U16".to_string(), - PrimitiveType::I32 => "I32".to_string(), - PrimitiveType::U32 => "U32".to_string(), - PrimitiveType::I64 => "I64".to_string(), - PrimitiveType::U64 => "U64".to_string(), - PrimitiveType::F32 => "F32".to_string(), - PrimitiveType::F64 => "F64".to_string(), - PrimitiveType::I128 => "I128".to_string(), - PrimitiveType::U128 => "U128".to_string(), - PrimitiveType::I256 => "I256".to_string(), - PrimitiveType::U256 => "U256".to_string(), + PrimitiveType::I8 => "Int8".to_string(), + PrimitiveType::U8 => "UInt8".to_string(), + PrimitiveType::I16 => "Int16".to_string(), + PrimitiveType::U16 => "UInt16".to_string(), + PrimitiveType::I32 => "Int32".to_string(), + PrimitiveType::U32 => "UInt32".to_string(), + PrimitiveType::I64 => "Int64".to_string(), + PrimitiveType::U64 => "UInt64".to_string(), + PrimitiveType::F32 => "Float".to_string(), + PrimitiveType::F64 => "Double".to_string(), + PrimitiveType::I128 => "Int128".to_string(), + PrimitiveType::U128 => "UInt128".to_string(), + PrimitiveType::I256 => "Int256".to_string(), + PrimitiveType::U256 => "UInt256".to_string(), }, AlgebraicTypeUse::String => "String".to_string(), AlgebraicTypeUse::Identity => "Identity".to_string(), @@ -3183,11 +3183,7 @@ fn get_array_element_type_name(module: &ModuleDef, elem: &AlgebraicTypeUse) -> S AlgebraicTypeUse::Ref(r) => type_ref_name(module, *r), AlgebraicTypeUse::Option(nested_inner) => { // Handle optional elements in arrays like Vec> - let inner_optional_name = get_optional_type_name(module, nested_inner); - inner_optional_name - .strip_prefix("Optional") - .unwrap_or(&inner_optional_name) - .to_string() + get_optional_type_name(module, nested_inner) } _ => "Unknown".to_string(), } @@ -3271,15 +3267,15 @@ fn generate_optional_type(optional_name: &str, module: &ModuleDef, api_macro: &s let module_name_pascal = module_name.to_case(Case::Pascal); format!("F{module_name_pascal}{inner_type_str}") } - _ if inner_type_str.starts_with("Vec") => { - // Handle OptionalVecXxx -> should use TArray - let elem_type_str = &inner_type_str[3..]; // Remove "Vec" prefix + _ if inner_type_str.starts_with("VecOptional") => { + // Handle specific optional array types like OptionalVecOptionalI32, OptionalVecOptionalString, etc. + let elem_type_str = &inner_type_str[11..]; // Remove "VecOptional" prefix let module_name_pascal = module_name.to_case(Case::Pascal); format!("TArray") } - _ if inner_type_str.starts_with("OptionalVec") => { - // Handle specific optional array types like OptionalVecI32, OptionalVecString, etc. - let elem_type_str = &inner_type_str[11..]; // Remove "OptionalVec" prefix + _ if inner_type_str.starts_with("Vec") => { + // Handle OptionalVecXxx -> should use TArray + let elem_type_str = &inner_type_str[3..]; // Remove "Vec" prefix let cpp_elem_type = get_cpp_type_for_array_element(elem_type_str, module, module_name); format!("TArray<{cpp_elem_type}>") } @@ -3295,7 +3291,7 @@ fn generate_optional_type(optional_name: &str, module: &ModuleDef, api_macro: &s type_name == inner_type_str && matches!( module.typespace_for_generate()[type_def.ty], - AlgebraicTypeDef::Sum(_) | AlgebraicTypeDef::PlainEnum(_) + AlgebraicTypeDef::PlainEnum(_) ) }); @@ -3321,41 +3317,10 @@ fn generate_optional_type(optional_name: &str, module: &ModuleDef, api_macro: &s // Basic types, no extra includes needed } _ if inner_type_str.starts_with("Vec") => { - // Handle OptionalVecXxx -> needs OptionalXxx include - let elem_type_str = &inner_type_str[3..]; // Remove "Vec" prefix - let module_name_pascal = module_name.to_case(Case::Pascal); - extra_includes.push(format!( - "ModuleBindings/Optionals/{module_name_pascal}Optional{elem_type_str}.g.h" - )); + // Not required } _ if inner_type_str.starts_with("OptionalVec") => { - // Handle includes for specific optional array types - let elem_type_str = &inner_type_str[11..]; // Remove "OptionalVec" prefix - match elem_type_str { - "Identity" | "ConnectionId" | "Timestamp" | "TimeDuration" | "ScheduleAt" => { - extra_includes.push("Types/Builtins.h".to_string()); - } - "I128" | "U128" | "I256" | "U256" => { - extra_includes.push("Types/LargeIntegers.h".to_string()); - } - _ if elem_type_str.starts_with("Int32") => { - // Handle nested optional includes like Int32 from OptionalInt32 - let module_name_pascal = module_name.to_case(Case::Pascal); - extra_includes.push(format!( - "ModuleBindings/Optionals/{module_name_pascal}OptionalInt32.g.h" - )); - } - _ if !elem_type_str.starts_with("Bool") - && !elem_type_str.starts_with("I") - && !elem_type_str.starts_with("U") - && !elem_type_str.starts_with("F") - && elem_type_str != "String" => - { - // Custom type, need its header - extra_includes.push(format!("ModuleBindings/Types/{elem_type_str}Type.g.h")); - } - _ => {} // Primitive types don't need extra includes - } + // Not required } _ if inner_type_str.starts_with("Optional") => { // Nested optional, need its header diff --git a/sdks/unreal/tests/TestClient/Source/TestClient/Private/ModuleBindings/SpacetimeDBClient.g.cpp b/sdks/unreal/tests/TestClient/Source/TestClient/Private/ModuleBindings/SpacetimeDBClient.g.cpp index 0c3b70abc29..96501d29030 100644 --- a/sdks/unreal/tests/TestClient/Source/TestClient/Private/ModuleBindings/SpacetimeDBClient.g.cpp +++ b/sdks/unreal/tests/TestClient/Source/TestClient/Private/ModuleBindings/SpacetimeDBClient.g.cpp @@ -4,104 +4,104 @@ #include "ModuleBindings/SpacetimeDBClient.g.h" #include "DBCache/WithBsatn.h" #include "BSATN/UEBSATNHelpers.h" -#include "ModuleBindings/Tables/UsersTable.g.h" -#include "ModuleBindings/Tables/UniqueI32Table.g.h" -#include "ModuleBindings/Tables/OneTimestampTable.g.h" -#include "ModuleBindings/Tables/VecI8Table.g.h" -#include "ModuleBindings/Tables/BTreeU32Table.g.h" -#include "ModuleBindings/Tables/VecByteStructTable.g.h" -#include "ModuleBindings/Tables/VecI256Table.g.h" -#include "ModuleBindings/Tables/VecU16Table.g.h" +#include "ModuleBindings/Tables/IndexedSimpleEnumTable.g.h" +#include "ModuleBindings/Tables/OneU128Table.g.h" +#include "ModuleBindings/Tables/UniqueI8Table.g.h" #include "ModuleBindings/Tables/UniqueU16Table.g.h" -#include "ModuleBindings/Tables/VecI128Table.g.h" -#include "ModuleBindings/Tables/ScheduledTableTable.g.h" -#include "ModuleBindings/Tables/VecI16Table.g.h" -#include "ModuleBindings/Tables/PkBoolTable.g.h" -#include "ModuleBindings/Tables/OneI32Table.g.h" -#include "ModuleBindings/Tables/VecU256Table.g.h" -#include "ModuleBindings/Tables/OneEveryPrimitiveStructTable.g.h" -#include "ModuleBindings/Tables/PkI64Table.g.h" -#include "ModuleBindings/Tables/PkI32Table.g.h" -#include "ModuleBindings/Tables/OneStringTable.g.h" -#include "ModuleBindings/Tables/OptionStringTable.g.h" -#include "ModuleBindings/Tables/OneUnitStructTable.g.h" -#include "ModuleBindings/Tables/PkIdentityTable.g.h" +#include "ModuleBindings/Tables/BTreeU32Table.g.h" +#include "ModuleBindings/Tables/IndexedTableTable.g.h" #include "ModuleBindings/Tables/OptionEveryPrimitiveStructTable.g.h" -#include "ModuleBindings/Tables/UniqueI64Table.g.h" -#include "ModuleBindings/Tables/PkU128Table.g.h" -#include "ModuleBindings/Tables/PkConnectionIdTable.g.h" -#include "ModuleBindings/Tables/VecConnectionIdTable.g.h" -#include "ModuleBindings/Tables/VecI32Table.g.h" -#include "ModuleBindings/Tables/VecTimestampTable.g.h" -#include "ModuleBindings/Tables/OneEveryVecStructTable.g.h" -#include "ModuleBindings/Tables/OneF32Table.g.h" -#include "ModuleBindings/Tables/OneU8Table.g.h" +#include "ModuleBindings/Tables/UniqueI256Table.g.h" #include "ModuleBindings/Tables/OptionVecOptionI32Table.g.h" +#include "ModuleBindings/Tables/UniqueU128Table.g.h" +#include "ModuleBindings/Tables/VecI128Table.g.h" +#include "ModuleBindings/Tables/OneByteStructTable.g.h" +#include "ModuleBindings/Tables/VecI32Table.g.h" +#include "ModuleBindings/Tables/PkU32Table.g.h" +#include "ModuleBindings/Tables/PkStringTable.g.h" +#include "ModuleBindings/Tables/PkU128Table.g.h" +#include "ModuleBindings/Tables/VecU64Table.g.h" +#include "ModuleBindings/Tables/IndexedTable2Table.g.h" +#include "ModuleBindings/Tables/VecF64Table.g.h" #include "ModuleBindings/Tables/OneU256Table.g.h" -#include "ModuleBindings/Tables/UniqueIdentityTable.g.h" -#include "ModuleBindings/Tables/VecStringTable.g.h" -#include "ModuleBindings/Tables/UniqueConnectionIdTable.g.h" +#include "ModuleBindings/Tables/OneEveryPrimitiveStructTable.g.h" +#include "ModuleBindings/Tables/UniqueI128Table.g.h" +#include "ModuleBindings/Tables/VecI64Table.g.h" +#include "ModuleBindings/Tables/PkU8Table.g.h" +#include "ModuleBindings/Tables/OneF64Table.g.h" #include "ModuleBindings/Tables/VecU32Table.g.h" -#include "ModuleBindings/Tables/LargeTableTable.g.h" -#include "ModuleBindings/Tables/VecUnitStructTable.g.h" -#include "ModuleBindings/Tables/OneI8Table.g.h" #include "ModuleBindings/Tables/PkI16Table.g.h" -#include "ModuleBindings/Tables/UniqueI16Table.g.h" -#include "ModuleBindings/Tables/UniqueU8Table.g.h" -#include "ModuleBindings/Tables/OptionIdentityTable.g.h" -#include "ModuleBindings/Tables/UniqueI256Table.g.h" -#include "ModuleBindings/Tables/IndexedTableTable.g.h" -#include "ModuleBindings/Tables/VecEveryVecStructTable.g.h" -#include "ModuleBindings/Tables/VecU128Table.g.h" -#include "ModuleBindings/Tables/UniqueU256Table.g.h" -#include "ModuleBindings/Tables/VecF32Table.g.h" -#include "ModuleBindings/Tables/VecIdentityTable.g.h" -#include "ModuleBindings/Tables/OptionI32Table.g.h" -#include "ModuleBindings/Tables/OneU16Table.g.h" +#include "ModuleBindings/Tables/PkI32Table.g.h" #include "ModuleBindings/Tables/OneU32Table.g.h" -#include "ModuleBindings/Tables/OneI128Table.g.h" -#include "ModuleBindings/Tables/UniqueI128Table.g.h" -#include "ModuleBindings/Tables/UniqueU128Table.g.h" -#include "ModuleBindings/Tables/PkI8Table.g.h" +#include "ModuleBindings/Tables/UniqueBoolTable.g.h" +#include "ModuleBindings/Tables/OneSimpleEnumTable.g.h" +#include "ModuleBindings/Tables/VecI16Table.g.h" +#include "ModuleBindings/Tables/UniqueIdentityTable.g.h" +#include "ModuleBindings/Tables/PkU32TwoTable.g.h" +#include "ModuleBindings/Tables/VecBoolTable.g.h" #include "ModuleBindings/Tables/OneEnumWithPayloadTable.g.h" -#include "ModuleBindings/Tables/OneByteStructTable.g.h" -#include "ModuleBindings/Tables/PkU64Table.g.h" -#include "ModuleBindings/Tables/PkU256Table.g.h" -#include "ModuleBindings/Tables/TableHoldsTableTable.g.h" -#include "ModuleBindings/Tables/VecI64Table.g.h" -#include "ModuleBindings/Tables/UniqueU64Table.g.h" -#include "ModuleBindings/Tables/VecU8Table.g.h" -#include "ModuleBindings/Tables/OneF64Table.g.h" -#include "ModuleBindings/Tables/OneBoolTable.g.h" -#include "ModuleBindings/Tables/VecEnumWithPayloadTable.g.h" -#include "ModuleBindings/Tables/OneConnectionIdTable.g.h" +#include "ModuleBindings/Tables/UniqueI32Table.g.h" +#include "ModuleBindings/Tables/OneIdentityTable.g.h" +#include "ModuleBindings/Tables/OneUnitStructTable.g.h" +#include "ModuleBindings/Tables/UsersTable.g.h" +#include "ModuleBindings/Tables/VecConnectionIdTable.g.h" #include "ModuleBindings/Tables/PkI128Table.g.h" -#include "ModuleBindings/Tables/OneU128Table.g.h" -#include "ModuleBindings/Tables/IndexedTable2Table.g.h" +#include "ModuleBindings/Tables/PkSimpleEnumTable.g.h" +#include "ModuleBindings/Tables/PkI64Table.g.h" +#include "ModuleBindings/Tables/OneI128Table.g.h" +#include "ModuleBindings/Tables/PkConnectionIdTable.g.h" #include "ModuleBindings/Tables/OneI16Table.g.h" -#include "ModuleBindings/Tables/OneI64Table.g.h" +#include "ModuleBindings/Tables/LargeTableTable.g.h" +#include "ModuleBindings/Tables/VecEnumWithPayloadTable.g.h" +#include "ModuleBindings/Tables/OneI32Table.g.h" +#include "ModuleBindings/Tables/VecF32Table.g.h" +#include "ModuleBindings/Tables/UniqueConnectionIdTable.g.h" #include "ModuleBindings/Tables/OptionSimpleEnumTable.g.h" -#include "ModuleBindings/Tables/PkSimpleEnumTable.g.h" -#include "ModuleBindings/Tables/PkStringTable.g.h" -#include "ModuleBindings/Tables/PkU8Table.g.h" -#include "ModuleBindings/Tables/UniqueBoolTable.g.h" -#include "ModuleBindings/Tables/VecBoolTable.g.h" +#include "ModuleBindings/Tables/VecI256Table.g.h" +#include "ModuleBindings/Tables/VecTimestampTable.g.h" +#include "ModuleBindings/Tables/PkI8Table.g.h" +#include "ModuleBindings/Tables/OneI256Table.g.h" +#include "ModuleBindings/Tables/ScheduledTableTable.g.h" +#include "ModuleBindings/Tables/UniqueU256Table.g.h" +#include "ModuleBindings/Tables/OneStringTable.g.h" +#include "ModuleBindings/Tables/VecU16Table.g.h" +#include "ModuleBindings/Tables/VecU256Table.g.h" +#include "ModuleBindings/Tables/OptionStringTable.g.h" +#include "ModuleBindings/Tables/OneU16Table.g.h" +#include "ModuleBindings/Tables/PkIdentityTable.g.h" +#include "ModuleBindings/Tables/PkU64Table.g.h" +#include "ModuleBindings/Tables/UniqueI16Table.g.h" #include "ModuleBindings/Tables/VecEveryPrimitiveStructTable.g.h" -#include "ModuleBindings/Tables/IndexedSimpleEnumTable.g.h" -#include "ModuleBindings/Tables/PkU32Table.g.h" -#include "ModuleBindings/Tables/UniqueI8Table.g.h" -#include "ModuleBindings/Tables/UniqueStringTable.g.h" -#include "ModuleBindings/Tables/VecF64Table.g.h" +#include "ModuleBindings/Tables/VecU128Table.g.h" +#include "ModuleBindings/Tables/OneI8Table.g.h" +#include "ModuleBindings/Tables/UniqueU8Table.g.h" +#include "ModuleBindings/Tables/VecByteStructTable.g.h" #include "ModuleBindings/Tables/OneU64Table.g.h" -#include "ModuleBindings/Tables/OneIdentityTable.g.h" -#include "ModuleBindings/Tables/PkU16Table.g.h" +#include "ModuleBindings/Tables/TableHoldsTableTable.g.h" +#include "ModuleBindings/Tables/UniqueStringTable.g.h" +#include "ModuleBindings/Tables/OneTimestampTable.g.h" #include "ModuleBindings/Tables/VecSimpleEnumTable.g.h" -#include "ModuleBindings/Tables/OneSimpleEnumTable.g.h" -#include "ModuleBindings/Tables/PkU32TwoTable.g.h" -#include "ModuleBindings/Tables/UniqueU32Table.g.h" -#include "ModuleBindings/Tables/VecU64Table.g.h" +#include "ModuleBindings/Tables/OptionI32Table.g.h" +#include "ModuleBindings/Tables/PkBoolTable.g.h" #include "ModuleBindings/Tables/PkI256Table.g.h" -#include "ModuleBindings/Tables/OneI256Table.g.h" +#include "ModuleBindings/Tables/OneEveryVecStructTable.g.h" +#include "ModuleBindings/Tables/UniqueI64Table.g.h" +#include "ModuleBindings/Tables/UniqueU32Table.g.h" +#include "ModuleBindings/Tables/PkU16Table.g.h" +#include "ModuleBindings/Tables/OneConnectionIdTable.g.h" +#include "ModuleBindings/Tables/OneI64Table.g.h" +#include "ModuleBindings/Tables/UniqueU64Table.g.h" +#include "ModuleBindings/Tables/PkU256Table.g.h" +#include "ModuleBindings/Tables/VecU8Table.g.h" +#include "ModuleBindings/Tables/VecIdentityTable.g.h" +#include "ModuleBindings/Tables/OneU8Table.g.h" +#include "ModuleBindings/Tables/VecEveryVecStructTable.g.h" +#include "ModuleBindings/Tables/VecUnitStructTable.g.h" +#include "ModuleBindings/Tables/OneBoolTable.g.h" +#include "ModuleBindings/Tables/VecStringTable.g.h" +#include "ModuleBindings/Tables/OneF32Table.g.h" +#include "ModuleBindings/Tables/OptionIdentityTable.g.h" +#include "ModuleBindings/Tables/VecI8Table.g.h" static FReducer DecodeReducer(const FReducerEvent& Event) { @@ -1201,104 +1201,104 @@ UDbConnection::UDbConnection(const FObjectInitializer& ObjectInitializer) : Supe Reducers->SetCallReducerFlags = SetReducerFlags; Reducers->Conn = this; - RegisterTable(TEXT("users"), Db->Users); - RegisterTable(TEXT("unique_i32"), Db->UniqueI32); - RegisterTable(TEXT("one_timestamp"), Db->OneTimestamp); - RegisterTable(TEXT("vec_i8"), Db->VecI8); - RegisterTable(TEXT("btree_u32"), Db->BtreeU32); - RegisterTable(TEXT("vec_byte_struct"), Db->VecByteStruct); - RegisterTable(TEXT("vec_i256"), Db->VecI256); - RegisterTable(TEXT("vec_u16"), Db->VecU16); + RegisterTable(TEXT("indexed_simple_enum"), Db->IndexedSimpleEnum); + RegisterTable(TEXT("one_u128"), Db->OneU128); + RegisterTable(TEXT("unique_i8"), Db->UniqueI8); RegisterTable(TEXT("unique_u16"), Db->UniqueU16); - RegisterTable(TEXT("vec_i128"), Db->VecI128); - RegisterTable(TEXT("scheduled_table"), Db->ScheduledTable); - RegisterTable(TEXT("vec_i16"), Db->VecI16); - RegisterTable(TEXT("pk_bool"), Db->PkBool); - RegisterTable(TEXT("one_i32"), Db->OneI32); - RegisterTable(TEXT("vec_u256"), Db->VecU256); - RegisterTable(TEXT("one_every_primitive_struct"), Db->OneEveryPrimitiveStruct); - RegisterTable(TEXT("pk_i64"), Db->PkI64); - RegisterTable(TEXT("pk_i32"), Db->PkI32); - RegisterTable(TEXT("one_string"), Db->OneString); - RegisterTable(TEXT("option_string"), Db->OptionString); - RegisterTable(TEXT("one_unit_struct"), Db->OneUnitStruct); - RegisterTable(TEXT("pk_identity"), Db->PkIdentity); + RegisterTable(TEXT("btree_u32"), Db->BtreeU32); + RegisterTable(TEXT("indexed_table"), Db->IndexedTable); RegisterTable(TEXT("option_every_primitive_struct"), Db->OptionEveryPrimitiveStruct); - RegisterTable(TEXT("unique_i64"), Db->UniqueI64); - RegisterTable(TEXT("pk_u128"), Db->PkU128); - RegisterTable(TEXT("pk_connection_id"), Db->PkConnectionId); - RegisterTable(TEXT("vec_connection_id"), Db->VecConnectionId); - RegisterTable(TEXT("vec_i32"), Db->VecI32); - RegisterTable(TEXT("vec_timestamp"), Db->VecTimestamp); - RegisterTable(TEXT("one_every_vec_struct"), Db->OneEveryVecStruct); - RegisterTable(TEXT("one_f32"), Db->OneF32); - RegisterTable(TEXT("one_u8"), Db->OneU8); + RegisterTable(TEXT("unique_i256"), Db->UniqueI256); RegisterTable(TEXT("option_vec_option_i32"), Db->OptionVecOptionI32); + RegisterTable(TEXT("unique_u128"), Db->UniqueU128); + RegisterTable(TEXT("vec_i128"), Db->VecI128); + RegisterTable(TEXT("one_byte_struct"), Db->OneByteStruct); + RegisterTable(TEXT("vec_i32"), Db->VecI32); + RegisterTable(TEXT("pk_u32"), Db->PkU32); + RegisterTable(TEXT("pk_string"), Db->PkString); + RegisterTable(TEXT("pk_u128"), Db->PkU128); + RegisterTable(TEXT("vec_u64"), Db->VecU64); + RegisterTable(TEXT("indexed_table_2"), Db->IndexedTable2); + RegisterTable(TEXT("vec_f64"), Db->VecF64); RegisterTable(TEXT("one_u256"), Db->OneU256); - RegisterTable(TEXT("unique_identity"), Db->UniqueIdentity); - RegisterTable(TEXT("vec_string"), Db->VecString); - RegisterTable(TEXT("unique_connection_id"), Db->UniqueConnectionId); + RegisterTable(TEXT("one_every_primitive_struct"), Db->OneEveryPrimitiveStruct); + RegisterTable(TEXT("unique_i128"), Db->UniqueI128); + RegisterTable(TEXT("vec_i64"), Db->VecI64); + RegisterTable(TEXT("pk_u8"), Db->PkU8); + RegisterTable(TEXT("one_f64"), Db->OneF64); RegisterTable(TEXT("vec_u32"), Db->VecU32); - RegisterTable(TEXT("large_table"), Db->LargeTable); - RegisterTable(TEXT("vec_unit_struct"), Db->VecUnitStruct); - RegisterTable(TEXT("one_i8"), Db->OneI8); RegisterTable(TEXT("pk_i16"), Db->PkI16); - RegisterTable(TEXT("unique_i16"), Db->UniqueI16); - RegisterTable(TEXT("unique_u8"), Db->UniqueU8); - RegisterTable(TEXT("option_identity"), Db->OptionIdentity); - RegisterTable(TEXT("unique_i256"), Db->UniqueI256); - RegisterTable(TEXT("indexed_table"), Db->IndexedTable); - RegisterTable(TEXT("vec_every_vec_struct"), Db->VecEveryVecStruct); - RegisterTable(TEXT("vec_u128"), Db->VecU128); - RegisterTable(TEXT("unique_u256"), Db->UniqueU256); - RegisterTable(TEXT("vec_f32"), Db->VecF32); - RegisterTable(TEXT("vec_identity"), Db->VecIdentity); - RegisterTable(TEXT("option_i32"), Db->OptionI32); - RegisterTable(TEXT("one_u16"), Db->OneU16); + RegisterTable(TEXT("pk_i32"), Db->PkI32); RegisterTable(TEXT("one_u32"), Db->OneU32); - RegisterTable(TEXT("one_i128"), Db->OneI128); - RegisterTable(TEXT("unique_i128"), Db->UniqueI128); - RegisterTable(TEXT("unique_u128"), Db->UniqueU128); - RegisterTable(TEXT("pk_i8"), Db->PkI8); + RegisterTable(TEXT("unique_bool"), Db->UniqueBool); + RegisterTable(TEXT("one_simple_enum"), Db->OneSimpleEnum); + RegisterTable(TEXT("vec_i16"), Db->VecI16); + RegisterTable(TEXT("unique_identity"), Db->UniqueIdentity); + RegisterTable(TEXT("pk_u32_two"), Db->PkU32Two); + RegisterTable(TEXT("vec_bool"), Db->VecBool); RegisterTable(TEXT("one_enum_with_payload"), Db->OneEnumWithPayload); - RegisterTable(TEXT("one_byte_struct"), Db->OneByteStruct); - RegisterTable(TEXT("pk_u64"), Db->PkU64); - RegisterTable(TEXT("pk_u256"), Db->PkU256); - RegisterTable(TEXT("table_holds_table"), Db->TableHoldsTable); - RegisterTable(TEXT("vec_i64"), Db->VecI64); - RegisterTable(TEXT("unique_u64"), Db->UniqueU64); - RegisterTable(TEXT("vec_u8"), Db->VecU8); - RegisterTable(TEXT("one_f64"), Db->OneF64); - RegisterTable(TEXT("one_bool"), Db->OneBool); - RegisterTable(TEXT("vec_enum_with_payload"), Db->VecEnumWithPayload); - RegisterTable(TEXT("one_connection_id"), Db->OneConnectionId); + RegisterTable(TEXT("unique_i32"), Db->UniqueI32); + RegisterTable(TEXT("one_identity"), Db->OneIdentity); + RegisterTable(TEXT("one_unit_struct"), Db->OneUnitStruct); + RegisterTable(TEXT("users"), Db->Users); + RegisterTable(TEXT("vec_connection_id"), Db->VecConnectionId); RegisterTable(TEXT("pk_i128"), Db->PkI128); - RegisterTable(TEXT("one_u128"), Db->OneU128); - RegisterTable(TEXT("indexed_table_2"), Db->IndexedTable2); + RegisterTable(TEXT("pk_simple_enum"), Db->PkSimpleEnum); + RegisterTable(TEXT("pk_i64"), Db->PkI64); + RegisterTable(TEXT("one_i128"), Db->OneI128); + RegisterTable(TEXT("pk_connection_id"), Db->PkConnectionId); RegisterTable(TEXT("one_i16"), Db->OneI16); - RegisterTable(TEXT("one_i64"), Db->OneI64); + RegisterTable(TEXT("large_table"), Db->LargeTable); + RegisterTable(TEXT("vec_enum_with_payload"), Db->VecEnumWithPayload); + RegisterTable(TEXT("one_i32"), Db->OneI32); + RegisterTable(TEXT("vec_f32"), Db->VecF32); + RegisterTable(TEXT("unique_connection_id"), Db->UniqueConnectionId); RegisterTable(TEXT("option_simple_enum"), Db->OptionSimpleEnum); - RegisterTable(TEXT("pk_simple_enum"), Db->PkSimpleEnum); - RegisterTable(TEXT("pk_string"), Db->PkString); - RegisterTable(TEXT("pk_u8"), Db->PkU8); - RegisterTable(TEXT("unique_bool"), Db->UniqueBool); - RegisterTable(TEXT("vec_bool"), Db->VecBool); + RegisterTable(TEXT("vec_i256"), Db->VecI256); + RegisterTable(TEXT("vec_timestamp"), Db->VecTimestamp); + RegisterTable(TEXT("pk_i8"), Db->PkI8); + RegisterTable(TEXT("one_i256"), Db->OneI256); + RegisterTable(TEXT("scheduled_table"), Db->ScheduledTable); + RegisterTable(TEXT("unique_u256"), Db->UniqueU256); + RegisterTable(TEXT("one_string"), Db->OneString); + RegisterTable(TEXT("vec_u16"), Db->VecU16); + RegisterTable(TEXT("vec_u256"), Db->VecU256); + RegisterTable(TEXT("option_string"), Db->OptionString); + RegisterTable(TEXT("one_u16"), Db->OneU16); + RegisterTable(TEXT("pk_identity"), Db->PkIdentity); + RegisterTable(TEXT("pk_u64"), Db->PkU64); + RegisterTable(TEXT("unique_i16"), Db->UniqueI16); RegisterTable(TEXT("vec_every_primitive_struct"), Db->VecEveryPrimitiveStruct); - RegisterTable(TEXT("indexed_simple_enum"), Db->IndexedSimpleEnum); - RegisterTable(TEXT("pk_u32"), Db->PkU32); - RegisterTable(TEXT("unique_i8"), Db->UniqueI8); - RegisterTable(TEXT("unique_string"), Db->UniqueString); - RegisterTable(TEXT("vec_f64"), Db->VecF64); + RegisterTable(TEXT("vec_u128"), Db->VecU128); + RegisterTable(TEXT("one_i8"), Db->OneI8); + RegisterTable(TEXT("unique_u8"), Db->UniqueU8); + RegisterTable(TEXT("vec_byte_struct"), Db->VecByteStruct); RegisterTable(TEXT("one_u64"), Db->OneU64); - RegisterTable(TEXT("one_identity"), Db->OneIdentity); - RegisterTable(TEXT("pk_u16"), Db->PkU16); + RegisterTable(TEXT("table_holds_table"), Db->TableHoldsTable); + RegisterTable(TEXT("unique_string"), Db->UniqueString); + RegisterTable(TEXT("one_timestamp"), Db->OneTimestamp); RegisterTable(TEXT("vec_simple_enum"), Db->VecSimpleEnum); - RegisterTable(TEXT("one_simple_enum"), Db->OneSimpleEnum); - RegisterTable(TEXT("pk_u32_two"), Db->PkU32Two); - RegisterTable(TEXT("unique_u32"), Db->UniqueU32); - RegisterTable(TEXT("vec_u64"), Db->VecU64); + RegisterTable(TEXT("option_i32"), Db->OptionI32); + RegisterTable(TEXT("pk_bool"), Db->PkBool); RegisterTable(TEXT("pk_i256"), Db->PkI256); - RegisterTable(TEXT("one_i256"), Db->OneI256); + RegisterTable(TEXT("one_every_vec_struct"), Db->OneEveryVecStruct); + RegisterTable(TEXT("unique_i64"), Db->UniqueI64); + RegisterTable(TEXT("unique_u32"), Db->UniqueU32); + RegisterTable(TEXT("pk_u16"), Db->PkU16); + RegisterTable(TEXT("one_connection_id"), Db->OneConnectionId); + RegisterTable(TEXT("one_i64"), Db->OneI64); + RegisterTable(TEXT("unique_u64"), Db->UniqueU64); + RegisterTable(TEXT("pk_u256"), Db->PkU256); + RegisterTable(TEXT("vec_u8"), Db->VecU8); + RegisterTable(TEXT("vec_identity"), Db->VecIdentity); + RegisterTable(TEXT("one_u8"), Db->OneU8); + RegisterTable(TEXT("vec_every_vec_struct"), Db->VecEveryVecStruct); + RegisterTable(TEXT("vec_unit_struct"), Db->VecUnitStruct); + RegisterTable(TEXT("one_bool"), Db->OneBool); + RegisterTable(TEXT("vec_string"), Db->VecString); + RegisterTable(TEXT("one_f32"), Db->OneF32); + RegisterTable(TEXT("option_identity"), Db->OptionIdentity); + RegisterTable(TEXT("vec_i8"), Db->VecI8); } FContextBase::FContextBase(UDbConnection* InConn) @@ -1333,205 +1333,205 @@ void URemoteTables::Initialize() { /** Creating tables */ - Users = NewObject(this); - UniqueI32 = NewObject(this); - OneTimestamp = NewObject(this); - VecI8 = NewObject(this); - BtreeU32 = NewObject(this); - VecByteStruct = NewObject(this); - VecI256 = NewObject(this); - VecU16 = NewObject(this); + IndexedSimpleEnum = NewObject(this); + OneU128 = NewObject(this); + UniqueI8 = NewObject(this); UniqueU16 = NewObject(this); - VecI128 = NewObject(this); - ScheduledTable = NewObject(this); - VecI16 = NewObject(this); - PkBool = NewObject(this); - OneI32 = NewObject(this); - VecU256 = NewObject(this); - OneEveryPrimitiveStruct = NewObject(this); - PkI64 = NewObject(this); - PkI32 = NewObject(this); - OneString = NewObject(this); - OptionString = NewObject(this); - OneUnitStruct = NewObject(this); - PkIdentity = NewObject(this); + BtreeU32 = NewObject(this); + IndexedTable = NewObject(this); OptionEveryPrimitiveStruct = NewObject(this); - UniqueI64 = NewObject(this); - PkU128 = NewObject(this); - PkConnectionId = NewObject(this); - VecConnectionId = NewObject(this); - VecI32 = NewObject(this); - VecTimestamp = NewObject(this); - OneEveryVecStruct = NewObject(this); - OneF32 = NewObject(this); - OneU8 = NewObject(this); + UniqueI256 = NewObject(this); OptionVecOptionI32 = NewObject(this); + UniqueU128 = NewObject(this); + VecI128 = NewObject(this); + OneByteStruct = NewObject(this); + VecI32 = NewObject(this); + PkU32 = NewObject(this); + PkString = NewObject(this); + PkU128 = NewObject(this); + VecU64 = NewObject(this); + IndexedTable2 = NewObject(this); + VecF64 = NewObject(this); OneU256 = NewObject(this); - UniqueIdentity = NewObject(this); - VecString = NewObject(this); - UniqueConnectionId = NewObject(this); + OneEveryPrimitiveStruct = NewObject(this); + UniqueI128 = NewObject(this); + VecI64 = NewObject(this); + PkU8 = NewObject(this); + OneF64 = NewObject(this); VecU32 = NewObject(this); - LargeTable = NewObject(this); - VecUnitStruct = NewObject(this); - OneI8 = NewObject(this); PkI16 = NewObject(this); - UniqueI16 = NewObject(this); - UniqueU8 = NewObject(this); - OptionIdentity = NewObject(this); - UniqueI256 = NewObject(this); - IndexedTable = NewObject(this); - VecEveryVecStruct = NewObject(this); - VecU128 = NewObject(this); - UniqueU256 = NewObject(this); - VecF32 = NewObject(this); - VecIdentity = NewObject(this); - OptionI32 = NewObject(this); - OneU16 = NewObject(this); + PkI32 = NewObject(this); OneU32 = NewObject(this); - OneI128 = NewObject(this); - UniqueI128 = NewObject(this); - UniqueU128 = NewObject(this); - PkI8 = NewObject(this); + UniqueBool = NewObject(this); + OneSimpleEnum = NewObject(this); + VecI16 = NewObject(this); + UniqueIdentity = NewObject(this); + PkU32Two = NewObject(this); + VecBool = NewObject(this); OneEnumWithPayload = NewObject(this); - OneByteStruct = NewObject(this); - PkU64 = NewObject(this); - PkU256 = NewObject(this); - TableHoldsTable = NewObject(this); - VecI64 = NewObject(this); - UniqueU64 = NewObject(this); - VecU8 = NewObject(this); - OneF64 = NewObject(this); - OneBool = NewObject(this); - VecEnumWithPayload = NewObject(this); - OneConnectionId = NewObject(this); + UniqueI32 = NewObject(this); + OneIdentity = NewObject(this); + OneUnitStruct = NewObject(this); + Users = NewObject(this); + VecConnectionId = NewObject(this); PkI128 = NewObject(this); - OneU128 = NewObject(this); - IndexedTable2 = NewObject(this); + PkSimpleEnum = NewObject(this); + PkI64 = NewObject(this); + OneI128 = NewObject(this); + PkConnectionId = NewObject(this); OneI16 = NewObject(this); - OneI64 = NewObject(this); + LargeTable = NewObject(this); + VecEnumWithPayload = NewObject(this); + OneI32 = NewObject(this); + VecF32 = NewObject(this); + UniqueConnectionId = NewObject(this); OptionSimpleEnum = NewObject(this); - PkSimpleEnum = NewObject(this); - PkString = NewObject(this); - PkU8 = NewObject(this); - UniqueBool = NewObject(this); - VecBool = NewObject(this); + VecI256 = NewObject(this); + VecTimestamp = NewObject(this); + PkI8 = NewObject(this); + OneI256 = NewObject(this); + ScheduledTable = NewObject(this); + UniqueU256 = NewObject(this); + OneString = NewObject(this); + VecU16 = NewObject(this); + VecU256 = NewObject(this); + OptionString = NewObject(this); + OneU16 = NewObject(this); + PkIdentity = NewObject(this); + PkU64 = NewObject(this); + UniqueI16 = NewObject(this); VecEveryPrimitiveStruct = NewObject(this); - IndexedSimpleEnum = NewObject(this); - PkU32 = NewObject(this); - UniqueI8 = NewObject(this); - UniqueString = NewObject(this); - VecF64 = NewObject(this); + VecU128 = NewObject(this); + OneI8 = NewObject(this); + UniqueU8 = NewObject(this); + VecByteStruct = NewObject(this); OneU64 = NewObject(this); - OneIdentity = NewObject(this); - PkU16 = NewObject(this); + TableHoldsTable = NewObject(this); + UniqueString = NewObject(this); + OneTimestamp = NewObject(this); VecSimpleEnum = NewObject(this); - OneSimpleEnum = NewObject(this); - PkU32Two = NewObject(this); - UniqueU32 = NewObject(this); - VecU64 = NewObject(this); + OptionI32 = NewObject(this); + PkBool = NewObject(this); PkI256 = NewObject(this); - OneI256 = NewObject(this); + OneEveryVecStruct = NewObject(this); + UniqueI64 = NewObject(this); + UniqueU32 = NewObject(this); + PkU16 = NewObject(this); + OneConnectionId = NewObject(this); + OneI64 = NewObject(this); + UniqueU64 = NewObject(this); + PkU256 = NewObject(this); + VecU8 = NewObject(this); + VecIdentity = NewObject(this); + OneU8 = NewObject(this); + VecEveryVecStruct = NewObject(this); + VecUnitStruct = NewObject(this); + OneBool = NewObject(this); + VecString = NewObject(this); + OneF32 = NewObject(this); + OptionIdentity = NewObject(this); + VecI8 = NewObject(this); /**/ /** Initialization */ - Users->PostInitialize(); - UniqueI32->PostInitialize(); - OneTimestamp->PostInitialize(); - VecI8->PostInitialize(); - BtreeU32->PostInitialize(); - VecByteStruct->PostInitialize(); - VecI256->PostInitialize(); - VecU16->PostInitialize(); + IndexedSimpleEnum->PostInitialize(); + OneU128->PostInitialize(); + UniqueI8->PostInitialize(); UniqueU16->PostInitialize(); - VecI128->PostInitialize(); - ScheduledTable->PostInitialize(); - VecI16->PostInitialize(); - PkBool->PostInitialize(); - OneI32->PostInitialize(); - VecU256->PostInitialize(); - OneEveryPrimitiveStruct->PostInitialize(); - PkI64->PostInitialize(); - PkI32->PostInitialize(); - OneString->PostInitialize(); - OptionString->PostInitialize(); - OneUnitStruct->PostInitialize(); - PkIdentity->PostInitialize(); + BtreeU32->PostInitialize(); + IndexedTable->PostInitialize(); OptionEveryPrimitiveStruct->PostInitialize(); - UniqueI64->PostInitialize(); - PkU128->PostInitialize(); - PkConnectionId->PostInitialize(); - VecConnectionId->PostInitialize(); - VecI32->PostInitialize(); - VecTimestamp->PostInitialize(); - OneEveryVecStruct->PostInitialize(); - OneF32->PostInitialize(); - OneU8->PostInitialize(); + UniqueI256->PostInitialize(); OptionVecOptionI32->PostInitialize(); + UniqueU128->PostInitialize(); + VecI128->PostInitialize(); + OneByteStruct->PostInitialize(); + VecI32->PostInitialize(); + PkU32->PostInitialize(); + PkString->PostInitialize(); + PkU128->PostInitialize(); + VecU64->PostInitialize(); + IndexedTable2->PostInitialize(); + VecF64->PostInitialize(); OneU256->PostInitialize(); - UniqueIdentity->PostInitialize(); - VecString->PostInitialize(); - UniqueConnectionId->PostInitialize(); + OneEveryPrimitiveStruct->PostInitialize(); + UniqueI128->PostInitialize(); + VecI64->PostInitialize(); + PkU8->PostInitialize(); + OneF64->PostInitialize(); VecU32->PostInitialize(); - LargeTable->PostInitialize(); - VecUnitStruct->PostInitialize(); - OneI8->PostInitialize(); PkI16->PostInitialize(); - UniqueI16->PostInitialize(); - UniqueU8->PostInitialize(); - OptionIdentity->PostInitialize(); - UniqueI256->PostInitialize(); - IndexedTable->PostInitialize(); - VecEveryVecStruct->PostInitialize(); - VecU128->PostInitialize(); - UniqueU256->PostInitialize(); - VecF32->PostInitialize(); - VecIdentity->PostInitialize(); - OptionI32->PostInitialize(); - OneU16->PostInitialize(); + PkI32->PostInitialize(); OneU32->PostInitialize(); - OneI128->PostInitialize(); - UniqueI128->PostInitialize(); - UniqueU128->PostInitialize(); - PkI8->PostInitialize(); + UniqueBool->PostInitialize(); + OneSimpleEnum->PostInitialize(); + VecI16->PostInitialize(); + UniqueIdentity->PostInitialize(); + PkU32Two->PostInitialize(); + VecBool->PostInitialize(); OneEnumWithPayload->PostInitialize(); - OneByteStruct->PostInitialize(); - PkU64->PostInitialize(); - PkU256->PostInitialize(); - TableHoldsTable->PostInitialize(); - VecI64->PostInitialize(); - UniqueU64->PostInitialize(); - VecU8->PostInitialize(); - OneF64->PostInitialize(); - OneBool->PostInitialize(); - VecEnumWithPayload->PostInitialize(); - OneConnectionId->PostInitialize(); + UniqueI32->PostInitialize(); + OneIdentity->PostInitialize(); + OneUnitStruct->PostInitialize(); + Users->PostInitialize(); + VecConnectionId->PostInitialize(); PkI128->PostInitialize(); - OneU128->PostInitialize(); - IndexedTable2->PostInitialize(); + PkSimpleEnum->PostInitialize(); + PkI64->PostInitialize(); + OneI128->PostInitialize(); + PkConnectionId->PostInitialize(); OneI16->PostInitialize(); - OneI64->PostInitialize(); + LargeTable->PostInitialize(); + VecEnumWithPayload->PostInitialize(); + OneI32->PostInitialize(); + VecF32->PostInitialize(); + UniqueConnectionId->PostInitialize(); OptionSimpleEnum->PostInitialize(); - PkSimpleEnum->PostInitialize(); - PkString->PostInitialize(); - PkU8->PostInitialize(); - UniqueBool->PostInitialize(); - VecBool->PostInitialize(); + VecI256->PostInitialize(); + VecTimestamp->PostInitialize(); + PkI8->PostInitialize(); + OneI256->PostInitialize(); + ScheduledTable->PostInitialize(); + UniqueU256->PostInitialize(); + OneString->PostInitialize(); + VecU16->PostInitialize(); + VecU256->PostInitialize(); + OptionString->PostInitialize(); + OneU16->PostInitialize(); + PkIdentity->PostInitialize(); + PkU64->PostInitialize(); + UniqueI16->PostInitialize(); VecEveryPrimitiveStruct->PostInitialize(); - IndexedSimpleEnum->PostInitialize(); - PkU32->PostInitialize(); - UniqueI8->PostInitialize(); - UniqueString->PostInitialize(); - VecF64->PostInitialize(); + VecU128->PostInitialize(); + OneI8->PostInitialize(); + UniqueU8->PostInitialize(); + VecByteStruct->PostInitialize(); OneU64->PostInitialize(); - OneIdentity->PostInitialize(); - PkU16->PostInitialize(); + TableHoldsTable->PostInitialize(); + UniqueString->PostInitialize(); + OneTimestamp->PostInitialize(); VecSimpleEnum->PostInitialize(); - OneSimpleEnum->PostInitialize(); - PkU32Two->PostInitialize(); - UniqueU32->PostInitialize(); - VecU64->PostInitialize(); + OptionI32->PostInitialize(); + PkBool->PostInitialize(); PkI256->PostInitialize(); - OneI256->PostInitialize(); + OneEveryVecStruct->PostInitialize(); + UniqueI64->PostInitialize(); + UniqueU32->PostInitialize(); + PkU16->PostInitialize(); + OneConnectionId->PostInitialize(); + OneI64->PostInitialize(); + UniqueU64->PostInitialize(); + PkU256->PostInitialize(); + VecU8->PostInitialize(); + VecIdentity->PostInitialize(); + OneU8->PostInitialize(); + VecEveryVecStruct->PostInitialize(); + VecUnitStruct->PostInitialize(); + OneBool->PostInitialize(); + VecString->PostInitialize(); + OneF32->PostInitialize(); + OptionIdentity->PostInitialize(); + VecI8->PostInitialize(); /**/ } @@ -4593,7 +4593,7 @@ bool URemoteReducers::InvokeInsertOptionString(const FReducerEventContext& Conte return true; } -void URemoteReducers::InsertOptionVecOptionI32(const FTestClientOptionalVecInt32& V) +void URemoteReducers::InsertOptionVecOptionI32(const FTestClientOptionalVecOptionalInt32& V) { if (!Conn) { diff --git a/sdks/unreal/tests/TestClient/Source/TestClient/Private/Tests/SpacetimeFullClientTests.cpp b/sdks/unreal/tests/TestClient/Source/TestClient/Private/Tests/SpacetimeFullClientTests.cpp index 68e0025ec5f..6f4dfd86feb 100644 --- a/sdks/unreal/tests/TestClient/Source/TestClient/Private/Tests/SpacetimeFullClientTests.cpp +++ b/sdks/unreal/tests/TestClient/Source/TestClient/Private/Tests/SpacetimeFullClientTests.cpp @@ -1007,14 +1007,14 @@ bool FInsertOptionSomeTest::RunTest(const FString &Parameters) Handler->ExpectedIdentityType = FTestClientOptionalIdentity(Identity); Handler->ExpectedEnumType = FTestClientOptionalSimpleEnum(ESimpleEnumType::Zero); Handler->ExpectedEveryPrimitiveStructType = FTestClientOptionalEveryPrimitiveStruct(); - Handler->ExpectedVecOptionI32Type = FTestClientOptionalVecInt32(TArray({ FTestClientOptionalInt32(0), FTestClientOptionalInt32() })); + Handler->ExpectedVecOptionI32Type = FTestClientOptionalVecOptionalInt32(TArray({ FTestClientOptionalInt32(0), FTestClientOptionalInt32() })); Ctx.Reducers->InsertOptionI32(FTestClientOptionalInt32(0)); Ctx.Reducers->InsertOptionString(FTestClientOptionalString("string")); Ctx.Reducers->InsertOptionIdentity(FTestClientOptionalIdentity(Identity)); Ctx.Reducers->InsertOptionSimpleEnum(FTestClientOptionalSimpleEnum(ESimpleEnumType::Zero)); Ctx.Reducers->InsertOptionEveryPrimitiveStruct(FTestClientOptionalEveryPrimitiveStruct()); - Ctx.Reducers->InsertOptionVecOptionI32(FTestClientOptionalVecInt32(TArray({ FTestClientOptionalInt32(0), FTestClientOptionalInt32() }))); + Ctx.Reducers->InsertOptionVecOptionI32(FTestClientOptionalVecOptionalInt32(TArray({ FTestClientOptionalInt32(0), FTestClientOptionalInt32() }))); }); }); // Wait for the test counter to signal completion. @@ -1068,7 +1068,7 @@ bool FInsertOptionNoneTest::RunTest(const FString &Parameters) Ctx.Reducers->InsertOptionIdentity(FTestClientOptionalIdentity()); Ctx.Reducers->InsertOptionSimpleEnum(FTestClientOptionalSimpleEnum()); Ctx.Reducers->InsertOptionEveryPrimitiveStruct(FTestClientOptionalEveryPrimitiveStruct()); - Ctx.Reducers->InsertOptionVecOptionI32(FTestClientOptionalVecInt32()); + Ctx.Reducers->InsertOptionVecOptionI32(FTestClientOptionalVecOptionalInt32()); }); }); // Wait for the test counter to signal completion. diff --git a/sdks/unreal/tests/TestClient/Source/TestClient/Public/ModuleBindings/Optionals/TestClientOptionalVecInt32.g.h b/sdks/unreal/tests/TestClient/Source/TestClient/Public/ModuleBindings/Optionals/TestClientOptionalVecOptionalInt32.g.h similarity index 57% rename from sdks/unreal/tests/TestClient/Source/TestClient/Public/ModuleBindings/Optionals/TestClientOptionalVecInt32.g.h rename to sdks/unreal/tests/TestClient/Source/TestClient/Public/ModuleBindings/Optionals/TestClientOptionalVecOptionalInt32.g.h index aeb08e45ee1..5b896723bc1 100644 --- a/sdks/unreal/tests/TestClient/Source/TestClient/Public/ModuleBindings/Optionals/TestClientOptionalVecInt32.g.h +++ b/sdks/unreal/tests/TestClient/Source/TestClient/Public/ModuleBindings/Optionals/TestClientOptionalVecOptionalInt32.g.h @@ -4,11 +4,10 @@ #pragma once #include "CoreMinimal.h" #include "BSATN/UESpacetimeDB.h" -#include "ModuleBindings/Optionals/TestClientOptionalInt32.g.h" -#include "TestClientOptionalVecInt32.g.generated.h" +#include "TestClientOptionalVecOptionalInt32.g.generated.h" USTRUCT(BlueprintType) -struct TESTCLIENT_API FTestClientOptionalVecInt32 +struct TESTCLIENT_API FTestClientOptionalVecOptionalInt32 { GENERATED_BODY() @@ -18,33 +17,33 @@ struct TESTCLIENT_API FTestClientOptionalVecInt32 UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "SpacetimeDB", meta = (EditCondition = "bHasValue")) TArray Value; - FTestClientOptionalVecInt32() = default; + FTestClientOptionalVecOptionalInt32() = default; - explicit FTestClientOptionalVecInt32(const TArray& InValue) + explicit FTestClientOptionalVecOptionalInt32(const TArray& InValue) : bHasValue(true), Value(InValue) {} bool IsSet() const { return bHasValue; } void Reset() { bHasValue = false; } - FORCEINLINE bool operator==(const FTestClientOptionalVecInt32& Other) const + FORCEINLINE bool operator==(const FTestClientOptionalVecOptionalInt32& Other) const { if (bHasValue != Other.bHasValue) return false; return !bHasValue || Value == Other.Value; } - FORCEINLINE bool operator!=(const FTestClientOptionalVecInt32& Other) const + FORCEINLINE bool operator!=(const FTestClientOptionalVecOptionalInt32& Other) const { return !(*this == Other); } }; /** - * Custom hash function for FTestClientOptionalVecInt32. + * Custom hash function for FTestClientOptionalVecOptionalInt32. * Hashes the HasValue flag and the Value if present. - * @param Optional The FTestClientOptionalVecInt32 instance to hash. + * @param Optional The FTestClientOptionalVecOptionalInt32 instance to hash. * @return The combined hash value. */ -FORCEINLINE uint32 GetTypeHash(const FTestClientOptionalVecInt32& Optional) +FORCEINLINE uint32 GetTypeHash(const FTestClientOptionalVecOptionalInt32& Optional) { uint32 Hash = GetTypeHash(Optional.bHasValue); if (Optional.bHasValue) @@ -56,7 +55,7 @@ FORCEINLINE uint32 GetTypeHash(const FTestClientOptionalVecInt32& Optional) namespace UE::SpacetimeDB { - UE_SPACETIMEDB_ENABLE_TARRAY(FTestClientOptionalVecInt32); + UE_SPACETIMEDB_ENABLE_TARRAY(FTestClientOptionalVecOptionalInt32); - UE_SPACETIMEDB_OPTIONAL(FTestClientOptionalVecInt32, bHasValue, Value); + UE_SPACETIMEDB_OPTIONAL(FTestClientOptionalVecOptionalInt32, bHasValue, Value); } diff --git a/sdks/unreal/tests/TestClient/Source/TestClient/Public/ModuleBindings/Reducers/InsertOptionVecOptionI32.g.h b/sdks/unreal/tests/TestClient/Source/TestClient/Public/ModuleBindings/Reducers/InsertOptionVecOptionI32.g.h index a68ff536b24..2b9ca5ec048 100644 --- a/sdks/unreal/tests/TestClient/Source/TestClient/Public/ModuleBindings/Reducers/InsertOptionVecOptionI32.g.h +++ b/sdks/unreal/tests/TestClient/Source/TestClient/Public/ModuleBindings/Reducers/InsertOptionVecOptionI32.g.h @@ -5,7 +5,7 @@ #include "CoreMinimal.h" #include "BSATN/UESpacetimeDB.h" #include "ModuleBindings/Optionals/TestClientOptionalInt32.g.h" -#include "ModuleBindings/Optionals/TestClientOptionalVecInt32.g.h" +#include "ModuleBindings/Optionals/TestClientOptionalVecOptionalInt32.g.h" #include "ModuleBindings/ReducerBase.g.h" #include "InsertOptionVecOptionI32.g.generated.h" @@ -16,11 +16,11 @@ struct TESTCLIENT_API FInsertOptionVecOptionI32Args GENERATED_BODY() UPROPERTY(BlueprintReadWrite, Category="SpacetimeDB") - FTestClientOptionalVecInt32 V; + FTestClientOptionalVecOptionalInt32 V; FInsertOptionVecOptionI32Args() = default; - FInsertOptionVecOptionI32Args(const FTestClientOptionalVecInt32& InV) + FInsertOptionVecOptionI32Args(const FTestClientOptionalVecOptionalInt32& InV) : V(InV) {} @@ -48,7 +48,7 @@ class TESTCLIENT_API UInsertOptionVecOptionI32Reducer : public UReducerBase public: UPROPERTY(BlueprintReadOnly, Category="SpacetimeDB") - FTestClientOptionalVecInt32 V; + FTestClientOptionalVecOptionalInt32 V; }; diff --git a/sdks/unreal/tests/TestClient/Source/TestClient/Public/ModuleBindings/SpacetimeDBClient.g.h b/sdks/unreal/tests/TestClient/Source/TestClient/Public/ModuleBindings/SpacetimeDBClient.g.h index 8428edf82da..929f0949693 100644 --- a/sdks/unreal/tests/TestClient/Source/TestClient/Public/ModuleBindings/SpacetimeDBClient.g.h +++ b/sdks/unreal/tests/TestClient/Source/TestClient/Public/ModuleBindings/SpacetimeDBClient.g.h @@ -1,7 +1,7 @@ // THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE // WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. -// This was generated using spacetimedb cli version 1.4.0 (commit ddbebbcd844fa5d4626e797dce954ba3b1528a51). +// This was generated using spacetimedb cli version 1.5.0 (commit 4a57de1003025dd579c6e22bb49bd56e2aa27ff2). #pragma once #include "CoreMinimal.h" @@ -17,7 +17,7 @@ #include "ModuleBindings/Optionals/TestClientOptionalInt32.g.h" #include "ModuleBindings/Optionals/TestClientOptionalSimpleEnum.g.h" #include "ModuleBindings/Optionals/TestClientOptionalString.g.h" -#include "ModuleBindings/Optionals/TestClientOptionalVecInt32.g.h" +#include "ModuleBindings/Optionals/TestClientOptionalVecOptionalInt32.g.h" #include "ModuleBindings/ReducerBase.g.h" #include "ModuleBindings/Reducers/DeleteFromBtreeU32.g.h" #include "ModuleBindings/Reducers/DeleteLargeTable.g.h" @@ -6888,298 +6888,298 @@ class TESTCLIENT_API URemoteTables : public UObject void Initialize(); UPROPERTY(BlueprintReadOnly, Category="SpacetimeDB") - UUsersTable* Users; + UIndexedSimpleEnumTable* IndexedSimpleEnum; UPROPERTY(BlueprintReadOnly, Category="SpacetimeDB") - UUniqueI32Table* UniqueI32; + UOneU128Table* OneU128; UPROPERTY(BlueprintReadOnly, Category="SpacetimeDB") - UOneTimestampTable* OneTimestamp; + UUniqueI8Table* UniqueI8; UPROPERTY(BlueprintReadOnly, Category="SpacetimeDB") - UVecI8Table* VecI8; + UUniqueU16Table* UniqueU16; UPROPERTY(BlueprintReadOnly, Category="SpacetimeDB") UBTreeU32Table* BtreeU32; UPROPERTY(BlueprintReadOnly, Category="SpacetimeDB") - UVecByteStructTable* VecByteStruct; + UIndexedTableTable* IndexedTable; UPROPERTY(BlueprintReadOnly, Category="SpacetimeDB") - UVecI256Table* VecI256; + UOptionEveryPrimitiveStructTable* OptionEveryPrimitiveStruct; UPROPERTY(BlueprintReadOnly, Category="SpacetimeDB") - UVecU16Table* VecU16; + UUniqueI256Table* UniqueI256; UPROPERTY(BlueprintReadOnly, Category="SpacetimeDB") - UUniqueU16Table* UniqueU16; + UOptionVecOptionI32Table* OptionVecOptionI32; UPROPERTY(BlueprintReadOnly, Category="SpacetimeDB") - UVecI128Table* VecI128; + UUniqueU128Table* UniqueU128; UPROPERTY(BlueprintReadOnly, Category="SpacetimeDB") - UScheduledTableTable* ScheduledTable; + UVecI128Table* VecI128; UPROPERTY(BlueprintReadOnly, Category="SpacetimeDB") - UVecI16Table* VecI16; + UOneByteStructTable* OneByteStruct; UPROPERTY(BlueprintReadOnly, Category="SpacetimeDB") - UPkBoolTable* PkBool; + UVecI32Table* VecI32; UPROPERTY(BlueprintReadOnly, Category="SpacetimeDB") - UOneI32Table* OneI32; + UPkU32Table* PkU32; UPROPERTY(BlueprintReadOnly, Category="SpacetimeDB") - UVecU256Table* VecU256; + UPkStringTable* PkString; UPROPERTY(BlueprintReadOnly, Category="SpacetimeDB") - UOneEveryPrimitiveStructTable* OneEveryPrimitiveStruct; + UPkU128Table* PkU128; UPROPERTY(BlueprintReadOnly, Category="SpacetimeDB") - UPkI64Table* PkI64; + UVecU64Table* VecU64; UPROPERTY(BlueprintReadOnly, Category="SpacetimeDB") - UPkI32Table* PkI32; + UIndexedTable2Table* IndexedTable2; UPROPERTY(BlueprintReadOnly, Category="SpacetimeDB") - UOneStringTable* OneString; + UVecF64Table* VecF64; UPROPERTY(BlueprintReadOnly, Category="SpacetimeDB") - UOptionStringTable* OptionString; + UOneU256Table* OneU256; UPROPERTY(BlueprintReadOnly, Category="SpacetimeDB") - UOneUnitStructTable* OneUnitStruct; + UOneEveryPrimitiveStructTable* OneEveryPrimitiveStruct; UPROPERTY(BlueprintReadOnly, Category="SpacetimeDB") - UPkIdentityTable* PkIdentity; + UUniqueI128Table* UniqueI128; UPROPERTY(BlueprintReadOnly, Category="SpacetimeDB") - UOptionEveryPrimitiveStructTable* OptionEveryPrimitiveStruct; + UVecI64Table* VecI64; UPROPERTY(BlueprintReadOnly, Category="SpacetimeDB") - UUniqueI64Table* UniqueI64; + UPkU8Table* PkU8; UPROPERTY(BlueprintReadOnly, Category="SpacetimeDB") - UPkU128Table* PkU128; + UOneF64Table* OneF64; UPROPERTY(BlueprintReadOnly, Category="SpacetimeDB") - UPkConnectionIdTable* PkConnectionId; + UVecU32Table* VecU32; UPROPERTY(BlueprintReadOnly, Category="SpacetimeDB") - UVecConnectionIdTable* VecConnectionId; + UPkI16Table* PkI16; UPROPERTY(BlueprintReadOnly, Category="SpacetimeDB") - UVecI32Table* VecI32; + UPkI32Table* PkI32; UPROPERTY(BlueprintReadOnly, Category="SpacetimeDB") - UVecTimestampTable* VecTimestamp; + UOneU32Table* OneU32; UPROPERTY(BlueprintReadOnly, Category="SpacetimeDB") - UOneEveryVecStructTable* OneEveryVecStruct; + UUniqueBoolTable* UniqueBool; UPROPERTY(BlueprintReadOnly, Category="SpacetimeDB") - UOneF32Table* OneF32; + UOneSimpleEnumTable* OneSimpleEnum; UPROPERTY(BlueprintReadOnly, Category="SpacetimeDB") - UOneU8Table* OneU8; + UVecI16Table* VecI16; UPROPERTY(BlueprintReadOnly, Category="SpacetimeDB") - UOptionVecOptionI32Table* OptionVecOptionI32; + UUniqueIdentityTable* UniqueIdentity; UPROPERTY(BlueprintReadOnly, Category="SpacetimeDB") - UOneU256Table* OneU256; + UPkU32TwoTable* PkU32Two; UPROPERTY(BlueprintReadOnly, Category="SpacetimeDB") - UUniqueIdentityTable* UniqueIdentity; + UVecBoolTable* VecBool; UPROPERTY(BlueprintReadOnly, Category="SpacetimeDB") - UVecStringTable* VecString; + UOneEnumWithPayloadTable* OneEnumWithPayload; UPROPERTY(BlueprintReadOnly, Category="SpacetimeDB") - UUniqueConnectionIdTable* UniqueConnectionId; + UUniqueI32Table* UniqueI32; UPROPERTY(BlueprintReadOnly, Category="SpacetimeDB") - UVecU32Table* VecU32; + UOneIdentityTable* OneIdentity; UPROPERTY(BlueprintReadOnly, Category="SpacetimeDB") - ULargeTableTable* LargeTable; + UOneUnitStructTable* OneUnitStruct; UPROPERTY(BlueprintReadOnly, Category="SpacetimeDB") - UVecUnitStructTable* VecUnitStruct; + UUsersTable* Users; UPROPERTY(BlueprintReadOnly, Category="SpacetimeDB") - UOneI8Table* OneI8; + UVecConnectionIdTable* VecConnectionId; UPROPERTY(BlueprintReadOnly, Category="SpacetimeDB") - UPkI16Table* PkI16; + UPkI128Table* PkI128; UPROPERTY(BlueprintReadOnly, Category="SpacetimeDB") - UUniqueI16Table* UniqueI16; + UPkSimpleEnumTable* PkSimpleEnum; UPROPERTY(BlueprintReadOnly, Category="SpacetimeDB") - UUniqueU8Table* UniqueU8; + UPkI64Table* PkI64; UPROPERTY(BlueprintReadOnly, Category="SpacetimeDB") - UOptionIdentityTable* OptionIdentity; + UOneI128Table* OneI128; UPROPERTY(BlueprintReadOnly, Category="SpacetimeDB") - UUniqueI256Table* UniqueI256; + UPkConnectionIdTable* PkConnectionId; UPROPERTY(BlueprintReadOnly, Category="SpacetimeDB") - UIndexedTableTable* IndexedTable; + UOneI16Table* OneI16; UPROPERTY(BlueprintReadOnly, Category="SpacetimeDB") - UVecEveryVecStructTable* VecEveryVecStruct; + ULargeTableTable* LargeTable; UPROPERTY(BlueprintReadOnly, Category="SpacetimeDB") - UVecU128Table* VecU128; + UVecEnumWithPayloadTable* VecEnumWithPayload; UPROPERTY(BlueprintReadOnly, Category="SpacetimeDB") - UUniqueU256Table* UniqueU256; + UOneI32Table* OneI32; UPROPERTY(BlueprintReadOnly, Category="SpacetimeDB") UVecF32Table* VecF32; UPROPERTY(BlueprintReadOnly, Category="SpacetimeDB") - UVecIdentityTable* VecIdentity; + UUniqueConnectionIdTable* UniqueConnectionId; UPROPERTY(BlueprintReadOnly, Category="SpacetimeDB") - UOptionI32Table* OptionI32; + UOptionSimpleEnumTable* OptionSimpleEnum; UPROPERTY(BlueprintReadOnly, Category="SpacetimeDB") - UOneU16Table* OneU16; + UVecI256Table* VecI256; UPROPERTY(BlueprintReadOnly, Category="SpacetimeDB") - UOneU32Table* OneU32; + UVecTimestampTable* VecTimestamp; UPROPERTY(BlueprintReadOnly, Category="SpacetimeDB") - UOneI128Table* OneI128; + UPkI8Table* PkI8; UPROPERTY(BlueprintReadOnly, Category="SpacetimeDB") - UUniqueI128Table* UniqueI128; + UOneI256Table* OneI256; UPROPERTY(BlueprintReadOnly, Category="SpacetimeDB") - UUniqueU128Table* UniqueU128; + UScheduledTableTable* ScheduledTable; UPROPERTY(BlueprintReadOnly, Category="SpacetimeDB") - UPkI8Table* PkI8; + UUniqueU256Table* UniqueU256; UPROPERTY(BlueprintReadOnly, Category="SpacetimeDB") - UOneEnumWithPayloadTable* OneEnumWithPayload; + UOneStringTable* OneString; UPROPERTY(BlueprintReadOnly, Category="SpacetimeDB") - UOneByteStructTable* OneByteStruct; + UVecU16Table* VecU16; UPROPERTY(BlueprintReadOnly, Category="SpacetimeDB") - UPkU64Table* PkU64; + UVecU256Table* VecU256; UPROPERTY(BlueprintReadOnly, Category="SpacetimeDB") - UPkU256Table* PkU256; + UOptionStringTable* OptionString; UPROPERTY(BlueprintReadOnly, Category="SpacetimeDB") - UTableHoldsTableTable* TableHoldsTable; + UOneU16Table* OneU16; UPROPERTY(BlueprintReadOnly, Category="SpacetimeDB") - UVecI64Table* VecI64; + UPkIdentityTable* PkIdentity; UPROPERTY(BlueprintReadOnly, Category="SpacetimeDB") - UUniqueU64Table* UniqueU64; + UPkU64Table* PkU64; UPROPERTY(BlueprintReadOnly, Category="SpacetimeDB") - UVecU8Table* VecU8; + UUniqueI16Table* UniqueI16; UPROPERTY(BlueprintReadOnly, Category="SpacetimeDB") - UOneF64Table* OneF64; + UVecEveryPrimitiveStructTable* VecEveryPrimitiveStruct; UPROPERTY(BlueprintReadOnly, Category="SpacetimeDB") - UOneBoolTable* OneBool; + UVecU128Table* VecU128; UPROPERTY(BlueprintReadOnly, Category="SpacetimeDB") - UVecEnumWithPayloadTable* VecEnumWithPayload; + UOneI8Table* OneI8; UPROPERTY(BlueprintReadOnly, Category="SpacetimeDB") - UOneConnectionIdTable* OneConnectionId; + UUniqueU8Table* UniqueU8; UPROPERTY(BlueprintReadOnly, Category="SpacetimeDB") - UPkI128Table* PkI128; + UVecByteStructTable* VecByteStruct; UPROPERTY(BlueprintReadOnly, Category="SpacetimeDB") - UOneU128Table* OneU128; + UOneU64Table* OneU64; UPROPERTY(BlueprintReadOnly, Category="SpacetimeDB") - UIndexedTable2Table* IndexedTable2; + UTableHoldsTableTable* TableHoldsTable; UPROPERTY(BlueprintReadOnly, Category="SpacetimeDB") - UOneI16Table* OneI16; + UUniqueStringTable* UniqueString; UPROPERTY(BlueprintReadOnly, Category="SpacetimeDB") - UOneI64Table* OneI64; + UOneTimestampTable* OneTimestamp; UPROPERTY(BlueprintReadOnly, Category="SpacetimeDB") - UOptionSimpleEnumTable* OptionSimpleEnum; + UVecSimpleEnumTable* VecSimpleEnum; UPROPERTY(BlueprintReadOnly, Category="SpacetimeDB") - UPkSimpleEnumTable* PkSimpleEnum; + UOptionI32Table* OptionI32; UPROPERTY(BlueprintReadOnly, Category="SpacetimeDB") - UPkStringTable* PkString; + UPkBoolTable* PkBool; UPROPERTY(BlueprintReadOnly, Category="SpacetimeDB") - UPkU8Table* PkU8; + UPkI256Table* PkI256; UPROPERTY(BlueprintReadOnly, Category="SpacetimeDB") - UUniqueBoolTable* UniqueBool; + UOneEveryVecStructTable* OneEveryVecStruct; UPROPERTY(BlueprintReadOnly, Category="SpacetimeDB") - UVecBoolTable* VecBool; + UUniqueI64Table* UniqueI64; UPROPERTY(BlueprintReadOnly, Category="SpacetimeDB") - UVecEveryPrimitiveStructTable* VecEveryPrimitiveStruct; + UUniqueU32Table* UniqueU32; UPROPERTY(BlueprintReadOnly, Category="SpacetimeDB") - UIndexedSimpleEnumTable* IndexedSimpleEnum; + UPkU16Table* PkU16; UPROPERTY(BlueprintReadOnly, Category="SpacetimeDB") - UPkU32Table* PkU32; + UOneConnectionIdTable* OneConnectionId; UPROPERTY(BlueprintReadOnly, Category="SpacetimeDB") - UUniqueI8Table* UniqueI8; + UOneI64Table* OneI64; UPROPERTY(BlueprintReadOnly, Category="SpacetimeDB") - UUniqueStringTable* UniqueString; + UUniqueU64Table* UniqueU64; UPROPERTY(BlueprintReadOnly, Category="SpacetimeDB") - UVecF64Table* VecF64; + UPkU256Table* PkU256; UPROPERTY(BlueprintReadOnly, Category="SpacetimeDB") - UOneU64Table* OneU64; + UVecU8Table* VecU8; UPROPERTY(BlueprintReadOnly, Category="SpacetimeDB") - UOneIdentityTable* OneIdentity; + UVecIdentityTable* VecIdentity; UPROPERTY(BlueprintReadOnly, Category="SpacetimeDB") - UPkU16Table* PkU16; + UOneU8Table* OneU8; UPROPERTY(BlueprintReadOnly, Category="SpacetimeDB") - UVecSimpleEnumTable* VecSimpleEnum; + UVecEveryVecStructTable* VecEveryVecStruct; UPROPERTY(BlueprintReadOnly, Category="SpacetimeDB") - UOneSimpleEnumTable* OneSimpleEnum; + UVecUnitStructTable* VecUnitStruct; UPROPERTY(BlueprintReadOnly, Category="SpacetimeDB") - UPkU32TwoTable* PkU32Two; + UOneBoolTable* OneBool; UPROPERTY(BlueprintReadOnly, Category="SpacetimeDB") - UUniqueU32Table* UniqueU32; + UVecStringTable* VecString; UPROPERTY(BlueprintReadOnly, Category="SpacetimeDB") - UVecU64Table* VecU64; + UOneF32Table* OneF32; UPROPERTY(BlueprintReadOnly, Category="SpacetimeDB") - UPkI256Table* PkI256; + UOptionIdentityTable* OptionIdentity; UPROPERTY(BlueprintReadOnly, Category="SpacetimeDB") - UOneI256Table* OneI256; + UVecI8Table* VecI8; }; @@ -8218,13 +8218,13 @@ class TESTCLIENT_API URemoteReducers : public UObject DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams( FInsertOptionVecOptionI32Handler, const FReducerEventContext&, Context, - const FTestClientOptionalVecInt32&, V + const FTestClientOptionalVecOptionalInt32&, V ); UPROPERTY(BlueprintAssignable, Category="SpacetimeDB") FInsertOptionVecOptionI32Handler OnInsertOptionVecOptionI32; UFUNCTION(BlueprintCallable, Category="SpacetimeDB") - void InsertOptionVecOptionI32(const FTestClientOptionalVecInt32& V); + void InsertOptionVecOptionI32(const FTestClientOptionalVecOptionalInt32& V); bool InvokeInsertOptionVecOptionI32(const FReducerEventContext& Context, const UInsertOptionVecOptionI32Reducer* Args); diff --git a/sdks/unreal/tests/TestClient/Source/TestClient/Public/ModuleBindings/Types/EnumWithPayloadType.g.h b/sdks/unreal/tests/TestClient/Source/TestClient/Public/ModuleBindings/Types/EnumWithPayloadType.g.h index 35d4e0f816c..40f8b284a02 100644 --- a/sdks/unreal/tests/TestClient/Source/TestClient/Public/ModuleBindings/Types/EnumWithPayloadType.g.h +++ b/sdks/unreal/tests/TestClient/Source/TestClient/Public/ModuleBindings/Types/EnumWithPayloadType.g.h @@ -4,8 +4,8 @@ #pragma once #include "CoreMinimal.h" #include "BSATN/UESpacetimeDB.h" -#include "ModuleBindings/Types/SimpleEnumType.g.h" #include "Types/Builtins.h" +#include "ModuleBindings/Types/SimpleEnumType.g.h" #include "Kismet/BlueprintFunctionLibrary.h" #include "EnumWithPayloadType.g.generated.h" @@ -45,7 +45,7 @@ struct TESTCLIENT_API FEnumWithPayloadType public: FEnumWithPayloadType() = default; - TVariant, float, bool, double, FSpacetimeDBUInt128, uint64, FSpacetimeDBInt256, FSpacetimeDBUInt256, uint32, FSpacetimeDBIdentity, int64, uint8, int8, FSpacetimeDBInt128, FSpacetimeDBConnectionId, FSpacetimeDBTimestamp, uint16, TArray, TArray, FString, int16, TArray, int32> MessageData; + TVariant, uint8, TArray, FSpacetimeDBTimestamp, FSpacetimeDBConnectionId, TArray, float, FSpacetimeDBUInt128, uint64, FSpacetimeDBInt256, FString, FSpacetimeDBIdentity, TArray> MessageData; UPROPERTY(BlueprintReadOnly) EEnumWithPayloadTag Tag = static_cast(0); diff --git a/sdks/unreal/tests/TestClient/Source/TestClient/Public/ModuleBindings/Types/EveryPrimitiveStructType.g.h b/sdks/unreal/tests/TestClient/Source/TestClient/Public/ModuleBindings/Types/EveryPrimitiveStructType.g.h index 680f2f7489c..f2209266a99 100644 --- a/sdks/unreal/tests/TestClient/Source/TestClient/Public/ModuleBindings/Types/EveryPrimitiveStructType.g.h +++ b/sdks/unreal/tests/TestClient/Source/TestClient/Public/ModuleBindings/Types/EveryPrimitiveStructType.g.h @@ -49,13 +49,13 @@ struct TESTCLIENT_API FEveryPrimitiveStructType FSpacetimeDBInt256 L; UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "SpacetimeDB") - bool M = false;; + bool M = false; UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "SpacetimeDB") - float N = 0.f; + float N = 0.0f; UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "SpacetimeDB") - double O = 0.f; + double O = 0.0; UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "SpacetimeDB") FString P; diff --git a/sdks/unreal/tests/TestClient/Source/TestClient/Public/ModuleBindings/Types/IndexedTable2Type.g.h b/sdks/unreal/tests/TestClient/Source/TestClient/Public/ModuleBindings/Types/IndexedTable2Type.g.h index 86dca8a2354..2bf5a36f38e 100644 --- a/sdks/unreal/tests/TestClient/Source/TestClient/Public/ModuleBindings/Types/IndexedTable2Type.g.h +++ b/sdks/unreal/tests/TestClient/Source/TestClient/Public/ModuleBindings/Types/IndexedTable2Type.g.h @@ -15,7 +15,7 @@ struct TESTCLIENT_API FIndexedTable2Type uint32 PlayerId = 0; UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "SpacetimeDB") - float PlayerSnazz = 0.f; + float PlayerSnazz = 0.0f; FORCEINLINE bool operator==(const FIndexedTable2Type& Other) const { diff --git a/sdks/unreal/tests/TestClient/Source/TestClient/Public/ModuleBindings/Types/LargeTableType.g.h b/sdks/unreal/tests/TestClient/Source/TestClient/Public/ModuleBindings/Types/LargeTableType.g.h index 7c1817c675d..35083fecceb 100644 --- a/sdks/unreal/tests/TestClient/Source/TestClient/Public/ModuleBindings/Types/LargeTableType.g.h +++ b/sdks/unreal/tests/TestClient/Source/TestClient/Public/ModuleBindings/Types/LargeTableType.g.h @@ -55,13 +55,13 @@ struct TESTCLIENT_API FLargeTableType FSpacetimeDBInt256 L; UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "SpacetimeDB") - bool M = false;; + bool M = false; UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "SpacetimeDB") - float N = 0.f; + float N = 0.0f; UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "SpacetimeDB") - double O = 0.f; + double O = 0.0; UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "SpacetimeDB") FString P; diff --git a/sdks/unreal/tests/TestClient/Source/TestClient/Public/ModuleBindings/Types/OneBoolType.g.h b/sdks/unreal/tests/TestClient/Source/TestClient/Public/ModuleBindings/Types/OneBoolType.g.h index 3526af0e2e4..6cb12870968 100644 --- a/sdks/unreal/tests/TestClient/Source/TestClient/Public/ModuleBindings/Types/OneBoolType.g.h +++ b/sdks/unreal/tests/TestClient/Source/TestClient/Public/ModuleBindings/Types/OneBoolType.g.h @@ -12,7 +12,7 @@ struct TESTCLIENT_API FOneBoolType GENERATED_BODY() UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "SpacetimeDB") - bool B = false;; + bool B = false; FORCEINLINE bool operator==(const FOneBoolType& Other) const { diff --git a/sdks/unreal/tests/TestClient/Source/TestClient/Public/ModuleBindings/Types/OneF32Type.g.h b/sdks/unreal/tests/TestClient/Source/TestClient/Public/ModuleBindings/Types/OneF32Type.g.h index 278542d7098..94257434a22 100644 --- a/sdks/unreal/tests/TestClient/Source/TestClient/Public/ModuleBindings/Types/OneF32Type.g.h +++ b/sdks/unreal/tests/TestClient/Source/TestClient/Public/ModuleBindings/Types/OneF32Type.g.h @@ -12,7 +12,7 @@ struct TESTCLIENT_API FOneF32Type GENERATED_BODY() UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "SpacetimeDB") - float F = 0.f; + float F = 0.0f; FORCEINLINE bool operator==(const FOneF32Type& Other) const { diff --git a/sdks/unreal/tests/TestClient/Source/TestClient/Public/ModuleBindings/Types/OneF64Type.g.h b/sdks/unreal/tests/TestClient/Source/TestClient/Public/ModuleBindings/Types/OneF64Type.g.h index 3cd55dcd0f0..68eefb3acd1 100644 --- a/sdks/unreal/tests/TestClient/Source/TestClient/Public/ModuleBindings/Types/OneF64Type.g.h +++ b/sdks/unreal/tests/TestClient/Source/TestClient/Public/ModuleBindings/Types/OneF64Type.g.h @@ -12,7 +12,7 @@ struct TESTCLIENT_API FOneF64Type GENERATED_BODY() UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "SpacetimeDB") - double F = 0.f; + double F = 0.0; FORCEINLINE bool operator==(const FOneF64Type& Other) const { diff --git a/sdks/unreal/tests/TestClient/Source/TestClient/Public/ModuleBindings/Types/OptionVecOptionI32Type.g.h b/sdks/unreal/tests/TestClient/Source/TestClient/Public/ModuleBindings/Types/OptionVecOptionI32Type.g.h index e9ad2a8fab2..0235bc8702e 100644 --- a/sdks/unreal/tests/TestClient/Source/TestClient/Public/ModuleBindings/Types/OptionVecOptionI32Type.g.h +++ b/sdks/unreal/tests/TestClient/Source/TestClient/Public/ModuleBindings/Types/OptionVecOptionI32Type.g.h @@ -5,7 +5,7 @@ #include "CoreMinimal.h" #include "BSATN/UESpacetimeDB.h" #include "ModuleBindings/Optionals/TestClientOptionalInt32.g.h" -#include "ModuleBindings/Optionals/TestClientOptionalVecInt32.g.h" +#include "ModuleBindings/Optionals/TestClientOptionalVecOptionalInt32.g.h" #include "OptionVecOptionI32Type.g.generated.h" USTRUCT(BlueprintType) @@ -14,7 +14,7 @@ struct TESTCLIENT_API FOptionVecOptionI32Type GENERATED_BODY() UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "SpacetimeDB") - FTestClientOptionalVecInt32 V; + FTestClientOptionalVecOptionalInt32 V; FORCEINLINE bool operator==(const FOptionVecOptionI32Type& Other) const { diff --git a/sdks/unreal/tests/TestClient/Source/TestClient/Public/ModuleBindings/Types/PkBoolType.g.h b/sdks/unreal/tests/TestClient/Source/TestClient/Public/ModuleBindings/Types/PkBoolType.g.h index 28a25e660f1..8772d328cee 100644 --- a/sdks/unreal/tests/TestClient/Source/TestClient/Public/ModuleBindings/Types/PkBoolType.g.h +++ b/sdks/unreal/tests/TestClient/Source/TestClient/Public/ModuleBindings/Types/PkBoolType.g.h @@ -12,7 +12,7 @@ struct TESTCLIENT_API FPkBoolType GENERATED_BODY() UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "SpacetimeDB") - bool B = false;; + bool B = false; UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "SpacetimeDB") int32 Data = 0; diff --git a/sdks/unreal/tests/TestClient/Source/TestClient/Public/ModuleBindings/Types/UniqueBoolType.g.h b/sdks/unreal/tests/TestClient/Source/TestClient/Public/ModuleBindings/Types/UniqueBoolType.g.h index 7123d709e26..b31144dad81 100644 --- a/sdks/unreal/tests/TestClient/Source/TestClient/Public/ModuleBindings/Types/UniqueBoolType.g.h +++ b/sdks/unreal/tests/TestClient/Source/TestClient/Public/ModuleBindings/Types/UniqueBoolType.g.h @@ -12,7 +12,7 @@ struct TESTCLIENT_API FUniqueBoolType GENERATED_BODY() UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "SpacetimeDB") - bool B = false;; + bool B = false; UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "SpacetimeDB") int32 Data = 0; diff --git a/sdks/unreal/tests/TestClient/Source/TestClient/Public/Tests/TestHandler.h b/sdks/unreal/tests/TestClient/Source/TestClient/Public/Tests/TestHandler.h index 7e2c0e0d8e5..5b064b17878 100644 --- a/sdks/unreal/tests/TestClient/Source/TestClient/Public/Tests/TestHandler.h +++ b/sdks/unreal/tests/TestClient/Source/TestClient/Public/Tests/TestHandler.h @@ -383,7 +383,7 @@ class UOptionActionsHandler : public UTestHandler FTestClientOptionalIdentity ExpectedIdentityType; FTestClientOptionalSimpleEnum ExpectedEnumType; FTestClientOptionalEveryPrimitiveStruct ExpectedEveryPrimitiveStructType; - FTestClientOptionalVecInt32 ExpectedVecOptionI32Type; + FTestClientOptionalVecOptionalInt32 ExpectedVecOptionI32Type; }; UCLASS() From b9b7dcff06603e24c7151ea894d67b6619477939 Mon Sep 17 00:00:00 2001 From: Jason Larabie Date: Tue, 14 Oct 2025 11:27:28 -0700 Subject: [PATCH 17/25] Remove use of DDC during CI tests (#3367) # Description of Changes Removes the use of the Derived Data Cache during CI, will in # API and ABI breaking changes None # Expected complexity level and risk 1 - Small change for CI # Testing - [x] Re-ran tests on both Linux + Windows with the change --- sdks/unreal/tests/TestClient/Config/DefaultEngine.ini | 6 ++++++ sdks/unreal/tests/test.rs | 5 ++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/sdks/unreal/tests/TestClient/Config/DefaultEngine.ini b/sdks/unreal/tests/TestClient/Config/DefaultEngine.ini index 9c45040c09f..92c3bae9b3e 100644 --- a/sdks/unreal/tests/TestClient/Config/DefaultEngine.ini +++ b/sdks/unreal/tests/TestClient/Config/DefaultEngine.ini @@ -1,4 +1,10 @@ +[Zen] +bEnableZen=false +bEnableZenLocal=false +[/Script/Zen.ZenServiceSettings] +bEnabled=False +bAutoLaunch=False [Audio] UseAudioMixer=True diff --git a/sdks/unreal/tests/test.rs b/sdks/unreal/tests/test.rs index 7ef7fcd2c28..2f4d4f82d94 100644 --- a/sdks/unreal/tests/test.rs +++ b/sdks/unreal/tests/test.rs @@ -83,7 +83,10 @@ fn make_test(test_name: &str) -> Test { // Run automation test let run_command = format!( - "\"{editor_exe}\" \"{uproject_path}\" -NullRHI -Unattended -NoSound -nop4 -NoSplash -DDC-ForceMemoryCache -ddc=InstalledNoZenLocalFallback -ExecCmds=\"Automation RunTests SpacetimeDB.TestClient.{test_name}; Quit\"" + // Updated to -NoZen and -dcc=InstalledNoZenLocalFallback to stop Unreal from trying to install Zen Server in CI + // This is failing during tests as each test tries to install Zen and create a race condition where two tests try to handle this at the same time + // Zen Server and the Derived Cache seem like a good idea during tests but they were not designed with mutli-threaded tests in mind, it is suggested to allow each test to run in isolation + "\"{editor_exe}\" \"{uproject_path}\" -NullRHI -Unattended -NoSound -nop4 -NoSplash -NoZen -ddc=InstalledNoZenLocalFallback -ExecCmds=\"Automation RunTests SpacetimeDB.TestClient.{test_name}; Quit\"" ); Test::builder() From d06012b9f0cf952c544fac635729273e70ac7ed1 Mon Sep 17 00:00:00 2001 From: Zeke Foppa <196249+bfops@users.noreply.github.com> Date: Tue, 14 Oct 2025 20:09:55 -0700 Subject: [PATCH 18/25] Windows - Set `linker = "lld-link"` (#3406) # Description of Changes See the inline comments for the motivation. This was originally introduced to our Windows CI in #3351. This PR moves it from CI to general Windows target builds, since it seems like Windows builds are now generally having this issue. # API and ABI breaking changes None # Expected complexity level and risk 1 # Testing - [x] Windows CI still passes --------- Co-authored-by: Zeke Foppa --- .cargo/config.toml | 7 +++++++ .github/workflows/ci.yml | 6 ------ 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/.cargo/config.toml b/.cargo/config.toml index 7a3e270ebbd..e82ef3def08 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -3,3 +3,10 @@ rustflags = ["--cfg", "tokio_unstable"] [alias] bump-versions = "run -p upgrade-version --" + +[target.x86_64-pc-windows-msvc] +# Use a different linker. Otherwise, the build fails with some obscure linker error that +# seems to be a result of us producing a massive PDB file. +# I (@bfops) tried a variety of other link options besides switching linkers, but this +# seems to be the only thing that worked. +linker = "lld-link" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index aa33bcf152c..8adaa8396d6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -58,12 +58,6 @@ jobs: $ErrorActionPreference = 'Stop' $PSNativeCommandUseErrorActionPreference = $true - # Use a different linker. Otherwise, the build fails with some obscure linker error that - # seems to be a result of us producing a massive PDB file. - # I (@bfops) tried a variety of other link options besides switching linkers, but this - # seems to be the only thing that worked. - $env:RUSTFLAGS='-Clinker=lld-link' - cargo build -p spacetimedb-cli -p spacetimedb-standalone -p spacetimedb-update Start-Process target/debug/spacetimedb-cli.exe -ArgumentList 'start --pg-port 5432' cd modules From 5d35686c6c046658cb7116915b449ddce2c60d3f Mon Sep 17 00:00:00 2001 From: Phoebe Goldman Date: Wed, 15 Oct 2025 11:18:15 -0400 Subject: [PATCH 19/25] Also capture a snapshot every new commitlog segment (#3405) # Description of Changes We've run into a problem on Maincloud caused by a database that was writing a relatively small number of very large transactions. This was accruing many commitlog segments consuming hundreds of gigabytes of disk, but had not ever taken a snapshot, or compressed or archived any data, as the database had not progressed past one million transactions. With this PR, we take a snapshot every time the commitlog segment rotates. We still also snapshot every million transactions. One BitCraft database we looked at had 2.5 million transactions per commitlog segment, meaning that this change will not meaningfully affect the frequency of snapshots. The offending Maincloud database, however, had only 50 transactions per segment! # API and ABI breaking changes N/a # Expected complexity level and risk 3: Hastily made changes to finnicky code across several crates. # Testing I am unsure how to test these changes. - [ ] - [ ] --- crates/commitlog/Cargo.toml | 2 +- crates/commitlog/src/lib.rs | 23 ++++++----- crates/commitlog/src/repo/fs.rs | 29 ++++++++++++-- crates/commitlog/src/repo/mod.rs | 1 + crates/commitlog/tests/random_payload/mod.rs | 3 ++ crates/commitlog/tests/streaming/mod.rs | 6 +-- crates/core/src/db/persistence.rs | 2 +- crates/core/src/db/relational_db.rs | 40 ++++++++++++++------ crates/core/src/db/snapshot.rs | 11 ++++++ crates/core/src/host/host_controller.rs | 8 +++- crates/durability/src/imp/local.rs | 14 ++++++- 11 files changed, 108 insertions(+), 31 deletions(-) diff --git a/crates/commitlog/Cargo.toml b/crates/commitlog/Cargo.toml index b3fbe077d10..09c8f359c94 100644 --- a/crates/commitlog/Cargo.toml +++ b/crates/commitlog/Cargo.toml @@ -18,7 +18,7 @@ async-stream = { workspace = true, optional = true } bitflags.workspace = true bytes= { workspace = true, optional = true } crc32c.workspace = true -futures = { workspace = true, optional = true} +futures = { workspace = true, optional = true } itertools.workspace = true log.workspace = true memmap2 = "0.9.4" diff --git a/crates/commitlog/src/lib.rs b/crates/commitlog/src/lib.rs index f7e6501bf37..784ced88dcd 100644 --- a/crates/commitlog/src/lib.rs +++ b/crates/commitlog/src/lib.rs @@ -2,11 +2,11 @@ use std::{ io, num::{NonZeroU16, NonZeroU64}, ops::RangeBounds, - sync::RwLock, + sync::{Arc, RwLock}, }; use log::trace; -use repo::Repo; +use repo::{fs::OnNewSegmentFn, Repo}; use spacetimedb_paths::server::CommitLogDir; pub mod commit; @@ -157,8 +157,8 @@ impl Commitlog { /// This is only necessary when opening the commitlog for writing. See the /// free-standing functions in this module for how to traverse a read-only /// commitlog. - pub fn open(root: CommitLogDir, opts: Options) -> io::Result { - let inner = commitlog::Generic::open(repo::Fs::new(root)?, opts)?; + pub fn open(root: CommitLogDir, opts: Options, on_new_segment: Option>) -> io::Result { + let inner = commitlog::Generic::open(repo::Fs::new(root, on_new_segment)?, opts)?; Ok(Self { inner: RwLock::new(inner), @@ -390,6 +390,10 @@ impl Commitlog { /// I.e. the argument is not guaranteed to be flushed after the method /// returns. If that is desired, [`Self::flush`] must be called explicitly. /// + /// If writing `txdata` to the commitlog results in a new segment file being opened, + /// we will send a message down `on_new_segment`. + /// This will be hooked up to the `request_snapshot` channel of a `SnapshotWorker`. + /// /// # Errors /// /// If the log needs to be flushed, but an I/O error occurs, ownership of @@ -403,6 +407,7 @@ impl Commitlog { if let Err(source) = inner.commit() { return Err(error::Append { txdata, source }); } + // `inner.commit.n` must be zero at this point let res = inner.append(txdata); debug_assert!(res.is_ok(), "failed to append while holding write lock"); @@ -526,7 +531,7 @@ impl Commitlog { /// /// Unlike `open`, no segment will be created in an empty `repo`. pub fn committed_meta(root: CommitLogDir) -> Result, error::SegmentMetadata> { - commitlog::committed_meta(repo::Fs::new(root)?) + commitlog::committed_meta(repo::Fs::new(root, None)?) } /// Obtain an iterator which traverses the commitlog located at the `root` @@ -547,7 +552,7 @@ pub fn commits_from( root: CommitLogDir, offset: u64, ) -> io::Result>> { - commitlog::commits_from(repo::Fs::new(root)?, DEFAULT_LOG_FORMAT_VERSION, offset) + commitlog::commits_from(repo::Fs::new(root, None)?, DEFAULT_LOG_FORMAT_VERSION, offset) } /// Obtain an iterator which traverses the commitlog located at the `root` @@ -582,7 +587,7 @@ where D::Error: From, T: 'a, { - commitlog::transactions_from(repo::Fs::new(root)?, DEFAULT_LOG_FORMAT_VERSION, offset, de) + commitlog::transactions_from(repo::Fs::new(root, None)?, DEFAULT_LOG_FORMAT_VERSION, offset, de) } /// Traverse the commitlog located at the `root` directory from the start and @@ -608,7 +613,7 @@ where D: Decoder, D::Error: From + From, { - commitlog::fold_transactions_from(repo::Fs::new(root)?, DEFAULT_LOG_FORMAT_VERSION, offset, de) + commitlog::fold_transactions_from(repo::Fs::new(root, None)?, DEFAULT_LOG_FORMAT_VERSION, offset, de) } pub fn fold_transaction_range(root: CommitLogDir, range: impl RangeBounds, de: D) -> Result<(), D::Error> @@ -616,5 +621,5 @@ where D: Decoder, D::Error: From + From, { - commitlog::fold_transaction_range(repo::Fs::new(root)?, DEFAULT_LOG_FORMAT_VERSION, range, de) + commitlog::fold_transaction_range(repo::Fs::new(root, None)?, DEFAULT_LOG_FORMAT_VERSION, range, de) } diff --git a/crates/commitlog/src/repo/fs.rs b/crates/commitlog/src/repo/fs.rs index 196dffbf922..3e45a1f2a5c 100644 --- a/crates/commitlog/src/repo/fs.rs +++ b/crates/commitlog/src/repo/fs.rs @@ -1,5 +1,6 @@ use std::fs::{self, File}; use std::io; +use std::sync::Arc; use log::{debug, warn}; use spacetimedb_fs_utils::compression::{compress_with_zstd, CompressReader}; @@ -23,21 +24,35 @@ const SEGMENT_FILE_EXT: &str = ".stdb.log"; // - io_uring // +pub type OnNewSegmentFn = dyn Fn() + Send + Sync + 'static; + /// A commitlog repository [`Repo`] which stores commits in ordinary files on /// disk. -#[derive(Clone, Debug)] +#[derive(Clone)] pub struct Fs { /// The base directory within which segment files will be stored. root: CommitLogDir, + + /// Channel through which to send a message whenever we create a new segment. + /// + /// The other end of this channel will be a `SnapshotWorker`, + /// which will capture a snapshot each time we rotate segments. + on_new_segment: Option>, +} + +impl std::fmt::Debug for Fs { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Fs").field("root", &self.root).finish_non_exhaustive() + } } impl Fs { /// Create a commitlog repository which stores segments in the directory `root`. /// /// `root` must name an extant, accessible, writeable directory. - pub fn new(root: CommitLogDir) -> io::Result { + pub fn new(root: CommitLogDir, on_new_segment: Option>) -> io::Result { root.create()?; - Ok(Self { root }) + Ok(Self { root, on_new_segment }) } /// Get the filename for a segment starting with `offset` within this @@ -95,6 +110,14 @@ impl Repo for Fs { Err(e) }) + .inspect(|_| { + // We're rotating commitlog segments, so we should also take a snapshot at the earliest opportunity. + if let Some(on_new_segment) = self.on_new_segment.as_ref() { + // No need to handle the error here: if the snapshot worker is closed we'll eventually close too, + // and we don't want to die prematurely if there are still TXes to write. + on_new_segment(); + } + }) } fn open_segment_writer(&self, offset: u64) -> io::Result { diff --git a/crates/commitlog/src/repo/mod.rs b/crates/commitlog/src/repo/mod.rs index 7129b3be314..a6c45ed029f 100644 --- a/crates/commitlog/src/repo/mod.rs +++ b/crates/commitlog/src/repo/mod.rs @@ -15,6 +15,7 @@ pub(crate) mod fs; pub mod mem; pub use fs::Fs; +pub use fs::OnNewSegmentFn; #[cfg(any(test, feature = "test"))] pub use mem::Memory; diff --git a/crates/commitlog/tests/random_payload/mod.rs b/crates/commitlog/tests/random_payload/mod.rs index f3edd3be954..b2249cfb018 100644 --- a/crates/commitlog/tests/random_payload/mod.rs +++ b/crates/commitlog/tests/random_payload/mod.rs @@ -19,6 +19,7 @@ fn smoke() { max_records_in_commit: NonZeroU16::MIN, ..Options::default() }, + None, ) .unwrap(); @@ -48,6 +49,7 @@ fn resets() { max_records_in_commit: NonZeroU16::MIN, ..Options::default() }, + None, ) .unwrap(); @@ -85,6 +87,7 @@ fn compression() { max_records_in_commit: NonZeroU16::MIN, ..Options::default() }, + None, ) .unwrap(); diff --git a/crates/commitlog/tests/streaming/mod.rs b/crates/commitlog/tests/streaming/mod.rs index 648da7ec66d..0474ae75b30 100644 --- a/crates/commitlog/tests/streaming/mod.rs +++ b/crates/commitlog/tests/streaming/mod.rs @@ -193,7 +193,7 @@ fn default_options() -> Options { async fn fill_log(path: PathBuf) { spawn_blocking(move || { - let clog = Commitlog::open(CommitLogDir::from_path_unchecked(path), default_options()).unwrap(); + let clog = Commitlog::open(CommitLogDir::from_path_unchecked(path), default_options(), None).unwrap(); let payload = random_payload::gen_payload(); for _ in 0..100 { clog.append_maybe_flush(payload).unwrap(); @@ -211,12 +211,12 @@ async fn create_writer(path: PathBuf) -> io::Result> { } fn repo(at: &Path) -> repo::Fs { - repo::Fs::new(CommitLogDir::from_path_unchecked(at)).unwrap() + repo::Fs::new(CommitLogDir::from_path_unchecked(at), None).unwrap() } fn create_reader(path: &Path, range: impl RangeBounds) -> impl AsyncBufRead { BufReader::new(StreamReader::new(stream::commits( - repo::Fs::new(CommitLogDir::from_path_unchecked(path)).unwrap(), + repo::Fs::new(CommitLogDir::from_path_unchecked(path), None).unwrap(), range, ))) } diff --git a/crates/core/src/db/persistence.rs b/crates/core/src/db/persistence.rs index 9e808a514c3..1f9b91d3bc3 100644 --- a/crates/core/src/db/persistence.rs +++ b/crates/core/src/db/persistence.rs @@ -132,12 +132,12 @@ impl PersistenceProvider for LocalPersistenceProvider { let commitlog_dir = replica_dir.commit_log(); let snapshot_dir = replica_dir.snapshots(); - let (durability, disk_size) = relational_db::local_durability(commitlog_dir).await?; let database_identity = database.database_identity; let snapshot_worker = asyncify(move || relational_db::open_snapshot_repo(snapshot_dir, database_identity, replica_id)) .await .map(|repo| SnapshotWorker::new(repo, snapshot::Compression::Enabled))?; + let (durability, disk_size) = relational_db::local_durability(commitlog_dir, Some(&snapshot_worker)).await?; tokio::spawn(relational_db::snapshot_watching_commitlog_compressor( snapshot_worker.subscribe(), diff --git a/crates/core/src/db/relational_db.rs b/crates/core/src/db/relational_db.rs index 09f1f5934be..c7b05a7bbfb 100644 --- a/crates/core/src/db/relational_db.rs +++ b/crates/core/src/db/relational_db.rs @@ -8,6 +8,7 @@ use anyhow::{anyhow, Context}; use enum_map::EnumMap; use fs2::FileExt; use spacetimedb_commitlog as commitlog; +use spacetimedb_commitlog::repo::OnNewSegmentFn; use spacetimedb_data_structures::map::IntSet; use spacetimedb_datastore::db_metrics::DB_METRICS; use spacetimedb_datastore::error::{DatastoreError, TableError}; @@ -1563,9 +1564,20 @@ pub type LocalDurability = Arc>; /// /// Note that this operation can be expensive, as it needs to traverse a suffix /// of the commitlog. -pub async fn local_durability(commitlog_dir: CommitLogDir) -> io::Result<(LocalDurability, DiskSizeFn)> { +pub async fn local_durability( + commitlog_dir: CommitLogDir, + snapshot_worker: Option<&SnapshotWorker>, +) -> io::Result<(LocalDurability, DiskSizeFn)> { let rt = tokio::runtime::Handle::current(); // TODO: Should this better be spawn_blocking? + let on_new_segment = snapshot_worker.map(|snapshot_worker| { + let snapshot_worker = snapshot_worker.clone(); + Arc::new(move || { + // Ignore errors: we don't want our durability to die and start throwing away queued TXes + // just because the snapshot worker shut down. + snapshot_worker.request_snapshot_ignore_closed(); + }) as Arc + }); let local = spawn_rayon(move || { durability::Local::open( commitlog_dir, @@ -1577,6 +1589,9 @@ pub async fn local_durability(commitlog_dir: CommitLogDir) -> io::Result<(LocalD }, ..Default::default() }, + // Give the durability a handle to request a new snapshot run, + // which it will send down whenever we rotate commitlog segments. + on_new_segment, ) }) .await @@ -1819,8 +1834,6 @@ pub mod tests_utils { owner_identity: Identity, want_snapshot_repo: bool, ) -> Result<(RelationalDB, Arc>), DBError> { - let (local, disk_size_fn) = rt.block_on(local_durability(root.commit_log()))?; - let history = local.clone(); let snapshots = want_snapshot_repo .then(|| { open_snapshot_repo(root.snapshots(), db_identity, replica_id) @@ -1828,6 +1841,9 @@ pub mod tests_utils { }) .transpose()?; + let (local, disk_size_fn) = rt.block_on(local_durability(root.commit_log(), snapshots.as_ref()))?; + let history = local.clone(); + let persistence = Persistence { durability: local.clone(), disk_size: disk_size_fn, @@ -1957,17 +1973,18 @@ pub mod tests_utils { rt: tokio::runtime::Handle, want_snapshot_repo: bool, ) -> Result<(RelationalDB, Arc>), DBError> { - let (local, disk_size_fn) = rt.block_on(local_durability(root.commit_log()))?; + let snapshots = want_snapshot_repo + .then(|| { + open_snapshot_repo(root.snapshots(), Identity::ZERO, 0) + .map(|repo| SnapshotWorker::new(repo, snapshot::Compression::Disabled)) + }) + .transpose()?; + let (local, disk_size_fn) = rt.block_on(local_durability(root.commit_log(), snapshots.as_ref()))?; let history = local.clone(); let persistence = Persistence { durability: local.clone(), disk_size: disk_size_fn, - snapshots: want_snapshot_repo - .then(|| { - open_snapshot_repo(root.snapshots(), Identity::ZERO, 0) - .map(|repo| SnapshotWorker::new(repo, snapshot::Compression::Disabled)) - }) - .transpose()?, + snapshots, }; let db = Self::open_db(root, history, Some(persistence), None, 0)?; @@ -2994,7 +3011,8 @@ mod tests { row_ty, })); { - let clog = Commitlog::<()>::open(dir.commit_log(), Default::default()).expect("failed to open commitlog"); + let clog = + Commitlog::<()>::open(dir.commit_log(), Default::default(), None).expect("failed to open commitlog"); let decoder = Decoder(Rc::clone(&inputs)); clog.fold_transactions(decoder).unwrap(); } diff --git a/crates/core/src/db/snapshot.rs b/crates/core/src/db/snapshot.rs index 969e250a024..61c125f6e26 100644 --- a/crates/core/src/db/snapshot.rs +++ b/crates/core/src/db/snapshot.rs @@ -114,12 +114,23 @@ impl SnapshotWorker { /// /// The snapshot will be taken at some point in the future. /// The request is dropped if the handle is not yet fully initialized. + /// + /// Panics if the snapshot worker has closed the receive end of its queue(s), + /// which is likely due to it having panicked. pub fn request_snapshot(&self) { self.request_snapshot .unbounded_send(Request::TakeSnapshot) .expect("snapshot worker panicked"); } + /// Like [`Self::request_snapshot`], but doesn't propogate panics from the worker. + /// + /// Used by the durability to request snapshots on commitlog segment rotation, + /// since the durability should continue writing queued TXes even if the snapshot worker panics. + pub fn request_snapshot_ignore_closed(&self) { + let _ = self.request_snapshot.unbounded_send(Request::TakeSnapshot); + } + /// Subscribe to the [TxOffset]s of snapshots created by this worker. /// /// Note that the returned [`watch::Receiver`] only stores the most recent diff --git a/crates/core/src/host/host_controller.rs b/crates/core/src/host/host_controller.rs index f508bbbc5be..6519d738c81 100644 --- a/crates/core/src/host/host_controller.rs +++ b/crates/core/src/host/host_controller.rs @@ -768,7 +768,13 @@ impl Host { page_pool.clone(), )?, db::Storage::Disk => { - let (history, _) = relational_db::local_durability(replica_dir.commit_log()).await?; + // Open a read-only copy of the local durability to replay from. + let (history, _) = relational_db::local_durability( + replica_dir.commit_log(), + // No need to include a snapshot request channel here, 'cause we're only reading from this instance. + None, + ) + .await?; let persistence = persistence.persistence(&database, replica_id).await?; let (db, clients) = RelationalDB::open( &replica_dir, diff --git a/crates/durability/src/imp/local.rs b/crates/durability/src/imp/local.rs index 1e2862aca92..485566d5ff1 100644 --- a/crates/durability/src/imp/local.rs +++ b/crates/durability/src/imp/local.rs @@ -23,6 +23,8 @@ use tracing::instrument; use crate::{Durability, DurableOffset, History, TxOffset}; +pub use spacetimedb_commitlog::repo::OnNewSegmentFn; + /// [`Local`] configuration. #[derive(Clone, Copy, Debug)] pub struct Options { @@ -81,10 +83,18 @@ impl Local { /// The `root` directory must already exist. /// /// Background tasks are spawned onto the provided tokio runtime. - pub fn open(root: CommitLogDir, rt: tokio::runtime::Handle, opts: Options) -> io::Result { + /// + /// We will send a message down the `on_new_segment` channel whenever we begin a new commitlog segment. + /// This is used to capture a snapshot each new segment. + pub fn open( + root: CommitLogDir, + rt: tokio::runtime::Handle, + opts: Options, + on_new_segment: Option>, + ) -> io::Result { info!("open local durability"); - let clog = Arc::new(Commitlog::open(root, opts.commitlog)?); + let clog = Arc::new(Commitlog::open(root, opts.commitlog, on_new_segment)?); let (queue, rx) = mpsc::unbounded_channel(); let queue_depth = Arc::new(AtomicU64::new(0)); let (durable_tx, durable_rx) = watch::channel(clog.max_committed_offset()); From cbe8eecfa09ebae509bd0f0e94eb259caf873002 Mon Sep 17 00:00:00 2001 From: "samuel.engstrom@arvikasoft.se" <115142777+SamuelE-Arvika@users.noreply.github.com> Date: Wed, 15 Oct 2025 19:05:54 +0200 Subject: [PATCH 20/25] Add forgotten variance to table_index::can_merge (#3391) TableIndex merge logic to was missing variants. # Description of Changes This pull request fixes an issue where the TableIndex::can_merge function was missing match variants. # API and ABI breaking changes None. # Expected complexity level and risk 1 / 5 (Trivial) This is a very low-risk change. It simply adds a few lines to a match statement # Testing Manual testing confirm that creating two tables with indices with the for the missing variants no longer causes a panic. --- crates/table/src/table_index/mod.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/table/src/table_index/mod.rs b/crates/table/src/table_index/mod.rs index 5e3d2b96fbd..3b69490ff88 100644 --- a/crates/table/src/table_index/mod.rs +++ b/crates/table/src/table_index/mod.rs @@ -1278,6 +1278,8 @@ impl TableIndex { | (BtreeI128(_), BtreeI128(_)) | (BtreeU256(_), BtreeU256(_)) | (BtreeI256(_), BtreeI256(_)) + | (BtreeF32(_), BtreeF32(_)) + | (BtreeF64(_), BtreeF64(_)) | (BtreeString(_), BtreeString(_)) | (BtreeAV(_), BtreeAV(_)) => Ok(()), // For unique indices, we'll need to see if everything in `other` can be added to `idx`. @@ -1295,6 +1297,8 @@ impl TableIndex { (UniqueBtreeI128(idx), UniqueBtreeI128(other)) => idx.can_merge(other, ignore).map_err(|ptr| *ptr), (UniqueBtreeU256(idx), UniqueBtreeU256(other)) => idx.can_merge(other, ignore).map_err(|ptr| *ptr), (UniqueBtreeI256(idx), UniqueBtreeI256(other)) => idx.can_merge(other, ignore).map_err(|ptr| *ptr), + (UniqueBtreeF32(idx), UniqueBtreeF32(other)) => idx.can_merge(other, ignore).map_err(|ptr| *ptr), + (UniqueBtreeF64(idx), UniqueBtreeF64(other)) => idx.can_merge(other, ignore).map_err(|ptr| *ptr), (UniqueBtreeString(idx), UniqueBtreeString(other)) => idx.can_merge(other, ignore).map_err(|ptr| *ptr), (UniqueBtreeAV(idx), UniqueBtreeAV(other)) => idx.can_merge(other, ignore).map_err(|ptr| *ptr), (UniqueDirectU8(idx), UniqueDirectU8(other)) => idx.can_merge(other, ignore), From 5039ed6475a1082996cf2d1e6440a20bdcf9c852 Mon Sep 17 00:00:00 2001 From: joshua-spacetime Date: Wed, 8 Oct 2025 13:03:42 -0700 Subject: [PATCH 21/25] Generalize ReducerInfo to support views --- crates/bindings-macro/src/reducer.rs | 8 +++- crates/bindings-macro/src/table.rs | 2 +- crates/bindings/src/rt.rs | 28 +++++++------ crates/bindings/tests/ui/reducers.stderr | 50 +++++++----------------- 4 files changed, 38 insertions(+), 50 deletions(-) diff --git a/crates/bindings-macro/src/reducer.rs b/crates/bindings-macro/src/reducer.rs index ff98cc6250b..5585a3c7a6b 100644 --- a/crates/bindings-macro/src/reducer.rs +++ b/crates/bindings-macro/src/reducer.rs @@ -139,11 +139,15 @@ pub(crate) fn reducer_impl(args: ReducerArgs, original_function: &ItemFn) -> syn } } #[automatically_derived] - impl spacetimedb::rt::ReducerInfo for #func_name { + impl spacetimedb::rt::FnInfo for #func_name { + type Invoke = spacetimedb::rt::ReducerFn; const NAME: &'static str = #reducer_name; #(const LIFECYCLE: Option = Some(#lifecycle);)* const ARG_NAMES: &'static [Option<&'static str>] = &[#(#opt_arg_names),*]; - const INVOKE: spacetimedb::rt::ReducerFn = #func_name::invoke; + const INVOKE: Self::Invoke = #func_name::invoke; + fn return_type(_typespace: &mut impl spacetimedb::sats::typespace::TypespaceBuilder) -> Option { + None + } } }) } diff --git a/crates/bindings-macro/src/table.rs b/crates/bindings-macro/src/table.rs index 4e2b079992f..dd67ec22e9d 100644 --- a/crates/bindings-macro/src/table.rs +++ b/crates/bindings-macro/src/table.rs @@ -821,7 +821,7 @@ pub(crate) fn table_impl(mut args: TableArgs, item: &syn::DeriveInput) -> syn::R let reducer = &sched.reducer; let scheduled_at_id = scheduled_at_column.index; let desc = quote!(spacetimedb::table::ScheduleDesc { - reducer_name: <#reducer as spacetimedb::rt::ReducerInfo>::NAME, + reducer_name: <#reducer as spacetimedb::rt::FnInfo>::NAME, scheduled_at_column: #scheduled_at_id, }); diff --git a/crates/bindings/src/rt.rs b/crates/bindings/src/rt.rs index 6cdddde830d..d4e76cc5325 100644 --- a/crates/bindings/src/rt.rs +++ b/crates/bindings/src/rt.rs @@ -43,19 +43,25 @@ pub trait Reducer<'de, A: Args<'de>> { fn invoke(&self, ctx: &ReducerContext, args: A) -> ReducerResult; } -/// A trait for types that can *describe* a reducer. -pub trait ReducerInfo { - /// The name of the reducer. +/// A trait for types that can *describe* a callable function such as a reducer or view. +pub trait FnInfo { + /// The type of function to invoke. + type Invoke; + + /// The name of the function. const NAME: &'static str; - /// The lifecycle of the reducer, if there is one. + /// The lifecycle of the function, if there is one. const LIFECYCLE: Option = None; - /// A description of the parameter names of the reducer. + /// A description of the parameter names of the function. const ARG_NAMES: &'static [Option<&'static str>]; - /// The function to call to invoke the reducer. - const INVOKE: ReducerFn; + /// The function to invoke. + const INVOKE: Self::Invoke; + + /// The return type of this function. + fn return_type(typespace: &mut impl TypespaceBuilder) -> Option; } /// A trait of types representing the arguments of a reducer. @@ -70,7 +76,7 @@ pub trait Args<'de>: Sized { fn serialize_seq_product(&self, prod: &mut S) -> Result<(), S::Error>; /// Returns the schema for this reducer provided a `typespace`. - fn schema(typespace: &mut impl TypespaceBuilder) -> ProductType; + fn schema>(typespace: &mut impl TypespaceBuilder) -> ProductType; } /// A trait of types representing the result of executing a reducer. @@ -239,7 +245,7 @@ macro_rules! impl_reducer { #[inline] #[allow(non_snake_case, irrefutable_let_patterns)] - fn schema(_typespace: &mut impl TypespaceBuilder) -> ProductType { + fn schema(_typespace: &mut impl TypespaceBuilder) -> ProductType { // Extract the names of the arguments. let [.., $($T),*] = Info::ARG_NAMES else { panic!() }; ProductType::new(vec![ @@ -362,7 +368,7 @@ impl From> for RawIndexAlgorithm { } /// Registers a describer for the reducer `I` with arguments `A`. -pub fn register_reducer<'a, A: Args<'a>, I: ReducerInfo>(_: impl Reducer<'a, A>) { +pub fn register_reducer<'a, A: Args<'a>, I: FnInfo>(_: impl Reducer<'a, A>) { register_describer(|module| { let params = A::schema::(&mut module.inner); module.inner.add_reducer(I::NAME, params, I::LIFECYCLE); @@ -619,7 +625,7 @@ macro_rules! __make_register_reftype { #[cfg(feature = "unstable")] #[doc(hidden)] -pub fn volatile_nonatomic_schedule_immediate<'de, A: Args<'de>, R: Reducer<'de, A>, R2: ReducerInfo>( +pub fn volatile_nonatomic_schedule_immediate<'de, A: Args<'de>, R: Reducer<'de, A>, R2: FnInfo>( _reducer: R, args: A, ) { diff --git a/crates/bindings/tests/ui/reducers.stderr b/crates/bindings/tests/ui/reducers.stderr index 4b704ce4e3f..016755d7739 100644 --- a/crates/bindings/tests/ui/reducers.stderr +++ b/crates/bindings/tests/ui/reducers.stderr @@ -35,10 +35,10 @@ error[E0277]: invalid reducer signature = note: where each `Ti` type implements `SpacetimeType`. = note: note: required by a bound in `register_reducer` - --> src/rt.rs - | - | pub fn register_reducer<'a, A: Args<'a>, I: ReducerInfo>(_: impl Reducer<'a, A>) { - | ^^^^^^^^^^^^^^ required by this bound in `register_reducer` + --> src/rt.rs + | + | pub fn register_reducer<'a, A: Args<'a>, I: FnInfo>(_: impl Reducer<'a, A>) { + | ^^^^^^^^^^^^^^ required by this bound in `register_reducer` error[E0277]: the reducer argument `Test` does not implement `SpacetimeType` --> tests/ui/reducers.rs:6:40 @@ -96,10 +96,10 @@ error[E0277]: invalid reducer signature = note: where each `Ti` type implements `SpacetimeType`. = note: note: required by a bound in `register_reducer` - --> src/rt.rs - | - | pub fn register_reducer<'a, A: Args<'a>, I: ReducerInfo>(_: impl Reducer<'a, A>) { - | ^^^^^^^^^^^^^^ required by this bound in `register_reducer` + --> src/rt.rs + | + | pub fn register_reducer<'a, A: Args<'a>, I: FnInfo>(_: impl Reducer<'a, A>) { + | ^^^^^^^^^^^^^^ required by this bound in `register_reducer` error[E0277]: `Test` is not a valid reducer return type --> tests/ui/reducers.rs:9:46 @@ -129,30 +129,8 @@ error[E0277]: invalid reducer signature note: required by a bound in `invoke_reducer` --> src/rt.rs | - | pub fn invoke_reducer<'a, A: Args<'a>>( - | -------------- required by a bound in this function - | reducer: impl Reducer<'a, A>, - | ^^^^^^^^^^^^^^ required by this bound in `invoke_reducer` - -error[E0277]: invalid reducer signature - --> tests/ui/reducers.rs:23:4 - | - 22 | #[spacetimedb::reducer] - | ----------------------- required by a bound introduced by this call - 23 | fn missing_ctx(_a: u8) {} - | ^^^^^^^^^^^ this reducer signature is not valid - | - = help: the trait `Reducer<'_, _>` is not implemented for fn item `fn(u8) {missing_ctx}` - = note: - = note: reducer signatures must match the following pattern: - = note: `Fn(&ReducerContext, [T1, ...]) [-> Result<(), impl Display>]` - = note: where each `Ti` type implements `SpacetimeType`. - = note: -note: required by a bound in `register_reducer` - --> src/rt.rs - | - | pub fn register_reducer<'a, A: Args<'a>, I: ReducerInfo>(_: impl Reducer<'a, A>) { - | ^^^^^^^^^^^^^^ required by this bound in `register_reducer` + | pub fn register_reducer<'a, A: Args<'a>, I: FnInfo>(_: impl Reducer<'a, A>) { + | ^^^^^^^^^^^^^^ required by this bound in `register_reducer` error[E0277]: the first argument of a reducer must be `&ReducerContext` --> tests/ui/reducers.rs:23:20 @@ -200,10 +178,10 @@ error[E0277]: invalid reducer signature = note: where each `Ti` type implements `SpacetimeType`. = note: note: required by a bound in `register_reducer` - --> src/rt.rs - | - | pub fn register_reducer<'a, A: Args<'a>, I: ReducerInfo>(_: impl Reducer<'a, A>) { - | ^^^^^^^^^^^^^^ required by this bound in `register_reducer` + --> src/rt.rs + | + | pub fn register_reducer<'a, A: Args<'a>, I: FnInfo>(_: impl Reducer<'a, A>) { + | ^^^^^^^^^^^^^^ required by this bound in `register_reducer` error[E0277]: the first argument of a reducer must be `&ReducerContext` --> tests/ui/reducers.rs:26:21 From b621109ddffcc6e0355a0c520183be3425346323 Mon Sep 17 00:00:00 2001 From: joshua-spacetime Date: Wed, 8 Oct 2025 14:42:54 -0700 Subject: [PATCH 22/25] Add view macro and module describers --- crates/bindings-macro/src/lib.rs | 10 + crates/bindings-macro/src/reducer.rs | 3 - crates/bindings-macro/src/view.rs | 284 +++++++++++++++++++++ crates/bindings/src/lib.rs | 47 ++++ crates/bindings/src/rt.rs | 256 ++++++++++++++++++- crates/bindings/tests/ui/reducers.stderr | 52 ++-- crates/bindings/tests/ui/tables.stderr | 12 + crates/bindings/tests/ui/views.rs | 60 ++++- crates/bindings/tests/ui/views.stderr | 300 ++++++++++++++++++++++- crates/lib/src/db/raw_def/v9.rs | 50 ++++ 10 files changed, 1039 insertions(+), 35 deletions(-) create mode 100644 crates/bindings-macro/src/view.rs diff --git a/crates/bindings-macro/src/lib.rs b/crates/bindings-macro/src/lib.rs index cd85b80ef65..97c11392563 100644 --- a/crates/bindings-macro/src/lib.rs +++ b/crates/bindings-macro/src/lib.rs @@ -12,6 +12,7 @@ mod reducer; mod sats; mod table; mod util; +mod view; use proc_macro::TokenStream as StdTokenStream; use proc_macro2::TokenStream; @@ -38,6 +39,7 @@ mod sym { } symbol!(at); + symbol!(anonymous); symbol!(auto_inc); symbol!(btree); symbol!(client_connected); @@ -112,6 +114,14 @@ pub fn reducer(args: StdTokenStream, item: StdTokenStream) -> StdTokenStream { }) } +#[proc_macro_attribute] +pub fn view(args: StdTokenStream, item: StdTokenStream) -> StdTokenStream { + cvt_attr::(args, item, quote!(), |args, original_function| { + let args = view::ViewArgs::parse(args)?; + view::view_impl(args, original_function) + }) +} + /// It turns out to be shockingly difficult to construct an [`Attribute`]. /// That type is not [`Parse`], instead having two distinct methods /// for parsing "inner" vs "outer" attributes. diff --git a/crates/bindings-macro/src/reducer.rs b/crates/bindings-macro/src/reducer.rs index 5585a3c7a6b..2ca10c48b4f 100644 --- a/crates/bindings-macro/src/reducer.rs +++ b/crates/bindings-macro/src/reducer.rs @@ -145,9 +145,6 @@ pub(crate) fn reducer_impl(args: ReducerArgs, original_function: &ItemFn) -> syn #(const LIFECYCLE: Option = Some(#lifecycle);)* const ARG_NAMES: &'static [Option<&'static str>] = &[#(#opt_arg_names),*]; const INVOKE: Self::Invoke = #func_name::invoke; - fn return_type(_typespace: &mut impl spacetimedb::sats::typespace::TypespaceBuilder) -> Option { - None - } } }) } diff --git a/crates/bindings-macro/src/view.rs b/crates/bindings-macro/src/view.rs new file mode 100644 index 00000000000..3feed09c408 --- /dev/null +++ b/crates/bindings-macro/src/view.rs @@ -0,0 +1,284 @@ +use proc_macro2::TokenStream; +use quote::quote; +use syn::parse::Parser; +use syn::{FnArg, ItemFn}; + +use crate::sym; +use crate::util::{ident_to_litstr, match_meta}; + +pub(crate) struct ViewArgs { + anonymous: bool, +} + +impl ViewArgs { + /// Parse `#[view(public)]` where public is required + pub(crate) fn parse(input: TokenStream) -> syn::Result { + if input.is_empty() { + return Err(syn::Error::new( + proc_macro2::Span::call_site(), + "views must be declared as #[view(public)]; public is required", + )); + } + let mut public = false; + let mut anonymous = false; + syn::meta::parser(|meta| { + match_meta!(match meta { + sym::public => { + public = true; + } + sym::anonymous => { + anonymous = true; + } + }); + Ok(()) + }) + .parse2(input)?; + if !public { + return Err(syn::Error::new( + proc_macro2::Span::call_site(), + "views must be declared as #[view(public)]; public is required", + )); + } + Ok(Self { anonymous }) + } +} + +fn view_impl_anon(original_function: &ItemFn) -> syn::Result { + let func_name = &original_function.sig.ident; + let view_name = ident_to_litstr(func_name); + let vis = &original_function.vis; + + for param in &original_function.sig.generics.params { + let err = |msg| syn::Error::new_spanned(param, msg); + match param { + syn::GenericParam::Lifetime(_) => {} + syn::GenericParam::Type(_) => return Err(err("type parameters are not allowed on views")), + syn::GenericParam::Const(_) => return Err(err("const parameters are not allowed on views")), + } + } + + // Extract all function parameters, except for `self` ones that aren't allowed. + let typed_args = original_function + .sig + .inputs + .iter() + .map(|arg| match arg { + FnArg::Typed(arg) => Ok(arg), + FnArg::Receiver(_) => Err(syn::Error::new_spanned(arg, "`self` arguments not allowed in views")), + }) + .collect::>>()?; + + // Extract all function parameter names. + let opt_arg_names = typed_args.iter().map(|arg| { + if let syn::Pat::Ident(i) = &*arg.pat { + let name = i.ident.to_string(); + quote!(Some(#name)) + } else { + quote!(None) + } + }); + + let arg_tys = typed_args.iter().map(|arg| arg.ty.as_ref()).collect::>(); + + // Extract the context type + let ctx_ty = arg_tys.first().ok_or_else(|| { + syn::Error::new_spanned( + original_function.sig.fn_token, + "An anonymous view must have `&AnonymousViewContext` as its first argument", + ) + })?; + + // Extract the return type + let ret_ty = match &original_function.sig.output { + syn::ReturnType::Default => None, + syn::ReturnType::Type(_, t) => Some(&**t), + } + .ok_or_else(|| { + syn::Error::new_spanned( + original_function.sig.fn_token, + "views must return `Vec` where `T` is a `SpacetimeType`", + ) + })?; + + // Extract the non-context parameters + let arg_tys = arg_tys.iter().skip(1); + + let register_describer_symbol = format!("__preinit__20_register_describer_{}", view_name.value()); + + let lt_params = &original_function.sig.generics; + let lt_where_clause = <_params.where_clause; + + let generated_describe_function = quote! { + #[export_name = #register_describer_symbol] + pub extern "C" fn __register_describer() { + spacetimedb::rt::register_anonymous_view::<_, #func_name, _>(#func_name) + } + }; + + Ok(quote! { + const _: () = { #generated_describe_function }; + + #[allow(non_camel_case_types)] + #vis struct #func_name { _never: ::core::convert::Infallible } + + const _: () = { + fn _assert_args #lt_params () #lt_where_clause { + let _ = <#ctx_ty as spacetimedb::rt::AnonymousViewContextArg>::_ITEM; + let _ = <#ret_ty as spacetimedb::rt::ViewReturn>::_ITEM; + #(let _ = <#arg_tys as spacetimedb::rt::ViewArg>::_ITEM;)* + } + }; + + impl #func_name { + fn invoke(__ctx: spacetimedb::AnonymousViewContext, __args: &[u8]) -> Vec { + spacetimedb::rt::invoke_anonymous_view(#func_name, __ctx, __args) + } + } + + #[automatically_derived] + impl spacetimedb::rt::FnInfo for #func_name { + /// The type of this function + type Invoke = spacetimedb::rt::AnonymousFn; + + /// The name of this function + const NAME: &'static str = #view_name; + + /// The parameter names of this function + const ARG_NAMES: &'static [Option<&'static str>] = &[#(#opt_arg_names),*]; + + /// The pointer for invoking this function + const INVOKE: Self::Invoke = #func_name::invoke; + + /// The return type of this function + fn return_type( + ts: &mut impl spacetimedb::sats::typespace::TypespaceBuilder + ) -> Option { + Some(<#ret_ty as spacetimedb::SpacetimeType>::make_type(ts)) + } + } + }) +} + +fn view_impl_client(original_function: &ItemFn) -> syn::Result { + let func_name = &original_function.sig.ident; + let view_name = ident_to_litstr(func_name); + let vis = &original_function.vis; + + for param in &original_function.sig.generics.params { + let err = |msg| syn::Error::new_spanned(param, msg); + match param { + syn::GenericParam::Lifetime(_) => {} + syn::GenericParam::Type(_) => return Err(err("type parameters are not allowed on views")), + syn::GenericParam::Const(_) => return Err(err("const parameters are not allowed on views")), + } + } + + // Extract all function parameters, except for `self` ones that aren't allowed. + let typed_args = original_function + .sig + .inputs + .iter() + .map(|arg| match arg { + FnArg::Typed(arg) => Ok(arg), + FnArg::Receiver(_) => Err(syn::Error::new_spanned(arg, "`self` arguments not allowed in views")), + }) + .collect::>>()?; + + // Extract all function parameter names. + let opt_arg_names = typed_args.iter().map(|arg| { + if let syn::Pat::Ident(i) = &*arg.pat { + let name = i.ident.to_string(); + quote!(Some(#name)) + } else { + quote!(None) + } + }); + + let arg_tys = typed_args.iter().map(|arg| arg.ty.as_ref()).collect::>(); + + // Extract the context type + let ctx_ty = arg_tys.first().ok_or_else(|| { + syn::Error::new_spanned( + original_function.sig.fn_token, + "A view must have `&ViewContext` as its first argument", + ) + })?; + + // Extract the return type + let ret_ty = match &original_function.sig.output { + syn::ReturnType::Default => None, + syn::ReturnType::Type(_, t) => Some(&**t), + } + .ok_or_else(|| { + syn::Error::new_spanned( + original_function.sig.fn_token, + "views must return `Vec` where `T` is a `SpacetimeType`", + ) + })?; + + // Extract the non-context parameters + let arg_tys = arg_tys.iter().skip(1); + + let register_describer_symbol = format!("__preinit__20_register_describer_{}", view_name.value()); + + let lt_params = &original_function.sig.generics; + let lt_where_clause = <_params.where_clause; + + let generated_describe_function = quote! { + #[export_name = #register_describer_symbol] + pub extern "C" fn __register_describer() { + spacetimedb::rt::register_view::<_, #func_name, _>(#func_name) + } + }; + + Ok(quote! { + const _: () = { #generated_describe_function }; + + #[allow(non_camel_case_types)] + #vis struct #func_name { _never: ::core::convert::Infallible } + + const _: () = { + fn _assert_args #lt_params () #lt_where_clause { + let _ = <#ctx_ty as spacetimedb::rt::ViewContextArg>::_ITEM; + let _ = <#ret_ty as spacetimedb::rt::ViewReturn>::_ITEM; + #(let _ = <#arg_tys as spacetimedb::rt::ViewArg>::_ITEM;)* + } + }; + + impl #func_name { + fn invoke(__ctx: spacetimedb::ViewContext, __args: &[u8]) -> Vec { + spacetimedb::rt::invoke_view(#func_name, __ctx, __args) + } + } + + #[automatically_derived] + impl spacetimedb::rt::FnInfo for #func_name { + /// The type of this function + type Invoke = spacetimedb::rt::ViewFn; + + /// The name of this function + const NAME: &'static str = #view_name; + + /// The parameter names of this function + const ARG_NAMES: &'static [Option<&'static str>] = &[#(#opt_arg_names),*]; + + /// The pointer for invoking this function + const INVOKE: Self::Invoke = #func_name::invoke; + + /// The return type of this function + fn return_type( + ts: &mut impl spacetimedb::sats::typespace::TypespaceBuilder + ) -> Option { + Some(<#ret_ty as spacetimedb::SpacetimeType>::make_type(ts)) + } + } + }) +} + +pub(crate) fn view_impl(args: ViewArgs, original_function: &ItemFn) -> syn::Result { + if args.anonymous { + view_impl_anon(original_function) + } else { + view_impl_client(original_function) + } +} diff --git a/crates/bindings/src/lib.rs b/crates/bindings/src/lib.rs index b7bbb18101d..38dbd190712 100644 --- a/crates/bindings/src/lib.rs +++ b/crates/bindings/src/lib.rs @@ -666,6 +666,53 @@ pub use spacetimedb_bindings_macro::table; #[doc(inline)] pub use spacetimedb_bindings_macro::reducer; +/// Marks a function as a spacetimedb view. +/// +/// A view is a function with read-only access to the database. +/// +/// The first argument of a view is always a [`&ViewContext`] or [`&AnonymousViewContext`]. +/// The former can only read from the database whereas latter can also access info about the caller. +/// +/// After this, a view can take any number of arguments just like reducers. +/// These arguments must implement the [`SpacetimeType`], [`Serialize`], and [`Deserialize`] traits. +/// All of these traits can be derived at once by marking a type with `#[derive(SpacetimeType)]`. +/// +/// Views return `Vec` where `T` is a `SpacetimeType`. +/// +/// ```no_run +/// # mod demo { +/// use spacetimedb::{view, table, AnonymousViewContext, ViewContext}; +/// +/// #[table(name = player)] +/// struct Player { +/// #[unique] +/// identity: Identity, +/// #[index(btree)] +/// level: u32, +/// } +/// +/// #[view(public)] +/// pub fn player(ctx: &ViewContext) -> Vec { +/// ctx.db.player().identity().find(ctx.sender).into_iter().collect() +/// } +/// +/// #[view(public, anonymous)] +/// pub fn player(ctx: &AnonymousViewContext, level: u32) -> Vec { +/// ctx.db.player().level().filter(level).collect() +/// } +/// # } +/// ``` +/// +/// Just like reducers, views are limited in their ability to interact with the outside world. +/// They have no access to any network or filesystem interfaces. +/// Calling methods from [`std::io`], [`std::net`], or [`std::fs`] will result in runtime errors. +/// +/// Views are callable by reducers and other views simply by passing their `ViewContext`.. +/// This is a regular function call. +/// The callee will run within the caller's transaction. +#[doc(inline)] +pub use spacetimedb_bindings_macro::view; + /// One of two possible types that can be passed as the first argument to a `#[view]`. /// The other is [`ViewContext`]. /// Use this type if the view does not depend on the caller's identity. diff --git a/crates/bindings/src/rt.rs b/crates/bindings/src/rt.rs index d4e76cc5325..6087e532241 100644 --- a/crates/bindings/src/rt.rs +++ b/crates/bindings/src/rt.rs @@ -1,14 +1,16 @@ #![deny(unsafe_op_in_unsafe_fn)] use crate::table::IndexAlgo; -use crate::{sys, IterBuf, ReducerContext, ReducerResult, SpacetimeType, Table}; +use crate::{ + sys, AnonymousViewContext, IterBuf, LocalReadOnly, ReducerContext, ReducerResult, SpacetimeType, Table, ViewContext, +}; pub use spacetimedb_lib::db::raw_def::v9::Lifecycle as LifecycleReducer; use spacetimedb_lib::db::raw_def::v9::{RawIndexAlgorithm, RawModuleDefV9Builder, TableType}; use spacetimedb_lib::de::{self, Deserialize, Error as _, SeqProductAccess}; use spacetimedb_lib::sats::typespace::TypespaceBuilder; use spacetimedb_lib::sats::{impl_deserialize, impl_serialize, ProductTypeElement}; use spacetimedb_lib::ser::{Serialize, SerializeSeqProduct}; -use spacetimedb_lib::{bsatn, ConnectionId, Identity, ProductType, RawModuleDef, Timestamp}; +use spacetimedb_lib::{bsatn, AlgebraicType, ConnectionId, Identity, ProductType, RawModuleDef, Timestamp}; use spacetimedb_primitives::*; use std::fmt; use std::marker::PhantomData; @@ -43,6 +45,62 @@ pub trait Reducer<'de, A: Args<'de>> { fn invoke(&self, ctx: &ReducerContext, args: A) -> ReducerResult; } +/// Invoke a caller-specific view. +/// Returns a bsatn encoded vec of rows. +pub fn invoke_view<'a, A: Args<'a>, T: SpacetimeType + Serialize>( + view: impl View<'a, A, T>, + ctx: ViewContext, + args: &'a [u8], +) -> Vec { + // Deserialize the arguments from a bsatn encoding. + let SerDeArgs(args) = bsatn::from_slice(args).expect("unable to decode args"); + let rows: Vec = view.invoke(&ctx, args); + let mut buf = IterBuf::take(); + buf.serialize_into(&rows).expect("unable to encode rows"); + std::mem::take(&mut *buf) +} +/// A trait for types representing the *execution logic* of a caller-specific view. +#[diagnostic::on_unimplemented( + message = "invalid view signature", + label = "this view signature is not valid", + note = "", + note = "view signatures must match:", + note = " `Fn(&ViewContext, [T1, ...]) -> Vec`", + note = "where each `Ti` implements `SpacetimeType`.", + note = "" +)] +pub trait View<'de, A: Args<'de>, T: SpacetimeType + Serialize> { + fn invoke(&self, ctx: &ViewContext, args: A) -> Vec; +} + +/// Invoke an anonymous view. +/// Returns a bsatn encoded vec of rows. +pub fn invoke_anonymous_view<'a, A: Args<'a>, T: SpacetimeType + Serialize>( + view: impl AnonymousView<'a, A, T>, + ctx: AnonymousViewContext, + args: &'a [u8], +) -> Vec { + // Deserialize the arguments from a bsatn encoding. + let SerDeArgs(args) = bsatn::from_slice(args).expect("unable to decode args"); + let rows: Vec = view.invoke(&ctx, args); + let mut buf = IterBuf::take(); + buf.serialize_into(&rows).expect("unable to encode rows"); + std::mem::take(&mut *buf) +} +/// A trait for types representing the *execution logic* of an anonymous view. +#[diagnostic::on_unimplemented( + message = "invalid view signature", + label = "this view signature is not valid", + note = "", + note = "view signatures must match:", + note = " `Fn(&AnonymousViewContext, [T1, ...]) -> Vec`", + note = "where each `Ti` implements `SpacetimeType`.", + note = "" +)] +pub trait AnonymousView<'de, A: Args<'de>, T: SpacetimeType + Serialize> { + fn invoke(&self, ctx: &AnonymousViewContext, args: A) -> Vec; +} + /// A trait for types that can *describe* a callable function such as a reducer or view. pub trait FnInfo { /// The type of function to invoke. @@ -61,7 +119,10 @@ pub trait FnInfo { const INVOKE: Self::Invoke; /// The return type of this function. - fn return_type(typespace: &mut impl TypespaceBuilder) -> Option; + /// Currently only implemented for views. + fn return_type(_ts: &mut impl TypespaceBuilder) -> Option { + None + } } /// A trait of types representing the arguments of a reducer. @@ -75,8 +136,8 @@ pub trait Args<'de>: Sized { /// Serialize the arguments in `self` into the sequence `prod` according to the type `S`. fn serialize_seq_product(&self, prod: &mut S) -> Result<(), S::Error>; - /// Returns the schema for this reducer provided a `typespace`. - fn schema>(typespace: &mut impl TypespaceBuilder) -> ProductType; + /// Returns the schema of the args for this function provided a `typespace`. + fn schema(typespace: &mut impl TypespaceBuilder) -> ProductType; } /// A trait of types representing the result of executing a reducer. @@ -125,6 +186,39 @@ pub trait ReducerArg { } impl ReducerArg for T {} +#[diagnostic::on_unimplemented(message = "A view must have `&ViewContext` as its first argument")] +pub trait ViewContextArg { + #[doc(hidden)] + const _ITEM: () = (); +} +impl ViewContextArg for &ViewContext {} + +#[diagnostic::on_unimplemented(message = "An anonymous view must have `&AnonymousViewContext` as its first argument")] +pub trait AnonymousViewContextArg { + #[doc(hidden)] + const _ITEM: () = (); +} +impl AnonymousViewContextArg for &AnonymousViewContext {} + +/// A trait of types that can be an argument of a view. +#[diagnostic::on_unimplemented( + message = "the view argument `{Self}` does not implement `SpacetimeType`", + note = "if you own the type, try adding `#[derive(SpacetimeType)]` to its definition" +)] +pub trait ViewArg { + #[doc(hidden)] + const _ITEM: () = (); +} +impl ViewArg for T {} + +/// A trait of types that can be the return type of a view. +#[diagnostic::on_unimplemented(message = "Views must return `Vec` where `T` is a `SpacetimeType`")] +pub trait ViewReturn { + #[doc(hidden)] + const _ITEM: () = (); +} +impl ViewReturn for Vec {} + /// Assert that a reducer type-checks with a given type. pub const fn scheduled_reducer_typecheck<'de, Row>(_x: impl ReducerForScheduledTable<'de, Row>) where @@ -270,6 +364,35 @@ macro_rules! impl_reducer { } } + // Implement `View<..., ViewContext>` for the tuple type `($($T,)*)`. + impl<'de, Func, Elem, $($T),*> + View<'de, ($($T,)*), Elem> for Func + where + $($T: SpacetimeType + Deserialize<'de> + Serialize,)* + Func: Fn(&ViewContext, $($T),*) -> Vec, + Elem: SpacetimeType + Serialize, + { + #[allow(non_snake_case)] + fn invoke(&self, ctx: &ViewContext, args: ($($T,)*)) -> Vec { + let ($($T,)*) = args; + self(ctx, $($T),*) + } + } + + // Implement `View<..., AnonymousViewContext>` for the tuple type `($($T,)*)`. + impl<'de, Func, Elem, $($T),*> + AnonymousView<'de, ($($T,)*), Elem> for Func + where + $($T: SpacetimeType + Deserialize<'de> + Serialize,)* + Func: Fn(&AnonymousViewContext, $($T),*) -> Vec, + Elem: SpacetimeType + Serialize, + { + #[allow(non_snake_case)] + fn invoke(&self, ctx: &AnonymousViewContext, args: ($($T,)*)) -> Vec { + let ($($T,)*) = args; + self(ctx, $($T),*) + } + } }; // Counts the number of elements in the tuple. (@count $($T:ident)*) => { @@ -376,6 +499,36 @@ pub fn register_reducer<'a, A: Args<'a>, I: FnInfo>(_: impl }) } +/// Registers a describer for the view `I` with arguments `A` and return type `Vec`. +pub fn register_view<'a, A, I, T>(_: impl View<'a, A, T>) +where + A: Args<'a>, + I: FnInfo, + T: SpacetimeType + Serialize, +{ + register_describer(|module| { + let params = A::schema::(&mut module.inner); + let return_type = I::return_type(&mut module.inner).unwrap(); + module.inner.add_view(I::NAME, false, params, return_type); + module.views.push(I::INVOKE); + }) +} + +/// Registers a describer for the anonymous view `I` with arguments `A` and return type `Vec`. +pub fn register_anonymous_view<'a, A, I, T>(_: impl AnonymousView<'a, A, T>) +where + A: Args<'a>, + I: FnInfo, + T: SpacetimeType + Serialize, +{ + register_describer(|module| { + let params = A::schema::(&mut module.inner); + let return_type = I::return_type(&mut module.inner).unwrap(); + module.inner.add_view(I::NAME, true, params, return_type); + module.views_anon.push(I::INVOKE); + }) +} + /// Registers a row-level security policy. pub fn register_row_level_security(sql: &'static str) { register_describer(|module| { @@ -385,11 +538,15 @@ pub fn register_row_level_security(sql: &'static str) { /// A builder for a module. #[derive(Default)] -struct ModuleBuilder { +pub struct ModuleBuilder { /// The module definition. inner: RawModuleDefV9Builder, /// The reducers of the module. reducers: Vec, + /// The client specific views of the module. + views: Vec, + /// The anonymous views of the module. + views_anon: Vec, } // Not actually a mutex; because WASM is single-threaded this basically just turns into a refcell. @@ -400,6 +557,14 @@ static DESCRIBERS: Mutex>> = Mutex::new(Vec::new()); pub type ReducerFn = fn(ReducerContext, &[u8]) -> ReducerResult; static REDUCERS: OnceLock> = OnceLock::new(); +/// A view function takes in `(ViewContext, Args)` and returns a Vec of bytes. +pub type ViewFn = fn(ViewContext, &[u8]) -> Vec; +static VIEWS: OnceLock> = OnceLock::new(); + +/// An anonymous view function takes in `(AnonymousViewContext, Args)` and returns a Vec of bytes. +pub type AnonymousFn = fn(AnonymousViewContext, &[u8]) -> Vec; +static ANONYMOUS_VIEWS: OnceLock> = OnceLock::new(); + /// Called by the host when the module is initialized /// to describe the module into a serialized form that is returned. /// @@ -428,8 +593,10 @@ extern "C" fn __describe_module__(description: BytesSink) { let module_def = RawModuleDef::V9(module_def); let bytes = bsatn::to_vec(&module_def).expect("unable to serialize typespace"); - // Write the set of reducers. + // Write the set of reducers and views. REDUCERS.set(module.reducers).ok().unwrap(); + VIEWS.set(module.views).ok().unwrap(); + ANONYMOUS_VIEWS.set(module.views_anon).ok().unwrap(); // Write the bsatn data into the sink. write_to_sink(description, &bytes); @@ -516,6 +683,81 @@ extern "C" fn __call_reducer__( } } +/// Called by the host to execute an anonymous view. +/// +/// The `args` is a `BytesSource`, registered on the host side, +/// which can be read with `bytes_source_read`. +/// The contents of the buffer are the BSATN-encoding of the arguments to the view. +/// In the case of empty arguments, `args` will be 0, that is, invalid. +/// +/// The output of the view is written to a `BytesSink`, +/// registered on the host side, with `bytes_sink_write`. +#[no_mangle] +extern "C" fn __call_view_anon__(id: usize, args: BytesSource, sink: BytesSink) -> i16 { + let views = ANONYMOUS_VIEWS.get().unwrap(); + write_to_sink( + sink, + &with_read_args(args, |args| { + views[id](AnonymousViewContext { db: LocalReadOnly {} }, args) + }), + ); + 0 +} + +/// Called by the host to execute a view when the `sender` calls the view identified by `id` with `args`. +/// See [`__call_reducer__`] for more commentary on the arguments. +/// +/// The `args` is a `BytesSource`, registered on the host side, +/// which can be read with `bytes_source_read`. +/// The contents of the buffer are the BSATN-encoding of the arguments to the view. +/// In the case of empty arguments, `args` will be 0, that is, invalid. +/// +/// The output of the view is written to a `BytesSink`, +/// registered on the host side, with `bytes_sink_write`. +#[no_mangle] +extern "C" fn __call_view__( + id: usize, + sender_0: u64, + sender_1: u64, + sender_2: u64, + sender_3: u64, + conn_id_0: u64, + conn_id_1: u64, + args: BytesSource, + sink: BytesSink, +) -> i16 { + // Piece together `sender_i` into an `Identity`. + let sender = [sender_0, sender_1, sender_2, sender_3]; + let sender: [u8; 32] = bytemuck::must_cast(sender); + let sender = Identity::from_byte_array(sender); // The LITTLE-ENDIAN constructor. + + // Piece together `conn_id_i` into a `ConnectionId`. + // The all-zeros `ConnectionId` (`ConnectionId::ZERO`) is interpreted as `None`. + let conn_id = [conn_id_0, conn_id_1]; + let conn_id: [u8; 16] = bytemuck::must_cast(conn_id); + let conn_id = ConnectionId::from_le_byte_array(conn_id); // The LITTLE-ENDIAN constructor. + let conn_id = (conn_id != ConnectionId::ZERO).then_some(conn_id); + + let views = VIEWS.get().unwrap(); + let db = LocalReadOnly {}; + let connection_id = conn_id; + + write_to_sink( + sink, + &with_read_args(args, |args| { + views[id]( + ViewContext { + sender, + connection_id, + db, + }, + args, + ) + }), + ); + 0 +} + /// Run `logic` with `args` read from the host into a `&[u8]`. fn with_read_args(args: BytesSource, logic: impl FnOnce(&[u8]) -> R) -> R { if args == BytesSource::INVALID { diff --git a/crates/bindings/tests/ui/reducers.stderr b/crates/bindings/tests/ui/reducers.stderr index 016755d7739..1d73ad4d15d 100644 --- a/crates/bindings/tests/ui/reducers.stderr +++ b/crates/bindings/tests/ui/reducers.stderr @@ -35,10 +35,10 @@ error[E0277]: invalid reducer signature = note: where each `Ti` type implements `SpacetimeType`. = note: note: required by a bound in `register_reducer` - --> src/rt.rs - | - | pub fn register_reducer<'a, A: Args<'a>, I: FnInfo>(_: impl Reducer<'a, A>) { - | ^^^^^^^^^^^^^^ required by this bound in `register_reducer` + --> src/rt.rs + | + | pub fn register_reducer<'a, A: Args<'a>, I: FnInfo>(_: impl Reducer<'a, A>) { + | ^^^^^^^^^^^^^^ required by this bound in `register_reducer` error[E0277]: the reducer argument `Test` does not implement `SpacetimeType` --> tests/ui/reducers.rs:6:40 @@ -96,10 +96,10 @@ error[E0277]: invalid reducer signature = note: where each `Ti` type implements `SpacetimeType`. = note: note: required by a bound in `register_reducer` - --> src/rt.rs - | - | pub fn register_reducer<'a, A: Args<'a>, I: FnInfo>(_: impl Reducer<'a, A>) { - | ^^^^^^^^^^^^^^ required by this bound in `register_reducer` + --> src/rt.rs + | + | pub fn register_reducer<'a, A: Args<'a>, I: FnInfo>(_: impl Reducer<'a, A>) { + | ^^^^^^^^^^^^^^ required by this bound in `register_reducer` error[E0277]: `Test` is not a valid reducer return type --> tests/ui/reducers.rs:9:46 @@ -129,8 +129,30 @@ error[E0277]: invalid reducer signature note: required by a bound in `invoke_reducer` --> src/rt.rs | - | pub fn register_reducer<'a, A: Args<'a>, I: FnInfo>(_: impl Reducer<'a, A>) { - | ^^^^^^^^^^^^^^ required by this bound in `register_reducer` + | pub fn invoke_reducer<'a, A: Args<'a>>( + | -------------- required by a bound in this function + | reducer: impl Reducer<'a, A>, + | ^^^^^^^^^^^^^^ required by this bound in `invoke_reducer` + +error[E0277]: invalid reducer signature + --> tests/ui/reducers.rs:23:4 + | + 22 | #[spacetimedb::reducer] + | ----------------------- required by a bound introduced by this call + 23 | fn missing_ctx(_a: u8) {} + | ^^^^^^^^^^^ this reducer signature is not valid + | + = help: the trait `Reducer<'_, _>` is not implemented for fn item `fn(u8) {missing_ctx}` + = note: + = note: reducer signatures must match the following pattern: + = note: `Fn(&ReducerContext, [T1, ...]) [-> Result<(), impl Display>]` + = note: where each `Ti` type implements `SpacetimeType`. + = note: +note: required by a bound in `register_reducer` + --> src/rt.rs + | + | pub fn register_reducer<'a, A: Args<'a>, I: FnInfo>(_: impl Reducer<'a, A>) { + | ^^^^^^^^^^^^^^ required by this bound in `register_reducer` error[E0277]: the first argument of a reducer must be `&ReducerContext` --> tests/ui/reducers.rs:23:20 @@ -178,10 +200,10 @@ error[E0277]: invalid reducer signature = note: where each `Ti` type implements `SpacetimeType`. = note: note: required by a bound in `register_reducer` - --> src/rt.rs - | - | pub fn register_reducer<'a, A: Args<'a>, I: FnInfo>(_: impl Reducer<'a, A>) { - | ^^^^^^^^^^^^^^ required by this bound in `register_reducer` + --> src/rt.rs + | + | pub fn register_reducer<'a, A: Args<'a>, I: FnInfo>(_: impl Reducer<'a, A>) { + | ^^^^^^^^^^^^^^ required by this bound in `register_reducer` error[E0277]: the first argument of a reducer must be `&ReducerContext` --> tests/ui/reducers.rs:26:21 @@ -232,4 +254,4 @@ note: required by a bound in `scheduled_reducer_typecheck` --> src/rt.rs | | pub const fn scheduled_reducer_typecheck<'de, Row>(_x: impl ReducerForScheduledTable<'de, Row>) - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `scheduled_reducer_typecheck` + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `scheduled_reducer_typecheck` \ No newline at end of file diff --git a/crates/bindings/tests/ui/tables.stderr b/crates/bindings/tests/ui/tables.stderr index 4dcf4f1dc26..897ea77fd64 100644 --- a/crates/bindings/tests/ui/tables.stderr +++ b/crates/bindings/tests/ui/tables.stderr @@ -10,6 +10,18 @@ error: const parameters are not allowed on tables 14 | struct ConstParam {} | ^^^^^^^^^^^ +error[E0412]: cannot find type `ReducerContext` in this scope + --> tests/ui/tables.rs:30:1 + | +30 | #[spacetimedb::reducer] + | ^^^^^^^^^^^^^^^^^^^^^^^ not found in this scope + | + = note: this error originates in the attribute macro `spacetimedb::reducer` (in Nightly builds, run with -Z macro-backtrace for more info) +help: consider importing this struct + | + 1 + use spacetimedb::ReducerContext; + | + error[E0277]: the trait bound `Test: SpacetimeType` is not satisfied --> tests/ui/tables.rs:5:8 | diff --git a/crates/bindings/tests/ui/views.rs b/crates/bindings/tests/ui/views.rs index 131a0cf2f47..32ae227d690 100644 --- a/crates/bindings/tests/ui/views.rs +++ b/crates/bindings/tests/ui/views.rs @@ -1,4 +1,4 @@ -use spacetimedb::{reducer, table, ReducerContext}; +use spacetimedb::{reducer, table, view, AnonymousViewContext, Identity, ReducerContext, ViewContext}; #[table(name = test)] struct Test { @@ -64,4 +64,62 @@ fn read_only_btree_index_no_delete(ctx: &ReducerContext) { read_only.db.test().x().delete(0u32..); } +#[table(name = player)] +struct Player { + #[unique] + identity: Identity, +} + +/// Private views not allowed; must be `#[view(public)]` +#[view] +fn view_def_no_public(_: &ViewContext) -> Vec { + vec![] +} + +/// A `ViewContext` is required +#[view(public)] +fn view_def_no_context() -> Vec { + vec![] +} + +/// A `ViewContext` is required +#[view(public)] +fn view_def_wrong_context_1(_: &ReducerContext) -> Vec { + vec![] +} + +/// A `ViewContext` is required +#[view(public)] +fn view_def_wrong_context_2(_: &AnonymousViewContext) -> Vec { + vec![] +} + +/// An `AnonymousViewContext` is required +#[view(public, anonymous)] +fn anonymous_view_def_no_context() -> Vec { + vec![] +} + +/// An `AnonymousViewContext` is required +#[view(public, anonymous)] +fn anonymous_view_def_wrong_context_1(_: &ReducerContext) -> Vec { + vec![] +} + +/// An `AnonymousViewContext` is required +#[view(public, anonymous)] +fn anonymous_view_def_wrong_context_2(_: &ViewContext) -> Vec { + vec![] +} + +/// Must return `Vec` where `T` is a SpacetimeType +#[view(public)] +fn view_def_no_return(_: &ViewContext) {} + +/// Must return `Vec` where `T` is a SpacetimeType +#[view(public)] +fn view_def_wrong_return(_: &ViewContext) -> Option { + None +} + fn main() {} diff --git a/crates/bindings/tests/ui/views.stderr b/crates/bindings/tests/ui/views.stderr index 94dd576c8df..349de146a19 100644 --- a/crates/bindings/tests/ui/views.stderr +++ b/crates/bindings/tests/ui/views.stderr @@ -1,3 +1,29 @@ +error: views must be declared as #[view(public)]; public is required + --> tests/ui/views.rs:74:1 + | +74 | #[view] + | ^^^^^^^ + | + = note: this error originates in the attribute macro `view` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: A view must have `&ViewContext` as its first argument + --> tests/ui/views.rs:81:1 + | +81 | fn view_def_no_context() -> Vec { + | ^^ + +error: An anonymous view must have `&AnonymousViewContext` as its first argument + --> tests/ui/views.rs:99:1 + | +99 | fn anonymous_view_def_no_context() -> Vec { + | ^^ + +error: views must return `Vec` where `T` is a `SpacetimeType` + --> tests/ui/views.rs:117:1 + | +117 | fn view_def_no_return(_: &ViewContext) {} + | ^^ + error[E0599]: no method named `iter` found for reference `&test__ViewHandle` in the current scope --> tests/ui/views.rs:15:34 | @@ -25,6 +51,9 @@ error[E0599]: `&test__ViewHandle` is not an iterator which is required by `&mut test__ViewHandle: Iterator` note: the trait `Iterator` must be implemented --> $RUST/core/src/iter/traits/iterator.rs + | + | pub trait Iterator { + | ^^^^^^^^^^^^^^^^^^ = help: items from traits can only be used if the trait is implemented and in scope = note: the following traits define an item `count`, perhaps you need to implement one of them: candidate #1: `Table` @@ -44,16 +73,19 @@ error[E0599]: no method named `insert` found for reference `&test__ViewHandle` i candidate #4: `ppv_lite86::types::Vec4` error[E0599]: no method named `try_insert` found for reference `&test__ViewHandle` in the current scope - --> tests/ui/views.rs:36:25 - | -36 | read_only.db.test().try_insert(Test { id: 0, x: 0 }); - | ^^^^^^^^^^ - | - = help: items from traits can only be used if the trait is implemented and in scope - = note: the following trait defines an item `try_insert`, perhaps you need to implement it: - candidate #1: `Table` + --> tests/ui/views.rs:36:25 + | + 36 | read_only.db.test().try_insert(Test { id: 0, x: 0 }); + | ^^^^^^^^^^ + | + = help: items from traits can only be used if the trait is implemented and in scope + = note: the following trait defines an item `try_insert`, perhaps you need to implement it: + candidate #1: `Table` help: there is a method `try_into` with a similar name, but with different arguments - --> $RUST/core/src/convert/mod.rs + --> $RUST/core/src/convert/mod.rs + | + | fn try_into(self) -> Result; + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ error[E0599]: no method named `delete` found for reference `&test__ViewHandle` in the current scope --> tests/ui/views.rs:43:25 @@ -82,3 +114,253 @@ error[E0599]: no method named `delete` found for struct `RangedIndexReadOnly` in | 64 | read_only.db.test().x().delete(0u32..); | ^^^^^^ method not found in `RangedIndexReadOnly` + +error[E0277]: invalid view signature + --> tests/ui/views.rs:87:4 + | + 86 | #[view(public)] + | --------------- required by a bound introduced by this call + 87 | fn view_def_wrong_context_1(_: &ReducerContext) -> Vec { + | ^^^^^^^^^^^^^^^^^^^^^^^^ this view signature is not valid + | + = help: the trait `spacetimedb::rt::View<'_, _, _>` is not implemented for fn item `for<'a> fn(&'a ReducerContext) -> Vec {view_def_wrong_context_1}` + = note: + = note: view signatures must match: + = note: `Fn(&ViewContext, [T1, ...]) -> Vec` + = note: where each `Ti` implements `SpacetimeType`. + = note: +note: required by a bound in `register_view` + --> src/rt.rs + | + | pub fn register_view<'a, A, I, T>(_: impl View<'a, A, T>) + | ^^^^^^^^^^^^^^ required by this bound in `register_view` + +error[E0277]: A view must have `&ViewContext` as its first argument + --> tests/ui/views.rs:87:32 + | +87 | fn view_def_wrong_context_1(_: &ReducerContext) -> Vec { + | ^^^^^^^^^^^^^^^ the trait `ViewContextArg` is not implemented for `&ReducerContext` + | + = help: the trait `ViewContextArg` is implemented for `&ViewContext` + +error[E0277]: invalid view signature + --> tests/ui/views.rs:87:4 + | +86 | #[view(public)] + | --------------- required by a bound introduced by this call +87 | fn view_def_wrong_context_1(_: &ReducerContext) -> Vec { + | ^^^^^^^^^^^^^^^^^^^^^^^^ this view signature is not valid + | + = help: the trait `spacetimedb::rt::View<'_, _, _>` is not implemented for fn item `for<'a> fn(&'a ReducerContext) -> Vec {view_def_wrong_context_1}` + = note: + = note: view signatures must match: + = note: `Fn(&ViewContext, [T1, ...]) -> Vec` + = note: where each `Ti` implements `SpacetimeType`. + = note: +note: required by a bound in `invoke_view` + --> src/rt.rs + | + | pub fn invoke_view<'a, A: Args<'a>, T: SpacetimeType + Serialize>( + | ----------- required by a bound in this function + | view: impl View<'a, A, T>, + | ^^^^^^^^^^^^^^ required by this bound in `invoke_view` + +error[E0277]: invalid view signature + --> tests/ui/views.rs:93:4 + | + 92 | #[view(public)] + | --------------- required by a bound introduced by this call + 93 | fn view_def_wrong_context_2(_: &AnonymousViewContext) -> Vec { + | ^^^^^^^^^^^^^^^^^^^^^^^^ this view signature is not valid + | + = help: the trait `spacetimedb::rt::View<'_, _, _>` is not implemented for fn item `for<'a> fn(&'a AnonymousViewContext) -> Vec {view_def_wrong_context_2}` + = note: + = note: view signatures must match: + = note: `Fn(&ViewContext, [T1, ...]) -> Vec` + = note: where each `Ti` implements `SpacetimeType`. + = note: +note: required by a bound in `register_view` + --> src/rt.rs + | + | pub fn register_view<'a, A, I, T>(_: impl View<'a, A, T>) + | ^^^^^^^^^^^^^^ required by this bound in `register_view` + +error[E0277]: A view must have `&ViewContext` as its first argument + --> tests/ui/views.rs:93:32 + | +93 | fn view_def_wrong_context_2(_: &AnonymousViewContext) -> Vec { + | ^^^^^^^^^^^^^^^^^^^^^ the trait `ViewContextArg` is not implemented for `&AnonymousViewContext` + | + = help: the trait `ViewContextArg` is implemented for `&ViewContext` + +error[E0277]: invalid view signature + --> tests/ui/views.rs:93:4 + | +92 | #[view(public)] + | --------------- required by a bound introduced by this call +93 | fn view_def_wrong_context_2(_: &AnonymousViewContext) -> Vec { + | ^^^^^^^^^^^^^^^^^^^^^^^^ this view signature is not valid + | + = help: the trait `spacetimedb::rt::View<'_, _, _>` is not implemented for fn item `for<'a> fn(&'a AnonymousViewContext) -> Vec {view_def_wrong_context_2}` + = note: + = note: view signatures must match: + = note: `Fn(&ViewContext, [T1, ...]) -> Vec` + = note: where each `Ti` implements `SpacetimeType`. + = note: +note: required by a bound in `invoke_view` + --> src/rt.rs + | + | pub fn invoke_view<'a, A: Args<'a>, T: SpacetimeType + Serialize>( + | ----------- required by a bound in this function + | view: impl View<'a, A, T>, + | ^^^^^^^^^^^^^^ required by this bound in `invoke_view` + +error[E0277]: invalid view signature + --> tests/ui/views.rs:105:4 + | +104 | #[view(public, anonymous)] + | -------------------------- required by a bound introduced by this call +105 | fn anonymous_view_def_wrong_context_1(_: &ReducerContext) -> Vec { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ this view signature is not valid + | + = help: the trait `AnonymousView<'_, _, _>` is not implemented for fn item `for<'a> fn(&'a ReducerContext) -> Vec {anonymous_view_def_wrong_context_1}` + = note: + = note: view signatures must match: + = note: `Fn(&AnonymousViewContext, [T1, ...]) -> Vec` + = note: where each `Ti` implements `SpacetimeType`. + = note: +note: required by a bound in `register_anonymous_view` + --> src/rt.rs + | + | pub fn register_anonymous_view<'a, A, I, T>(_: impl AnonymousView<'a, A, T>) + | ^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `register_anonymous_view` + +error[E0277]: An anonymous view must have `&AnonymousViewContext` as its first argument + --> tests/ui/views.rs:105:42 + | +105 | fn anonymous_view_def_wrong_context_1(_: &ReducerContext) -> Vec { + | ^^^^^^^^^^^^^^^ the trait `AnonymousViewContextArg` is not implemented for `&ReducerContext` + | + = help: the trait `AnonymousViewContextArg` is implemented for `&AnonymousViewContext` + +error[E0277]: invalid view signature + --> tests/ui/views.rs:105:4 + | +104 | #[view(public, anonymous)] + | -------------------------- required by a bound introduced by this call +105 | fn anonymous_view_def_wrong_context_1(_: &ReducerContext) -> Vec { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ this view signature is not valid + | + = help: the trait `AnonymousView<'_, _, _>` is not implemented for fn item `for<'a> fn(&'a ReducerContext) -> Vec {anonymous_view_def_wrong_context_1}` + = note: + = note: view signatures must match: + = note: `Fn(&AnonymousViewContext, [T1, ...]) -> Vec` + = note: where each `Ti` implements `SpacetimeType`. + = note: +note: required by a bound in `invoke_anonymous_view` + --> src/rt.rs + | + 78 | pub fn invoke_anonymous_view<'a, A: Args<'a>, T: SpacetimeType + Serialize>( + | --------------------- required by a bound in this function + 79 | view: impl AnonymousView<'a, A, T>, + | ^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `invoke_anonymous_view` + +error[E0277]: invalid view signature + --> tests/ui/views.rs:111:4 + | +110 | #[view(public, anonymous)] + | -------------------------- required by a bound introduced by this call +111 | fn anonymous_view_def_wrong_context_2(_: &ViewContext) -> Vec { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ this view signature is not valid + | + = help: the trait `AnonymousView<'_, _, _>` is not implemented for fn item `for<'a> fn(&'a ViewContext) -> Vec {anonymous_view_def_wrong_context_2}` + = note: + = note: view signatures must match: + = note: `Fn(&AnonymousViewContext, [T1, ...]) -> Vec` + = note: where each `Ti` implements `SpacetimeType`. + = note: +note: required by a bound in `register_anonymous_view` + --> src/rt.rs + | + | pub fn register_anonymous_view<'a, A, I, T>(_: impl AnonymousView<'a, A, T>) + | ^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `register_anonymous_view` + +error[E0277]: An anonymous view must have `&AnonymousViewContext` as its first argument + --> tests/ui/views.rs:111:42 + | +111 | fn anonymous_view_def_wrong_context_2(_: &ViewContext) -> Vec { + | ^^^^^^^^^^^^ the trait `AnonymousViewContextArg` is not implemented for `&ViewContext` + | + = help: the trait `AnonymousViewContextArg` is implemented for `&AnonymousViewContext` + +error[E0277]: invalid view signature + --> tests/ui/views.rs:111:4 + | +110 | #[view(public, anonymous)] + | -------------------------- required by a bound introduced by this call +111 | fn anonymous_view_def_wrong_context_2(_: &ViewContext) -> Vec { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ this view signature is not valid + | + = help: the trait `AnonymousView<'_, _, _>` is not implemented for fn item `for<'a> fn(&'a ViewContext) -> Vec {anonymous_view_def_wrong_context_2}` + = note: + = note: view signatures must match: + = note: `Fn(&AnonymousViewContext, [T1, ...]) -> Vec` + = note: where each `Ti` implements `SpacetimeType`. + = note: +note: required by a bound in `invoke_anonymous_view` + --> src/rt.rs + | + 78 | pub fn invoke_anonymous_view<'a, A: Args<'a>, T: SpacetimeType + Serialize>( + | --------------------- required by a bound in this function + 79 | view: impl AnonymousView<'a, A, T>, + | ^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `invoke_anonymous_view` + +error[E0277]: invalid view signature + --> tests/ui/views.rs:121:4 + | +120 | #[view(public)] + | --------------- required by a bound introduced by this call +121 | fn view_def_wrong_return(_: &ViewContext) -> Option { + | ^^^^^^^^^^^^^^^^^^^^^ this view signature is not valid + | + = help: the trait `spacetimedb::rt::View<'_, _, _>` is not implemented for fn item `for<'a> fn(&'a ViewContext) -> Option {view_def_wrong_return}` + = note: + = note: view signatures must match: + = note: `Fn(&ViewContext, [T1, ...]) -> Vec` + = note: where each `Ti` implements `SpacetimeType`. + = note: +note: required by a bound in `register_view` + --> src/rt.rs + | + | pub fn register_view<'a, A, I, T>(_: impl View<'a, A, T>) + | ^^^^^^^^^^^^^^ required by this bound in `register_view` + +error[E0277]: Views must return `Vec` where `T` is a `SpacetimeType` + --> tests/ui/views.rs:121:46 + | +121 | fn view_def_wrong_return(_: &ViewContext) -> Option { + | ^^^^^^^^^^^^^^ the trait `ViewReturn` is not implemented for `Option` + | + = help: the trait `ViewReturn` is implemented for `Vec` + +error[E0277]: invalid view signature + --> tests/ui/views.rs:121:4 + | +120 | #[view(public)] + | --------------- required by a bound introduced by this call +121 | fn view_def_wrong_return(_: &ViewContext) -> Option { + | ^^^^^^^^^^^^^^^^^^^^^ this view signature is not valid + | + = help: the trait `spacetimedb::rt::View<'_, _, _>` is not implemented for fn item `for<'a> fn(&'a ViewContext) -> Option {view_def_wrong_return}` + = note: + = note: view signatures must match: + = note: `Fn(&ViewContext, [T1, ...]) -> Vec` + = note: where each `Ti` implements `SpacetimeType`. + = note: +note: required by a bound in `invoke_view` + --> src/rt.rs + | + 50 | pub fn invoke_view<'a, A: Args<'a>, T: SpacetimeType + Serialize>( + | ----------- required by a bound in this function + 51 | view: impl View<'a, A, T>, + | ^^^^^^^^^^^^^^ required by this bound in `invoke_view` diff --git a/crates/lib/src/db/raw_def/v9.rs b/crates/lib/src/db/raw_def/v9.rs index 58816384383..f396092a9a7 100644 --- a/crates/lib/src/db/raw_def/v9.rs +++ b/crates/lib/src/db/raw_def/v9.rs @@ -379,6 +379,7 @@ pub enum RawMiscModuleExportV9 { ColumnDefaultValue(RawColumnDefaultValueV9), /// A procedure definition. Procedure(RawProcedureDefV9), + View(RawViewDefV9), } /// Marks a particular table's column as having a particular default. @@ -396,6 +397,38 @@ pub struct RawColumnDefaultValueV9 { pub value: Box<[u8]>, } +/// A view definition. +#[derive(Debug, Clone, SpacetimeType)] +#[sats(crate = crate)] +#[cfg_attr(feature = "test", derive(PartialEq, Eq, PartialOrd, Ord))] +pub struct RawViewDefV9 { + /// The name of the view. + pub name: RawIdentifier, + + /// Is this view anonymous? + 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, + + /// A reference to a `ProductType` containing the columns of this view. + /// This is the single source of truth for the view'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 `RawTypeDefv9` for this type. + pub return_type: AlgebraicType, + + /// Currently unused, but we may want to define indexes on materialized views in the future. + pub indexes: Vec, + + /// Whether this view is public or private. + /// Only public is supported right now. + /// Private views may be added in the future. + pub access: TableAccess, +} + /// A type declaration. /// /// Exactly of these must be attached to every `Product` and `Sum` type used by a module. @@ -692,6 +725,23 @@ impl RawModuleDefV9Builder { })) } + pub fn add_view( + &mut self, + name: impl Into, + is_anonymous: bool, + params: ProductType, + return_type: AlgebraicType, + ) { + self.module.misc_exports.push(RawMiscModuleExportV9::View(RawViewDefV9 { + name: name.into(), + is_anonymous, + params, + return_type, + indexes: vec![], + access: TableAccess::Public, + })); + } + /// Add a row-level security policy to the module. /// /// The `sql` expression should be a valid SQL expression that will be used to filter rows. From b1368a54c5a2a39309f95b9dda017fdf0e2900da Mon Sep 17 00:00:00 2001 From: joshua-spacetime Date: Fri, 10 Oct 2025 17:00:06 -0700 Subject: [PATCH 23/25] fix tests --- crates/bindings/src/lib.rs | 4 ++++ crates/bindings/tests/ui/tables.stderr | 12 ------------ crates/bindings/tests/ui/views.stderr | 24 +++++++++--------------- 3 files changed, 13 insertions(+), 27 deletions(-) diff --git a/crates/bindings/src/lib.rs b/crates/bindings/src/lib.rs index 38dbd190712..6d7dfc5ccf5 100644 --- a/crates/bindings/src/lib.rs +++ b/crates/bindings/src/lib.rs @@ -710,6 +710,10 @@ pub use spacetimedb_bindings_macro::reducer; /// Views are callable by reducers and other views simply by passing their `ViewContext`.. /// This is a regular function call. /// The callee will run within the caller's transaction. +/// +/// +/// [`&ViewContext`]: `ViewContext` +/// [`&AnonymousViewContext`]: `AnonymousViewContext` #[doc(inline)] pub use spacetimedb_bindings_macro::view; diff --git a/crates/bindings/tests/ui/tables.stderr b/crates/bindings/tests/ui/tables.stderr index 897ea77fd64..4dcf4f1dc26 100644 --- a/crates/bindings/tests/ui/tables.stderr +++ b/crates/bindings/tests/ui/tables.stderr @@ -10,18 +10,6 @@ error: const parameters are not allowed on tables 14 | struct ConstParam {} | ^^^^^^^^^^^ -error[E0412]: cannot find type `ReducerContext` in this scope - --> tests/ui/tables.rs:30:1 - | -30 | #[spacetimedb::reducer] - | ^^^^^^^^^^^^^^^^^^^^^^^ not found in this scope - | - = note: this error originates in the attribute macro `spacetimedb::reducer` (in Nightly builds, run with -Z macro-backtrace for more info) -help: consider importing this struct - | - 1 + use spacetimedb::ReducerContext; - | - error[E0277]: the trait bound `Test: SpacetimeType` is not satisfied --> tests/ui/tables.rs:5:8 | diff --git a/crates/bindings/tests/ui/views.stderr b/crates/bindings/tests/ui/views.stderr index 349de146a19..c2de27fff53 100644 --- a/crates/bindings/tests/ui/views.stderr +++ b/crates/bindings/tests/ui/views.stderr @@ -51,9 +51,6 @@ error[E0599]: `&test__ViewHandle` is not an iterator which is required by `&mut test__ViewHandle: Iterator` note: the trait `Iterator` must be implemented --> $RUST/core/src/iter/traits/iterator.rs - | - | pub trait Iterator { - | ^^^^^^^^^^^^^^^^^^ = help: items from traits can only be used if the trait is implemented and in scope = note: the following traits define an item `count`, perhaps you need to implement one of them: candidate #1: `Table` @@ -73,19 +70,16 @@ error[E0599]: no method named `insert` found for reference `&test__ViewHandle` i candidate #4: `ppv_lite86::types::Vec4` error[E0599]: no method named `try_insert` found for reference `&test__ViewHandle` in the current scope - --> tests/ui/views.rs:36:25 - | - 36 | read_only.db.test().try_insert(Test { id: 0, x: 0 }); - | ^^^^^^^^^^ - | - = help: items from traits can only be used if the trait is implemented and in scope - = note: the following trait defines an item `try_insert`, perhaps you need to implement it: - candidate #1: `Table` + --> tests/ui/views.rs:36:25 + | +36 | read_only.db.test().try_insert(Test { id: 0, x: 0 }); + | ^^^^^^^^^^ + | + = help: items from traits can only be used if the trait is implemented and in scope + = note: the following trait defines an item `try_insert`, perhaps you need to implement it: + candidate #1: `Table` help: there is a method `try_into` with a similar name, but with different arguments - --> $RUST/core/src/convert/mod.rs - | - | fn try_into(self) -> Result; - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + --> $RUST/core/src/convert/mod.rs error[E0599]: no method named `delete` found for reference `&test__ViewHandle` in the current scope --> tests/ui/views.rs:43:25 From 660bd63622bf70f6861c1e393652cd01e6283f3c Mon Sep 17 00:00:00 2001 From: joshua-spacetime Date: Wed, 15 Oct 2025 15:44:54 -0700 Subject: [PATCH 24/25] validate module def and update system tables --- crates/core/src/host/module_host.rs | 24 ++++- crates/lib/src/db/raw_def/v9.rs | 14 +-- crates/sats/src/algebraic_type.rs | 5 + crates/schema/src/def.rs | 103 +++++++++++++++++- crates/schema/src/def/validate/v9.rs | 155 ++++++++++++++++++++++++--- crates/schema/src/error.rs | 39 +++++++ crates/schema/src/schema.rs | 61 ++++++++++- 7 files changed, 373 insertions(+), 28 deletions(-) diff --git a/crates/core/src/host/module_host.rs b/crates/core/src/host/module_host.rs index d9eade69aeb..ae5fab0ee61 100644 --- a/crates/core/src/host/module_host.rs +++ b/crates/core/src/host/module_host.rs @@ -51,7 +51,7 @@ use spacetimedb_query::compile_subscription; use spacetimedb_sats::ProductValue; use spacetimedb_schema::auto_migrate::{AutoMigrateError, MigrationPolicy}; use spacetimedb_schema::def::deserialize::ArgsSeed; -use spacetimedb_schema::def::{ModuleDef, ReducerDef, TableDef}; +use spacetimedb_schema::def::{ModuleDef, ReducerDef, TableDef, ViewDef}; use spacetimedb_schema::schema::{Schema, TableSchema}; use spacetimedb_vm::relation::RelValue; use std::collections::VecDeque; @@ -413,6 +413,19 @@ pub fn create_table_from_def( Ok(()) } +/// Creates the table for `view_def` in `stdb`. +pub fn create_table_from_view_def( + stdb: &RelationalDB, + tx: &mut MutTxId, + module_def: &ModuleDef, + view_def: &ViewDef, +) -> anyhow::Result<()> { + let schema = TableSchema::try_from_view_def(module_def, view_def)?; + stdb.create_table(tx, schema) + .with_context(|| format!("failed to create table for view {}", &view_def.name))?; + Ok(()) +} + /// If the module instance's replica_ctx is uninitialized, initialize it. fn init_database( replica_ctx: &ReplicaContext, @@ -436,6 +449,15 @@ fn init_database( logger.info(&format!("Creating table `{}`", &def.name)); create_table_from_def(stdb, tx, module_def, def)?; } + + let mut view_defs: Vec<_> = module_def.views().collect(); + view_defs.sort_by(|a, b| a.name.cmp(&b.name)); + + for def in view_defs { + logger.info(&format!("Creating table for view `{}`", &def.name)); + create_table_from_view_def(stdb, tx, module_def, def)?; + } + // Insert the late-bound row-level security expressions. for rls in module_def.row_level_security() { logger.info(&format!("Creating row level security `{}`", rls.sql)); diff --git a/crates/lib/src/db/raw_def/v9.rs b/crates/lib/src/db/raw_def/v9.rs index f396092a9a7..b8c01a8b2e3 100644 --- a/crates/lib/src/db/raw_def/v9.rs +++ b/crates/lib/src/db/raw_def/v9.rs @@ -372,7 +372,7 @@ pub struct RawRowLevelSecurityDefV9 { /// If/when we define `RawModuleDefV10`, these should allbe moved out of `misc_exports` and into their own fields. #[derive(Debug, Clone, SpacetimeType)] #[sats(crate = crate)] -#[cfg_attr(feature = "test", derive(PartialEq, Eq, PartialOrd, Ord))] +#[cfg_attr(feature = "test", derive(PartialEq, Eq, PartialOrd, Ord, derive_more::From))] #[non_exhaustive] pub enum RawMiscModuleExportV9 { /// A default value for a column added during a supervised automigration. @@ -408,6 +408,11 @@ pub struct RawViewDefV9 { /// Is this view anonymous? pub is_anonymous: bool, + /// Whether this view is public or private. + /// Only public is supported right now. + /// Private views may be added in the future. + pub is_public: bool, + /// The types and optional names of the parameters, in order. /// This `ProductType` need not be registered in the typespace. pub params: ProductType, @@ -422,11 +427,6 @@ pub struct RawViewDefV9 { /// Currently unused, but we may want to define indexes on materialized views in the future. pub indexes: Vec, - - /// Whether this view is public or private. - /// Only public is supported right now. - /// Private views may be added in the future. - pub access: TableAccess, } /// A type declaration. @@ -735,10 +735,10 @@ impl RawModuleDefV9Builder { self.module.misc_exports.push(RawMiscModuleExportV9::View(RawViewDefV9 { name: name.into(), is_anonymous, + is_public: true, params, return_type, indexes: vec![], - access: TableAccess::Public, })); } diff --git a/crates/sats/src/algebraic_type.rs b/crates/sats/src/algebraic_type.rs index c4a45fe1d3b..e42ba015639 100644 --- a/crates/sats/src/algebraic_type.rs +++ b/crates/sats/src/algebraic_type.rs @@ -202,6 +202,11 @@ impl AlgebraicType { matches!(self, Self::Sum(p) if p.is_empty()) } + /// Returns whether this type is an option type. + pub fn is_option(&self) -> bool { + matches!(self, Self::Sum(p) if p.is_option()) + } + /// If this type is the standard option type, returns the type of the `some` variant. /// Otherwise, returns `None`. pub fn as_option(&self) -> Option<&AlgebraicType> { diff --git a/crates/schema/src/def.rs b/crates/schema/src/def.rs index fb8db9fccf3..eea6f225d75 100644 --- a/crates/schema/src/def.rs +++ b/crates/schema/src/def.rs @@ -35,7 +35,7 @@ use spacetimedb_lib::db::raw_def::v9::{ Lifecycle, RawColumnDefaultValueV9, RawConstraintDataV9, RawConstraintDefV9, RawIdentifier, RawIndexAlgorithm, RawIndexDefV9, RawMiscModuleExportV9, RawModuleDefV9, RawProcedureDefV9, RawReducerDefV9, RawRowLevelSecurityDefV9, RawScheduleDefV9, RawScopedTypeNameV9, RawSequenceDefV9, RawSql, RawTableDefV9, RawTypeDefV9, - RawUniqueConstraintDataV9, TableAccess, TableType, + RawUniqueConstraintDataV9, RawViewDefV9, TableAccess, TableType, }; use spacetimedb_lib::{ProductType, RawModuleDef}; use spacetimedb_primitives::{ColId, ColList, ColOrCols, ColSet, ProcedureId, ReducerId, TableId}; @@ -109,6 +109,11 @@ pub struct ModuleDef { /// so that `__call_procedure__` receives stable integer IDs. procedures: IndexMap, + /// The views of the module definition. + /// Note: this is using IndexMap because view order is important + /// and must be preserved for future calls to `__call_view__`. + views: IndexMap, + /// A map from lifecycle reducer kind to reducer id. lifecycle_reducers: EnumMap>, @@ -172,6 +177,11 @@ impl ModuleDef { self.procedures.values() } + /// The views of the module definition. + pub fn views(&self) -> impl Iterator { + self.views.values() + } + /// The type definitions of the module definition. pub fn types(&self) -> impl Iterator { self.types.values() @@ -364,6 +374,7 @@ impl From for RawModuleDefV9 { fn from(val: ModuleDef) -> Self { let ModuleDef { tables, + views, reducers, lifecycle_reducers: _, types, @@ -382,7 +393,8 @@ impl From for RawModuleDefV9 { // TODO: Do we need to include default values here? misc_exports: procedures .into_iter() - .map(|(_, def)| RawMiscModuleExportV9::Procedure(def.into())) + .map(|(_, def)| def.into()) + .chain(views.into_iter().map(|(_, def)| def.into())) .collect(), typespace, row_level_security: row_level_security_raw.into_iter().map(|(_, def)| def).collect(), @@ -1030,6 +1042,81 @@ impl From for RawProcedureDefV9 { } } +impl From for RawMiscModuleExportV9 { + fn from(def: ProcedureDef) -> Self { + Self::Procedure(def.into()) + } +} + +/// A view exported by the module. +#[derive(Debug, Clone, Eq, PartialEq)] +#[non_exhaustive] +pub struct ViewDef { + /// The name of the view. This must be unique within the module. + pub name: Identifier, + + /// Is this view anonymous? + pub is_anonymous: bool, + + /// Is this view public or private? + pub is_public: bool, + + /// The parameters of the view. + /// + /// This `ProductType` need not be registered in the module's `Typespace`. + pub params: ProductType, + + /// The parameters of the view, formatted for client codegen. + /// + /// This `ProductType` need not be registered in the module's `TypespaceForGenerate`. + pub params_for_generate: ProductTypeDef, + + /// The return type of the procedure. + /// + /// If this is a non-special compound type, it should be registered in the module's `Typespace` + /// and indirected through an [`AlgebraicType::Ref`]. + pub return_type: AlgebraicType, + + /// The return type of the procedure. + /// + /// 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 columns of this view. This stores the information in + /// `return_type` in a more convenient-to-access format. + pub columns: Vec, +} + +impl From for RawViewDefV9 { + fn from(val: ViewDef) -> Self { + let ViewDef { + name, + is_anonymous, + is_public, + params, + params_for_generate: _, + return_type, + return_type_for_generate: _, + columns: _, + } = val; + RawViewDefV9 { + name: name.into(), + is_anonymous, + is_public, + params, + return_type, + indexes: vec![], + } + } +} + +impl From for RawMiscModuleExportV9 { + fn from(def: ViewDef) -> Self { + Self::View(def.into()) + } +} + impl ModuleDefLookup for TableDef { type Key<'a> = &'a Identifier; @@ -1149,6 +1236,18 @@ impl ModuleDefLookup for ReducerDef { } } +impl ModuleDefLookup for ViewDef { + type Key<'a> = &'a Identifier; + + fn key(&self) -> Self::Key<'_> { + &self.name + } + + fn lookup<'a>(view_def: &'a ModuleDef, key: Self::Key<'_>) -> Option<&'a Self> { + view_def.views.get(key) + } +} + fn to_raw(data: HashMap) -> Vec where Def: ModuleDefLookup + Into, diff --git a/crates/schema/src/def/validate/v9.rs b/crates/schema/src/def/validate/v9.rs index fa7a8b88067..63266bb3bc4 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::v9::RawViewDefV9; use spacetimedb_lib::ProductType; use spacetimedb_primitives::col_list; use spacetimedb_sats::{bsatn::de::Deserializer, de::DeserializeSeed, WithTypespace}; @@ -54,13 +55,19 @@ pub fn validate(def: RawModuleDefV9) -> Result { // Later on, in `check_function_names_are_unique`, we'll transform this into an `IndexMap`. .collect_all_errors::>(); - let (procedures, non_procedure_misc_exports) = + let (procedures, misc_exports) = misc_exports .into_iter() .partition::, _>(|misc_export| { matches!(misc_export, RawMiscModuleExportV9::Procedure(_)) }); + let (views, misc_exports) = misc_exports + .into_iter() + .partition::, _>(|misc_export| { + matches!(misc_export, RawMiscModuleExportV9::View(_)) + }); + let procedures = procedures .into_iter() .map(|procedure| { @@ -78,6 +85,21 @@ pub fn validate(def: RawModuleDefV9) -> Result { // Later on, in `check_function_names_are_unique`, we'll transform this into an `IndexMap`. .collect_all_errors::>(); + let views = views + .into_iter() + .map(|view| { + let RawMiscModuleExportV9::View(view) = view else { + unreachable!("Already partitioned views separate from other `RawMiscModuleExportV9` variants"); + }; + view + }) + .map(|view| { + validator + .validate_view_def(view) + .map(|view_def| (view_def.name.clone(), view_def)) + }) + .collect_all_errors(); + let tables = tables .into_iter() .map(|table| { @@ -103,18 +125,17 @@ pub fn validate(def: RawModuleDefV9) -> Result { }) .collect_all_errors::>(); - let tables_types_reducers_procedures = - (tables, types, reducers, procedures) - .combine_errors() - .and_then(|(mut tables, types, reducers, procedures)| { - let ((reducers, procedures), ()) = ( - check_function_names_are_unique(reducers, procedures), - check_non_procedure_misc_exports(non_procedure_misc_exports, &validator, &mut tables), - ) - .combine_errors()?; - check_scheduled_functions_exist(&mut tables, &reducers, &procedures)?; - Ok((tables, types, reducers, procedures)) - }); + let tables_types_reducers_procedures_views = (tables, types, reducers, procedures, views) + .combine_errors() + .and_then(|(mut tables, types, reducers, procedures, views)| { + let ((reducers, procedures, views), ()) = ( + check_function_names_are_unique(reducers, procedures, views), + check_non_procedure_misc_exports(misc_exports, &validator, &mut tables), + ) + .combine_errors()?; + check_scheduled_functions_exist(&mut tables, &reducers, &procedures)?; + Ok((tables, types, reducers, procedures, views)) + }); let ModuleValidator { stored_in_table_def, @@ -123,14 +144,15 @@ pub fn validate(def: RawModuleDefV9) -> Result { .. } = validator; - let (tables, types, reducers, procedures) = - (tables_types_reducers_procedures).map_err(|errors| errors.sort_deduplicate())?; + 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, @@ -426,6 +448,89 @@ impl ModuleValidator<'_> { }) } + /// Validate a view definition. + fn validate_view_def(&mut self, view_def: RawViewDefV9) -> Result { + let RawViewDefV9 { + name, + is_anonymous, + is_public, + params, + return_type, + indexes: _, + } = view_def; + + let params_for_generate = self.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( + &TypeLocation::ViewReturn { + view_name: Cow::Borrowed(&name), + }, + &return_type, + ); + + // We exit early if we don't find the product type ref, + // since this breaks all the other checks. + let product_type_ref = return_type + .as_array() + .and_then(|array_type| match *array_type.elem_ty { + AlgebraicType::Ref(ref_type) => Some(ref_type), + _ => None, + }) + .ok_or_else(|| { + ValidationErrors::from(ValidationError::InvalidViewReturnType { + view: name.clone(), + ty: return_type.clone().into(), + }) + })?; + + let product_type = self + .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 mut table_in_progress = TableValidator { + raw_name: name.clone(), + product_type_ref, + product_type, + module_validator: self, + has_sequence: Default::default(), + }; + + let columns = (0..product_type.elements.len()) + .map(|id| table_in_progress.validate_column_def(id.into())) + .collect_all_errors(); + + // views don't live in the global namespace. + let name = identifier(name); + + let (name, params_for_generate, return_type_for_generate, columns) = + (name, params_for_generate, return_type_for_generate, columns).combine_errors()?; + + Ok(ViewDef { + name, + is_anonymous, + is_public, + params, + 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, + columns, + }) + } + fn validate_column_default_value( &self, tables: &HashMap, @@ -1033,10 +1138,16 @@ fn check_scheduled_functions_exist( /// Check that all function (reducer and procedure) names are unique, /// 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( reducers: Vec<(Identifier, ReducerDef)>, procedures: Vec<(Identifier, ProcedureDef)>, -) -> Result<(IndexMap, IndexMap)> { + views: Vec<(Identifier, ViewDef)>, +) -> Result<( + IndexMap, + IndexMap, + IndexMap, +)> { let mut errors = vec![]; let mut reducers_map = IndexMap::with_capacity(reducers.len()); @@ -1059,7 +1170,17 @@ fn check_function_names_are_unique( } } - ErrorStream::add_extra_errors(Ok((reducers_map, procedures_map)), errors) + let mut views_map = IndexMap::with_capacity(views.len()); + + for (name, def) in views { + if reducers_map.contains_key(&name) || procedures_map.contains_key(&name) || views_map.contains_key(&name) { + errors.push(ValidationError::DuplicateFunctionName { name }); + } else { + views_map.insert(name, def); + } + } + + ErrorStream::add_extra_errors(Ok((reducers_map, procedures_map, views_map)), errors) } fn check_non_procedure_misc_exports( diff --git a/crates/schema/src/error.rs b/crates/schema/src/error.rs index f7d05c8febf..b5ee5ee9d29 100644 --- a/crates/schema/src/error.rs +++ b/crates/schema/src/error.rs @@ -82,6 +82,11 @@ pub enum ValidationError { start: Option, max_value: Option, }, + #[error("View {view} has invalid return type {ty}")] + InvalidViewReturnType { + view: RawIdentifier, + ty: PrettyAlgebraicType, + }, #[error("Table {table} has invalid product_type_ref {ref_}")] InvalidProductTypeRef { table: RawIdentifier, @@ -182,8 +187,16 @@ pub enum TypeLocation<'a> { position: usize, arg_name: Option>, }, + /// A view argument. + ViewArg { + view_name: Cow<'a, str>, + position: usize, + arg_name: Option>, + }, /// A procedure return type. ProcedureReturn { procedure_name: Cow<'a, str> }, + /// A view return type. + ViewReturn { view_name: Cow<'a, str> }, /// A type in the typespace. InTypespace { /// The reference to the type within the typespace. @@ -213,9 +226,21 @@ impl TypeLocation<'_> { position, arg_name: arg_name.map(|s| s.to_string().into()), }, + TypeLocation::ViewArg { + view_name, + position, + arg_name, + } => TypeLocation::ViewArg { + view_name: view_name.to_string().into(), + position, + arg_name: arg_name.map(|s| s.to_string().into()), + }, Self::ProcedureReturn { procedure_name } => TypeLocation::ProcedureReturn { procedure_name: procedure_name.to_string().into(), }, + Self::ViewReturn { view_name } => TypeLocation::ViewReturn { + view_name: view_name.to_string().into(), + }, // needed to convince rustc this is allowed. TypeLocation::InTypespace { ref_ } => TypeLocation::InTypespace { ref_ }, } @@ -247,9 +272,23 @@ impl fmt::Display for TypeLocation<'_> { } Ok(()) } + TypeLocation::ViewArg { + view_name, + position, + arg_name, + } => { + write!(f, "view `{view_name}` argument {position}")?; + if let Some(arg_name) = arg_name { + write!(f, " (`{arg_name}`)")?; + } + Ok(()) + } TypeLocation::ProcedureReturn { procedure_name } => { write!(f, "procedure `{procedure_name}` return value") } + TypeLocation::ViewReturn { view_name } => { + write!(f, "view `{view_name}` return value") + } TypeLocation::InTypespace { ref_ } => { write!(f, "typespace ref `{ref_}`") } diff --git a/crates/schema/src/schema.rs b/crates/schema/src/schema.rs index 149cb08eb5b..04e3b488624 100644 --- a/crates/schema/src/schema.rs +++ b/crates/schema/src/schema.rs @@ -8,6 +8,7 @@ use crate::def::error::{DefType, SchemaError}; use crate::relation::{combine_constraints, Column, DbTable, FieldName, Header}; +use anyhow::bail; use core::mem; use itertools::Itertools; use spacetimedb_lib::db::auth::{StAccess, StTableType}; @@ -21,7 +22,7 @@ use std::sync::Arc; use crate::def::{ ColumnDef, ConstraintData, ConstraintDef, IndexAlgorithm, IndexDef, ModuleDef, ModuleDefLookup, ScheduleDef, - SequenceDef, TableDef, UniqueConstraintData, + SequenceDef, TableDef, UniqueConstraintData, ViewDef, }; use crate::identifier::Identifier; @@ -587,6 +588,64 @@ pub fn column_schemas_from_defs(module_def: &ModuleDef, columns: &[ColumnDef], t .collect() } +impl TableSchema { + pub fn try_from_view_def(module_def: &ModuleDef, view_def: &ViewDef) -> anyhow::Result { + module_def.expect_contains(view_def); + + let ViewDef { + name, + is_anonymous, + is_public, + params, + params_for_generate: _, + return_type: _, + return_type_for_generate: _, + columns, + } = view_def; + + let mut columns = column_schemas_from_defs(module_def, columns, TableId::SENTINEL); + let n = columns.len(); + + for (i, elem) in params.elements.iter().cloned().enumerate() { + let Some(col_name) = elem.name else { bail!("hello") }; + columns.push(ColumnSchema { + table_id: TableId::SENTINEL, + col_pos: (n + i).into(), + col_name, + col_type: elem.algebraic_type, + }); + } + + if !is_anonymous { + columns.push(ColumnSchema { + table_id: TableId::SENTINEL, + col_pos: columns.len().into(), + col_name: "sender".into(), + col_type: AlgebraicType::identity(), + }); + } + + let table_access = if *is_public { + StAccess::Public + } else { + StAccess::Private + }; + + Ok(TableSchema::new( + TableId::SENTINEL, + (*name).clone().into(), + columns, + vec![], + vec![], + vec![], + StTableType::User, + table_access, + None, + None, + )) + } +} + impl Schema for TableSchema { type Def = TableDef; type Id = TableId; From 583240fdd8dd2fed80f12e5c67fa074996d33477 Mon Sep 17 00:00:00 2001 From: joshua-spacetime Date: Thu, 16 Oct 2025 14:46:27 -0700 Subject: [PATCH 25/25] system tables --- crates/core/src/db/relational_db.rs | 11 +- crates/core/src/host/module_host.rs | 3 +- .../locking_tx_datastore/committed_state.rs | 9 +- .../src/locking_tx_datastore/mut_tx.rs | 82 ++++++++- crates/datastore/src/system_tables.rs | 173 +++++++++++++++++- crates/primitives/src/ids.rs | 6 + crates/primitives/src/lib.rs | 4 +- crates/sats/src/de/impls.rs | 1 + crates/sats/src/ser/impls.rs | 1 + crates/sats/src/typespace.rs | 1 + crates/table/src/read_column.rs | 1 + 11 files changed, 277 insertions(+), 15 deletions(-) diff --git a/crates/core/src/db/relational_db.rs b/crates/core/src/db/relational_db.rs index c7b05a7bbfb..639ae8f0ba4 100644 --- a/crates/core/src/db/relational_db.rs +++ b/crates/core/src/db/relational_db.rs @@ -42,7 +42,7 @@ use spacetimedb_primitives::*; use spacetimedb_sats::algebraic_type::fmt::fmt_algebraic_type; use spacetimedb_sats::memory_usage::MemoryUsage; use spacetimedb_sats::{AlgebraicType, AlgebraicValue, ProductType, ProductValue}; -use spacetimedb_schema::def::{ModuleDef, TableDef}; +use spacetimedb_schema::def::{ModuleDef, TableDef, ViewDef}; use spacetimedb_schema::schema::{ ColumnSchema, IndexSchema, RowLevelSecuritySchema, Schema, SequenceSchema, TableSchema, }; @@ -1055,6 +1055,15 @@ impl RelationalDB { Ok(self.inner.create_table_mut_tx(tx, schema)?) } + pub fn create_view_table( + &self, + tx: &mut MutTx, + module_def: &ModuleDef, + view_def: &ViewDef, + ) -> Result<(ViewId, TableId), DBError> { + Ok(tx.create_view_with_backing_table(module_def, view_def)?) + } + pub fn create_table_for_test_with_the_works( &self, name: &str, diff --git a/crates/core/src/host/module_host.rs b/crates/core/src/host/module_host.rs index ae5fab0ee61..7108d842891 100644 --- a/crates/core/src/host/module_host.rs +++ b/crates/core/src/host/module_host.rs @@ -420,8 +420,7 @@ pub fn create_table_from_view_def( module_def: &ModuleDef, view_def: &ViewDef, ) -> anyhow::Result<()> { - let schema = TableSchema::try_from_view_def(module_def, view_def)?; - stdb.create_table(tx, schema) + stdb.create_view_table(tx, module_def, view_def) .with_context(|| format!("failed to create table for view {}", &view_def.name))?; Ok(()) } diff --git a/crates/datastore/src/locking_tx_datastore/committed_state.rs b/crates/datastore/src/locking_tx_datastore/committed_state.rs index 1f17695e135..c3799006d6d 100644 --- a/crates/datastore/src/locking_tx_datastore/committed_state.rs +++ b/crates/datastore/src/locking_tx_datastore/committed_state.rs @@ -6,7 +6,10 @@ use super::{ tx_state::{IndexIdMap, PendingSchemaChange, TxState}, IterByColEqTx, }; -use crate::system_tables::{ST_CONNECTION_CREDENTIALS_ID, ST_CONNECTION_CREDENTIALS_IDX}; +use crate::system_tables::{ + ST_CONNECTION_CREDENTIALS_ID, ST_CONNECTION_CREDENTIALS_IDX, ST_VIEW_COLUMN_ID, ST_VIEW_COLUMN_IDX, ST_VIEW_ID, + ST_VIEW_IDX, ST_VIEW_PARAM_ID, ST_VIEW_PARAM_IDX, +}; use crate::{ db_metrics::DB_METRICS, error::{DatastoreError, IndexError, TableError}, @@ -253,6 +256,10 @@ impl CommittedState { schemas[ST_CONNECTION_CREDENTIALS_IDX].clone(), ); + self.create_table(ST_VIEW_ID, schemas[ST_VIEW_IDX].clone()); + self.create_table(ST_VIEW_PARAM_ID, schemas[ST_VIEW_PARAM_IDX].clone()); + self.create_table(ST_VIEW_COLUMN_ID, schemas[ST_VIEW_COLUMN_IDX].clone()); + // Insert the sequences into `st_sequences` let (st_sequences, blob_store, pool) = self.get_table_and_blob_store_or_create(ST_SEQUENCE_ID, &schemas[ST_SEQUENCE_IDX]); diff --git a/crates/datastore/src/locking_tx_datastore/mut_tx.rs b/crates/datastore/src/locking_tx_datastore/mut_tx.rs index b38cb992d8d..d87afe9d8b3 100644 --- a/crates/datastore/src/locking_tx_datastore/mut_tx.rs +++ b/crates/datastore/src/locking_tx_datastore/mut_tx.rs @@ -8,11 +8,9 @@ use super::{ tx_state::{IndexIdMap, PendingSchemaChange, TxState, TxTableForInsertion}, SharedMutexGuard, SharedWriteGuard, }; -use crate::execution_context::ExecutionContext; -use crate::execution_context::Workload; use crate::system_tables::{ - system_tables, ConnectionIdViaU128, StConnectionCredentialsFields, StConnectionCredentialsRow, - ST_CONNECTION_CREDENTIALS_ID, + system_tables, ConnectionIdViaU128, StConnectionCredentialsFields, StConnectionCredentialsRow, StViewFields, + StViewParamRow, ST_CONNECTION_CREDENTIALS_ID, ST_VIEW_COLUMN_ID, ST_VIEW_ID, }; use crate::traits::{InsertFlags, RowTypeForTable, TxData, UpdateFlags}; use crate::{ @@ -25,6 +23,8 @@ use crate::{ ST_SEQUENCE_ID, ST_TABLE_ID, }, }; +use crate::{execution_context::ExecutionContext, system_tables::StViewColumnRow}; +use crate::{execution_context::Workload, system_tables::StViewRow}; use core::ops::RangeBounds; use core::{cell::RefCell, mem}; use core::{iter, ops::Bound}; @@ -37,7 +37,7 @@ use spacetimedb_lib::{ ConnectionId, Identity, }; use spacetimedb_primitives::{ - col_list, ColId, ColList, ColSet, ConstraintId, IndexId, ScheduleId, SequenceId, TableId, + col_list, ColId, ColList, ColSet, ConstraintId, IndexId, ScheduleId, SequenceId, TableId, ViewId, }; use spacetimedb_sats::{ bsatn::{self, to_writer, DecodeError, Deserializer}, @@ -45,8 +45,9 @@ use spacetimedb_sats::{ ser::Serialize, AlgebraicType, AlgebraicValue, ProductType, ProductValue, WithTypespace, }; -use spacetimedb_schema::schema::{ - ColumnSchema, ConstraintSchema, IndexSchema, RowLevelSecuritySchema, SequenceSchema, TableSchema, +use spacetimedb_schema::{ + def::{ColumnDef, ModuleDef, ViewDef}, + schema::{ColumnSchema, ConstraintSchema, IndexSchema, RowLevelSecuritySchema, SequenceSchema, TableSchema}, }; use spacetimedb_table::{ blob_store::BlobStore, @@ -183,6 +184,41 @@ impl MutTxId { Ok(()) } + pub fn create_view_with_backing_table( + &mut self, + module_def: &ModuleDef, + view_def: &ViewDef, + ) -> Result<(ViewId, TableId)> { + let table_schema = TableSchema::try_from_view_def(module_def, view_def)?; + let table_id = self.create_table(table_schema)?; + + let ViewDef { + name, + is_anonymous, + is_public, + params, + columns, + .. + } = view_def; + + let row = StViewRow { + id: ViewId::SENTINEL, + name: name.clone().into(), + table_id: Some(table_id), + is_public: *is_public, + is_anonymous: *is_anonymous, + }; + let view_id = self + .insert_via_serialize_bsatn(ST_VIEW_ID, &row)? + .1 + .collapse() + .read_col(StViewFields::ViewId)?; + + self.insert_into_st_view_param(view_id, params)?; + self.insert_into_st_view_column(view_id, columns)?; + Ok((view_id, table_id)) + } + /// Create a table. /// /// Requires: @@ -287,6 +323,38 @@ impl MutTxId { }) } + fn insert_into_st_view_param(&mut self, view_id: ViewId, params: &ProductType) -> Result<()> { + for (param_pos, param_name, param_type) in params + .elements + .iter() + .cloned() + .enumerate() + .map(|(i, elem)| (i.into(), elem.name, elem.algebraic_type.into())) + { + let Some(param_name) = param_name else { + return Err(anyhow::anyhow!("hello").into()); + }; + self.insert_via_serialize_bsatn( + ST_VIEW_COLUMN_ID, + &StViewParamRow { + view_id, + param_pos, + param_name, + param_type, + }, + )?; + } + Ok(()) + } + + fn insert_into_st_view_column(&mut self, view_id: ViewId, columns: &[ColumnDef]) -> Result<()> { + for def in columns { + let row = StViewColumnRow::from_column_def(view_id, def.clone()); + self.insert_via_serialize_bsatn(ST_VIEW_COLUMN_ID, &row)?; + } + Ok(()) + } + fn create_table_internal(&mut self, schema: Arc) { // Construct the in memory tables. let table_id = schema.table_id; diff --git a/crates/datastore/src/system_tables.rs b/crates/datastore/src/system_tables.rs index f5515e379b8..b7edcf96fee 100644 --- a/crates/datastore/src/system_tables.rs +++ b/crates/datastore/src/system_tables.rs @@ -24,7 +24,7 @@ use spacetimedb_sats::hash::Hash; use spacetimedb_sats::product_value::InvalidFieldError; use spacetimedb_sats::{impl_deserialize, impl_serialize, impl_st, u256, AlgebraicType, AlgebraicValue, ArrayValue}; use spacetimedb_schema::def::{ - BTreeAlgorithm, ConstraintData, DirectAlgorithm, IndexAlgorithm, ModuleDef, UniqueConstraintData, + BTreeAlgorithm, ColumnDef, ConstraintData, DirectAlgorithm, IndexAlgorithm, ModuleDef, UniqueConstraintData, }; use spacetimedb_schema::schema::{ ColumnSchema, ConstraintSchema, IndexSchema, RowLevelSecuritySchema, ScheduleSchema, Schema, SequenceSchema, @@ -65,6 +65,13 @@ pub const ST_ROW_LEVEL_SECURITY_ID: TableId = TableId(10); /// The static ID of the table that stores the credentials for each connection. pub const ST_CONNECTION_CREDENTIALS_ID: TableId = TableId(11); +/// The static ID of the table that tracks views +pub const ST_VIEW_ID: TableId = TableId(12); +/// The static ID of the table that tracks view parameters +pub const ST_VIEW_PARAM_ID: TableId = TableId(13); +/// The static ID of the table that tracks view columns +pub const ST_VIEW_COLUMN_ID: TableId = TableId(14); + pub(crate) const ST_CONNECTION_CREDENTIALS_NAME: &str = "st_connection_credentials"; pub const ST_TABLE_NAME: &str = "st_table"; pub const ST_COLUMN_NAME: &str = "st_column"; @@ -76,6 +83,9 @@ pub(crate) const ST_CLIENT_NAME: &str = "st_client"; pub(crate) const ST_SCHEDULED_NAME: &str = "st_scheduled"; pub(crate) const ST_VAR_NAME: &str = "st_var"; pub(crate) const ST_ROW_LEVEL_SECURITY_NAME: &str = "st_row_level_security"; +pub(crate) const ST_VIEW_NAME: &str = "st_view"; +pub(crate) const ST_VIEW_PARAM_NAME: &str = "st_view_param"; +pub(crate) const ST_VIEW_COLUMN_NAME: &str = "st_view_column"; /// Reserved range of sequence values used for system tables. /// /// Ids for user-created tables will start at `ST_RESERVED_SEQUENCE_RANGE`. @@ -104,7 +114,7 @@ pub enum SystemTable { st_row_level_security, } -pub fn system_tables() -> [TableSchema; 11] { +pub fn system_tables() -> [TableSchema; 14] { [ // The order should match the `id` of the system table, that start with [ST_TABLE_IDX]. st_table_schema(), @@ -118,6 +128,9 @@ pub fn system_tables() -> [TableSchema; 11] { st_row_level_security_schema(), st_sequence_schema(), st_connection_credential_schema(), + st_view_schema(), + st_view_param_schema(), + st_view_column_schema(), ] } @@ -157,6 +170,9 @@ pub(crate) const ST_SCHEDULED_IDX: usize = 7; pub(crate) const ST_ROW_LEVEL_SECURITY_IDX: usize = 8; pub(crate) const ST_SEQUENCE_IDX: usize = 9; pub(crate) const ST_CONNECTION_CREDENTIALS_IDX: usize = 10; +pub(crate) const ST_VIEW_IDX: usize = 11; +pub(crate) const ST_VIEW_PARAM_IDX: usize = 12; +pub(crate) const ST_VIEW_COLUMN_IDX: usize = 13; macro_rules! st_fields_enum { ($(#[$attr:meta])* enum $ty_name:ident { $($name:expr, $var:ident = $discr:expr,)* }) => { @@ -275,6 +291,29 @@ st_fields_enum!(enum StScheduledFields { "at_column", AtColumn = 4, }); +st_fields_enum!(enum StViewFields { + "id", ViewId = 0, + "name", ViewName = 1, + "table_id", TableId = 2, + "symbol", Symbol = 3, + "is_public", IsPublic = 4, + "is_anonymous", IsAnonymous = 5, +}); + +st_fields_enum!(enum StViewParamFields { + "view_id", ViewId = 0, + "param_pos", ParamPos = 1, + "param_name", ParamName = 2, + "param_type", ParamType = 3, +}); + +st_fields_enum!(enum StViewColumnFields { + "view_id", ViewId = 0, + "col_pos", ColPos = 1, + "col_name", ColName = 2, + "col_type", ColType = 3, +}); + /// Helper method to check that a system table has the correct fields. /// Does not check field types since those aren't included in `StFields` types. /// If anything in here is not true, the system is completely broken, so it's fine to assert. @@ -304,7 +343,7 @@ fn system_module_def() -> ModuleDef { .with_unique_constraint(StTableFields::TableName) .with_index_no_accessor_name(btree(StTableFields::TableName)); - let st_raw_column_type = builder.add_type::(); + let st_raw_column_type = builder.add_type::(); let st_col_row_unique_cols = [StColumnFields::TableId.col_id(), StColumnFields::ColPos.col_id()]; builder .build_table(ST_COLUMN_NAME, *st_raw_column_type.as_ref().expect("should be ref")) @@ -312,6 +351,32 @@ fn system_module_def() -> ModuleDef { .with_unique_constraint(st_col_row_unique_cols) .with_index_no_accessor_name(btree(st_col_row_unique_cols)); + let st_view_type = builder.add_type::(); + builder + .build_table(ST_VIEW_NAME, *st_view_type.as_ref().expect("should be ref")) + .with_type(TableType::System) + .with_auto_inc_primary_key(StViewFields::ViewId) + .with_index_no_accessor_name(btree(StViewFields::ViewId)) + .with_unique_constraint(StViewFields::ViewName) + .with_index_no_accessor_name(btree(StViewFields::ViewName)) + .with_unique_constraint(StViewFields::Symbol); + + let st_view_col_type = builder.add_type::(); + let st_view_col_unique_cols = [StViewColumnFields::ViewId.col_id(), StViewColumnFields::ColPos.col_id()]; + builder + .build_table(ST_VIEW_COLUMN_NAME, *st_view_col_type.as_ref().expect("should be ref")) + .with_type(TableType::System) + .with_unique_constraint(st_view_col_unique_cols) + .with_index_no_accessor_name(btree(st_view_col_unique_cols)); + + let st_view_param_type = builder.add_type::(); + let st_view_param_unique_cols = [StViewParamFields::ViewId.col_id(), StViewParamFields::ParamPos.col_id()]; + builder + .build_table(ST_VIEW_PARAM_NAME, *st_view_param_type.as_ref().expect("should be ref")) + .with_type(TableType::System) + .with_unique_constraint(st_view_param_unique_cols) + .with_index_no_accessor_name(btree(st_view_param_unique_cols)); + let st_index_type = builder.add_type::(); builder .build_table(ST_INDEX_NAME, *st_index_type.as_ref().expect("should be ref")) @@ -408,6 +473,9 @@ fn system_module_def() -> ModuleDef { validate_system_table::(&result, ST_VAR_NAME); validate_system_table::(&result, ST_SCHEDULED_NAME); validate_system_table::(&result, ST_CONNECTION_CREDENTIALS_NAME); + validate_system_table::(&result, ST_VIEW_NAME); + validate_system_table::(&result, ST_VIEW_PARAM_NAME); + validate_system_table::(&result, ST_VIEW_COLUMN_NAME); result } @@ -443,6 +511,11 @@ lazy_static::lazy_static! { m.insert("st_scheduled_table_id_key", ConstraintId(10)); m.insert("st_row_level_security_sql_key", ConstraintId(11)); m.insert("st_connection_credentials_connection_id_key", ConstraintId(12)); + m.insert("st_view_id_key", ConstraintId(13)); + m.insert("st_view_name_key", ConstraintId(14)); + m.insert("st_view_symbol_key", ConstraintId(15)); + m.insert("st_view_param_view_id_param_pos_key", ConstraintId(16)); + m.insert("st_view_column_view_id_col_pos_key", ConstraintId(17)); m }; } @@ -465,6 +538,10 @@ lazy_static::lazy_static! { m.insert("st_row_level_security_table_id_idx_btree", IndexId(11)); m.insert("st_row_level_security_sql_idx_btree", IndexId(12)); m.insert("st_connection_credentials_connection_id_idx_btree", IndexId(13)); + m.insert("st_view_id_idx_btree", IndexId(14)); + m.insert("st_view_name_idx_btree", IndexId(15)); + m.insert("st_view_param_view_id_param_pos_idx_btree", IndexId(16)); + m.insert("st_view_column_view_id_col_pos_idx_btree", IndexId(17)); m }; } @@ -479,6 +556,7 @@ lazy_static::lazy_static! { m.insert("st_constraint_constraint_id_seq", SequenceId(3)); m.insert("st_scheduled_schedule_id_seq", SequenceId(4)); m.insert("st_sequence_sequence_id_seq", SequenceId(5)); + m.insert("st_view_id_seq", SequenceId(6)); m }; } @@ -579,6 +657,17 @@ pub fn st_var_schema() -> TableSchema { st_schema(ST_VAR_NAME, ST_VAR_ID) } +pub fn st_view_schema() -> TableSchema { + st_schema(ST_VIEW_NAME, ST_VIEW_ID) +} + +pub fn st_view_param_schema() -> TableSchema { + st_schema(ST_VIEW_PARAM_NAME, ST_VIEW_PARAM_ID) +} +pub fn st_view_column_schema() -> TableSchema { + st_schema(ST_VIEW_COLUMN_NAME, ST_VIEW_COLUMN_ID) +} + /// If `table_id` refers to a known system table, return its schema. /// /// Used when restoring from a snapshot; system tables are reinstantiated with this schema, @@ -598,6 +687,9 @@ pub(crate) fn system_table_schema(table_id: TableId) -> Option { ST_CONNECTION_CREDENTIALS_ID => Some(st_connection_credential_schema()), ST_VAR_ID => Some(st_var_schema()), ST_SCHEDULED_ID => Some(st_scheduled_schema()), + ST_VIEW_ID => Some(st_view_schema()), + ST_VIEW_PARAM_ID => Some(st_view_param_schema()), + ST_VIEW_COLUMN_ID => Some(st_view_column_schema()), _ => None, } } @@ -633,6 +725,21 @@ impl From for ProductValue { } } +/// System Table [ST_VIEW_NAME] +/// +/// | view_id | view_name | table_id | symbol | is_public | is_anonymous | +/// |----------|-------------|------------|--------------|-----------|--------------| +/// | 4 | "customers" | "user" | "public" | true | true | +#[derive(Debug, Clone, PartialEq, Eq, SpacetimeType)] +#[sats(crate = spacetimedb_lib)] +pub struct StViewRow { + pub id: ViewId, + pub name: Box, + pub table_id: Option, + pub is_public: bool, + pub is_anonymous: bool, +} + /// A wrapper around `AlgebraicType` that acts like `AlgegbraicType::bytes()` for serialization purposes. #[derive(Debug, Clone, PartialEq, Eq)] pub struct AlgebraicTypeViaBytes(pub AlgebraicType); @@ -709,6 +816,66 @@ impl From for StColumnRow { } } +/// System Table [ST_VIEW_PARAM_NAME] +/// +/// | view_id | param_pos | param_name | param_type | +/// |---------|-----------|------------|-----------------------| +/// | 1 | 0 | "id" | AlgebraicType::U32 | +#[derive(Debug, Clone, PartialEq, Eq, SpacetimeType)] +#[sats(crate = spacetimedb_lib)] +pub struct StViewParamRow { + pub view_id: ViewId, + pub param_pos: ColId, + pub param_name: Box, + pub param_type: AlgebraicTypeViaBytes, +} + +/// System Table [ST_VIEW_COLUMN_NAME] +/// +/// | view_id | col_pos | col_name | col_type | +/// |---------|---------|----------|--------------------| +/// | 1 | 0 | "id" | AlgebraicType::U32 | +#[derive(Debug, Clone, PartialEq, Eq, SpacetimeType)] +#[sats(crate = spacetimedb_lib)] +pub struct StViewColumnRow { + pub view_id: ViewId, + pub col_pos: ColId, + pub col_name: Box, + pub col_type: AlgebraicTypeViaBytes, +} + +impl StViewColumnRow { + pub fn from_column_schema(view_id: ViewId, schema: ColumnSchema) -> Self { + let ColumnSchema { + col_pos, + col_name, + col_type, + .. + } = schema; + Self { + view_id, + col_pos, + col_name, + col_type: col_type.into(), + } + } + + pub fn from_column_def(view_id: ViewId, def: ColumnDef) -> Self { + let ColumnDef { + name, + col_id: col_pos, + ty, + .. + } = def; + Self { + view_id, + col_pos, + col_name: name.into(), + col_type: ty.into(), + } + } +} + /// System Table [ST_INDEX_NAME] /// /// | index_id | table_id | index_name | index_algorithm | diff --git a/crates/primitives/src/ids.rs b/crates/primitives/src/ids.rs index 77e1352635b..5aadfb8455b 100644 --- a/crates/primitives/src/ids.rs +++ b/crates/primitives/src/ids.rs @@ -78,6 +78,12 @@ system_id! { } auto_inc_system_id!(TableId); +system_id! { + /// An identifier for a view, unique within a database. + pub struct ViewId(pub u32); +} +auto_inc_system_id!(ViewId); + system_id! { /// An identifier for a sequence, unique within a database. pub struct SequenceId(pub u32); diff --git a/crates/primitives/src/lib.rs b/crates/primitives/src/lib.rs index d88e541f195..715fc77cec0 100644 --- a/crates/primitives/src/lib.rs +++ b/crates/primitives/src/lib.rs @@ -7,7 +7,9 @@ mod ids; pub use attr::{AttributeKind, ColumnAttribute, ConstraintKind, Constraints}; pub use col_list::{ColList, ColOrCols, ColSet}; -pub use ids::{ColId, ConstraintId, FunctionId, IndexId, ProcedureId, ReducerId, ScheduleId, SequenceId, TableId}; +pub use ids::{ + ColId, ConstraintId, FunctionId, IndexId, ProcedureId, ReducerId, ScheduleId, SequenceId, TableId, ViewId, +}; /// The minimum size of a chunk yielded by a wasm abi RowIter. pub const ROW_ITER_CHUNK_SIZE: usize = 32 * 1024; diff --git a/crates/sats/src/de/impls.rs b/crates/sats/src/de/impls.rs index 0e50206703f..cf1090253af 100644 --- a/crates/sats/src/de/impls.rs +++ b/crates/sats/src/de/impls.rs @@ -742,6 +742,7 @@ impl FieldNameVisitor<'_> for TupleNameVisitor<'_> { } impl_deserialize!([] spacetimedb_primitives::TableId, de => u32::deserialize(de).map(Self)); +impl_deserialize!([] spacetimedb_primitives::ViewId, de => u32::deserialize(de).map(Self)); impl_deserialize!([] spacetimedb_primitives::SequenceId, de => u32::deserialize(de).map(Self)); impl_deserialize!([] spacetimedb_primitives::IndexId, de => u32::deserialize(de).map(Self)); impl_deserialize!([] spacetimedb_primitives::ConstraintId, de => u32::deserialize(de).map(Self)); diff --git a/crates/sats/src/ser/impls.rs b/crates/sats/src/ser/impls.rs index 15b2bc67638..dc737c5b50c 100644 --- a/crates/sats/src/ser/impls.rs +++ b/crates/sats/src/ser/impls.rs @@ -258,6 +258,7 @@ impl_serialize!([] ValueWithType<'_, ArrayValue>, (self, ser) => { }); impl_serialize!([] spacetimedb_primitives::TableId, (self, ser) => ser.serialize_u32(self.0)); +impl_serialize!([] spacetimedb_primitives::ViewId, (self, ser) => ser.serialize_u32(self.0)); impl_serialize!([] spacetimedb_primitives::SequenceId, (self, ser) => ser.serialize_u32(self.0)); impl_serialize!([] spacetimedb_primitives::IndexId, (self, ser) => ser.serialize_u32(self.0)); impl_serialize!([] spacetimedb_primitives::ConstraintId, (self, ser) => ser.serialize_u32(self.0)); diff --git a/crates/sats/src/typespace.rs b/crates/sats/src/typespace.rs index 46aeb3ddaa5..36058c79195 100644 --- a/crates/sats/src/typespace.rs +++ b/crates/sats/src/typespace.rs @@ -412,6 +412,7 @@ impl_st!([T] Option, ts => AlgebraicType::option(T::make_type(ts))); impl_st!([] spacetimedb_primitives::ColId, AlgebraicType::U16); impl_st!([] spacetimedb_primitives::TableId, AlgebraicType::U32); +impl_st!([] spacetimedb_primitives::ViewId, AlgebraicType::U32); impl_st!([] spacetimedb_primitives::IndexId, AlgebraicType::U32); impl_st!([] spacetimedb_primitives::SequenceId, AlgebraicType::U32); impl_st!([] spacetimedb_primitives::ConstraintId, AlgebraicType::U32); diff --git a/crates/table/src/read_column.rs b/crates/table/src/read_column.rs index 123b751f5a3..be58029f714 100644 --- a/crates/table/src/read_column.rs +++ b/crates/table/src/read_column.rs @@ -326,6 +326,7 @@ macro_rules! impl_read_column_via_from { impl_read_column_via_from! { u16 => spacetimedb_primitives::ColId; + u32 => spacetimedb_primitives::ViewId; u32 => spacetimedb_primitives::TableId; u32 => spacetimedb_primitives::IndexId; u32 => spacetimedb_primitives::ConstraintId;