docs/docs/00200-core-concepts/00300-tables/00240-constraints.md
import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import { CppModuleVersionNotice } from "@site/src/components/CppModuleVersionNotice";
Constraints enforce data integrity rules on your tables. SpacetimeDB supports primary key and unique constraints.
A primary key uniquely identifies each row in a table. It represents the identity of a row and determines how updates and deletes are handled.
<Tabs groupId="server-language" queryString> <TabItem value="typescript" label="TypeScript">import { table, t } from 'spacetimedb/server';
const user = table(
{ name: 'user', public: true },
{
id: t.u64().primaryKey(),
name: t.string(),
email: t.string(),
}
);
Use the .primaryKey() method on a column builder to mark it as the primary key.
[SpacetimeDB.Table(Accessor = "User", Public = true)]
public partial struct User
{
[SpacetimeDB.PrimaryKey]
public ulong Id;
public string Name;
public string Email;
}
Use the [SpacetimeDB.PrimaryKey] attribute to mark a field as the primary key.
#[spacetimedb::table(accessor = user, public)]
pub struct User {
#[primary_key]
id: u64,
name: String,
email: String,
}
Use the #[primary_key] attribute to mark a field as the primary key.
struct User {
uint64_t id;
std::string name;
std::string email;
};
SPACETIMEDB_STRUCT(User, id, name, email)
SPACETIMEDB_TABLE(User, user, Public)
FIELD_PrimaryKey(user, id)
Use FIELD_PrimaryKey(table, field) after table registration to mark the primary key.
Because of the unique constraint, SpacetimeDB implements primary keys using a unique index. This index is created automatically.
SpacetimeDB does not yet support multi-column (composite) primary keys. If you need to look up rows by multiple columns, use a multi-column btree index combined with an auto-increment primary key:
<Tabs groupId="server-language" queryString> <TabItem value="typescript" label="TypeScript">const inventory = table(
{
name: 'inventory',
public: true,
indexes: [
{ accessor: 'by_user_item', algorithm: 'btree', columns: ['userId', 'itemId'] },
],
},
{
id: t.u64().primaryKey().autoInc(),
userId: t.u64(),
itemId: t.u64(),
quantity: t.u32(),
}
);
[SpacetimeDB.Table(Accessor = "Inventory", Public = true)]
[SpacetimeDB.Index.BTree(Accessor = "by_user_item", Columns = new[] { nameof(UserId), nameof(ItemId) })]
public partial struct Inventory
{
[SpacetimeDB.PrimaryKey]
[SpacetimeDB.AutoInc]
public ulong Id;
public ulong UserId;
public ulong ItemId;
public uint Quantity;
}
#[spacetimedb::table(accessor = inventory, public, index(accessor = inventory_index, btree(columns = [user_id, item_id])))]
pub struct Inventory {
#[primary_key]
#[auto_inc]
id: u64,
user_id: u64,
item_id: u64,
quantity: u32,
}
struct Inventory {
uint64_t id;
uint64_t user_id;
uint64_t item_id;
uint32_t quantity;
};
SPACETIMEDB_STRUCT(Inventory, id, user_id, item_id, quantity)
SPACETIMEDB_TABLE(Inventory, inventory, Public)
FIELD_PrimaryKeyAutoInc(inventory, id)
// Named multi-column btree index on (user_id, item_id)
FIELD_NamedMultiColumnIndex(inventory, by_user_item, user_id, item_id)
This gives you efficient lookups by the column combination while using a simple auto-increment value as the primary key.
When you update a row, SpacetimeDB uses the primary key to determine whether it's a modification or a replacement:
export const update_user_name = spacetimedb.reducer({ id: t.u64(), newName: t.string() }, (ctx, { id, newName }) => {
const user = ctx.db.user.id.find(id);
if (user) {
// This is an update — primary key (id) stays the same
ctx.db.user.id.update({ ...user, name: newName });
}
});
[SpacetimeDB.Reducer]
public static void UpdateUserName(ReducerContext ctx, ulong id, string newName)
{
var user = ctx.Db.User.Id.Find(id);
if (user != null)
{
// This is an update — primary key (Id) stays the same
user.Name = newName;
ctx.Db.User.Id.Update(user);
}
}
#[spacetimedb::reducer]
fn update_user_name(ctx: &ReducerContext, id: u64, new_name: String) -> Result<(), String> {
if let Some(mut user) = ctx.db.user().id().find(id) {
// This is an update — primary key (id) stays the same
user.name = new_name;
ctx.db.user().id().update(user);
}
Ok(())
}
SPACETIMEDB_REDUCER(update_user_name, ReducerContext ctx, uint64_t id, std::string new_name) {
auto user_opt = ctx.db[user_id].find(id);
if (user_opt.has_value()) {
User user_update = user_opt.value();
user_update.name = new_name;
ctx.db[user_id].update(user_update);
}
return Ok();
}
Tables don't require a primary key. Without one, the entire row acts as the primary key:
SpacetimeDB always maintains set semantics regardless of whether you define a primary key. The difference is what defines uniqueness: a primary key column, or the entire row.
Primary keys add indexing overhead. If your table is only accessed by iterating over all rows (no lookups by key), omitting the primary key can improve performance.
Auto-incrementing IDs: Combine primaryKey() with autoInc() for automatically assigned unique identifiers:
#[spacetimedb::table(accessor = post, public)]
pub struct Post {
#[primary_key]
#[auto_inc]
id: u64,
title: String,
content: String,
}
Identity as primary key: Use the caller's identity as the primary key for user-specific data:
#[spacetimedb::table(accessor = user_profile, public)]
pub struct UserProfile {
#[primary_key]
identity: Identity,
display_name: String,
bio: String,
}
This pattern ensures each identity can only have one profile and makes lookups by identity efficient.
Mark columns as unique to ensure no two rows can have the same value for that column.
<Tabs groupId="server-language" queryString> <TabItem value="typescript" label="TypeScript">const user = table(
{ name: 'user', public: true },
{
id: t.u32().primaryKey(),
email: t.string().unique(),
username: t.string().unique(),
}
);
Use the .unique() method on a column builder.
[SpacetimeDB.Table(Accessor = "User", Public = true)]
public partial struct User
{
[SpacetimeDB.PrimaryKey]
public uint Id;
[SpacetimeDB.Unique]
public string Email;
[SpacetimeDB.Unique]
public string Username;
}
Use the [SpacetimeDB.Unique] attribute.
#[spacetimedb::table(accessor = user, public)]
pub struct User {
#[primary_key]
id: u32,
#[unique]
email: String,
#[unique]
username: String,
}
Use the #[unique] attribute.
struct User {
uint32_t id;
std::string email;
std::string username;
};
SPACETIMEDB_STRUCT(User, id, email, username)
SPACETIMEDB_TABLE(User, user, Public)
FIELD_PrimaryKey(user, id)
FIELD_Unique(user, email)
FIELD_Unique(user, username)
Use FIELD_Unique(table, field) after table registration to mark columns as unique.
Unlike primary keys, you can have multiple unique columns on a single table. Unique columns also create an index that enables efficient lookups.
Both primary keys and unique columns enforce uniqueness, but they serve different purposes:
| Aspect | Primary Key | Unique Column |
|---|---|---|
| Purpose | Row identity | Data integrity |
| Count per table | One | Multiple allowed |
| Update behavior | Delete + Insert | In-place update |
| Required | No | No |