clockworklabs/SpacetimeDB · error

`table_id` must not be `TableId::SENTINEL` in `{row_level_se

Error message

`table_id` must not be `TableId::SENTINEL` in `{row_level_security_schema:#?}`

What it means

create_row_level_security validates its input: a row-level-security policy must attach to a real table, but the RowLevelSecuritySchema arrived with table_id == TableId::SENTINEL, the placeholder used before the id allocator assigns a real TableId. The call is rejected before any row is written to st_row_level_security.

Source

Thrown at crates/datastore/src/locking_tx_datastore/mut_tx.rs:2551

        .map(|mut iter| {
            iter.next()
                .map(|row| row.read_col(StConstraintFields::ConstraintId).unwrap())
        })
    }

    /// Create a row level security policy.
    ///
    /// Requires:
    /// - `row_level_security_schema.table_id != TableId::SENTINEL`
    /// - `row_level_security_schema.sql` must be unique.
    ///
    /// Ensures:
    ///
    /// - The row level security policy metadata is inserted into the system tables (and other data structures reflecting them).
    /// - The returned `sql` is unique.
    pub fn create_row_level_security(&mut self, row_level_security_schema: RowLevelSecuritySchema) -> Result<RawSql> {
        if row_level_security_schema.table_id == TableId::SENTINEL {
            return Err(anyhow::anyhow!(
                "`table_id` must not be `TableId::SENTINEL` in `{row_level_security_schema:#?}`"
            )
            .into());
        }

        log::trace!(
            "ROW LEVEL SECURITY CREATING for table: {}",
            row_level_security_schema.table_id
        );

        // Insert the row into st_row_level_security
        // NOTE: Because st_row_level_security has a unique index on sql, this will
        // fail if already exists.
        let row = StRowLevelSecurityRow {
            table_id: row_level_security_schema.table_id,
            sql: row_level_security_schema.sql,
        };

View on GitHub (pinned to 9e0d92412f)

Solutions

  1. Create the table first, then copy the assigned id into the policy (row_level_security_schema.table_id = table_schema.table_id) before calling create_row_level_security
  2. If the schema came from deserialization, resolve table_id from the table name via the committed schema
  3. Add a debug_assert!/unit test over generated schemas asserting table_id != TableId::SENTINEL

Example fix

// before: schema built before the table exists, id never assigned
let rls = RowLevelSecuritySchema { table_id: TableId::SENTINEL, ..Default::default() };
tx.create_row_level_security(rls)?;

// after: create the table first and reuse its assigned id
let table = tx.create_table(table_schema)?;
let mut rls = RowLevelSecuritySchema { ..Default::default() };
rls.table_id = table.table_id;
tx.create_row_level_security(rls)?;
Defensive patterns

Strategy: validation

Validate before calling

fn rls_schema_is_attachable(rls: &RowLevelSecuritySchema) -> bool {
    rls.table_id != TableId::SENTINEL
}

// before creating the policy:
assert!(rls_schema_is_attachable(&rls), "RLS policy must reference a created table");

Prevention

When it happens

Trigger: Building a RowLevelSecuritySchema by hand or via Default/deserialization and calling create_row_level_security before the target table exists or before its assigned id was copied into the schema. TableSchema::from_module_def starts with TableId::SENTINEL until create_table assigns the real id.

Common situations: Code-first module tooling that defines RLS policies in the same pass as table creation; migrations reordered so the RLS step runs before table creation; module manifests deserialized from JSON/YAML that omit table_id.

Understand the failure class

Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.

Related errors


AI-assisted analysis of clockworklabs/SpacetimeDB@9e0d92412f (2026-08-20). Data as JSON: /api/errors/471756175123749b. Report an issue: GitHub.