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 <jameshgilles@gmail.com>
This commit is contained in:
Shubham Mishra
2025-09-16 14:09:52 +05:30
committed by GitHub
parent 8adef2b93b
commit cfb185a235
5 changed files with 95 additions and 8 deletions
+2 -1
View File
@@ -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)
}
+75 -6
View File
@@ -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<syn::Expr>,
}
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<ColumnAttr> {
if let Ok(expr) = attr.parse_args::<syn::Expr>() {
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<TokenStream> {
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<TokenStream> = 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<spacetimedb::table::ColumnDefault> {
[#(#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::<Vec<_>>();
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<spacetimedb::table::ScheduleDesc<'static>> = 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
+4
View File
@@ -342,6 +342,10 @@ pub fn register_table<T: 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();
})
}
+12 -1
View File
@@ -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<ColumnDefault>;
}
/// 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)]
+2
View File
@@ -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<TestC>,
}