clockworklabs/SpacetimeDB · critical · InvalidOperationException
Unique index point scan returned >1 rows
Error message
Unique index point scan returned >1 rows
What it means
UniqueIndex<Handle,Row,T,RW>.FindSingle serializes the key, does a point scan (FFI.datastore_index_scan_point_bsatn), and enforces the unique-index invariant in user code: after the first matching row, a second MoveNext() must fail. Two rows sharing one key under a declared-unique index is a data-integrity violation — the datastore no longer satisfies the module's index definition, so every FindSingle-based accessor (FindByUnique-style lookups and updates routed through DoUpdate) throws.
Source
Thrown at crates/bindings-csharp/Runtime/Internal/IIndex.cs:141
}
protected Row? FindSingle(T key)
{
using var s = new MemoryStream();
using var w = new BinaryWriter(s);
new RW().Write(w, key);
var point = s.ToArray();
using var e = new RawPointIter(indexId, point).GetEnumerator();
if (!e.MoveNext())
{
return null;
}
var row = e.Current;
if (e.MoveNext())
{
throw new InvalidOperationException("Unique index point scan returned >1 rows");
}
return row;
}
protected Row DoUpdate(Row row)
{
// Insert the row.
var bytes = IStructuralReadWrite.ToBytes(row);
var bytes_len = bytes.Length;
FFI.datastore_update_bsatn(ITableView<Handle, Row>.tableId, indexId, bytes, ref bytes_len);
return ITableView<Handle, Row>.IntegrateGeneratedColumns(row, bytes, bytes_len);
}
}
public abstract class RefUniqueIndex<Handle, Row, T, RW>(string name) : IndexBase<Row>(name)
where Handle : ITableView<Handle, Row>View on GitHub (pinned to 6dee26c6ef)
Solutions
- Dedupe the table: scan the index's Filter(key) for affected keys and delete/merge extra rows
- If the index was recently made unique, clear or migrate the data before republishing the module
- Audit code paths that insert or update rows to delete the old row before inserting a new one with the same unique key
- If data is clean and it still throws, capture a database snapshot and report a SpacetimeDB datastore bug
Example fix
// before - index on Email was made [Unique] but old rows still contain duplicates
var user = MyTable.UniqueEmail.FindSingle(email); // throws: 2 rows match
// after - dedupe first, then lookups hold the invariant
foreach (var group in MyTable.Iter().GroupBy(r => r.Email).Where(g => g.Count() > 1))
foreach (var extra in group.Skip(1)) MyTable.Delete(extra.Id);
var user = MyTable.UniqueEmail.FindSingle(email); Defensive patterns
Strategy: validation
Validate before calling
// Before inserting/updating through a unique index, check the key's uniqueness yourself:
var existing = MyTable.Iter().Where(r => r.UniqueEmail == email).Take(2).ToList();
if (existing.Count > 1) { /* duplicates already present: dedupe before any unique-index op */ }
if (existing.Count == 1 && existing[0].Id != row.Id) { /* key already taken: reject or update in place */ } Try / catch
try { var row = uniqueIndexAccessor(key); }
catch (InvalidOperationException ioe) when (ioe.Message == "Unique index point scan returned >1 rows")
{
// datastore integrity is broken: run a dedupe migration, then retry
} Prevention
- Deduplicate existing data before publishing a module that declares an index unique
- When changing a row's unique key, delete the old row before inserting the new one
- Bulk-import test data through the normal table API so index maintenance runs
When it happens
Trigger: Calling the generated unique-index accessor (FindSingle) for a key that matches 2+ rows: legacy data already containing duplicates when a unique index is added or made unique; writes that bypassed index maintenance (raw datastore_update_bsatn paths, FFI writes); restoring a database dump into a module whose schema now declares the index unique.
Common situations: Adding [Unique] / changing an index from non-unique to unique on a live table without deduplicating first; module updates that republish schema over existing data; concurrent test harnesses writing directly to the datastore; corrupted database after a crash mid-transaction.
Related errors
- When providing a name, you must also provide the object.
- wrong number of elements
- Transaction context was not initialised.
- `table_id` must not be `TableId::SENTINEL` in `{index_schema
- unique constraint on table {table_id} column(s) {col_list:?}
AI-assisted analysis of clockworklabs/SpacetimeDB@6dee26c6ef (2026-08-20).
Data as JSON: /api/errors/47b5c2f6a7dd232b.
Report an issue: GitHub.