clockworklabs/SpacetimeDB · error · anyhow::Error

view definition cannot read from other views

Error message

view definition cannot read from other views

What it means

View materialization only reads base tables. After compiling the view's SQL, the host checks each plan with reads_from_view(); any reference to another view -- even indirectly -- is rejected because views cannot be layered (it would require ordered/recursive materialization).

Source

Thrown at crates/core/src/host/wasm_common/module_host_actor.rs:197

    // Views bypass RLS, since views should enforce their own access control procedurally.
    let auth = AuthCtx::for_current(database_identity);
    let schema_view = SchemaViewer::new(&*tx, &auth);

    // Compile to subscription plans.
    let (plans, has_params) = SubscriptionPlan::compile(the_query, &schema_view, &auth)?;
    ensure!(
        !has_params,
        "parameterized SQL is not supported for view materialization yet"
    );

    // Validate shape and disallow views-on-views.
    for plan in &plans {
        let Some(source_schema) = plan.return_table() else {
            bail!("query does not return plain table rows");
        };
        if plan.reads_from_view(true) || plan.reads_from_view(false) {
            bail!("view definition cannot read from other views");
        }
        if source_schema.row_type != *expected_row_type {
            bail!(
                "query returns `{}` but view expects `{}`",
                fmt_algebraic_type(&AlgebraicType::Product(source_schema.row_type.clone())),
                fmt_algebraic_type(&AlgebraicType::Product(expected_row_type.clone())),
            );
        }
    }

    let op = FuncCallType::View(call_info.clone());
    let mut metrics = ExecutionMetrics::default();
    let mut rows = Vec::new();

    let params = ExecutionParams::from_auth(&auth);

    for plan in plans {
        // Track read sets for all tables involved in this plan.

View on GitHub (pinned to 9e0d92412f)

Solutions

  1. Inline the underlying view's SQL: reference the base table(s) directly in the new view definition
  2. Copy the filter/WHERE logic of the inner view into the outer definition
  3. If composition is the goal, put the combined logic in one flat view or query it client-side via a subscription

Example fix

-- before: users_v is itself a view
CREATE VIEW active_v AS SELECT * FROM users_v WHERE active;

-- after: reference the base table instead
CREATE VIEW active_v AS SELECT * FROM users WHERE active;
Defensive patterns

Strategy: validation

Validate before calling

#!/usr/bin/env bash
# naming views with a _v suffix makes view-on-view refs greppable
if grep -rinE 'from[[:space:]]+[a-z0-9_]+_v\b' sql/; then
  echo 'possible view-on-view reference' >&2; exit 1
fi

Prevention

When it happens

Trigger: CREATE VIEW v2 AS SELECT ... FROM v1 where v1 is itself a view; a view whose subquery selects from another view; refactoring a base table into a view and forgetting that dependent views now read a view.

Common situations: Building 'derived' views on top of convenience views for readability; incremental migrations that turn tables into views; copy-pasting view SQL that chains layers.

Related errors


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