From f9858af168eca9a2c7ae5fea295ecebc4b31726a Mon Sep 17 00:00:00 2001 From: joshua-spacetime Date: Mon, 3 Nov 2025 08:08:44 -0800 Subject: [PATCH 1/2] Generate client bindings for views --- Cargo.toml | 2 +- crates/codegen/src/csharp.rs | 7 +- crates/codegen/src/lib.rs | 21 +++- crates/codegen/src/rust.rs | 10 +- crates/codegen/src/typescript.rs | 6 +- crates/codegen/src/unrealcpp.rs | 6 +- .../snapshots/codegen__codegen_csharp.snap | 30 +++++- .../snapshots/codegen__codegen_rust.snap | 100 ++++++++++++++++++ .../codegen__codegen_typescript.snap | 78 ++++++++++++++ .../src/locking_tx_datastore/mut_tx.rs | 2 +- crates/schema/src/def.rs | 72 ++++++++++--- crates/schema/src/def/validate/v9.rs | 1 + crates/schema/src/schema.rs | 52 ++++++++- crates/schema/tests/ensure_same_schema.rs | 8 ++ modules/module-test/src/lib.rs | 12 ++- 15 files changed, 364 insertions(+), 43 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 5bb1d180aed..72f8ce068cc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -183,7 +183,7 @@ futures-util = "0.3" getrandom02 = { package = "getrandom", version = "0.2" } git2 = "0.19" glob = "0.3.1" -hashbrown = { version = "0.15", default-features = false, features = ["equivalent", "inline-more"] } +hashbrown = { version = "0.15", default-features = false, features = ["default-hasher", "equivalent", "inline-more"] } headers = "0.4" heck = "0.4" hex = "0.4.3" diff --git a/crates/codegen/src/csharp.rs b/crates/codegen/src/csharp.rs index 2084b0c9844..51201863203 100644 --- a/crates/codegen/src/csharp.rs +++ b/crates/codegen/src/csharp.rs @@ -15,7 +15,7 @@ use convert_case::{Case, Casing}; use spacetimedb_lib::sats::layout::PrimitiveType; use spacetimedb_primitives::ColId; use spacetimedb_schema::def::{BTreeAlgorithm, IndexAlgorithm, ModuleDef, TableDef, TypeDef}; -use spacetimedb_schema::schema::{Schema, TableSchema}; +use spacetimedb_schema::schema::TableSchema; use spacetimedb_schema::type_for_generate::{ AlgebraicTypeDef, AlgebraicTypeUse, PlainEnumTypeDef, ProductTypeDef, SumTypeDef, TypespaceForGenerate, }; @@ -433,7 +433,7 @@ pub struct Csharp<'opts> { } impl Lang for Csharp<'_> { - fn generate_table_file(&self, module: &ModuleDef, table: &TableDef) -> OutputFile { + fn generate_table_file_from_schema(&self, module: &ModuleDef, table: &TableDef, schema: TableSchema) -> OutputFile { let mut output = CsharpAutogen::new( self.namespace, &[ @@ -447,9 +447,6 @@ impl Lang for Csharp<'_> { writeln!(output, "public sealed partial class RemoteTables"); indented_block(&mut output, |output| { - let schema = TableSchema::from_module_def(module, table, (), 0.into()) - .validated() - .expect("Failed to generate table due to validation errors"); let csharp_table_name = table.name.deref().to_case(Case::Pascal); let csharp_table_class_name = csharp_table_name.clone() + "Handle"; let table_type = type_ref_name(module, table.product_type_ref); diff --git a/crates/codegen/src/lib.rs b/crates/codegen/src/lib.rs index f9493329402..a38b1135eac 100644 --- a/crates/codegen/src/lib.rs +++ b/crates/codegen/src/lib.rs @@ -1,4 +1,5 @@ -use spacetimedb_schema::def::{ModuleDef, ReducerDef, TableDef, TypeDef}; +use spacetimedb_schema::def::{ModuleDef, ReducerDef, TableDef, TypeDef, ViewDef}; +use spacetimedb_schema::schema::{Schema, TableSchema}; mod code_indenter; pub mod csharp; @@ -16,6 +17,7 @@ pub use util::AUTO_GENERATED_PREFIX; pub fn generate(module: &ModuleDef, lang: &dyn Lang) -> Vec { itertools::chain!( module.tables().map(|tbl| lang.generate_table_file(module, tbl)), + module.views().map(|view| lang.generate_view_file(module, view)), module.types().flat_map(|typ| lang.generate_type_files(module, typ)), util::iter_reducers(module).map(|reducer| lang.generate_reducer_file(module, reducer)), lang.generate_global_files(module), @@ -29,8 +31,23 @@ pub struct OutputFile { } pub trait Lang { - fn generate_table_file(&self, module: &ModuleDef, tbl: &TableDef) -> OutputFile; + fn generate_table_file_from_schema(&self, module: &ModuleDef, tbl: &TableDef, schema: TableSchema) -> OutputFile; fn generate_type_files(&self, module: &ModuleDef, typ: &TypeDef) -> Vec; fn generate_reducer_file(&self, module: &ModuleDef, reducer: &ReducerDef) -> OutputFile; fn generate_global_files(&self, module: &ModuleDef) -> Vec; + + fn generate_table_file(&self, module: &ModuleDef, tbl: &TableDef) -> OutputFile { + let schema = TableSchema::from_module_def(module, tbl, (), 0.into()) + .validated() + .expect("Failed to generate table due to validation errors"); + self.generate_table_file_from_schema(module, tbl, schema) + } + + fn generate_view_file(&self, module: &ModuleDef, view: &ViewDef) -> OutputFile { + let tbl = TableDef::from(view.clone()); + let schema = TableSchema::from_view_def_for_codegen(module, view) + .validated() + .expect("Failed to generate table due to validation errors"); + self.generate_table_file_from_schema(module, &tbl, schema) + } } diff --git a/crates/codegen/src/rust.rs b/crates/codegen/src/rust.rs index 4bce52293af..97dea6815a5 100644 --- a/crates/codegen/src/rust.rs +++ b/crates/codegen/src/rust.rs @@ -10,7 +10,7 @@ use spacetimedb_lib::sats::layout::PrimitiveType; use spacetimedb_lib::sats::AlgebraicTypeRef; use spacetimedb_schema::def::{ModuleDef, ReducerDef, ScopedTypeName, TableDef, TypeDef}; use spacetimedb_schema::identifier::Identifier; -use spacetimedb_schema::schema::{Schema, TableSchema}; +use spacetimedb_schema::schema::TableSchema; use spacetimedb_schema::type_for_generate::{AlgebraicTypeDef, AlgebraicTypeUse}; use std::collections::BTreeSet; use std::fmt::{self, Write}; @@ -70,11 +70,7 @@ impl __sdk::InModule for {type_name} {{ code: output.into_inner(), }] } - fn generate_table_file(&self, module: &ModuleDef, table: &TableDef) -> OutputFile { - let schema = TableSchema::from_module_def(module, table, (), 0.into()) - .validated() - .expect("Failed to generate table due to validation errors"); - + fn generate_table_file_from_schema(&self, module: &ModuleDef, table: &TableDef, schema: TableSchema) -> OutputFile { let type_ref = table.product_type_ref; let mut output = CodeIndenter::new(String::new(), INDENT); @@ -200,7 +196,7 @@ pub(super) fn register_table(client_cache: &mut __sdk::ClientCache OutputFile { - let schema = TableSchema::from_module_def(module, table, (), 0.into()) - .validated() - .expect("Failed to generate table due to validation errors"); - + fn generate_table_file_from_schema(&self, module: &ModuleDef, table: &TableDef, schema: TableSchema) -> OutputFile { let mut output = CodeIndenter::new(String::new(), INDENT); let out = &mut output; diff --git a/crates/codegen/src/unrealcpp.rs b/crates/codegen/src/unrealcpp.rs index 35ec3baecd7..70cde2359a8 100644 --- a/crates/codegen/src/unrealcpp.rs +++ b/crates/codegen/src/unrealcpp.rs @@ -36,7 +36,7 @@ impl UnrealCpp<'_> { } impl Lang for UnrealCpp<'_> { - fn generate_table_file(&self, module: &ModuleDef, table: &TableDef) -> OutputFile { + fn generate_table_file_from_schema(&self, module: &ModuleDef, table: &TableDef, schema: TableSchema) -> OutputFile { let struct_name = type_ref_name(module, table.product_type_ref); let self_header = struct_name.clone() + "Table"; @@ -58,10 +58,6 @@ impl Lang for UnrealCpp<'_> { let table_name = table.name.deref().to_string(); let table_pascal = struct_name.clone(); - let schema = TableSchema::from_module_def(module, table, (), 0.into()) - .validated() - .expect("table schema should validate"); - // Generate unique index classes first let product_type = module.typespace_for_generate()[table.product_type_ref].as_product(); diff --git a/crates/codegen/tests/snapshots/codegen__codegen_csharp.snap b/crates/codegen/tests/snapshots/codegen__codegen_csharp.snap index 77755cee286..601a470272d 100644 --- a/crates/codegen/tests/snapshots/codegen__codegen_csharp.snap +++ b/crates/codegen/tests/snapshots/codegen__codegen_csharp.snap @@ -1,6 +1,5 @@ --- source: crates/codegen/tests/codegen.rs -assertion_line: 37 expression: outfiles --- "Reducers/Add.g.cs" = ''' @@ -1602,6 +1601,35 @@ namespace SpacetimeDB } } ''' +"Tables/MyPlayer.g.cs" = ''' +// 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 SpacetimeDB.BSATN; +using SpacetimeDB.ClientApi; +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace SpacetimeDB +{ + public sealed partial class RemoteTables + { + public sealed class MyPlayerHandle : RemoteTableHandle + { + protected override string RemoteTableName => "my_player"; + + internal MyPlayerHandle(DbConnection conn) : base(conn) + { + } + } + + public readonly MyPlayerHandle MyPlayer; + } +} +''' "Tables/Person.g.cs" = ''' // THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE // WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. diff --git a/crates/codegen/tests/snapshots/codegen__codegen_rust.snap b/crates/codegen/tests/snapshots/codegen__codegen_rust.snap index 79ecb333d8c..121416c4069 100644 --- a/crates/codegen/tests/snapshots/codegen__codegen_rust.snap +++ b/crates/codegen/tests/snapshots/codegen__codegen_rust.snap @@ -2273,6 +2273,106 @@ fn register_tables(client_cache: &mut __sdk::ClientCache) { } } ''' +"my_player_table.rs" = ''' +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{ + self as __sdk, + __lib, + __sats, + __ws, +}; +use super::player_type::Player; + +/// Table handle for the table `my_player`. +/// +/// Obtain a handle from the [`MyPlayerTableAccess::my_player`] method on [`super::RemoteTables`], +/// like `ctx.db.my_player()`. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.my_player().on_insert(...)`. +pub struct MyPlayerTableHandle<'ctx> { + imp: __sdk::TableHandle, + ctx: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the table `my_player`. +/// +/// Implemented for [`super::RemoteTables`]. +pub trait MyPlayerTableAccess { + #[allow(non_snake_case)] + /// Obtain a [`MyPlayerTableHandle`], which mediates access to the table `my_player`. + fn my_player(&self) -> MyPlayerTableHandle<'_>; +} + +impl MyPlayerTableAccess for super::RemoteTables { + fn my_player(&self) -> MyPlayerTableHandle<'_> { + MyPlayerTableHandle { + imp: self.imp.get_table::("my_player"), + ctx: std::marker::PhantomData, + } + } +} + +pub struct MyPlayerInsertCallbackId(__sdk::CallbackId); +pub struct MyPlayerDeleteCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::Table for MyPlayerTableHandle<'ctx> { + type Row = Player; + type EventContext = super::EventContext; + + fn count(&self) -> u64 { self.imp.count() } + fn iter(&self) -> impl Iterator + '_ { self.imp.iter() } + + type InsertCallbackId = MyPlayerInsertCallbackId; + + fn on_insert( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> MyPlayerInsertCallbackId { + MyPlayerInsertCallbackId(self.imp.on_insert(Box::new(callback))) + } + + fn remove_on_insert(&self, callback: MyPlayerInsertCallbackId) { + self.imp.remove_on_insert(callback.0) + } + + type DeleteCallbackId = MyPlayerDeleteCallbackId; + + fn on_delete( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> MyPlayerDeleteCallbackId { + MyPlayerDeleteCallbackId(self.imp.on_delete(Box::new(callback))) + } + + fn remove_on_delete(&self, callback: MyPlayerDeleteCallbackId) { + self.imp.remove_on_delete(callback.0) + } +} + +#[doc(hidden)] +pub(super) fn register_table(client_cache: &mut __sdk::ClientCache) { + + let _table = client_cache.get_or_make_table::("my_player"); +} + +#[doc(hidden)] +pub(super) fn parse_table_update( + raw_updates: __ws::TableUpdate<__ws::BsatnFormat>, +) -> __sdk::Result<__sdk::TableUpdate> { + __sdk::TableUpdate::parse_table_update(raw_updates).map_err(|e| { + __sdk::InternalError::failed_parse( + "TableUpdate", + "TableUpdate", + ).with_cause(e).into() + }) +} +''' "namespace_test_c_type.rs" = ''' // THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE // WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. diff --git a/crates/codegen/tests/snapshots/codegen__codegen_typescript.snap b/crates/codegen/tests/snapshots/codegen__codegen_typescript.snap index ee6178b0c03..7ad3de0f26a 100644 --- a/crates/codegen/tests/snapshots/codegen__codegen_typescript.snap +++ b/crates/codegen/tests/snapshots/codegen__codegen_typescript.snap @@ -1731,6 +1731,84 @@ export class LoggedOutPlayerTableHandle implements __T return this.tableCache.removeOnUpdate(cb); }} ''' +"my_player_table.ts" = ''' +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +/* eslint-disable */ +/* tslint:disable */ +import { + AlgebraicType as __AlgebraicTypeValue, + BinaryReader as __BinaryReader, + BinaryWriter as __BinaryWriter, + ClientCache as __ClientCache, + ConnectionId as __ConnectionId, + DbConnectionBuilder as __DbConnectionBuilder, + DbConnectionImpl as __DbConnectionImpl, + Identity as __Identity, + SubscriptionBuilderImpl as __SubscriptionBuilderImpl, + TableCache as __TableCache, + TimeDuration as __TimeDuration, + Timestamp as __Timestamp, + deepEqual as __deepEqual, + type AlgebraicType as __AlgebraicTypeType, + type AlgebraicTypeVariants as __AlgebraicTypeVariants, + type CallReducerFlags as __CallReducerFlags, + type ErrorContextInterface as __ErrorContextInterface, + type Event as __Event, + type EventContextInterface as __EventContextInterface, + type ReducerEventContextInterface as __ReducerEventContextInterface, + type SubscriptionEventContextInterface as __SubscriptionEventContextInterface, + type TableHandle as __TableHandle, +} from "spacetimedb"; +import { Player } from "./player_type"; +import { type EventContext, type Reducer, RemoteReducers, RemoteTables } from "."; +declare type __keep = [EventContext, Reducer, RemoteReducers, RemoteTables]; + +/** + * Table handle for the table `my_player`. + * + * Obtain a handle from the [`myPlayer`] property on [`RemoteTables`], + * like `ctx.db.myPlayer`. + * + * Users are encouraged not to explicitly reference this type, + * but to directly chain method calls, + * like `ctx.db.myPlayer.on_insert(...)`. + */ +export class MyPlayerTableHandle implements __TableHandle { + // phantom type to track the table name + readonly tableName!: TableName; + tableCache: __TableCache; + + constructor(tableCache: __TableCache) { + this.tableCache = tableCache; + } + + count(): number { + return this.tableCache.count(); + } + + iter(): Iterable { + return this.tableCache.iter(); + } + + onInsert = (cb: (ctx: EventContext, row: Player) => void) => { + return this.tableCache.onInsert(cb); + } + + removeOnInsert = (cb: (ctx: EventContext, row: Player) => void) => { + return this.tableCache.removeOnInsert(cb); + } + + onDelete = (cb: (ctx: EventContext, row: Player) => void) => { + return this.tableCache.onDelete(cb); + } + + removeOnDelete = (cb: (ctx: EventContext, row: Player) => void) => { + return this.tableCache.removeOnDelete(cb); + } +} +''' "namespace_test_c_type.ts" = ''' // THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE // WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. diff --git a/crates/datastore/src/locking_tx_datastore/mut_tx.rs b/crates/datastore/src/locking_tx_datastore/mut_tx.rs index 98ae9b93a19..0ea3155b639 100644 --- a/crates/datastore/src/locking_tx_datastore/mut_tx.rs +++ b/crates/datastore/src/locking_tx_datastore/mut_tx.rs @@ -303,7 +303,7 @@ impl MutTxId { /// - The returned [`ViewId`] is unique and not [`ViewId::SENTINEL`]. /// - All view metadata maintained by the datastore is created atomically pub fn create_view(&mut self, module_def: &ModuleDef, view_def: &ViewDef) -> Result<(ViewId, TableId)> { - let table_schema = TableSchema::from_view_def(module_def, view_def); + let table_schema = TableSchema::from_view_def_for_datastore(module_def, view_def); let table_id = self.create_table(table_schema)?; let ViewDef { diff --git a/crates/schema/src/def.rs b/crates/schema/src/def.rs index 146782fb019..59e9e409395 100644 --- a/crates/schema/src/def.rs +++ b/crates/schema/src/def.rs @@ -25,11 +25,10 @@ use crate::schema::{Schema, TableSchema}; use crate::type_for_generate::{AlgebraicTypeUse, ProductTypeDef, TypespaceForGenerate}; use deserialize::ArgsSeed; use enum_map::EnumMap; -use hashbrown::Equivalent; +use hashbrown::{Equivalent, HashMap}; use indexmap::IndexMap; use itertools::Itertools; use spacetimedb_data_structures::error_stream::{CollectAllErrors, CombineErrors, ErrorStream}; -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, @@ -523,6 +522,31 @@ impl From for RawTableDefV9 { } } +impl From for TableDef { + fn from(def: ViewDef) -> Self { + use TableAccess::*; + let ViewDef { + name, + is_public, + product_type_ref, + return_columns, + .. + } = def; + Self { + name, + product_type_ref, + primary_key: None, + columns: return_columns.into_iter().map(ColumnDef::from).collect(), + indexes: <_>::default(), + constraints: <_>::default(), + sequences: <_>::default(), + schedule: None, + table_type: TableType::User, + table_access: if is_public { Public } else { Private }, + } + } +} + /// A sequence definition for a database table column. #[derive(Debug, Clone, Eq, PartialEq)] pub struct SequenceDef { @@ -719,6 +743,26 @@ pub struct ColumnDef { pub default_value: Option, } +impl From for ColumnDef { + fn from(def: ViewColumnDef) -> Self { + let ViewColumnDef { + name, + col_id, + ty, + ty_for_generate, + view_name: table_name, + } = def; + Self { + name, + col_id, + ty, + ty_for_generate, + table_name, + default_value: None, + } + } +} + /// A struct representing a validated view column #[derive(Debug, Clone, Eq, PartialEq)] #[non_exhaustive] @@ -1067,19 +1111,26 @@ pub struct ViewDef { pub params_for_generate: ProductTypeDef, /// The return type of the view. - /// Either `T`, `Option`, or `Vec` where `T` is a [`ProductType`]. + /// Either `Option` or `Vec` where: /// - /// Here `Option` refers to [`AlgebraicType::option()`] and `Vec` refers to [`AlgebraicType::array()`]. - /// - /// `T` defines the columns of the view. - /// `T` will be registered in the module's `Typespace`. + /// 1. `T` is a [`ProductType`] containing the columns of the view, + /// 2. `T` is registered in the module's typespace, + /// 3. `Option` refers to [`AlgebraicType::option()`], and + /// 4. `Vec` refers to [`AlgebraicType::array()`] pub return_type: AlgebraicType, /// The return type of the view, formatted for client codegen. pub return_type_for_generate: AlgebraicTypeUse, + /// The single source of truth for the view's columns. + /// + /// If a view can return only `Option` or `Vec`, + /// this is a reference to the inner product type `T`. + /// All elements of `T` must have names. + pub product_type_ref: AlgebraicTypeRef, + /// The return columns of this view. - /// The same information is stored in `return_type`. + /// The same information is stored in `product_type_ref`. /// This is just a more convenient-to-access format. pub return_columns: Vec, @@ -1108,11 +1159,8 @@ impl From for RawViewDefV9 { is_anonymous, is_public, params, - params_for_generate: _, return_type, - return_type_for_generate: _, - return_columns: _, - param_columns: _, + .. } = val; RawViewDefV9 { name: name.into(), diff --git a/crates/schema/src/def/validate/v9.rs b/crates/schema/src/def/validate/v9.rs index 3f811dca6ed..85e6c14cc88 100644 --- a/crates/schema/src/def/validate/v9.rs +++ b/crates/schema/src/def/validate/v9.rs @@ -547,6 +547,7 @@ impl ModuleValidator<'_> { }, return_type, return_type_for_generate, + product_type_ref, return_columns, param_columns, }) diff --git a/crates/schema/src/schema.rs b/crates/schema/src/schema.rs index 86c748237c8..f1eeee13c8f 100644 --- a/crates/schema/src/schema.rs +++ b/crates/schema/src/schema.rs @@ -588,7 +588,52 @@ pub fn column_schemas_from_defs(module_def: &ModuleDef, columns: &[ColumnDef], t } impl TableSchema { - /// Every view is materialized by default. For example: + /// Generates a [`TableSchema`] for the purpose of client codegen. + /// + /// This is the schema defined in the module. + /// It does not have any internal columns like the schema for the datastore. + /// See [`Self::from_view_def_for_datastore`] for more details. + pub fn from_view_def_for_codegen(module_def: &ModuleDef, view_def: &ViewDef) -> Self { + module_def.expect_contains(view_def); + + let ViewDef { + name, + is_public, + return_columns, + .. + } = view_def; + + let columns = return_columns + .iter() + .map(|def| ColumnSchema::from_view_column_def(module_def, def)) + .enumerate() + .map(|(i, schema)| (ColId::from(i), schema)) + .map(|(col_pos, schema)| ColumnSchema { col_pos, ..schema }) + .collect(); + + let table_access = if *is_public { + StAccess::Public + } else { + StAccess::Private + }; + + TableSchema::new( + TableId::SENTINEL, + (*name).clone().into(), + columns, + vec![], + vec![], + vec![], + StTableType::User, + table_access, + None, + None, + ) + } + + /// Generate a [`TableSchema`] for the purpose of materializing in the datastore. + /// + /// Note, every view is materialized by default. For example: /// ```rust,ignore /// #[table] /// pub struct MyTable { @@ -617,8 +662,9 @@ impl TableSchema { /// |-------------|--------|-----|-----| /// | (none = ()) | u64 | u32 | u32 | /// - /// Note, `arg_id` is a foreign key into `st_view_arg`. - pub fn from_view_def(module_def: &ModuleDef, view_def: &ViewDef) -> Self { + /// Note, `sender` and `arg_id` are internal columns not defined by the module, + /// where `arg_id` is a foreign key into `st_view_arg`. + pub fn from_view_def_for_datastore(module_def: &ModuleDef, view_def: &ViewDef) -> Self { module_def.expect_contains(view_def); let ViewDef { diff --git a/crates/schema/tests/ensure_same_schema.rs b/crates/schema/tests/ensure_same_schema.rs index 9e54ccc88fc..25039c0aba5 100644 --- a/crates/schema/tests/ensure_same_schema.rs +++ b/crates/schema/tests/ensure_same_schema.rs @@ -28,6 +28,14 @@ fn assert_identical_modules(module_name_prefix: &str, lang_name: &str, suffix: & ) }); + // TODO: Remove this once we have view bindings for C# and TypeScript + diff.retain(|step| { + !matches!( + step, + AutoMigrateStep::AddView(_) | AutoMigrateStep::RemoveView(_) | AutoMigrateStep::UpdateView(_) + ) + }); + assert!( diff.is_empty(), "Rust and {lang_name} modules are not identical. Here are the steps to migrate from {lang_name} to Rust: {diff:#?}" diff --git a/modules/module-test/src/lib.rs b/modules/module-test/src/lib.rs index 831b50b117a..4544461c9f6 100644 --- a/modules/module-test/src/lib.rs +++ b/modules/module-test/src/lib.rs @@ -4,7 +4,7 @@ use std::time::Duration; use spacetimedb::spacetimedb_lib::db::raw_def::v9::TableAccess; use spacetimedb::spacetimedb_lib::{self, bsatn}; use spacetimedb::{ - duration, table, ConnectionId, Deserialize, Identity, ReducerContext, SpacetimeType, Table, Timestamp, + duration, table, ConnectionId, Deserialize, Identity, ReducerContext, SpacetimeType, Table, Timestamp, ViewContext, }; use spacetimedb::{log, ProcedureContext}; @@ -180,6 +180,16 @@ impl Foo<'_> { bsatn::from_slice(data).unwrap() } } + +// ───────────────────────────────────────────────────────────────────────────── +// VIEWS +// ───────────────────────────────────────────────────────────────────────────── + +#[spacetimedb::view(name = my_player, public)] +fn my_player(ctx: &ViewContext) -> Option { + ctx.db.player().identity().find(ctx.sender) +} + // ───────────────────────────────────────────────────────────────────────────── // REDUCERS // ───────────────────────────────────────────────────────────────────────────── From 9c7bbae34fe68825e80258bbce25550848cadc29 Mon Sep 17 00:00:00 2001 From: joshua-spacetime Date: Tue, 4 Nov 2025 12:36:39 -0800 Subject: [PATCH 2/2] remove default hasher --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 176f900d18f..d1dc2344c4d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -183,7 +183,7 @@ futures-util = "0.3" getrandom02 = { package = "getrandom", version = "0.2" } git2 = "0.19" glob = "0.3.1" -hashbrown = { version = "0.15", default-features = false, features = ["default-hasher", "equivalent", "inline-more"] } +hashbrown = { version = "0.15", default-features = false, features = ["equivalent", "inline-more"] } headers = "0.4" heck = "0.4" hex = "0.4.3"