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

  1. 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
  2. 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>)
  3. Rebuild and rerun; this and all sibling 'Skipping ... because circular reference error is set' messages disappear once the flag is never set
  4. 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

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


AI-assisted analysis of clockworklabs/SpacetimeDB@524b4487d9 (2026-08-16). Data as JSON: /api/errors/59584122e1755ad9. Report an issue: GitHub.