{"record":{"id":"47b5c2f6a7dd232b","repo":"clockworklabs/SpacetimeDB","slug":"unique-index-point-scan-returned-1-rows","errorCode":null,"errorMessage":"Unique index point scan returned >1 rows","messagePattern":"Unique index point scan returned >1 rows","errorType":"exception","errorClass":"InvalidOperationException","httpStatus":null,"severity":"critical","filePath":"crates/bindings-csharp/Runtime/Internal/IIndex.cs","lineNumber":141,"sourceCode":"    }\n\n    protected Row? FindSingle(T key)\n    {\n        using var s = new MemoryStream();\n        using var w = new BinaryWriter(s);\n        new RW().Write(w, key);\n        var point = s.ToArray();\n\n        using var e = new RawPointIter(indexId, point).GetEnumerator();\n        if (!e.MoveNext())\n        {\n            return null;\n        }\n\n        var row = e.Current;\n        if (e.MoveNext())\n        {\n            throw new InvalidOperationException(\"Unique index point scan returned >1 rows\");\n        }\n\n        return row;\n    }\n\n    protected Row DoUpdate(Row row)\n    {\n        // Insert the row.\n        var bytes = IStructuralReadWrite.ToBytes(row);\n        var bytes_len = bytes.Length;\n        FFI.datastore_update_bsatn(ITableView<Handle, Row>.tableId, indexId, bytes, ref bytes_len);\n\n        return ITableView<Handle, Row>.IntegrateGeneratedColumns(row, bytes, bytes_len);\n    }\n}\n\npublic abstract class RefUniqueIndex<Handle, Row, T, RW>(string name) : IndexBase<Row>(name)\n    where Handle : ITableView<Handle, Row>","sourceCodeStart":123,"sourceCodeEnd":159,"githubUrl":"https://github.com/clockworklabs/SpacetimeDB/blob/6dee26c6efc2856793e12b148a59742964f5d783/crates/bindings-csharp/Runtime/Internal/IIndex.cs#L123-L159","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before - index on Email was made [Unique] but old rows still contain duplicates\nvar user = MyTable.UniqueEmail.FindSingle(email); // throws: 2 rows match\n\n// after - dedupe first, then lookups hold the invariant\nforeach (var group in MyTable.Iter().GroupBy(r => r.Email).Where(g => g.Count() > 1))\n    foreach (var extra in group.Skip(1)) MyTable.Delete(extra.Id);\nvar user = MyTable.UniqueEmail.FindSingle(email);","handlingStrategy":"validation","validationCode":"// Before inserting/updating through a unique index, check the key's uniqueness yourself:\nvar existing = MyTable.Iter().Where(r => r.UniqueEmail == email).Take(2).ToList();\nif (existing.Count > 1) { /* duplicates already present: dedupe before any unique-index op */ }\nif (existing.Count == 1 && existing[0].Id != row.Id) { /* key already taken: reject or update in place */ }","typeGuard":null,"tryCatchPattern":"try { var row = uniqueIndexAccessor(key); }\ncatch (InvalidOperationException ioe) when (ioe.Message == \"Unique index point scan returned >1 rows\")\n{\n    // datastore integrity is broken: run a dedupe migration, then retry\n}","preventionTips":["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"],"tags":["csharp","index","unique-constraint","datastore","spacetimedb-module"],"backgroundTag":"unique-constraint-violation","analyzedSha":"6dee26c6efc2856793e12b148a59742964f5d783","analyzedAt":"2026-08-20T06:08:37.179Z","contentChangedAt":"2026-08-20T06:08:37.179Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}