clockworklabs/SpacetimeDB · error
TableId `{table_id}` does not exist
Error message
TableId `{table_id}` does not exist What it means
A read-only transaction (TxId) called table_scan for a TableId the committed state does not know. TableIds are numeric ids assigned at table creation; this one was never created, was dropped, or belongs to an older schema generation — committed_state_shared_lock has no entry for it, so the scan cannot even start.
Source
Thrown at crates/datastore/src/locking_tx_datastore/tx.rs:61
= IndexScanRangeIter<'a>
where
Self: 'a;
type PointIndexIter<'a>
= IndexScanPointIter<'a>
where
Self: 'a;
fn row_count(&self, table_id: TableId) -> u64 {
self.committed_state_shared_lock
.table_row_count(table_id)
.unwrap_or_default()
}
fn table_scan<'a>(&'a self, table_id: TableId) -> anyhow::Result<Self::TableIter<'a>> {
self.committed_state_shared_lock
.table_scan(table_id)
.ok_or_else(|| anyhow::anyhow!("TableId `{table_id}` does not exist"))
}
fn index_scan_range<'a>(
&'a self,
table_id: TableId,
index_id: IndexId,
range: &impl RangeBounds<AlgebraicValue>,
) -> anyhow::Result<Self::RangeIndexIter<'a>> {
self.with_index(table_id, index_id, |i| i.seek_range_via_algebraic_value(range))?
.map_err(|IndexCannotSeekRange| IndexError::IndexCannotSeekRange(index_id).into())
}
fn index_scan_point<'a>(
&'a self,
table_id: TableId,
index_id: IndexId,
point: &AlgebraicValue,
) -> anyhow::Result<Self::PointIndexIter<'a>> {View on GitHub (pinned to 6dee26c6ef)
Solutions
- Resolve the table name to its current TableId (schema_for_table or iterating st_table) before scanning
- Refresh query/subscription plans after schema changes instead of reusing compiled ids
- Handle the error by re-planning or re-subscribing rather than retrying the same id
- Check that the table actually exists in this database/replica
Example fix
// before: scan with a TableId cached before a module update let iter = tx.table_scan(cached_table_id)?; // after: resolve the id from the current schema by name first let schema = committed_state.schema_for_table_by_name(table_name)?; let iter = tx.table_scan(schema.table_id)?;
Defensive patterns
Strategy: validation
Validate before calling
// Resolve the id from the live schema before scanning
fn resolve_table_id(tx: &TxId, name: &str) -> anyhow::Result<TableId> {
tx.schema_for_table_by_name(name)
.map(|s| s.table_id)
.ok_or_else(|| anyhow::anyhow!("table {name} does not exist"))
} Try / catch
On this error, treat the TableId as stale: re-resolve by name from the current schema; if the table was dropped, invalidate cached plans and re-subscribe instead of retrying the same id.
Prevention
- Never cache TableIds across module updates; resolve by name per session and refresh on schema-change notifications
- Recompile query/subscription plans whenever the module schema version changes
- Reject fabricated or SENTINEL ids at the API boundary with a clear message
When it happens
Trigger: Scanning with a TableId captured before a module update dropped or recreated the table; query or subscription plans compiled against a previous schema and replayed after it changed; passing TableId::SENTINEL or an id invented by the caller.
Common situations: Hot module updates where old clients hold compiled plans; internal code caching table ids across schema changes; tools reading table ids from stale metadata files.
Related errors
- IndexId `{index_id}` does not exist
- Unique constraint violation during merge: {violation:?}
- cannot serialize refs without a typespace
- length didn't fit in `u32`
- Deletion for non-existent table {table_id:?}... huh?
AI-assisted analysis of clockworklabs/SpacetimeDB@6dee26c6ef (2026-08-20).
Data as JSON: /api/errors/030a640f1b13c086.
Report an issue: GitHub.