mirror of
https://github.com/clockworklabs/SpacetimeDB.git
synced 2026-05-06 07:26:43 -04:00
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:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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>,
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user