From 1467c5ed4d81aa0ba09907f63c282d8fa0c24d00 Mon Sep 17 00:00:00 2001 From: James Gilles Date: Thu, 6 Mar 2025 13:18:51 -0500 Subject: [PATCH] Bring over module doc fixes from the C# module docs --- crates/bindings/README.md | 41 ++++++++++++++++++++++++------------ crates/bindings/src/lib.rs | 40 ++++++++++++++++++++++------------- crates/bindings/src/table.rs | 9 +++++--- 3 files changed, 59 insertions(+), 31 deletions(-) diff --git a/crates/bindings/README.md b/crates/bindings/README.md index e704e5e8c8d..4144260288a 100644 --- a/crates/bindings/README.md +++ b/crates/bindings/README.md @@ -19,7 +19,7 @@ but you need to use `./bindings-doctests.sh` to actually test them. --> -[SpacetimeDB](https://spacetimedb.com/) allows using the Rust language to write server-side applications called **modules**. Modules run inside a relational database. They have direct access to database tables, and expose public functions called **reducers** that can be invoked over the network. Clients connect directly to the database to read data. +[SpacetimeDB](https://spacetimedb.com/) allows using the Rust language to write server-side applications called **modules**. Modules, which run inside a relational database, have direct access to database tables, and expose public functions called **reducers** that can be invoked over the network. Clients connect directly to the database to read data. ```text Client Application SpacetimeDB @@ -40,7 +40,7 @@ Rust modules are written with the the Rust Module Library (this crate). They are built using [cargo](https://doc.rust-lang.org/cargo/) and deployed using the [`spacetime` CLI tool](https://spacetimedb.com/install). Rust modules can import any Rust [crate](https://crates.io/) that supports being compiled to WebAssembly. -(Note: Rust can also be used to write **clients** of SpacetimeDB databases, but this requires using a completely different library, the SpacetimeDB Rust Client SDK. See the documentation on [clients] for more information.) +(Note: Rust can also be used to write **clients** of SpacetimeDB databases, but this requires using a different library, the SpacetimeDB Rust Client SDK. See the documentation on [clients] for more information.) This reference assumes you are familiar with the basics of Rust. If you aren't, check out Rust's [excellent documentation](https://www.rust-lang.org/learn). For a guided introduction to Rust Modules, see the [Rust Module Quickstart](https://spacetimedb.com/docs/modules/rust/quickstart). @@ -75,7 +75,7 @@ fn add_person(ctx: &ReducerContext, id: u32, name: String) { ``` -Note that reducers don't return data directly; they can only modify the database. Clients connect directly to the database and use SQL to query [public](#public-and-private-tables) tables. Clients can also open subscriptions to receive streaming updates as the results of a SQL query change. +Note that reducers don't return data directly; they can only modify the database. Clients connect directly to the database and use SQL to query [public](#public-and-private-tables) tables. Clients can also subscribe to a set of rows using SQL queries and receive streaming updates whenever any of those rows change. Tables and reducers in Rust modules can use any type that implements the [`SpacetimeType`] trait. @@ -184,7 +184,7 @@ For example: spacetime publish silly_demo_app ``` -When you publish your module, a database named will be created with the requested tables, and the module will be installed inside it. +When you publish your module, a database named `silly_demo_app` will be created with the requested tables, and the module will be installed inside it. The output of `spacetime publish` will end with a line: ```text @@ -257,8 +257,8 @@ fn do_nothing() { drop(person); } -// To interact with the database, you need a `ReducerContext`. -// The first argument of a reducer is always a `ReducerContext`. +// To interact with the database, you need a `ReducerContext`, +// which is provided as the first parameter of any reducer. #[reducer] fn do_something(ctx: &ReducerContext) { // `ctx.db.{table_name}()` gets a handle to a database table. @@ -382,7 +382,7 @@ ctx.db.person().ssn() Notice that updating a row is only possible if a row has a unique column -- there is no `update` method in the base [`Table`] trait. SpacetimeDB has no notion of rows having an "identity" aside from their unique / primary keys. -The `#[primary_key]` annotation is similar to the `#[unique]` annotation, except that it leads to additional methods being made available in the [client]-side SDKs. +The `#[primary_key]` annotation implies `#[unique]` annotation, but avails additional methods in the [client]-side SDKs. It is not currently possible to mark a group of fields as collectively unique. @@ -433,7 +433,6 @@ For example: ```no_run # #[cfg(target_arch = "wasm32")] mod demo { - use spacetimedb::table; #[table(name = paper, index(name = url_and_country, btree(columns = [url, country])))] @@ -454,6 +453,23 @@ Single-column indexes can also be declared using the column attribute. +For example: + +```no_run +# #[cfg(target_arch = "wasm32")] mod demo { +use spacetimedb::table; + +#[table(name = paper)] +struct Paper { + url: String, + country: String, + #[index(btree)] + venue: String +} +# } +``` + + Any index supports getting a [`RangedIndex`] using [`ctx`](crate::ReducerContext)`.db.{table}().{index}()`. For example, `ctx.db.person().name()`. [`RangedIndex`] provides: @@ -483,11 +499,11 @@ fn give_player_item( # } ``` -Every reducer runs inside a [database transaction](https://en.wikipedia.org/wiki/Database_transaction). This means that reducers will not observe the effects of other reducers modifying the database while they run. Also, if a reducer fails, all of its changes to the database will automatically be rolled back. Reducers can fail by [panicking](::std::panic!) or by returning an `Err`. +Every reducer runs inside a [database transaction](https://en.wikipedia.org/wiki/Database_transaction). This means that reducers will not observe the effects of other reducers modifying the database while they run. If a reducer fails, all of its changes to the database will automatically be rolled back. Reducers can fail by [panicking](::std::panic!) or by returning an `Err`. #### The `ReducerContext` Type -Reducers have access to a special [`ReducerContext`] argument. This argument allows reading and writing the database attached to a module. It also provides some additional functionality, like generating random numbers and scheduling future operations. +Reducers have access to a special [`ReducerContext`] parameter. This parameter allows reading and writing the database attached to a module. It also provides some additional functionality, like generating random numbers and scheduling future operations. [`ReducerContext`] provides access to the database tables via [the `.db` field](ReducerContext#structfield.db). The [`#[table]`](macro@crate::table) macro generates traits that add accessor methods to this field. @@ -516,14 +532,13 @@ the database and respond to client connections. See [Lifecycle Reducers](macro@c #### Scheduled Reducers -Reducers can be scheduled to run repeatedly. This can be used to implement timers, game loops, and -maintenance tasks. See [Scheduled Reducers](macro@crate::reducer#scheduled-reducers). +Reducers can schedule other reducers to run asynchronously. This allows calling the reducers at a particular time, or at repeating intervals. This can be used to implement timers, game loops, and maintenance tasks. See [Scheduled Reducers](macro@crate::reducer#scheduled-reducers). ## Automatic migrations When you `spacetime publish` a module that has already been published using `spacetime publish `, SpacetimeDB attempts to automatically migrate your existing database to the new schema. (The "schema" is just the collection -of tables and reducers you've declared in your code, together with the types they depend on.) This form of migration is very limited and only supports a few kinds of changes. +of tables and reducers you've declared in your code, together with the types they depend on.) This form of migration is limited and only supports a few kinds of changes. On the plus side, automatic migrations usually don't break clients. The situations that may break clients are documented below. The following changes are always allowed and never breaking: diff --git a/crates/bindings/src/lib.rs b/crates/bindings/src/lib.rs index 9efd665b9fe..e2d9809a997 100644 --- a/crates/bindings/src/lib.rs +++ b/crates/bindings/src/lib.rs @@ -548,21 +548,7 @@ pub use spacetimedb_bindings_macro::table; /// This allows calling the reducers at a particular time, or in a loop. /// This can be used for game loops. /// -/// Scheduled reducers are normal reducers, and may still be called by clients. -/// If a scheduled reducer should only be called by the scheduler, -/// consider beginning it with a check that the caller `Identity` is the module: -/// -/// ```no_run -/// # #[cfg(target_arch = "wasm32")] mod demo { -/// #[reducer] -/// fn scheduled(ctx: &ReducerContext, args: ScheduledArgs) -> Result<(), String> { -/// if ctx.sender != ctx.identity() { -/// return Err("Reducer `scheduled` may not be invoked by clients, only via scheduling."); -/// } -/// // Reducer body... -/// } -/// # } -/// ``` + /// /// The scheduling information for a reducer is stored in a table. /// This table has two mandatory fields: @@ -570,6 +556,7 @@ pub use spacetimedb_bindings_macro::table; /// - A [`ScheduleAt`] field that says when to call the reducer. /// /// Managing timers with a scheduled table is as simple as inserting or deleting rows from the table. +/// This makes scheduling transactional in SpacetimeDB. If a reducer A first schedules B but then errors for some other reason, B will not be scheduled to run. /// /// A [`ScheduleAt`] can be created from a [`spacetimedb::Timestamp`](crate::Timestamp), in which case the reducer will be scheduled once, /// or from a [`std::time::Duration`], in which case the reducer will be scheduled in a loop. In either case the conversion can be performed using [`Into::into`]. @@ -652,6 +639,29 @@ pub use spacetimedb_bindings_macro::table; /// Scheduled reducers are called on a best-effort basis and may be slightly delayed in their execution /// when a database is under heavy load. /// +/// ### Restricting scheduled reducers +/// +/// Scheduled reducers are normal reducers, and may still be called by clients. +/// If a scheduled reducer should only be called by the scheduler, +/// consider beginning it with a check that the caller `Identity` is the module: +/// +/// ```no_run +/// # #[cfg(target_arch = "wasm32")] mod demo { +/// use spacetimedb::{reducer, ReducerContext}; +/// +/// # #[derive(spacetimedb::SpacetimeType)] struct ScheduledArgs {} +/// +/// #[reducer] +/// fn scheduled(ctx: &ReducerContext, args: ScheduledArgs) -> Result<(), String> { +/// if ctx.sender != ctx.identity() { +/// return Err("Reducer `scheduled` may not be invoked by clients, only via scheduling.".into()); +/// } +/// // Reducer body... +/// # Ok(()) +/// } +/// # } +/// ``` +/// /// /// /// [`&ReducerContext`]: `ReducerContext` diff --git a/crates/bindings/src/table.rs b/crates/bindings/src/table.rs index 9bde3401cf9..ead00bb8613 100644 --- a/crates/bindings/src/table.rs +++ b/crates/bindings/src/table.rs @@ -26,15 +26,18 @@ pub trait Table: TableInternal { /// /// This takes into account modifications by the current transaction, /// even though those modifications have not yet been committed or broadcast to clients. + /// This applies generally to insertions, deletions, updates, and iteration as well. fn count(&self) -> u64 { sys::datastore_table_row_count(Self::table_id()).expect("datastore_table_row_count() call failed") } /// Iterate over all rows of the table. /// - /// For large tables, this can be a very slow operation! + /// For large tables, this can be a slow operation! /// Prefer [filtering](RangedIndex::filter) a [`RangedIndex`] or [finding](UniqueColumn::find) a [`UniqueColumn`] if /// possible. + /// + /// (This keeps track of changes made to the table since the start of this reducer invocation. For example, if rows have been deleted since the start of this reducer invocation, those rows will not be returned by `iter`. Similarly, inserted rows WILL be returned.) #[inline] fn iter(&self) -> impl Iterator { let table_id = Self::table_id(); @@ -373,10 +376,10 @@ impl> UniqueColumn 0, args.data) } - /// Deletes the row where the value in the unique column matches that in the corresponding field of `new_row`, + /// Deletes the row where the value in the unique column matches that in the corresponding field of `new_row`, and /// then inserts the `new_row`. /// - /// Returns the new row as actually inserted, with any auto-inc placeholders substituted for computed values. + /// Returns the new row as actually inserted, with computed values substituted for any auto-inc placeholders. /// /// # Panics /// Panics if no row was previously present with the matching value in the unique column,