clockworklabs/SpacetimeDB · error
ERROR: Skipping multi-column index registration '%s.%s' beca
Error message
ERROR: Skipping multi-column index registration '%s.%s' because circular reference error is set
What it means
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.
Source
Thrown at crates/bindings-cpp/include/spacetimedb/internal/v10_builder.h:199
if (constraint_bits & static_cast<int>(FieldConstraint::AutoInc)) {
RawSequenceDefV10 seq_def;
// Defer sequence naming to host-side canonical generation for Rust/C# parity.
seq_def.source_name = std::nullopt;
seq_def.column = field_idx;
seq_def.start = std::nullopt;
seq_def.increment = SpacetimeDB::I128(1);
seq_def.min_value = std::nullopt;
seq_def.max_value = std::nullopt;
table_it->sequences.push_back(std::move(seq_def));
}
}
template<typename T>
void AddMultiColumnIndex(const std::string& table_name,
const std::string& index_name,
const std::vector<std::string>& field_names) {
if (g_circular_ref_error) {
std::fprintf(stderr, "ERROR: Skipping multi-column index registration '%s.%s' because circular reference error is set\n",
table_name.c_str(), index_name.c_str());
return;
}
if (field_names.empty()) {
SetConstraintRegistrationError(
"MULTI_INDEX_EMPTY",
"table='" + table_name + "' index='" + index_name + "' has no fields");
return;
}
SpacetimeDB::field_registrar<T>::register_fields();
auto& descriptor_map = SpacetimeDB::get_table_descriptors();
auto it = descriptor_map.find(&typeid(T));
if (it == descriptor_map.end()) {
SetConstraintRegistrationError(
"NO_FIELD_DESCRIPTORS",
"table='" + table_name + "' index='" + index_name + "' has no registered field descriptors");
return;
}View on GitHub (pinned to 524b4487d9)
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
Example fix
// before — Node contains itself, sets g_circular_ref_error, index registration skipped
struct Node {
uint32_t id;
std::vector<Node> children; // cycle: Node -> Node
};
// after — reference by id, no cycle, AddMultiColumnIndex proceeds
struct Node {
uint32_t id;
std::vector<uint32_t> child_ids; // Node -> u32, acyclic
}; Defensive patterns
Strategy: validation
Validate before calling
// In a unit test that links the module (runs after static init):
#include <spacetimedb/internal/module_type_registration.h>
TEST(Schema, NoCircularTypeReferences) {
EXPECT_FALSE(SpacetimeDB::Internal::g_circular_ref_error);
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- ERROR: Skipping default-value registration '%s.%s' because c
- ERROR: Skipping reducer registration '%s' because circular r
- ERROR: Skipping lifecycle reducer registration '%s' because
- ERROR: Skipping view registration '%s' because circular refe
- ERROR: Skipping procedure registration '%s' because circular
AI-assisted analysis of clockworklabs/SpacetimeDB@524b4487d9 (2026-08-16).
Data as JSON: /api/errors/59584122e1755ad9.
Report an issue: GitHub.