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
- Inline the underlying view's SQL: reference the base table(s) directly in the new view definition
- Copy the filter/WHERE logic of the inner view into the outer definition
- 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
- Keep views flat: always author them against base tables
- Suffix view names (_v) so references are greppable in review
- When turning a table into a view, sweep for dependent views immediately
- Prefer one comprehensive view over chained convenience layers
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
- query does not return plain table rows
- Index '${indexLabel}' on table '${tableLabel}' must define a
- query returns `{}` but view expects `{}`
- Cannot define RLS rule on private table: {}. Please make tab
- Failed to read value from the `{}` column of `{}` for table_
AI-assisted analysis of clockworklabs/SpacetimeDB@9e0d92412f (2026-08-20).
Data as JSON: /api/errors/8f103dacabf7ae84.
Report an issue: GitHub.