{"record":{"id":"30382d210660f90e","repo":"scenic-views/scenic","slug":"a-transaction-is-required-to-perform-a-side-by-sid","errorCode":null,"errorMessage":"a transaction is required to perform a side-by-side update","messagePattern":"a transaction is required to perform a side-by-side update","errorType":"exception","errorClass":"RuntimeError","httpStatus":null,"severity":"error","filePath":"lib/scenic/statements.rb","lineNumber":132,"sourceCode":"          ArgumentError,\n          \"sql_definition and version cannot both be set\"\n        )\n      end\n\n      sql_definition ||= definition(name, version)\n\n      if materialized\n        options = materialized_options(materialized)\n\n        if options[:no_data] && options[:side_by_side]\n          raise(\n            ArgumentError,\n            \"no_data and side_by_side options cannot be combined\"\n          )\n        end\n\n        if options[:side_by_side] && !transaction_open?\n          raise \"a transaction is required to perform a side-by-side update\"\n        end\n\n        Scenic.database.update_materialized_view(\n          name,\n          sql_definition,\n          no_data: options[:no_data],\n          side_by_side: options[:side_by_side]\n        )\n      else\n        Scenic.database.update_view(name, sql_definition)\n      end\n    end\n\n    # Update a database view to a new version using `CREATE OR REPLACE VIEW`.\n    #\n    # The existing view is replaced using the supplied `version`\n    # parameter.\n    #","sourceCodeStart":114,"sourceCodeEnd":150,"githubUrl":"https://github.com/scenic-views/scenic/blob/f2162dbddb0fb0eb5d4b04d640dcec5303a387e8/lib/scenic/statements.rb#L114-L150","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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)"],"exampleFix":"# before\nclass UpdateReportsMatview < ActiveRecord::Migration[7.0]\n  disable_ddl_transaction!\n  def change\n    update_view :reports, version: 4, revert_to_version: 3, materialized: { side_by_side: true }\n  end\nend\n\n# after: directive removed; Rails wraps the migration in a DDL transaction\nclass UpdateReportsMatview < ActiveRecord::Migration[7.0]\n  def change\n    update_view :reports, version: 4, revert_to_version: 3, materialized: { side_by_side: true }\n  end\nend","handlingStrategy":"validation","validationCode":"ActiveRecord::Base.connection.transaction do\n  update_view :reports, version: 4, revert_to_version: 3, materialized: { side_by_side: true }\nend","typeGuard":null,"tryCatchPattern":"begin\n  update_view :reports, version: 4, materialized: { side_by_side: true }\nrescue RuntimeError => e\n  raise unless e.message.include?('a transaction is required')\n  ActiveRecord::Base.connection.transaction { retry }\nend","preventionTips":["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"],"tags":["rails","migration","transaction","materialized-view","scenic","ddl"],"backgroundTag":"requires-transaction","analyzedSha":"f2162dbddb0fb0eb5d4b04d640dcec5303a387e8","analyzedAt":"2026-08-23T09:20:35.041Z","schemaVersion":2},"datasetVersion":"2026-08-23T13:39:53.451Z"}