Files
SpacetimeDB/crates/codegen/src/rust.rs
T
3abaec9eaf More granular table traits (#4775)
# Description of Changes

Refactored the Rust SDK table trait surface into composable capabilities
and updated Rust codegen to match. [Discord
thread](https://discord.com/channels/1037340874172014652/1492137984902959164)

### Motivation

The granular traits are a useful type-system cleanup on their own, but
the main motivation is to let downstream integration crates like
[bevy_stdb](https://github.com/onx2/bevy_stdb) express callback
capabilities more directly instead of routing them through broader
semantic categories.

For example, `bevy_stdb` may want to support bindings like “forward only
inserts from this subscribed view as Bevy messages” for a relation that
is not semantically an `EventTable`. Today this can still be made to
work with broader abstractions, so this is not primarily about enabling
new runtime behavior. The value is mostly at compile time: more precise
trait bounds, clearer downstream APIs, and better guarantees that a
binding only requires the capabilities it actually uses.

Concretely, this makes it easier for integration crates to model things
like insert-only or delete-only bindings for ordinary tables or views
without conflating those behaviors with `EventTable` semantics.

Example:

```rust
// Current semantic shape:
plugin.add_event_table::<LogRow>(|reg, db| reg.bind(db.logs()));

// Potential capability-based shape:
plugin.add_insert_source::<CharacterRow>(|reg, db| reg.bind(db.characters_near_me()));
plugin.add_delete_source::<CharacterRow>(|reg, db| reg.bind(db.characters_near_me()));
```

### Summary
- Added a shared `TableLike` trait for common table handle
functionality:
  - `Row`
  - `EventContext`
  - `count`
  - `iter`
- Added capability traits:
  - `WithInsert`
  - `WithDelete`
  - `WithUpdate`

# API and ABI breaking changes

None. This is additive only.

# Expected complexity level and risk

2/5

This is additive but potential risks/complexity are that the surface
area of the API is larger and there isn't really overlap between
original approach and the additive. So maybe in usage we'll need to
consider satisfying those new traits too?

# Testing

- [x] Ran `cargo check --manifest-path
/Users/onx2/Documents/projects/SpacetimeDB/sdks/rust/Cargo.toml`
- [x] Ran `cargo test --manifest-path
/Users/onx2/Documents/projects/SpacetimeDB/Cargo.toml -p
spacetimedb-codegen test_codegen_rust`
- [x] Regenerated Rust client bindings using the workspace CLI and
verified generated impls match the new trait structure
- [ ] Reviewer: regenerate a Rust client with the workspace CLI and
confirm the generated bindings compile cleanly

---------

Co-authored-by: Zeke Foppa <[email protected]>
Co-authored-by: clockwork-labs-bot <[email protected]>
2026-06-30 13:02:47 +00:00

2183 lines
78 KiB
Rust

use super::code_indenter::{CodeIndenter, Indenter};
use super::util::{collect_case, iter_reducers, print_lines, type_ref_name};
use super::Lang;
use crate::util::{
iter_indexes, iter_procedures, iter_table_names_and_types, iter_tables, iter_types, iter_unique_cols, iter_views,
print_auto_generated_file_comment, print_auto_generated_version_comment, CodegenVisibility,
};
use crate::CodegenOptions;
use crate::OutputFile;
use convert_case::{Case, Casing};
use spacetimedb_lib::sats::layout::PrimitiveType;
use spacetimedb_lib::sats::AlgebraicTypeRef;
use spacetimedb_schema::def::{ModuleDef, ProcedureDef, ReducerDef, ScopedTypeName, TableDef, TypeDef};
use spacetimedb_schema::identifier::Identifier;
use spacetimedb_schema::reducer_name::ReducerName;
use spacetimedb_schema::schema::TableSchema;
use spacetimedb_schema::type_for_generate::{AlgebraicTypeDef, AlgebraicTypeUse};
use std::collections::BTreeSet;
use std::fmt::{self, Write};
use std::ops::Deref;
/// Pairs of (module_name, TypeName).
type Imports = BTreeSet<AlgebraicTypeRef>;
const INDENT: &str = " ";
pub struct Rust;
impl Lang for Rust {
fn generate_type_files(&self, module: &ModuleDef, typ: &TypeDef) -> Vec<OutputFile> {
let type_name = collect_case(Case::Pascal, typ.accessor_name.name_segments());
let mut output = CodeIndenter::new(String::new(), INDENT);
let out = &mut output;
print_file_header(out, false);
out.newline();
match &module.typespace_for_generate()[typ.ty] {
AlgebraicTypeDef::Product(product) => {
gen_and_print_imports(module, out, &product.elements, &[typ.ty]);
out.newline();
define_struct_for_product(module, out, &type_name, &product.elements, "pub");
}
AlgebraicTypeDef::Sum(sum) => {
gen_and_print_imports(module, out, &sum.variants, &[typ.ty]);
out.newline();
define_enum_for_sum(module, out, &type_name, &sum.variants, false);
}
AlgebraicTypeDef::PlainEnum(plain_enum) => {
let variants = plain_enum
.variants
.iter()
.cloned()
.map(|var| (var, AlgebraicTypeUse::Unit))
.collect::<Vec<_>>();
define_enum_for_sum(module, out, &type_name, &variants, true);
}
}
out.newline();
writeln!(
out,
"
impl __sdk::InModule for {type_name} {{
type Module = super::RemoteModule;
}}
",
);
// Do not implement query col types for nested types.
// as querying is only supported on top-level table row types.
let name = type_ref_name(module, typ.ty);
let implemented = match module
.tables()
.find(|t| type_ref_name(module, t.product_type_ref) == name)
{
Some(table) => {
implement_query_col_types_for_table_struct(module, out, table)
.expect("failed to implement query col types");
out.newline();
true
}
_ => false,
};
if !implemented
&& let Some(type_ref) = module
.views()
.map(|v| v.product_type_ref)
.find(|type_ref| type_ref_name(module, *type_ref) == name)
{
implement_query_col_types_for_struct(module, out, type_ref).expect("failed to implement query col types");
out.newline();
}
vec![OutputFile {
filename: type_module_name(&typ.accessor_name) + ".rs",
code: output.into_inner(),
}]
}
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);
let out = &mut output;
print_file_header(out, false);
let row_type = type_ref_name(module, type_ref);
let row_type_module = type_ref_module_name(module, type_ref);
writeln!(out, "use super::{row_type_module}::{row_type};");
let product_def = module.typespace_for_generate()[type_ref].as_product().unwrap();
// Import the types of all fields.
// We only need to import fields which have indices or unique constraints,
// but it's easier to just import all of 'em, since we have `#![allow(unused)]` anyway.
gen_and_print_imports(
module,
out,
&product_def.elements,
&[], // No need to skip any imports; we're not defining a type, so there's no chance of circular imports.
);
let table_name = table.name.deref();
let table_name_pascalcase = table.accessor_name.deref().to_case(Case::Pascal);
let table_handle = table_name_pascalcase.clone() + "TableHandle";
let insert_callback_id = table_name_pascalcase.clone() + "InsertCallbackId";
let delete_callback_id = table_name_pascalcase.clone() + "DeleteCallbackId";
let accessor_trait = table_access_trait_name(&table.accessor_name);
let accessor_method = table_method_name(&table.accessor_name);
write!(
out,
"
/// Table handle for the table `{table_name}`.
///
/// Obtain a handle from the [`{accessor_trait}::{accessor_method}`] method on [`super::RemoteTables`],
/// like `ctx.db.{accessor_method}()`.
///
/// Users are encouraged not to explicitly reference this type,
/// but to directly chain method calls,
/// like `ctx.db.{accessor_method}().on_insert(...)`.
pub struct {table_handle}<'ctx> {{
imp: __sdk::TableHandle<{row_type}>,
ctx: std::marker::PhantomData<&'ctx super::RemoteTables>,
}}
#[allow(non_camel_case_types)]
/// Extension trait for access to the table `{table_name}`.
///
/// Implemented for [`super::RemoteTables`].
pub trait {accessor_trait} {{
#[allow(non_snake_case)]
/// Obtain a [`{table_handle}`], which mediates access to the table `{table_name}`.
fn {accessor_method}(&self) -> {table_handle}<'_>;
}}
impl {accessor_trait} for super::RemoteTables {{
fn {accessor_method}(&self) -> {table_handle}<'_> {{
{table_handle} {{
imp: self.imp.get_table::<{row_type}>({table_name:?}),
ctx: std::marker::PhantomData,
}}
}}
}}
pub struct {insert_callback_id}(__sdk::CallbackId);
"
);
if table.is_event {
// Event tables: implement the `EventTable` trait, which exposes only on-insert callbacks,
// not on-delete or on-update.
// on-update callbacks aren't meaningful for event tables,
// as they never have resident rows, so they can never update an existing row.
// on-delete callbacks are meaningful, but exactly equivalent to the on-insert callbacks,
// so not particularly useful.
// Also, don't emit unique index accessors: no resident rows means these would always be empty,
// so no reason to have them.
write!(
out,
"
impl<'ctx> __sdk::TableLike for {table_handle}<'ctx> {{
type Row = {row_type};
type EventContext = super::EventContext;
fn count(&self) -> u64 {{ self.imp.count() }}
fn iter(&self) -> impl Iterator<Item = {row_type}> + '_ {{ self.imp.iter() }}
}}
impl<'ctx> __sdk::EventTable for {table_handle}<'ctx> {{
type Row = {row_type};
type EventContext = super::EventContext;
fn count(&self) -> u64 {{ self.imp.count() }}
fn iter(&self) -> impl Iterator<Item = {row_type}> + '_ {{ self.imp.iter() }}
type InsertCallbackId = {insert_callback_id};
fn on_insert(
&self,
callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static,
) -> {insert_callback_id} {{
{insert_callback_id}(self.imp.on_insert(Box::new(callback)))
}}
fn remove_on_insert(&self, callback: {insert_callback_id}) {{
self.imp.remove_on_insert(callback.0)
}}
}}
impl<'ctx> __sdk::WithInsert for {table_handle}<'ctx> {{
type InsertCallbackId = {insert_callback_id};
fn on_insert(
&self,
callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static,
) -> {insert_callback_id} {{
{insert_callback_id}(self.imp.on_insert(Box::new(callback)))
}}
fn remove_on_insert(&self, callback: {insert_callback_id}) {{
self.imp.remove_on_insert(callback.0)
}}
}}
"
);
} else {
// Non-event tables: implement the `Table` trait, which exposes on-insert and on-delete callbacks.
// Also possibly implement `TableWithPrimrayKey`, which exposes on-update callbacks,
// and emit accessors for unique columns.
write!(
out,
"pub struct {delete_callback_id}(__sdk::CallbackId);
impl<'ctx> __sdk::TableLike for {table_handle}<'ctx> {{
type Row = {row_type};
type EventContext = super::EventContext;
fn count(&self) -> u64 {{ self.imp.count() }}
fn iter(&self) -> impl Iterator<Item = {row_type}> + '_ {{ self.imp.iter() }}
}}
impl<'ctx> __sdk::Table for {table_handle}<'ctx> {{
type Row = {row_type};
type EventContext = super::EventContext;
fn count(&self) -> u64 {{ self.imp.count() }}
fn iter(&self) -> impl Iterator<Item = {row_type}> + '_ {{ self.imp.iter() }}
type InsertCallbackId = {insert_callback_id};
fn on_insert(
&self,
callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static,
) -> {insert_callback_id} {{
{insert_callback_id}(self.imp.on_insert(Box::new(callback)))
}}
fn remove_on_insert(&self, callback: {insert_callback_id}) {{
self.imp.remove_on_insert(callback.0)
}}
type DeleteCallbackId = {delete_callback_id};
fn on_delete(
&self,
callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static,
) -> {delete_callback_id} {{
{delete_callback_id}(self.imp.on_delete(Box::new(callback)))
}}
fn remove_on_delete(&self, callback: {delete_callback_id}) {{
self.imp.remove_on_delete(callback.0)
}}
}}
impl<'ctx> __sdk::WithInsert for {table_handle}<'ctx> {{
type InsertCallbackId = {insert_callback_id};
fn on_insert(
&self,
callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static,
) -> {insert_callback_id} {{
{insert_callback_id}(self.imp.on_insert(Box::new(callback)))
}}
fn remove_on_insert(&self, callback: {insert_callback_id}) {{
self.imp.remove_on_insert(callback.0)
}}
}}
impl<'ctx> __sdk::WithDelete for {table_handle}<'ctx> {{
type DeleteCallbackId = {delete_callback_id};
fn on_delete(
&self,
callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static,
) -> {delete_callback_id} {{
{delete_callback_id}(self.imp.on_delete(Box::new(callback)))
}}
fn remove_on_delete(&self, callback: {delete_callback_id}) {{
self.imp.remove_on_delete(callback.0)
}}
}}
"
);
if table.primary_key.is_some() {
// If the table has a primary key, implement the `TableWithPrimaryKey` trait
// to expose on-update callbacks.
let update_callback_id = table_name_pascalcase.clone() + "UpdateCallbackId";
write!(
out,
"
pub struct {update_callback_id}(__sdk::CallbackId);
impl<'ctx> __sdk::TableWithPrimaryKey for {table_handle}<'ctx> {{
type UpdateCallbackId = {update_callback_id};
fn on_update(
&self,
callback: impl FnMut(&Self::EventContext, &Self::Row, &Self::Row) + Send + 'static,
) -> {update_callback_id} {{
{update_callback_id}(self.imp.on_update(Box::new(callback)))
}}
fn remove_on_update(&self, callback: {update_callback_id}) {{
self.imp.remove_on_update(callback.0)
}}
}}
impl<'ctx> __sdk::WithUpdate for {table_handle}<'ctx> {{
type UpdateCallbackId = {update_callback_id};
fn on_update(
&self,
callback: impl FnMut(&Self::EventContext, &Self::Row, &Self::Row) + Send + 'static,
) -> {update_callback_id} {{
{update_callback_id}(self.imp.on_update(Box::new(callback)))
}}
fn remove_on_update(&self, callback: {update_callback_id}) {{
self.imp.remove_on_update(callback.0)
}}
}}
"
);
}
// Emit unique index accessors for all of the table's unique fields.
for (unique_field_ident, unique_field_type_use) in
iter_unique_cols(module.typespace_for_generate(), &schema, product_def)
{
let unique_field_name = unique_field_ident.deref().to_case(Case::Snake);
let unique_field_name_pascalcase = unique_field_name.to_case(Case::Pascal);
let unique_constraint = table_name_pascalcase.clone() + &unique_field_name_pascalcase + "Unique";
let unique_field_type = type_name(module, unique_field_type_use);
write!(
out,
"
/// Access to the `{unique_field_name}` unique index on the table `{table_name}`,
/// which allows point queries on the field of the same name
/// via the [`{unique_constraint}::find`] method.
///
/// Users are encouraged not to explicitly reference this type,
/// but to directly chain method calls,
/// like `ctx.db.{accessor_method}().{unique_field_name}().find(...)`.
pub struct {unique_constraint}<'ctx> {{
imp: __sdk::UniqueConstraintHandle<{row_type}, {unique_field_type}>,
phantom: std::marker::PhantomData<&'ctx super::RemoteTables>,
}}
impl<'ctx> {table_handle}<'ctx> {{
/// Get a handle on the `{unique_field_name}` unique index on the table `{table_name}`.
pub fn {unique_field_name}(&self) -> {unique_constraint}<'ctx> {{
{unique_constraint} {{
imp: self.imp.get_unique_constraint::<{unique_field_type}>({unique_field_name:?}),
phantom: std::marker::PhantomData,
}}
}}
}}
impl<'ctx> {unique_constraint}<'ctx> {{
/// Find the subscribed row whose `{unique_field_name}` column value is equal to `col_val`,
/// if such a row is present in the client cache.
pub fn find(&self, col_val: &{unique_field_type}) -> Option<{row_type}> {{
self.imp.find(col_val)
}}
}}
"
);
}
// TODO: expose non-unique indices.
}
// Regardless of event-ness, emit `register_table` and `parse_table_update`.
out.delimited_block(
"
#[doc(hidden)]
pub(super) fn register_table(client_cache: &mut __sdk::ClientCache<super::RemoteModule>) {
",
|out| {
writeln!(out, "let _table = client_cache.get_or_make_table::<{row_type}>({table_name:?});");
for (unique_field_ident, unique_field_type_use) in iter_unique_cols(module.typespace_for_generate(), &schema, product_def) {
let unique_field_name = unique_field_ident.deref().to_case(Case::Snake);
let unique_field_type = type_name(module, unique_field_type_use);
writeln!(
out,
"_table.add_unique_constraint::<{unique_field_type}>({unique_field_name:?}, |row| &row.{unique_field_name});",
);
}
},
"}",
);
out.newline();
write!(
out,
"
#[doc(hidden)]
pub(super) fn parse_table_update(
raw_updates: __ws::v2::TableUpdate,
) -> __sdk::Result<__sdk::TableUpdate<{row_type}>> {{
__sdk::TableUpdate::parse_table_update(raw_updates).map_err(|e| {{
__sdk::InternalError::failed_parse(
\"TableUpdate<{row_type}>\",
\"TableUpdate\",
).with_cause(e).into()
}})
}}
"
);
implement_query_table_accessor(table, out, &row_type).expect("failed to implement query table accessor");
OutputFile {
filename: table_module_name(&table.accessor_name) + ".rs",
code: output.into_inner(),
}
}
fn generate_reducer_file(&self, module: &ModuleDef, reducer: &ReducerDef) -> OutputFile {
let mut output = CodeIndenter::new(String::new(), INDENT);
let out = &mut output;
print_file_header(out, false);
out.newline();
gen_and_print_imports(
module,
out,
&reducer.params_for_generate.elements,
// No need to skip any imports; we're not emitting a type that other modules can import.
&[],
);
out.newline();
let reducer_name = reducer.name.deref();
let func_name = reducer_function_name(reducer);
let args_type = function_args_type_name(&reducer.accessor_name);
let enum_variant_name = reducer_variant_name(&reducer.accessor_name);
// Define an "args struct" for the reducer.
// This is not user-facing (note the `pub(super)` visibility);
// it is an internal helper for serialization and deserialization.
// We actually want to ser/de instances of `enum Reducer`, but:
// - `Reducer` will have struct-like variants, which SATS ser/de does not support.
// - The WS format does not contain a BSATN-serialized `Reducer` instance;
// it holds the reducer name or ID separately from the argument bytes.
// We could work up some magic with `DeserializeSeed`
// and/or custom `Serializer` and `Deserializer` types
// to account for this, but it's much easier to just use an intermediate struct per reducer.
define_struct_for_product(
module,
out,
&args_type,
&reducer.params_for_generate.elements,
"pub(super)",
);
out.newline();
let FormattedArglist {
arglist_no_delimiters,
arg_names,
} = FormattedArglist::for_arguments(module, &reducer.params_for_generate.elements);
write!(out, "impl From<{args_type}> for super::Reducer ");
out.delimited_block(
"{",
|out| {
write!(out, "fn from(args: {args_type}) -> Self ");
out.delimited_block(
"{",
|out| {
write!(out, "Self::{enum_variant_name}");
if !reducer.params_for_generate.elements.is_empty() {
// We generate "struct variants" for reducers with arguments,
// but "unit variants" for reducers of no arguments.
// These use different constructor syntax.
out.delimited_block(
" {",
|out| {
for (arg_ident, _ty) in &reducer.params_for_generate.elements[..] {
let arg_name = arg_ident.deref().to_case(Case::Snake);
writeln!(out, "{arg_name}: args.{arg_name},");
}
},
"}",
);
}
out.newline();
},
"}\n",
);
},
"}\n",
);
// TODO: check for lifecycle reducers and do not generate the invoke method.
writeln!(
out,
"
impl __sdk::InModule for {args_type} {{
type Module = super::RemoteModule;
}}
#[allow(non_camel_case_types)]
/// Extension trait for access to the reducer `{reducer_name}`.
///
/// Implemented for [`super::RemoteReducers`].
pub trait {func_name} {{
/// Request that the remote module invoke the reducer `{reducer_name}` to run as soon as possible.
///
/// This method returns immediately, and errors only if we are unable to send the request.
/// The reducer will run asynchronously in the future,
/// and this method provides no way to listen for its completion status.
/// /// Use [`{func_name}:{func_name}_then`] to run a callback after the reducer completes.
fn {func_name}(&self, {arglist_no_delimiters}) -> __sdk::Result<()> {{
self.{func_name}_then({arg_names} |_, _| {{}})
}}
/// Request that the remote module invoke the reducer `{reducer_name}` to run as soon as possible,
/// registering `callback` to run when we are notified that the reducer completed.
///
/// This method returns immediately, and errors only if we are unable to send the request.
/// The reducer will run asynchronously in the future,
/// and its status can be observed with the `callback`.
fn {func_name}_then(
&self,
{arglist_no_delimiters}
callback: impl FnOnce(&super::ReducerEventContext, Result<Result<(), String>, __sdk::InternalError>)
+ Send
+ 'static,
) -> __sdk::Result<()>;
}}
impl {func_name} for super::RemoteReducers {{
fn {func_name}_then(
&self,
{arglist_no_delimiters}
callback: impl FnOnce(&super::ReducerEventContext, Result<Result<(), String>, __sdk::InternalError>)
+ Send
+ 'static,
) -> __sdk::Result<()> {{
self.imp.invoke_reducer_with_callback({args_type} {{ {arg_names} }}, callback)
}}
}}
"
);
OutputFile {
filename: reducer_module_name(&reducer.accessor_name) + ".rs",
code: output.into_inner(),
}
}
fn generate_procedure_file(&self, module: &ModuleDef, procedure: &ProcedureDef) -> OutputFile {
let mut output = CodeIndenter::new(String::new(), INDENT);
let out = &mut output;
print_file_header(out, false);
out.newline();
let mut imports = Imports::new();
gen_imports(&mut imports, &procedure.params_for_generate.elements);
add_one_import(&mut imports, &procedure.return_type_for_generate);
print_imports(module, out, imports);
out.newline();
let procedure_name = procedure.name.deref();
let func_name = procedure_function_name(procedure);
let func_name_with_callback = procedure_function_with_callback_name(procedure);
let args_type = function_args_type_name(&procedure.accessor_name);
let res_ty_name = type_name(module, &procedure.return_type_for_generate);
// Define an "args struct" as a serialization helper.
// This is not user-facing, it's not used outside this file.
// Unlike with reducers, we don't have to deserialize procedure args to build events,
// as we don't broadcast procedure args.
define_struct_for_product(
module,
out,
&args_type,
&procedure.params_for_generate.elements,
// non-pub visibility.
"",
);
out.newline();
let FormattedArglist {
arglist_no_delimiters,
arg_names,
..
} = FormattedArglist::for_arguments(module, &procedure.params_for_generate.elements);
writeln!(
out,
"
impl __sdk::InModule for {args_type} {{
type Module = super::RemoteModule;
}}
#[allow(non_camel_case_types)]
/// Extension trait for access to the procedure `{procedure_name}`.
///
/// Implemented for [`super::RemoteProcedures`].
pub trait {func_name} {{
fn {func_name}(&self, {arglist_no_delimiters}) {{
self.{func_name_with_callback}({arg_names} |_, _| {{}});
}}
fn {func_name_with_callback}(
&self,
{arglist_no_delimiters}
__callback: impl FnOnce(&super::ProcedureEventContext, Result<{res_ty_name}, __sdk::InternalError>) + Send + 'static,
);
}}
impl {func_name} for super::RemoteProcedures {{
fn {func_name_with_callback}(
&self,
{arglist_no_delimiters}
__callback: impl FnOnce(&super::ProcedureEventContext, Result<{res_ty_name}, __sdk::InternalError>) + Send + 'static,
) {{
self.imp.invoke_procedure_with_callback::<_, {res_ty_name}>(
{procedure_name:?},
{args_type} {{ {arg_names} }},
__callback,
);
}}
}}
"
);
OutputFile {
filename: procedure_module_name(&procedure.accessor_name) + ".rs",
code: output.into_inner(),
}
}
fn generate_global_files(&self, module: &ModuleDef, options: &CodegenOptions) -> Vec<OutputFile> {
let mut output = CodeIndenter::new(String::new(), INDENT);
let out = &mut output;
print_file_header(out, true);
out.newline();
// Declare `pub mod` for each of the files generated.
print_module_decls(module, options.visibility, out);
out.newline();
// Re-export all the modules for the generated files.
print_module_reexports(module, options.visibility, out);
out.newline();
// Define `enum Reducer`.
print_reducer_enum_defn(module, options.visibility, out);
out.newline();
// Define `DbUpdate`.
print_db_update_defn(module, options.visibility, out);
out.newline();
// Define `AppliedDiff`.
print_applied_diff_defn(module, options.visibility, out);
out.newline();
// Define `RemoteModule`, `DbConnection`, `EventContext`, `RemoteTables`,
// `RemoteReducers`, `RemoteProcedures` and `SubscriptionHandle`.
// Note that these do not change based on the module.
print_const_db_context_types(out);
out.newline();
// Implement `SpacetimeModule` for `RemoteModule`.
// This includes a method for initializing the tables in the client cache.
print_impl_spacetime_module(module, options.visibility, out);
vec![OutputFile {
filename: "mod.rs".to_string(),
code: output.into_inner(),
}]
}
}
/// Implements `HasCols` for the given `AlgebraicTypeRef` struct type.
fn implement_query_col_types_for_struct(
module: &ModuleDef,
out: &mut impl Write,
type_ref: AlgebraicTypeRef,
) -> fmt::Result {
let struct_name = type_ref_name(module, type_ref);
let cols_struct = struct_name.clone() + "Cols";
let product_def = module.typespace_for_generate()[type_ref]
.as_product()
.expect("expected product type");
writeln!(
out,
"
/// Column accessor struct for the table `{struct_name}`.
///
/// Provides typed access to columns for query building.
pub struct {cols_struct} {{"
)?;
for element in &product_def.elements {
let field_name = &element.0.deref().to_case(Case::Snake);
let field_type = type_name(module, &element.1);
writeln!(
out,
" pub {field_name}: __sdk::__query_builder::Col<{struct_name}, {field_type}>,"
)?;
}
writeln!(out, "}}")?;
writeln!(
out,
"
impl __sdk::__query_builder::HasCols for {struct_name} {{
type Cols = {cols_struct};
fn cols(table_name: &'static str) -> Self::Cols {{
{cols_struct} {{"
)?;
for element in &product_def.elements {
let field_name = &element.0.deref().to_case(Case::Snake);
writeln!(
out,
" {field_name}: __sdk::__query_builder::Col::new(table_name, {field_name:?}),"
)?;
}
writeln!(
out,
r#"
}}
}}
}}"#
)
}
/// Implements `HasCols` and `HasIxCols` for the given table's row struct type.
fn implement_query_col_types_for_table_struct(
module: &ModuleDef,
out: &mut impl Write,
table: &TableDef,
) -> fmt::Result {
let type_ref = table.product_type_ref;
let struct_name = type_ref_name(module, type_ref);
implement_query_col_types_for_struct(module, out, type_ref)?;
let cols_ix = struct_name.clone() + "IxCols";
writeln!(
out,
"
/// Indexed column accessor struct for the table `{struct_name}`.
///
/// Provides typed access to indexed columns for query building.
pub struct {cols_ix} {{"
)?;
for index in iter_indexes(table) {
let cols = index.algorithm.columns();
if cols.len() != 1 {
continue;
}
let column = table
.columns
.iter()
.find(|col| col.col_id == cols.as_singleton().expect("singleton column"))
.unwrap();
let field_name = column.accessor_name.deref().to_case(Case::Snake);
let field_type = type_name(module, &column.ty_for_generate);
writeln!(
out,
" pub {field_name}: __sdk::__query_builder::IxCol<{struct_name}, {field_type}>,",
)?;
}
writeln!(out, "}}")?;
writeln!(
out,
"
impl __sdk::__query_builder::HasIxCols for {struct_name} {{
type IxCols = {cols_ix};
fn ix_cols(table_name: &'static str) -> Self::IxCols {{
{cols_ix} {{"
)?;
for index in iter_indexes(table) {
let cols = index.algorithm.columns();
if cols.len() != 1 {
continue;
}
let column = table
.columns
.iter()
.find(|col| col.col_id == cols.as_singleton().expect("singleton column"))
.expect("singleton column");
let field_name = column.accessor_name.deref().to_case(Case::Snake);
let col_name = column.name.deref();
writeln!(
out,
" {field_name}: __sdk::__query_builder::IxCol::new(table_name, {col_name:?}),",
)?;
}
writeln!(
out,
r#"
}}
}}
}}"#
)?;
// Event tables cannot be used as lookup tables in semijoins.
if !table.is_event {
writeln!(
out,
"\nimpl __sdk::__query_builder::CanBeLookupTable for {struct_name} {{}}"
)?;
}
Ok(())
}
pub fn implement_query_table_accessor(table: &TableDef, out: &mut impl Write, struct_name: &String) -> fmt::Result {
// NEW: Generate query table accessor trait and implementation
let accessor_method = table_method_name(&table.accessor_name);
let table_name = table.name.deref();
let query_accessor_trait = accessor_method.to_string() + "QueryTableAccess";
writeln!(
out,
"
#[allow(non_camel_case_types)]
/// Extension trait for query builder access to the table `{struct_name}`.
///
/// Implemented for [`__sdk::QueryTableAccessor`].
pub trait {query_accessor_trait} {{
#[allow(non_snake_case)]
/// Get a query builder for the table `{struct_name}`.
fn {accessor_method}(&self) -> __sdk::__query_builder::Table<{struct_name}>;
}}
impl {query_accessor_trait} for __sdk::QueryTableAccessor {{
fn {accessor_method}(&self) -> __sdk::__query_builder::Table<{struct_name}> {{
__sdk::__query_builder::Table::new({table_name:?})
}}
}}
"
)
}
pub fn write_type<W: Write>(module: &ModuleDef, out: &mut W, ty: &AlgebraicTypeUse) -> fmt::Result {
match ty {
AlgebraicTypeUse::Unit => write!(out, "()")?,
AlgebraicTypeUse::Never => write!(out, "std::convert::Infallible")?,
AlgebraicTypeUse::Identity => write!(out, "__sdk::Identity")?,
AlgebraicTypeUse::ConnectionId => write!(out, "__sdk::ConnectionId")?,
AlgebraicTypeUse::Timestamp => write!(out, "__sdk::Timestamp")?,
AlgebraicTypeUse::TimeDuration => write!(out, "__sdk::TimeDuration")?,
AlgebraicTypeUse::Uuid => write!(out, "__sdk::Uuid")?,
AlgebraicTypeUse::ScheduleAt => write!(out, "__sdk::ScheduleAt")?,
AlgebraicTypeUse::Option(inner_ty) => {
write!(out, "Option::<")?;
write_type(module, out, inner_ty)?;
write!(out, ">")?;
}
AlgebraicTypeUse::Result { ok_ty, err_ty } => {
write!(out, "Result::<")?;
write_type(module, out, ok_ty)?;
write!(out, ", ")?;
write_type(module, out, err_ty)?;
write!(out, ">")?;
}
AlgebraicTypeUse::Primitive(prim) => match prim {
PrimitiveType::Bool => write!(out, "bool")?,
PrimitiveType::I8 => write!(out, "i8")?,
PrimitiveType::U8 => write!(out, "u8")?,
PrimitiveType::I16 => write!(out, "i16")?,
PrimitiveType::U16 => write!(out, "u16")?,
PrimitiveType::I32 => write!(out, "i32")?,
PrimitiveType::U32 => write!(out, "u32")?,
PrimitiveType::I64 => write!(out, "i64")?,
PrimitiveType::U64 => write!(out, "u64")?,
PrimitiveType::I128 => write!(out, "i128")?,
PrimitiveType::U128 => write!(out, "u128")?,
PrimitiveType::I256 => write!(out, "__sats::i256")?,
PrimitiveType::U256 => write!(out, "__sats::u256")?,
PrimitiveType::F32 => write!(out, "f32")?,
PrimitiveType::F64 => write!(out, "f64")?,
},
AlgebraicTypeUse::String => write!(out, "String")?,
AlgebraicTypeUse::Array(elem_ty) => {
write!(out, "Vec::<")?;
write_type(module, out, elem_ty)?;
write!(out, ">")?;
}
AlgebraicTypeUse::Ref(r) => {
write!(out, "{}", type_ref_name(module, *r))?;
}
}
Ok(())
}
pub fn type_name(module: &ModuleDef, ty: &AlgebraicTypeUse) -> String {
let mut s = String::new();
write_type(module, &mut s, ty).unwrap();
s
}
/// Arguments to a reducer or procedure pretty-printed in various ways that are convenient to compute together.
struct FormattedArglist {
/// The arguments as `ident: ty, ident: ty, ident: ty,`,
/// like an argument list.
///
/// Always carries a trailing comma, unless it's zero elements.
arglist_no_delimiters: String,
/// The argument names as `ident, ident, ident,`,
/// for passing to function call and struct literal expressions.
///
/// Always carries a trailing comma, unless it's zero elements.
arg_names: String,
}
impl FormattedArglist {
fn for_arguments(module: &ModuleDef, params: &[(Identifier, AlgebraicTypeUse)]) -> Self {
let mut arglist_no_delimiters = String::new();
write_arglist_no_delimiters(module, &mut arglist_no_delimiters, params, None)
.expect("Writing to a String failed... huh?");
let mut arg_names = String::new();
for (arg_ident, _) in params {
let arg_name = arg_ident.deref().to_case(Case::Snake);
arg_names += &arg_name;
arg_names += ", ";
}
Self {
arglist_no_delimiters,
arg_names,
}
}
}
const ALLOW_LINTS: &str = "#![allow(unused, clippy::all)]";
const SPACETIMEDB_IMPORTS: &[&str] = &[
"use spacetimedb_sdk::__codegen::{",
"\tself as __sdk,",
"\t__lib,",
"\t__sats,",
"\t__ws,",
"};",
];
fn print_spacetimedb_imports(output: &mut Indenter) {
print_lines(output, SPACETIMEDB_IMPORTS);
}
fn print_file_header(output: &mut Indenter, include_version: bool) {
print_auto_generated_file_comment(output);
if include_version {
print_auto_generated_version_comment(output);
}
writeln!(output, "{ALLOW_LINTS}");
print_spacetimedb_imports(output);
}
// TODO: figure out if/when sum types should derive:
// - Clone
// - Debug
// - Copy
// - PartialEq, Eq
// - Hash
// - Complicated because `HashMap` is not `Hash`.
// - others?
const ENUM_DERIVES: &[&str] = &[
"#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]",
"#[sats(crate = __lib)]",
];
fn print_enum_derives(output: &mut Indenter) {
print_lines(output, ENUM_DERIVES);
}
const PLAIN_ENUM_EXTRA_DERIVES: &[&str] = &["#[derive(Copy, Eq, Hash)]"];
fn print_plain_enum_extra_derives(output: &mut Indenter) {
print_lines(output, PLAIN_ENUM_EXTRA_DERIVES);
}
/// Generate a file which defines an `enum` corresponding to the `sum_type`.
pub fn define_enum_for_sum(
module: &ModuleDef,
out: &mut Indenter,
name: &str,
variants: &[(Identifier, AlgebraicTypeUse)],
is_plain: bool,
) {
print_enum_derives(out);
if is_plain {
print_plain_enum_extra_derives(out);
}
write!(out, "pub enum {name} ");
out.delimited_block(
"{",
|out| {
for (ident, ty) in variants {
write_enum_variant(module, out, ident, ty);
out.newline();
}
},
"}\n",
);
out.newline()
}
fn write_enum_variant(module: &ModuleDef, out: &mut Indenter, ident: &Identifier, ty: &AlgebraicTypeUse) {
let name = ident.deref().to_case(Case::Pascal);
write!(out, "{name}");
// If the contained type is the unit type, i.e. this variant has no members,
// write it without parens or braces, like
// ```
// Foo,
// ```
if !matches!(ty, AlgebraicTypeUse::Unit) {
// If the contained type is not a product, i.e. this variant has a single
// member, write it tuple-style, with parens.
write!(out, "(");
write_type(module, out, ty).unwrap();
write!(out, ")");
}
writeln!(out, ",");
}
fn write_struct_type_fields_in_braces(
module: &ModuleDef,
out: &mut Indenter,
elements: &[(Identifier, AlgebraicTypeUse)],
// Whether to print a `pub` qualifier on the fields. Necessary for `struct` defns,
// disallowed for `enum` defns.
pub_qualifier: bool,
) {
out.delimited_block(
"{",
|out| write_arglist_no_delimiters(module, out, elements, pub_qualifier.then_some("pub")).unwrap(),
"}",
);
}
fn write_arglist_no_delimiters(
module: &ModuleDef,
out: &mut impl Write,
elements: &[(Identifier, AlgebraicTypeUse)],
// Written before each line. Useful for `pub`.
prefix: Option<&str>,
) -> anyhow::Result<()> {
for (ident, ty) in elements {
if let Some(prefix) = prefix {
write!(out, "{prefix} ")?;
}
let name = ident.deref().to_case(Case::Snake);
write!(out, "{name}: ")?;
write_type(module, out, ty)?;
writeln!(out, ",")?;
}
Ok(())
}
// TODO: figure out if/when product types should derive:
// - Clone
// - Debug
// - Copy
// - PartialEq, Eq
// - Hash
// - Complicated because `HashMap` is not `Hash`.
// - others?
const STRUCT_DERIVES: &[&str] = &[
"#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]",
"#[sats(crate = __lib)]",
];
fn print_struct_derives(output: &mut Indenter) {
print_lines(output, STRUCT_DERIVES);
}
fn define_struct_for_product(
module: &ModuleDef,
out: &mut Indenter,
name: &str,
elements: &[(Identifier, AlgebraicTypeUse)],
vis: &str,
) {
print_struct_derives(out);
write!(out, "{vis} struct {name} ");
// TODO: if elements is empty, define a unit struct with no brace-delimited list of fields.
write_struct_type_fields_in_braces(
module, out, elements, true, // `pub`-qualify fields.
);
out.newline();
}
fn type_ref_module_name(module: &ModuleDef, type_ref: AlgebraicTypeRef) -> String {
let (name, _) = module.type_def_from_ref(type_ref).unwrap();
type_module_name(name)
}
fn type_module_name(type_name: &ScopedTypeName) -> String {
collect_case(Case::Snake, type_name.name_segments()) + "_type"
}
fn table_module_name(table_name: &Identifier) -> String {
table_name.deref().to_case(Case::Snake) + "_table"
}
fn table_method_name(table_name: &Identifier) -> String {
table_name.deref().to_case(Case::Snake)
}
fn table_access_trait_name(table_name: &Identifier) -> String {
table_name.deref().to_case(Case::Pascal) + "TableAccess"
}
fn function_args_type_name(function_name: &str) -> String {
function_name.to_case(Case::Pascal) + "Args"
}
fn reducer_variant_name(reducer_name: &ReducerName) -> String {
reducer_name.deref().to_case(Case::Pascal)
}
fn reducer_module_name(reducer_name: &ReducerName) -> String {
reducer_name.deref().to_case(Case::Snake) + "_reducer"
}
fn reducer_function_name(reducer: &ReducerDef) -> String {
reducer.accessor_name.deref().to_case(Case::Snake)
}
fn procedure_module_name(procedure_name: &Identifier) -> String {
procedure_name.deref().to_case(Case::Snake) + "_procedure"
}
fn procedure_function_name(procedure: &ProcedureDef) -> String {
procedure.accessor_name.deref().to_case(Case::Snake)
}
fn procedure_function_with_callback_name(procedure: &ProcedureDef) -> String {
procedure_function_name(procedure) + "_then"
}
/// Iterate over all of the Rust `mod`s for types, reducers, views, and tables in the `module`.
fn iter_module_names(module: &ModuleDef, visibility: CodegenVisibility) -> impl Iterator<Item = String> + '_ {
itertools::chain!(
iter_types(module).map(|ty| type_module_name(&ty.accessor_name)),
iter_reducers(module, visibility).map(|r| reducer_module_name(&r.accessor_name)),
iter_tables(module, visibility).map(|tbl| table_module_name(&tbl.accessor_name)),
iter_views(module).map(|view| table_module_name(&view.accessor_name)),
iter_procedures(module, visibility).map(|proc| procedure_module_name(&proc.accessor_name)),
)
}
/// Print `pub mod` declarations for all the files that will be generated for `items`.
fn print_module_decls(module: &ModuleDef, visibility: CodegenVisibility, out: &mut Indenter) {
for module_name in iter_module_names(module, visibility) {
writeln!(out, "pub mod {module_name};");
}
}
/// Print appropriate reexports for all the files that will be generated for `items`.
fn print_module_reexports(module: &ModuleDef, visibility: CodegenVisibility, out: &mut Indenter) {
for ty in iter_types(module) {
let mod_name = type_module_name(&ty.accessor_name);
let type_name = collect_case(Case::Pascal, ty.accessor_name.name_segments());
writeln!(out, "pub use {mod_name}::{type_name};")
}
for (_, accessor_name, _) in iter_table_names_and_types(module, visibility) {
let mod_name = table_module_name(accessor_name);
// TODO: More precise reexport: we want:
// - The trait name.
// - The insert, delete and possibly update callback ids.
// We do not want:
// - The table handle.
writeln!(out, "pub use {mod_name}::*;");
}
for reducer in iter_reducers(module, visibility) {
let mod_name = reducer_module_name(&reducer.accessor_name);
let reducer_trait_name = reducer_function_name(reducer);
writeln!(out, "pub use {mod_name}::{reducer_trait_name};");
}
for procedure in iter_procedures(module, visibility) {
let mod_name = procedure_module_name(&procedure.accessor_name);
let trait_name = procedure_function_name(procedure);
writeln!(out, "pub use {mod_name}::{trait_name};");
}
}
fn print_reducer_enum_defn(module: &ModuleDef, visibility: CodegenVisibility, out: &mut Indenter) {
// Don't derive ser/de on this enum;
// it's not a proper SATS enum and the derive will fail.
writeln!(out, "#[derive(Clone, PartialEq, Debug)]");
writeln!(
out,
"
/// One of the reducers defined by this module.
///
/// Contained within a [`__sdk::ReducerEvent`] in [`EventContext`]s for reducer events
/// to indicate which reducer caused the event.
",
);
out.delimited_block(
"pub enum Reducer {",
|out| {
for reducer in iter_reducers(module, visibility) {
write!(out, "{} ", reducer_variant_name(&reducer.accessor_name));
if !reducer.params_for_generate.elements.is_empty() {
// If the reducer has any arguments, generate a "struct variant,"
// like `Foo { bar: Baz, }`.
// If it doesn't, generate a "unit variant" instead,
// like `Foo,`.
write_struct_type_fields_in_braces(module, out, &reducer.params_for_generate.elements, false);
}
writeln!(out, ",");
}
},
"}\n",
);
out.newline();
writeln!(
out,
"
impl __sdk::InModule for Reducer {{
type Module = RemoteModule;
}}
",
);
out.delimited_block(
"impl __sdk::Reducer for Reducer {",
|out| {
out.delimited_block(
"fn reducer_name(&self) -> &'static str {",
|out| {
out.delimited_block(
"match self {",
|out| {
for reducer in iter_reducers(module, visibility) {
write!(out, "Reducer::{}", reducer_variant_name(&reducer.accessor_name));
if !reducer.params_for_generate.elements.is_empty() {
// Because we're emitting unit variants when the payload is empty,
// we will emit different patterns for empty vs non-empty variants.
// This is not strictly required;
// Rust allows matching a struct-like pattern
// against a unit-like enum variant,
// but we prefer the clarity of not including the braces for unit variants.
write!(out, " {{ .. }}");
}
writeln!(out, " => {:?},", reducer.name.deref());
}
// Write a catch-all pattern to handle the case where the module defines zero reducers,
// 'cause references are always considered inhabited,
// even references to uninhabited types.
writeln!(out, "_ => unreachable!(),");
},
"}\n",
);
},
"}\n",
);
writeln!(out, "#[allow(clippy::clone_on_copy)]");
out.delimited_block(
"fn args_bsatn(&self) -> Result<Vec<u8>, __sats::bsatn::EncodeError> {",
|out| {
out.delimited_block(
"match self {",
|out| {
for reducer in iter_reducers(module, visibility) {
write!(out, "Reducer::{}", reducer_variant_name(&reducer.accessor_name));
if !reducer.params_for_generate.elements.is_empty() {
// Because we're emitting unit variants when the payload is empty,
// we will emit different patterns for empty vs non-empty variants.
// This is not strictly required;
// Rust allows matching a struct-like pattern
// against a unit-like enum variant,
// but we prefer the clarity of not including the braces for unit variants.
out.delimited_block(
"{",
|out| {
for (ident, _) in &reducer.params_for_generate.elements {
writeln!(out, "{},", ident.deref().to_case(Case::Snake));
}
},
"}",
);
}
write!(
out,
" => __sats::bsatn::to_vec(&{}::{}",
reducer_module_name(&reducer.accessor_name),
function_args_type_name(&reducer.accessor_name)
);
out.delimited_block(
" {",
|out| {
for (ident, _) in &reducer.params_for_generate.elements {
let field = ident.deref().to_case(Case::Snake);
writeln!(out, "{field}: {field}.clone(),");
}
},
"}),\n",
);
}
// Write a catch-all pattern to handle the case where the module defines zero reducers,
// 'cause references are always considered inhabited,
// even references to uninhabited types.
writeln!(out, "_ => unreachable!(),");
},
"}\n",
);
},
"}\n",
);
},
"}\n",
);
}
fn print_db_update_defn(module: &ModuleDef, visibility: CodegenVisibility, out: &mut Indenter) {
writeln!(out, "#[derive(Default, Debug)]");
writeln!(out, "#[allow(non_snake_case)]");
writeln!(out, "#[doc(hidden)]");
out.delimited_block(
"pub struct DbUpdate {",
|out| {
for (_, accessor_name, product_type_ref) in iter_table_names_and_types(module, visibility) {
writeln!(
out,
"{}: __sdk::TableUpdate<{}>,",
table_method_name(accessor_name),
type_ref_name(module, product_type_ref),
);
}
},
"}\n",
);
out.newline();
out.delimited_block(
"
impl TryFrom<__ws::v2::TransactionUpdate> for DbUpdate {
type Error = __sdk::Error;
fn try_from(raw: __ws::v2::TransactionUpdate) -> Result<Self, Self::Error> {
let mut db_update = DbUpdate::default();
for table_update in __sdk::transaction_update_iter_table_updates(raw) {
match &table_update.table_name[..] {
",
|out| {
for (name, accessor_name, _) in iter_table_names_and_types(module, visibility) {
writeln!(
out,
"{:?} => db_update.{}.append({}::parse_table_update(table_update)?),",
name.deref(),
table_method_name(accessor_name),
table_module_name(accessor_name),
);
}
},
"
unknown => {
return Err(__sdk::InternalError::unknown_name(
\"table\",
unknown,
\"DatabaseUpdate\",
).into());
}
}
}
Ok(db_update)
}
}",
);
out.newline();
writeln!(
out,
"
impl __sdk::InModule for DbUpdate {{
type Module = RemoteModule;
}}
",
);
out.delimited_block(
"impl __sdk::DbUpdate for DbUpdate {",
|out| {
out.delimited_block(
"fn apply_to_client_cache(&self, cache: &mut __sdk::ClientCache<RemoteModule>) -> AppliedDiff<'_> {
let mut diff = AppliedDiff::default();
",
|out| {
for table in iter_tables(module, visibility) {
let field_name = table_method_name(&table.accessor_name);
if table.is_event {
// Event tables bypass the client cache entirely.
// We construct an applied diff directly from the inserts,
// which will fire on_insert callbacks without storing rows.
writeln!(
out,
"diff.{field_name} = self.{field_name}.into_event_diff();",
);
} else {
let with_updates = table
.primary_key
.map(|col| {
let pk_field = table.get_column(col).unwrap().accessor_name.deref().to_case(Case::Snake);
format!(".with_updates_by_pk(|row| &row.{pk_field})")
})
.unwrap_or_default();
writeln!(
out,
"diff.{field_name} = cache.apply_diff_to_table::<{}>({:?}, &self.{field_name}){with_updates};",
type_ref_name(module, table.product_type_ref),
table.name.deref(),
);
}
}
for view in iter_views(module) {
let field_name = table_method_name(&view.accessor_name);
let with_updates = view
.primary_key
.map(|col| {
let pk_field = view.return_columns[col.idx()]
.accessor_name
.deref()
.to_case(Case::Snake);
format!(".with_updates_by_pk(|row| &row.{pk_field})")
})
.unwrap_or_default();
writeln!(
out,
"diff.{field_name} = cache.apply_diff_to_table::<{}>({:?}, &self.{field_name}){with_updates};",
type_ref_name(module, view.product_type_ref),
view.name.deref(),
);
}
},
"
diff
}\n",
);
out.delimited_block(
"fn parse_initial_rows(raw: __ws::v2::QueryRows) -> __sdk::Result<Self> {",
|out| {
writeln!(out, "let mut db_update = DbUpdate::default();");
out.delimited_block(
"for table_rows in raw.tables {",
|out| {
out.delimited_block(
"match &table_rows.table[..] {",
|out| {
for (name, accessor_name, _) in iter_table_names_and_types(module, visibility) {
writeln!(
out,
"{:?} => db_update.{}.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),",
name.deref(),
table_method_name(accessor_name),
);
}
writeln!(
out,
"unknown => {{ return Err(__sdk::InternalError::unknown_name(\"table\", unknown, \"QueryRows\").into()); }}"
);
},
"}",
);
},
"}",
);
writeln!(out, "Ok(db_update)");
},
"}\n",
);
out.delimited_block(
"fn parse_unsubscribe_rows(raw: __ws::v2::QueryRows) -> __sdk::Result<Self> {",
|out| {
writeln!(out, "let mut db_update = DbUpdate::default();");
out.delimited_block(
"for table_rows in raw.tables {",
|out| {
out.delimited_block(
"match &table_rows.table[..] {",
|out| {
for (name, accessor_name, _) in iter_table_names_and_types(module, visibility) {
writeln!(
out,
"{:?} => db_update.{}.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),",
name.deref(),
table_method_name(accessor_name),
);
}
writeln!(
out,
"unknown => {{ return Err(__sdk::InternalError::unknown_name(\"table\", unknown, \"QueryRows\").into()); }}"
);
},
"}",
);
},
"}",
);
writeln!(out, "Ok(db_update)");
},
"}\n",
);
},
"}\n",
);
}
fn print_applied_diff_defn(module: &ModuleDef, visibility: CodegenVisibility, out: &mut Indenter) {
writeln!(out, "#[derive(Default)]");
writeln!(out, "#[allow(non_snake_case)]");
writeln!(out, "#[doc(hidden)]");
out.delimited_block(
"pub struct AppliedDiff<'r> {",
|out| {
for (_, accessor_name, product_type_ref) in iter_table_names_and_types(module, visibility) {
writeln!(
out,
"{}: __sdk::TableAppliedDiff<'r, {}>,",
table_method_name(accessor_name),
type_ref_name(module, product_type_ref),
);
}
// Also write a `PhantomData` field which uses the lifetime `r`,
// in case the module defines zero tables,
// as unused lifetime params are an error.
writeln!(out, "__unused: std::marker::PhantomData<&'r ()>,",);
},
"}\n",
);
out.newline();
writeln!(
out,
"
impl __sdk::InModule for AppliedDiff<'_> {{
type Module = RemoteModule;
}}
",
);
out.delimited_block(
"impl<'r> __sdk::AppliedDiff<'r> for AppliedDiff<'r> {",
|out| {
out.delimited_block(
"fn invoke_row_callbacks(&self, event: &EventContext, callbacks: &mut __sdk::DbCallbacks<RemoteModule>) {",
|out| {
for (name, accessor_name, product_type_ref) in iter_table_names_and_types(module, visibility) {
writeln!(
out,
"callbacks.invoke_table_row_callbacks::<{}>({:?}, &self.{}, event);",
type_ref_name(module, product_type_ref),
name.deref(),
table_method_name(accessor_name),
);
}
},
"}\n",
);
},
"}\n",
);
}
fn print_impl_spacetime_module(module: &ModuleDef, visibility: CodegenVisibility, out: &mut Indenter) {
out.delimited_block(
"impl __sdk::SpacetimeModule for RemoteModule {",
|out| {
writeln!(
out,
"
type DbConnection = DbConnection;
type EventContext = EventContext;
type ReducerEventContext = ReducerEventContext;
type ProcedureEventContext = ProcedureEventContext;
type SubscriptionEventContext = SubscriptionEventContext;
type ErrorContext = ErrorContext;
type Reducer = Reducer;
type DbView = RemoteTables;
type Reducers = RemoteReducers;
type Procedures = RemoteProcedures;
type DbUpdate = DbUpdate;
type AppliedDiff<'r> = AppliedDiff<'r>;
type SubscriptionHandle = SubscriptionHandle;
type QueryBuilder = __sdk::QueryBuilder;
"
);
out.delimited_block(
"fn register_tables(client_cache: &mut __sdk::ClientCache<Self>) {",
|out| {
for (_, accessor_name, _) in iter_table_names_and_types(module, visibility) {
writeln!(
out,
"{}::register_table(client_cache);",
table_module_name(accessor_name)
);
}
},
"}\n",
);
out.delimited_block(
"const ALL_TABLE_NAMES: &'static [&'static str] = &[",
|out| {
for (name, _, _) in iter_table_names_and_types(module, visibility) {
writeln!(out, "\"{name}\",");
}
},
"];\n",
);
},
"}\n",
);
}
fn print_const_db_context_types(out: &mut Indenter) {
writeln!(
out,
"
#[doc(hidden)]
#[derive(Debug)]
pub struct RemoteModule;
impl __sdk::InModule for RemoteModule {{
type Module = Self;
}}
/// The `reducers` field of [`EventContext`] and [`DbConnection`],
/// with methods provided by extension traits for each reducer defined by the module.
pub struct RemoteReducers {{
imp: __sdk::DbContextImpl<RemoteModule>,
}}
impl __sdk::InModule for RemoteReducers {{
type Module = RemoteModule;
}}
/// The `procedures` field of [`DbConnection`] and other [`DbContext`] types,
/// with methods provided by extension traits for each procedure defined by the module.
pub struct RemoteProcedures {{
imp: __sdk::DbContextImpl<RemoteModule>,
}}
impl __sdk::InModule for RemoteProcedures {{
type Module = RemoteModule;
}}
/// The `db` field of [`EventContext`] and [`DbConnection`],
/// with methods provided by extension traits for each table defined by the module.
pub struct RemoteTables {{
imp: __sdk::DbContextImpl<RemoteModule>,
}}
impl __sdk::InModule for RemoteTables {{
type Module = RemoteModule;
}}
/// A connection to a remote module, including a materialized view of a subset of the database.
///
/// Connect to a remote module by calling [`DbConnection::builder`]
/// and using the [`__sdk::DbConnectionBuilder`] builder-pattern constructor.
///
/// You must explicitly advance the connection by calling any one of:
///
/// - [`DbConnection::frame_tick`].
#[cfg_attr(not(target_arch = \"wasm32\"), doc = \"- [`DbConnection::run_threaded`].\")]
#[cfg_attr(target_arch = \"wasm32\", doc = \"- [`DbConnection::run_background_task`].\")]
/// - [`DbConnection::run_async`].
/// - [`DbConnection::advance_one_message`].
#[cfg_attr(not(target_arch = \"wasm32\"), doc = \"- [`DbConnection::advance_one_message_blocking`].\")]
/// - [`DbConnection::advance_one_message_async`].
///
/// Which of these methods you should call depends on the specific needs of your application,
/// but you must call one of them, or else the connection will never progress.
pub struct DbConnection {{
/// Access to tables defined by the module via extension traits implemented for [`RemoteTables`].
pub db: RemoteTables,
/// Access to reducers defined by the module via extension traits implemented for [`RemoteReducers`].
pub reducers: RemoteReducers,
#[doc(hidden)]
/// Access to procedures defined by the module via extension traits implemented for [`RemoteProcedures`].
pub procedures: RemoteProcedures,
imp: __sdk::DbContextImpl<RemoteModule>,
}}
impl __sdk::InModule for DbConnection {{
type Module = RemoteModule;
}}
impl __sdk::DbContext for DbConnection {{
type DbView = RemoteTables;
type Reducers = RemoteReducers;
type Procedures = RemoteProcedures;
fn db(&self) -> &Self::DbView {{
&self.db
}}
fn reducers(&self) -> &Self::Reducers {{
&self.reducers
}}
fn procedures(&self) -> &Self::Procedures {{
&self.procedures
}}
fn is_active(&self) -> bool {{
self.imp.is_active()
}}
fn disconnect(&self) -> __sdk::Result<()> {{
self.imp.disconnect()
}}
type SubscriptionBuilder = __sdk::SubscriptionBuilder<RemoteModule>;
fn subscription_builder(&self) -> Self::SubscriptionBuilder {{
__sdk::SubscriptionBuilder::new(&self.imp)
}}
fn try_identity(&self) -> Option<__sdk::Identity> {{
self.imp.try_identity()
}}
fn connection_id(&self) -> __sdk::ConnectionId {{
self.imp.connection_id()
}}
fn try_connection_id(&self) -> Option<__sdk::ConnectionId> {{
self.imp.try_connection_id()
}}
}}
impl DbConnection {{
/// Builder-pattern constructor for a connection to a remote module.
///
/// See [`__sdk::DbConnectionBuilder`] for required and optional configuration for the new connection.
pub fn builder() -> __sdk::DbConnectionBuilder<RemoteModule> {{
__sdk::DbConnectionBuilder::new()
}}
/// If any WebSocket messages are waiting, process one of them.
///
/// Returns `true` if a message was processed, or `false` if the queue is empty.
/// Callers should invoke this message in a loop until it returns `false`
/// or for as much time is available to process messages.
///
/// Returns an error if the connection is disconnected.
/// If the disconnection in question was normal,
/// i.e. the result of a call to [`__sdk::DbContext::disconnect`],
/// the returned error will be downcastable to [`__sdk::DisconnectedError`].
///
/// This is a low-level primitive exposed for power users who need significant control over scheduling.
/// Most applications should call [`Self::frame_tick`] each frame
/// to fully exhaust the queue whenever time is available.
pub fn advance_one_message(&self) -> __sdk::Result<bool> {{
self.imp.advance_one_message()
}}
/// Process one WebSocket message, potentially blocking the current thread until one is received.
///
/// Returns an error if the connection is disconnected.
/// If the disconnection in question was normal,
/// i.e. the result of a call to [`__sdk::DbContext::disconnect`],
/// the returned error will be downcastable to [`__sdk::DisconnectedError`].
///
/// This is a low-level primitive exposed for power users who need significant control over scheduling.
/// Most applications should call [`Self::run_threaded`] to spawn a thread
/// which advances the connection automatically.
#[cfg(not(target_arch = \"wasm32\"))]
pub fn advance_one_message_blocking(&self) -> __sdk::Result<()> {{
self.imp.advance_one_message_blocking()
}}
/// Process one WebSocket message, `await`ing until one is received.
///
/// Returns an error if the connection is disconnected.
/// If the disconnection in question was normal,
/// i.e. the result of a call to [`__sdk::DbContext::disconnect`],
/// the returned error will be downcastable to [`__sdk::DisconnectedError`].
///
/// This is a low-level primitive exposed for power users who need significant control over scheduling.
/// Most applications should call [`Self::run_async`] to run an `async` loop
/// which advances the connection when polled.
pub async fn advance_one_message_async(&self) -> __sdk::Result<()> {{
self.imp.advance_one_message_async().await
}}
/// Process all WebSocket messages waiting in the queue,
/// then return without `await`ing or blocking the current thread.
pub fn frame_tick(&self) -> __sdk::Result<()> {{
self.imp.frame_tick()
}}
/// Spawn a thread which processes WebSocket messages as they are received.
#[cfg(not(target_arch = \"wasm32\"))]
pub fn run_threaded(&self) -> std::thread::JoinHandle<()> {{
self.imp.run_threaded()
}}
/// Spawn a background task which processes WebSocket messages as they are received.
#[cfg(target_arch = \"wasm32\")]
pub fn run_background_task(&self) {{
self.imp.run_background_task()
}}
/// Run an `async` loop which processes WebSocket messages when polled.
pub async fn run_async(&self) -> __sdk::Result<()> {{
self.imp.run_async().await
}}
}}
impl __sdk::DbConnection for DbConnection {{
fn new(imp: __sdk::DbContextImpl<RemoteModule>) -> Self {{
Self {{
db: RemoteTables {{ imp: imp.clone() }},
reducers: RemoteReducers {{ imp: imp.clone() }},
procedures: RemoteProcedures {{ imp: imp.clone() }},
imp,
}}
}}
}}
/// A handle on a subscribed query.
// TODO: Document this better after implementing the new subscription API.
#[derive(Clone)]
pub struct SubscriptionHandle {{
imp: __sdk::SubscriptionHandleImpl<RemoteModule>,
}}
impl __sdk::InModule for SubscriptionHandle {{
type Module = RemoteModule;
}}
impl __sdk::SubscriptionHandle for SubscriptionHandle {{
fn new(imp: __sdk::SubscriptionHandleImpl<RemoteModule>) -> Self {{
Self {{ imp }}
}}
/// Returns true if this subscription has been terminated due to an unsubscribe call or an error.
fn is_ended(&self) -> bool {{
self.imp.is_ended()
}}
/// Returns true if this subscription has been applied and has not yet been unsubscribed.
fn is_active(&self) -> bool {{
self.imp.is_active()
}}
/// Unsubscribe from the query controlled by this `SubscriptionHandle`,
/// then run `on_end` when its rows are removed from the client cache.
fn unsubscribe_then(self, on_end: __sdk::OnEndedCallback<RemoteModule>) -> __sdk::Result<()> {{
self.imp.unsubscribe_then(Some(on_end))
}}
fn unsubscribe(self) -> __sdk::Result<()> {{
self.imp.unsubscribe_then(None)
}}
}}
/// Alias trait for a [`__sdk::DbContext`] connected to this module,
/// with that trait's associated types bounded to this module's concrete types.
///
/// Users can use this trait as a boundary on definitions which should accept
/// either a [`DbConnection`] or an [`EventContext`] and operate on either.
pub trait RemoteDbContext: __sdk::DbContext<
DbView = RemoteTables,
Reducers = RemoteReducers,
SubscriptionBuilder = __sdk::SubscriptionBuilder<RemoteModule>,
> {{}}
impl<Ctx: __sdk::DbContext<
DbView = RemoteTables,
Reducers = RemoteReducers,
SubscriptionBuilder = __sdk::SubscriptionBuilder<RemoteModule>,
>> RemoteDbContext for Ctx {{}}
",
);
define_event_context(
out,
"EventContext",
Some("__sdk::Event<Reducer>"),
"[`__sdk::Table::on_insert`], [`__sdk::Table::on_delete`] and [`__sdk::TableWithPrimaryKey::on_update`] callbacks",
Some("[`__sdk::Event`]"),
);
define_event_context(
out,
"ReducerEventContext",
Some("__sdk::ReducerEvent<Reducer>"),
"on-reducer callbacks", // There's no single trait or method for reducer callbacks, so we can't usefully link to them.
Some("[`__sdk::ReducerEvent`]"),
);
define_event_context(
out,
"ProcedureEventContext",
None, // ProcedureEventContexts have no additional `event` info, so they don't even get that field.
"procedure callbacks", // There's no single trait or method for procedure callbacks, so we can't usefully link to them.
None,
);
define_event_context(
out,
"SubscriptionEventContext",
None, // SubscriptionEventContexts have no additional `event` info, so they don't even get that field.
"[`__sdk::SubscriptionBuilder::on_applied`] and [`SubscriptionHandle::unsubscribe_then`] callbacks",
None,
);
define_event_context(
out,
"ErrorContext",
Some("Option<__sdk::Error>"),
"[`__sdk::DbConnectionBuilder::on_disconnect`], [`__sdk::DbConnectionBuilder::on_connect_error`] and [`__sdk::SubscriptionBuilder::on_error`] callbacks",
Some("[`__sdk::Error`]"),
);
}
/// Define a type that implements `AbstractEventContext` and one of its concrete subtraits.
///
/// `struct_and_trait_name` should be the name of an event context trait,
/// and will also be used as the new struct's name.
///
/// `event_type`, if `Some`, should be a Rust type which will be the type of the new struct's `event` field.
/// If `None`, the new struct will not have such a field.
/// The `SubscriptionEventContext` will pass `None`, since there is no useful information to add.
///
/// `passed_to_callbacks_doc_link` should be a rustdoc-formatted phrase
/// which links to the callback-registering functions for the callbacks which accept this event context type.
/// It should be of the form "foo callbacks" or "foo, bar and baz callbacks",
/// with link formatting where appropriate, and no trailing punctuation.
///
/// If `event_type` is `Some`, `event_type_doc_link` should be as well.
/// It should be a rustdoc-formatted link (including square brackets and all) to the `event_type`.
/// This may differ (in the `strcmp` sense) from `event_type` because it should not include generic parameters.
fn define_event_context(
out: &mut Indenter,
struct_and_trait_name: &str,
event_type: Option<&str>,
passed_to_callbacks_doc_link: &str,
event_type_doc_link: Option<&str>,
) {
if let (Some(event_type), Some(event_type_doc_link)) = (event_type, event_type_doc_link) {
write!(
out,
"
/// An [`__sdk::DbContext`] augmented with a {event_type_doc_link},
/// passed to {passed_to_callbacks_doc_link}.
pub struct {struct_and_trait_name} {{
/// Access to tables defined by the module via extension traits implemented for [`RemoteTables`].
pub db: RemoteTables,
/// Access to reducers defined by the module via extension traits implemented for [`RemoteReducers`].
pub reducers: RemoteReducers,
/// Access to procedures defined by the module via extension traits implemented for [`RemoteProcedures`].
pub procedures: RemoteProcedures,
/// The event which caused these callbacks to run.
pub event: {event_type},
imp: __sdk::DbContextImpl<RemoteModule>,
}}
impl __sdk::AbstractEventContext for {struct_and_trait_name} {{
type Event = {event_type};
fn event(&self) -> &Self::Event {{
&self.event
}}
fn new(imp: __sdk::DbContextImpl<RemoteModule>, event: Self::Event) -> Self {{
Self {{
db: RemoteTables {{ imp: imp.clone() }},
reducers: RemoteReducers {{ imp: imp.clone() }},
procedures: RemoteProcedures {{ imp: imp.clone() }},
event,
imp,
}}
}}
}}
",
);
} else {
debug_assert!(event_type.is_none() && event_type_doc_link.is_none());
write!(
out,
"
/// An [`__sdk::DbContext`] passed to {passed_to_callbacks_doc_link}.
pub struct {struct_and_trait_name} {{
/// Access to tables defined by the module via extension traits implemented for [`RemoteTables`].
pub db: RemoteTables,
/// Access to reducers defined by the module via extension traits implemented for [`RemoteReducers`].
pub reducers: RemoteReducers,
/// Access to procedures defined by the module via extension traits implemented for [`RemoteProcedures`].
pub procedures: RemoteProcedures,
imp: __sdk::DbContextImpl<RemoteModule>,
}}
impl __sdk::AbstractEventContext for {struct_and_trait_name} {{
type Event = ();
fn event(&self) -> &Self::Event {{
&()
}}
fn new(imp: __sdk::DbContextImpl<RemoteModule>, _event: Self::Event) -> Self {{
Self {{
db: RemoteTables {{ imp: imp.clone() }},
reducers: RemoteReducers {{ imp: imp.clone() }},
procedures: RemoteProcedures {{ imp: imp.clone() }},
imp,
}}
}}
}}
",
);
}
write!(
out,
"
impl __sdk::InModule for {struct_and_trait_name} {{
type Module = RemoteModule;
}}
impl __sdk::DbContext for {struct_and_trait_name} {{
type DbView = RemoteTables;
type Reducers = RemoteReducers;
type Procedures = RemoteProcedures;
fn db(&self) -> &Self::DbView {{
&self.db
}}
fn reducers(&self) -> &Self::Reducers {{
&self.reducers
}}
fn procedures(&self) -> &Self::Procedures {{
&self.procedures
}}
fn is_active(&self) -> bool {{
self.imp.is_active()
}}
fn disconnect(&self) -> __sdk::Result<()> {{
self.imp.disconnect()
}}
type SubscriptionBuilder = __sdk::SubscriptionBuilder<RemoteModule>;
fn subscription_builder(&self) -> Self::SubscriptionBuilder {{
__sdk::SubscriptionBuilder::new(&self.imp)
}}
fn try_identity(&self) -> Option<__sdk::Identity> {{
self.imp.try_identity()
}}
fn connection_id(&self) -> __sdk::ConnectionId {{
self.imp.connection_id()
}}
fn try_connection_id(&self) -> Option<__sdk::ConnectionId> {{
self.imp.try_connection_id()
}}
}}
impl __sdk::{struct_and_trait_name} for {struct_and_trait_name} {{}}
"
);
}
/// Print `use super::` imports for each of the `imports`.
fn print_imports(module: &ModuleDef, out: &mut Indenter, imports: Imports) {
for typeref in imports {
let module_name = type_ref_module_name(module, typeref);
let type_name = type_ref_name(module, typeref);
writeln!(out, "use super::{module_name}::{type_name};");
}
}
fn add_one_import(imports: &mut Imports, import: &AlgebraicTypeUse) {
import.for_each_ref(|r| {
imports.insert(r);
})
}
fn gen_imports(imports: &mut Imports, roots: &[(Identifier, AlgebraicTypeUse)]) {
for (_, ty) in roots {
add_one_import(imports, ty);
}
}
fn remove_skipped_imports(imports: &mut Imports, dont_import: &[AlgebraicTypeRef]) {
for skip in dont_import {
imports.remove(skip);
}
}
/// Use `search_function` on `roots` to detect required imports, then print them with `print_imports`.
///
/// `this_file` is passed and excluded for the case of recursive types:
/// without it, the definition for a type like `struct Foo { foos: Vec<Foo> }`
/// would attempt to include `import super::foo::Foo`, which fails to compile.
fn gen_and_print_imports(
module: &ModuleDef,
out: &mut Indenter,
roots: &[(Identifier, AlgebraicTypeUse)],
dont_import: &[AlgebraicTypeRef],
) {
let mut imports = BTreeSet::new();
gen_imports(&mut imports, roots);
remove_skipped_imports(&mut imports, dont_import);
print_imports(module, out, imports);
}