scenic-views/scenic · error · RuntimeError
a transaction is required to perform a side-by-side update
Error message
a transaction is required to perform a side-by-side update
What it means
update_view with materialized: { side_by_side: true } builds the new materialized view under a temporary name, populates it, and swaps names atomically - steps that must commit or abort together. The statement raises a RuntimeError when the connection reports no open transaction (transaction_open? is false), because a crash mid-swap outside a transaction would strand both the half-built temporary view and the original.
Source
Thrown at lib/scenic/statements.rb:132
ArgumentError,
"sql_definition and version cannot both be set"
)
end
sql_definition ||= definition(name, version)
if materialized
options = materialized_options(materialized)
if options[:no_data] && options[:side_by_side]
raise(
ArgumentError,
"no_data and side_by_side options cannot be combined"
)
end
if options[:side_by_side] && !transaction_open?
raise "a transaction is required to perform a side-by-side update"
end
Scenic.database.update_materialized_view(
name,
sql_definition,
no_data: options[:no_data],
side_by_side: options[:side_by_side]
)
else
Scenic.database.update_view(name, sql_definition)
end
end
# Update a database view to a new version using `CREATE OR REPLACE VIEW`.
#
# The existing view is replaced using the supplied `version`
# parameter.
#View on GitHub (pinned to f2162dbddb)
Solutions
- Split the migration: move add_index(..., algorithm: :concurrently) into its own disable_ddl_transaction! migration and remove the directive from the one holding the side-by-side update, so Rails wraps it in the usual DDL transaction
- If the directive must stay, open the transaction yourself: ActiveRecord::Base.connection.transaction { update_view :reports, version: 4, materialized: { side_by_side: true } }
- Drop the strategy: update_view :reports, version: 4, materialized: true does an ordinary drop-and-recreate (longer lock on the view, no transaction requirement)
Example fix
# before
class UpdateReportsMatview < ActiveRecord::Migration[7.0]
disable_ddl_transaction!
def change
update_view :reports, version: 4, revert_to_version: 3, materialized: { side_by_side: true }
end
end
# after: directive removed; Rails wraps the migration in a DDL transaction
class UpdateReportsMatview < ActiveRecord::Migration[7.0]
def change
update_view :reports, version: 4, revert_to_version: 3, materialized: { side_by_side: true }
end
end Defensive patterns
Strategy: validation
Validate before calling
ActiveRecord::Base.connection.transaction do
update_view :reports, version: 4, revert_to_version: 3, materialized: { side_by_side: true }
end Try / catch
begin
update_view :reports, version: 4, materialized: { side_by_side: true }
rescue RuntimeError => e
raise unless e.message.include?('a transaction is required')
ActiveRecord::Base.connection.transaction { retry }
end Prevention
- Keep side-by-side materialized view updates in their own plain migration; never one marked disable_ddl_transaction!
- Put add_index(..., algorithm: :concurrently) in a separate migration - it is the usual reason disable_ddl_transaction! gets added
- Never call update_view with side_by_side from console or tasks without wrapping it in ActiveRecord::Base.connection.transaction
When it happens
Trigger: The migration class declares disable_ddl_transaction! (most often to permit add_index ..., algorithm: :concurrently), so Rails does not wrap the migration in a DDL transaction; or update_view ... materialized: { side_by_side: true } is invoked outside any transaction - console, ad-hoc rake task, or a migration runner that strips transactions.
Common situations: One migration combines a concurrent index creation with a side-by-side materialized view update; the developer adds disable_ddl_transaction! for the index and the view update then fails; teams running migrations through wrappers that disable per-migration transactions.
Related errors
- Materialized views require Postgres 9.3 or newer
- #{method} is reversible only if given a revert_to_version
- version is required
- Cannot replace materialized views
- Define view query in #{path} before migrating.
AI-assisted analysis of scenic-views/scenic@f2162dbddb (2026-08-23).
Data as JSON: /api/errors/30382d210660f90e.
Report an issue: GitHub.