clockworklabs/SpacetimeDB · error

expected ModuleDef to contain key, but it does not

Error message

expected ModuleDef to contain key, but it does not

What it means

Panic from `ModuleDef::lookup_expect`, SpacetimeDB's schema-module convenience lookup. It calls `T::lookup(self, key)` and `.expect("expected ModuleDef to contain key, but it does not")` when the key (table/view/sequence/constraint/type name, possibly namespace-qualified) is absent. The API contract states: only call it when existence is guaranteed (e.g. the key came from iterating the same ModuleDef); a panic means an unverified lookup assumption.

Solutions

  1. Replace the `lookup_expect` call with `lookup(...).ok_or_else(|| anyhow!("...not found..."))?` so callers receive a real error instead of a panic (the codebase already does this nearby for AddConstraint lookups).
  2. Verify the key's provenance: keys must come from the same ModuleDef instance or be re-validated after any publish/migration boundary.
  3. Print/log the exact key (and its namespace path) at failure to spot join/casing mismatches between constructed names and stored identifiers.
  4. Add a regression test covering the offending operation (e.g. the migration step) with the failing key.

Example fix

// before: panics on missing key
let table = module_def.lookup_expect::<TableDef>(key);

// after: return a typed error
let table = module_def
    .lookup::<TableDef>(key)
    .ok_or_else(|| anyhow::anyhow!("table `{key}` not found in module def"))?;
Defensive patterns

Strategy: validation

Validate before calling

// Replace lookup_expect with a checked lookup at the call boundary
let def = module_def
    .lookup::<TableDef>(key)
    .ok_or_else(|| anyhow::anyhow!("`{key}` not found in module def"))?;

Type guard

// Narrow before use when the key type is dynamic
fn has_table(md: &ModuleDef, name: &str) -> bool { md.table(name).is_some() }

Try / catch

let r = std::panic::catch_unwind(AssertUnwindSafe(|| md.lookup_expect::<T>(key)));
match r { Ok(v) => v, Err(_) => return Err(SchemaError::key_not_found(key)) }

Prevention

When it happens

Trigger: Calling `lookup_expect` with a key obtained from a DIFFERENT module version (old plan vs new def during migration), a name built by string concatenation that doesn't match an actual entry (submodule path join mismatch, casing), or a key validated against one collection but looked up in another (e.g. constraint name found in the wrong namespace map).

Common situations: Migration code that resolves names from a `DatabaseDef`/plan into a freshly published `ModuleDef` where names shifted; refactors renaming entities while stale references remain; submodule namespace-prefix inconsistencies; cross-crate code assuming another crate added a definition. Note the sibling code in update.rs:486 catches a similar case with a proper `anyhow!` error instead — the ideal fix direction.

Related errors


AI-assisted analysis of clockworklabs/SpacetimeDB@eddf9f5014 (2026-08-20). Data as JSON: /api/errors/72c89854d8353758. Report an issue: GitHub.

Appendix: source

Thrown at crates/schema/src/def.rs:590

    /// The `TableDef` an entity in the global namespace is stored in, if any.
    ///
    /// Generally, you will want to use the `lookup` method on the entity type instead.
    pub fn stored_in_table_def(&self, name: &RawIdentifier) -> Option<&TableDef> {
        self.stored_in_table_def
            .get(name)
            .and_then(|table_name| self.tables.get(table_name))
    }

    /// Lookup a definition by its key in `self`.
    pub fn lookup<T: ModuleDefLookup>(&self, key: T::Key<'_>) -> Option<&T> {
        T::lookup(self, key)
    }

    /// Lookup a definition by its key in `self`, panicking if not found.
    /// Only use this method if you are sure the key exists in the module definition.
    pub fn lookup_expect<T: ModuleDefLookup>(&self, key: T::Key<'_>) -> &T {
        T::lookup(self, key).expect("expected ModuleDef to contain key, but it does not")
    }

    /// Convenience method to look up a table, possibly by a string.
    pub fn table<K: ?Sized + Hash + Equivalent<Identifier>>(&self, name: &K) -> Option<&TableDef> {
        // If the string IS a valid identifier, we can just look it up.
        self.tables.get(name)
    }

    /// Convenience method to look up a view, possibly by a string.
    pub fn view<K: ?Sized + Hash + Equivalent<Identifier>>(&self, name: &K) -> Option<&ViewDef> {
        // If the string IS a valid identifier, we can just look it up.
        self.views.get(name)
    }

    /// Convenience method to look up a view, possibly by a string, returning its id as well.
    pub fn view_full<K: ?Sized + Hash + Equivalent<Identifier>>(&self, name: &K) -> Option<(ViewFnPtr, &ViewDef)> {
        // If the string IS a valid identifier, we can just look it up.
        self.views.get(name).map(|def| (def.fn_ptr, def))

View on GitHub (pinned to eddf9f5014)