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

  1. 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
  2. If the directive must stay, open the transaction yourself: ActiveRecord::Base.connection.transaction { update_view :reports, version: 4, materialized: { side_by_side: true } }
  3. 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

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


AI-assisted analysis of scenic-views/scenic@f2162dbddb (2026-08-23). Data as JSON: /api/errors/30382d210660f90e. Report an issue: GitHub.