From cfb185a235e2f4e986cc9d3d37e8c6cbdad56d2f Mon Sep 17 00:00:00 2001 From: Shubham Mishra Date: Tue, 16 Sep 2025 14:09:52 +0530 Subject: [PATCH] rust: `default` macro (#3177) # Description of Changes PR introduces support for column-level default values via a new `#[default(...)]` attribute. It also validates, `default` macro is not used along with `primary_key`, `unique` or `auto_inc`. # API and ABI breaking changes NA # Expected complexity level and risk 2 # Testing Start using macro in `module-test`. --------- Co-authored-by: James Gilles --- crates/bindings-macro/src/lib.rs | 3 +- crates/bindings-macro/src/table.rs | 81 +++++++++++++++++++++++++++--- crates/bindings/src/rt.rs | 4 ++ crates/bindings/src/table.rs | 13 ++++- modules/module-test/src/lib.rs | 2 + 5 files changed, 95 insertions(+), 8 deletions(-) diff --git a/crates/bindings-macro/src/lib.rs b/crates/bindings-macro/src/lib.rs index 1ebc56593..cd85b80ef 100644 --- a/crates/bindings-macro/src/lib.rs +++ b/crates/bindings-macro/src/lib.rs @@ -57,6 +57,7 @@ mod sym { symbol!(scheduled); symbol!(unique); symbol!(update); + symbol!(default); symbol!(u8); symbol!(i8); @@ -167,7 +168,7 @@ pub fn table(args: StdTokenStream, item: StdTokenStream) -> StdTokenStream { /// /// Provides helper attributes for `#[spacetimedb::table]`, so that we don't get unknown attribute errors. #[doc(hidden)] -#[proc_macro_derive(__TableHelper, attributes(sats, unique, auto_inc, primary_key, index))] +#[proc_macro_derive(__TableHelper, attributes(sats, unique, auto_inc, primary_key, index, default))] pub fn table_helper(input: StdTokenStream) -> StdTokenStream { schema_type(input) } diff --git a/crates/bindings-macro/src/table.rs b/crates/bindings-macro/src/table.rs index 882ed27ff..4f569c675 100644 --- a/crates/bindings-macro/src/table.rs +++ b/crates/bindings-macro/src/table.rs @@ -451,12 +451,13 @@ fn superize_vis(vis: &syn::Visibility) -> Cow<'_, syn::Visibility> { } } -#[derive(Copy, Clone)] +#[derive(Clone)] struct Column<'a> { index: u16, vis: &'a syn::Visibility, ident: &'a syn::Ident, ty: &'a syn::Type, + default_value: Option, } fn try_find_column<'a, 'b, T: ?Sized>(cols: &'a [Column<'b>], name: &T) -> Option<&'a Column<'b>> @@ -475,6 +476,7 @@ enum ColumnAttr { AutoInc(Span), PrimaryKey(Span), Index(IndexArg), + Default(syn::Expr, Span), } impl ColumnAttr { @@ -494,12 +496,25 @@ impl ColumnAttr { } else if ident == sym::primary_key { attr.meta.require_path_only()?; Some(ColumnAttr::PrimaryKey(ident.span())) + } else if ident == sym::default { + Some(parse_default_attr(attr, ident)?) } else { None }) } } +fn parse_default_attr(attr: &syn::Attribute, ident: &Ident) -> syn::Result { + if let Ok(expr) = attr.parse_args::() { + return Ok(ColumnAttr::Default(expr, ident.span())); + } + + Err(syn::Error::new_spanned( + &attr.meta, + "expected default value in format `#[default(CONSTANT_VALUE)]`", + )) +} + pub(crate) fn table_impl(mut args: TableArgs, item: &syn::DeriveInput) -> syn::Result { let vis = &item.vis; let sats_ty = sats::sats_type_from_derive(item, quote!(spacetimedb::spacetimedb_lib))?; @@ -548,6 +563,7 @@ pub(crate) fn table_impl(mut args: TableArgs, item: &syn::DeriveInput) -> syn::R let mut unique = None; let mut auto_inc = None; let mut primary_key = None; + let mut default_value = None; for attr in field.original_attrs { let Some(attr) = ColumnAttr::parse(attr, field_ident)? else { continue; @@ -566,28 +582,42 @@ pub(crate) fn table_impl(mut args: TableArgs, item: &syn::DeriveInput) -> syn::R primary_key = Some(span); } ColumnAttr::Index(index_arg) => args.indices.push(index_arg), + ColumnAttr::Default(expr, span) => { + check_duplicate(&default_value, span)?; + default_value = Some(expr); + } } } + if let Some(default_value) = &default_value { + if auto_inc.is_some() || primary_key.is_some() || unique.is_some() { + return Err(syn::Error::new( + default_value.span(), + "invalid combination: auto_inc, unique index or primary key cannot have a default value", + )); + }; + } + let column = Column { index: col_num, ident: field_ident, vis: field.vis, ty: field.ty, + default_value, }; if unique.is_some() || primary_key.is_some() { - unique_columns.push(column); + unique_columns.push(column.clone()); } if auto_inc.is_some() { - sequenced_columns.push(column); + sequenced_columns.push(column.clone()); } if let Some(span) = primary_key { check_duplicate_msg(&primary_key_column, span, "can only have one primary key per table")?; - primary_key_column = Some(column); + primary_key_column = Some(column.clone()); } - columns.push(column); + columns.push(column.clone()); } let row_type = quote!(#original_struct_ident); @@ -647,9 +677,45 @@ pub(crate) fn table_impl(mut args: TableArgs, item: &syn::DeriveInput) -> syn::R let table_access = args.access.iter().map(|acc| acc.to_value()); let unique_col_ids = unique_columns.iter().map(|col| col.index); - let primary_col_id = primary_key_column.iter().map(|col| col.index); + let primary_col_id = primary_key_column.clone().into_iter().map(|col| col.index); let sequence_col_ids = sequenced_columns.iter().map(|col| col.index); + let default_type_check: TokenStream = columns + .iter() + .filter_map(|col| { + if let Some(val) = &col.default_value { + let ty = &col.ty; + let ident_span = col.ident.span(); + Some(quote_spanned! { ident_span => + // This closure enforces that `val` is of type `ty` at compile-time. + let _check: #ty = #val; + }) + } else { + None + } + }) + .collect(); + + let col_defaults: Vec = columns.iter().filter_map(|col| { + if let Some(val) = &col.default_value { + let col_id = col.index; + Some(quote! { + spacetimedb::table::ColumnDefault { + col_id: #col_id, + value: #val.serialize(spacetimedb::sats::algebraic_value::ser::ValueSerializer).expect("default value serialization failed"), + } + }) + } else { + None + } + }).collect(); + + let default_fn: TokenStream = quote! { + fn get_default_col_values() -> Vec { + [#(#col_defaults)*].to_vec() + } + }; + let (schedule, schedule_typecheck) = args .scheduled .as_ref() @@ -719,6 +785,7 @@ pub(crate) fn table_impl(mut args: TableArgs, item: &syn::DeriveInput) -> syn::R let field_types = fields.iter().map(|f| f.ty).collect::>(); let tabletype_impl = quote! { + use spacetimedb::Serialize; impl spacetimedb::Table for #tablehandle_ident { type Row = #row_type; @@ -738,6 +805,7 @@ pub(crate) fn table_impl(mut args: TableArgs, item: &syn::DeriveInput) -> syn::R #(const SCHEDULE: Option> = Some(#schedule);)* #table_id_from_name_func + #default_fn } }; @@ -773,6 +841,7 @@ pub(crate) fn table_impl(mut args: TableArgs, item: &syn::DeriveInput) -> syn::R const _: () = { #(let _ = <#field_types as spacetimedb::rt::TableColumn>::_ITEM;)* #schedule_typecheck + #default_type_check }; #trait_def diff --git a/crates/bindings/src/rt.rs b/crates/bindings/src/rt.rs index 9c3507b65..e169e916f 100644 --- a/crates/bindings/src/rt.rs +++ b/crates/bindings/src/rt.rs @@ -342,6 +342,10 @@ pub fn register_table() { table = table.with_schedule(schedule.reducer_name, schedule.scheduled_at_column); } + for col in T::get_default_col_values().iter_mut() { + table = table.with_default_column_value(col.col_id, col.value.clone()) + } + table.finish(); }) } diff --git a/crates/bindings/src/table.rs b/crates/bindings/src/table.rs index 60b73dd43..fd281160b 100644 --- a/crates/bindings/src/table.rs +++ b/crates/bindings/src/table.rs @@ -3,8 +3,11 @@ use core::borrow::Borrow; use core::convert::Infallible; use core::fmt; use core::marker::PhantomData; -use spacetimedb_lib::buffer::{BufReader, Cursor, DecodeError}; pub use spacetimedb_lib::db::raw_def::v9::TableAccess; +use spacetimedb_lib::{ + buffer::{BufReader, Cursor, DecodeError}, + AlgebraicValue, +}; use spacetimedb_lib::{FilterableValue, IndexScanRangeBoundsTerminator}; pub use spacetimedb_primitives::{ColId, IndexId}; @@ -128,6 +131,8 @@ pub trait TableInternal: Sized { /// Returns the ID of this table. fn table_id() -> TableId; + + fn get_default_col_values() -> Vec; } /// Describe a named index with an index type over a set of columns identified by their IDs. @@ -148,6 +153,12 @@ pub struct ScheduleDesc<'a> { pub scheduled_at_column: u16, } +#[derive(Debug, Clone)] +pub struct ColumnDefault { + pub col_id: u16, + pub value: AlgebraicValue, +} + /// A row operation was attempted that would violate a unique constraint. // TODO: add column name for better error message #[derive(Debug)] diff --git a/modules/module-test/src/lib.rs b/modules/module-test/src/lib.rs index 3d5a51240..7986af928 100644 --- a/modules/module-test/src/lib.rs +++ b/modules/module-test/src/lib.rs @@ -40,8 +40,10 @@ pub enum TestC { Bar, } +const DEFAULT_TEST_C: TestC = TestC::Foo; #[table(name = test_d, public)] pub struct TestD { + #[default(Some(DEFAULT_TEST_C))] test_c: Option, }