{"record":{"id":"59584122e1755ad9","repo":"clockworklabs/SpacetimeDB","slug":"error-skipping-multi-column-index-registration","errorCode":null,"errorMessage":"ERROR: Skipping multi-column index registration '%s.%s' because circular reference error is set\n","messagePattern":"ERROR: Skipping multi-column index registration '(.+?)\\.(.+?)' because circular reference error is set\n","errorType":"console","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/bindings-cpp/include/spacetimedb/internal/v10_builder.h","lineNumber":199,"sourceCode":"        if (constraint_bits & static_cast<int>(FieldConstraint::AutoInc)) {\n            RawSequenceDefV10 seq_def;\n            // Defer sequence naming to host-side canonical generation for Rust/C# parity.\n            seq_def.source_name = std::nullopt;\n            seq_def.column = field_idx;\n            seq_def.start = std::nullopt;\n            seq_def.increment = SpacetimeDB::I128(1);\n            seq_def.min_value = std::nullopt;\n            seq_def.max_value = std::nullopt;\n            table_it->sequences.push_back(std::move(seq_def));\n        }\n    }\n\n    template<typename T>\n    void AddMultiColumnIndex(const std::string& table_name,\n                             const std::string& index_name,\n                             const std::vector<std::string>& field_names) {\n        if (g_circular_ref_error) {\n            std::fprintf(stderr, \"ERROR: Skipping multi-column index registration '%s.%s' because circular reference error is set\\n\",\n                        table_name.c_str(), index_name.c_str());\n            return;\n        }\n        if (field_names.empty()) {\n            SetConstraintRegistrationError(\n                \"MULTI_INDEX_EMPTY\",\n                \"table='\" + table_name + \"' index='\" + index_name + \"' has no fields\");\n            return;\n        }\n        SpacetimeDB::field_registrar<T>::register_fields();\n        auto& descriptor_map = SpacetimeDB::get_table_descriptors();\n        auto it = descriptor_map.find(&typeid(T));\n        if (it == descriptor_map.end()) {\n            SetConstraintRegistrationError(\n                \"NO_FIELD_DESCRIPTORS\",\n                \"table='\" + table_name + \"' index='\" + index_name + \"' has no registered field descriptors\");\n            return;\n        }","sourceCodeStart":181,"sourceCodeEnd":217,"githubUrl":"https://github.com/clockworklabs/SpacetimeDB/blob/524b4487d949b61a07d4f39c862d1290259dfd20/crates/bindings-cpp/include/spacetimedb/internal/v10_builder.h#L181-L217","documentation":"During static module build, the SpacetimeDB C++ V10 builder skips registering a multi-column btree index because the global flag g_circular_ref_error was set earlier by the type registrar. That flag is set in LazyTypeRegistrar::getOrRegister (module_type_registration.h) when a type's qualified name re-appears in the thread-local registration chain, i.e. a type graph cycle was detected. This line is a cascade message: the real defect is the type cycle printed earlier as '[CIRCULAR REFERENCE DETECTED]' with the full chain; preinit_99 will fail the module afterwards, so the module cannot publish successfully.","triggerScenarios":"A table's row type (or any type it references) is part of a reference cycle, e.g. struct Node { Vec<Node> children; } or A contains B and B contains A; the table macro for that table also declares a multi-column index (AddMultiColumnIndex via the multi_index/constraint clause). Static init registers types first, the cycle sets g_circular_ref_error, and the later AddMultiColumnIndex call for '<table>.<index>' is rejected with this message.","commonSituations":"Modeling tree/linked-list nodes with embedded self-references instead of ID columns; adding a new field that closes a cycle between two previously-acyclic types; upgrading bindings versions where a forward-declared type now resolves back into itself; mixing a type into its own table row via Option or Vec.","solutions":["Scroll to the first '[CIRCULAR REFERENCE DETECTED]' block in stderr and note the registration chain it prints — the last type in that chain is the one that closes the cycle","Break the cycle at the reported type by replacing the embedded value/Vec/Option member with an ID reference (e.g. Vec<uint64_t> child_ids instead of Vec<Node>)","Rebuild and rerun; this and all sibling 'Skipping ... because circular reference error is set' messages disappear once the flag is never set","If the cycle is intentional, restructure the schema so shared/repeated data lives in its own table referenced by ID, which is the SpacetimeDB-idiomatic shape"],"exampleFix":"// before — Node contains itself, sets g_circular_ref_error, index registration skipped\nstruct Node {\n    uint32_t id;\n    std::vector<Node> children;   // cycle: Node -> Node\n};\n// after — reference by id, no cycle, AddMultiColumnIndex proceeds\nstruct Node {\n    uint32_t id;\n    std::vector<uint32_t> child_ids;   // Node -> u32, acyclic\n};","handlingStrategy":"validation","validationCode":"// In a unit test that links the module (runs after static init):\n#include <spacetimedb/internal/module_type_registration.h>\nTEST(Schema, NoCircularTypeReferences) {\n    EXPECT_FALSE(SpacetimeDB::Internal::g_circular_ref_error);\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Never embed a row type inside itself (directly, via Vec, or via Option) — always reference rows by ID columns","Keep a schema unit test that links all table headers and asserts g_circular_ref_error == false in CI","Read stderr from the beginning: the '[CIRCULAR REFERENCE DETECTED]' block names the true culprit before any 'Skipping ...' cascade","Treat every 'because circular reference error is set' line as one root cause, not separate bugs"],"tags":["spacetimedb","cpp","schema","circular-reference","multi-column-index","static-initialization"],"backgroundTag":"circular-type-reference","analyzedSha":"524b4487d949b61a07d4f39c862d1290259dfd20","analyzedAt":"2026-08-16T23:58:54.611Z","schemaVersion":2},"datasetVersion":"2026-08-17T04:17:16.089Z"}