clockworklabs/SpacetimeDB · error · syn::Error

a direct index must be paired with a `#[unique] constraint

Error message

a direct index must be paired with a `#[unique] constraint

What it means

A `direct` index in SpacetimeDB maps each indexed value to at most one row, so it is only meaningful on a column that is also declared `#[unique]`. During index validation, an `IndexType::Direct` found on a column where `is_unique` is false is rejected at that column's span. Non-unique columns should use `btree` or `hash` indexes.

Source

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

        Ok(IndexArg::new(accessor, kind, None))
    }

    fn validate<'a>(&'a self, table_name: &str, cols: &'a [Column<'a>]) -> syn::Result<ValidatedIndex<'a>> {
        let find_column = |ident| find_column(cols, ident);
        let (kind, kind_str) = match &self.kind {
            IndexType::BTree { columns } => {
                let cols = columns.iter().map(find_column).collect::<syn::Result<Vec<_>>>()?;
                (ValidatedIndexType::BTree { cols }, "btree")
            }
            IndexType::Hash { columns } => {
                let cols = columns.iter().map(find_column).collect::<syn::Result<Vec<_>>>()?;
                (ValidatedIndexType::Hash { cols }, "hash")
            }
            IndexType::Direct { column } => {
                let col = find_column(column)?;

                if !self.is_unique {
                    return Err(syn::Error::new(
                        column.span(),
                        "a direct index must be paired with a `#[unique] constraint",
                    ));
                }

                (ValidatedIndexType::Direct { col }, "direct")
            }
        };
        let gen_index_name = || {
            // See crates/schema/src/validate/v9.rs for the format of index names.
            // It's slightly unnerving that we just trust that component to generate this format correctly,
            // but what can you do.
            let cols = kind.columns();
            let cols = cols.iter().map(|col| col.ident.to_string()).collect::<Vec<_>>();
            let cols = cols.join("_");
            format!("{table_name}_{cols}_idx_{kind_str}")
        };

View on GitHub (pinned to 524b4487d9)

Solutions

  1. Add `#[unique]` to the column the direct index targets
  2. If the column's values are not actually unique, switch the index to `btree` or `hash`
  3. Verify existing data satisfies uniqueness before publishing, otherwise inserts will fail at runtime

Example fix

// before
#[spacetimedb::table(accessor = user)]
struct User {
    #[index(direct)]
    email: String,
}

// after
#[spacetimedb::table(accessor = user)]
struct User {
    #[unique]
    #[index(direct)]
    email: String,
}
Defensive patterns

Strategy: validation

Validate before calling

# every direct index must sit on a #[unique] column — review each hit
grep -rn 'direct' src/ | grep -v unique || echo OK

Prevention

When it happens

Trigger: Declaring a direct index — table-level `index = direct(col)` or a column-level direct index — on a column that does not carry `#[unique]`.

Common situations: Switching an index from btree/hash to direct without adding the uniqueness constraint; assuming 'direct' is a generic faster index type rather than a unique value-to-row index.

Related errors


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