clockworklabs/SpacetimeDB · error · syn::Error

spacetimedb table must be a struct

Error message

spacetimedb table must be a struct

What it means

`#[spacetimedb::table]` derives the item's SATS type and matches on `SatsTypeData::Product`; only a struct yields Product data. Applying the macro to an enum fails this check and reports that a table must be a struct, at call site. (A union fails even earlier, in sats.rs, with 'unions not supported'.) Value types such as enums belong on columns via `SpacetimeType`, not on tables.

Source

Thrown at crates/bindings-macro/src/table.rs:838

    let mut set = GENERATED_STRUCTS.lock().expect("mutex poisoned");

    set.insert(struct_name.to_string())
}

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))?;

    let original_struct_ident = sats_ty.ident;
    let table_ident = &args.accessor;
    let explicit_table_name = args.name.as_ref().map(|s| s.value());
    let view_trait_ident = format_ident!("{}__view", table_ident);
    let query_trait_ident = format_ident!("{}__query", table_ident);
    let query_cols_struct = format_ident!("{}Cols", original_struct_ident);
    let query_ix_cols_struct = format_ident!("{}IxCols", original_struct_ident);
    let table_name = table_ident.unraw().to_string();
    let sats::SatsTypeData::Product(fields) = &sats_ty.data else {
        return Err(syn::Error::new(Span::call_site(), "spacetimedb table must be a struct"));
    };

    for param in &item.generics.params {
        let err = |msg| syn::Error::new_spanned(param, msg);
        match param {
            syn::GenericParam::Lifetime(_) => {}
            syn::GenericParam::Type(_) => return Err(err("type parameters are not allowed on tables")),
            syn::GenericParam::Const(_) => return Err(err("const parameters are not allowed on tables")),
        }
    }

    let table_id_from_name_func = quote! {
        fn table_id() -> spacetimedb::TableId {
            static TABLE_ID: std::sync::OnceLock<spacetimedb::TableId> = std::sync::OnceLock::new();
            *TABLE_ID.get_or_init(|| {
                spacetimedb::table_id_from_name(<Self as spacetimedb::table::TableInternal>::TABLE_NAME)
            })
        }

View on GitHub (pinned to 524b4487d9)

Solutions

  1. Change the table to a struct
  2. If it is a value type, derive `SpacetimeType` on the enum and use it as a column in a real table struct
  3. Model enum-keyed rows as a struct with an enum-typed column plus an index

Example fix

// before
#[spacetimedb::table(accessor = status)]
enum Status {
    Active,
    Banned,
}

// after
#[derive(spacetimedb::SpacetimeType)]
enum Status {
    Active,
    Banned,
}

#[spacetimedb::table(accessor = user)]
struct User {
    #[primary_key]
    #[auto_inc]
    id: u64,
    status: Status,
}
Defensive patterns

Strategy: validation

Validate before calling

# tables must be structs: flag non-struct items carrying the table attribute
grep -rn -A1 'spacetimedb::table' src/ | grep -E 'enum|union' || echo OK

Prevention

When it happens

Trigger: `#[spacetimedb::table(...)]` applied to an `enum` item.

Common situations: Trying to use an enum as a lookup table; new users applying the table attribute to every schema type they define.

Related errors


AI-assisted analysis of clockworklabs/SpacetimeDB@524b4487d9 (2026-08-16). Data as JSON: /api/errors/d4078636bbe8c5dd. Report an issue: GitHub.