scenic-views/scenic · error · ArgumentError

Cannot replace materialized views

Error message

Cannot replace materialized views

What it means

Postgres implements CREATE OR REPLACE VIEW for plain views only; there is no CREATE OR REPLACE MATERIALIZED VIEW statement. Because replace_view compiles to exactly that SQL, Scenic raises ArgumentError ('Cannot replace materialized views') whenever materialized is truthy, regardless of server version, rather than emitting SQL the server would reject anyway.

Source

Thrown at lib/scenic/statements.rb:168

    #
    # Does not work with materialized views due to lack of database support.
    #
    # @param name [String, Symbol] The name of the database view.
    # @param version [Fixnum] The version number of the view.
    # @param revert_to_version [Fixnum] The version number to rollback to on
    #   `rake db rollback`
    # @return The database response from executing the create statement.
    #
    # @example
    #   replace_view :engagement_reports, version: 3, revert_to_version: 2
    #
    def replace_view(name, version: nil, revert_to_version: nil, materialized: false)
      if version.blank?
        raise ArgumentError, "version is required"
      end

      if materialized
        raise ArgumentError, "Cannot replace materialized views"
      end

      sql_definition = definition(name, version)

      Scenic.database.replace_view(name, sql_definition)
    end

    private

    def definition(name, version)
      Scenic::Definition.new(name, version).to_sql
    end

    def materialized_options(materialized)
      if materialized.is_a? Hash
        {
          no_data: materialized.fetch(:no_data, false),
          side_by_side: materialized.fetch(:side_by_side, false)

View on GitHub (pinned to f2162dbddb)

Solutions

  1. Use the supported path: update_view :reports, version: 2, revert_to_version: 1, materialized: true (drop and recreate, with existing view indexes automatically reapplied)
  2. If lock duration is the concern, update_view :reports, version: 2, materialized: { side_by_side: true } inside a transactional migration swaps atomically with minimal lock time
  3. If replace_view was chosen to untangle view dependencies, order the migrations so dependents update first instead

Example fix

# before (ArgumentError: Cannot replace materialized views)
replace_view :reports, version: 2, materialized: true

# after: supported materialized update path
update_view :reports, version: 2, revert_to_version: 1, materialized: true
Defensive patterns

Strategy: validation

Validate before calling

view = Scenic.database.views.find { |v| v.name.to_s == 'reports' }
if view&.materialized
  update_view :reports, version: 2, materialized: true
else
  replace_view :reports, version: 2
end

Type guard

def materialized_view?(name)
  Scenic.database.views.find { |v| v.name.to_s == name.to_s }&.materialized == true
end

Prevention

When it happens

Trigger: replace_view :reports, version: 2, materialized: true; replace_view :reports, version: 2, materialized: { side_by_side: true } (any truthy value, including a Hash, hits the guard); copy-pasting a plain-view replace_view migration and turning the flag on.

Common situations: A team reaches for replace_view on a materialized view to dodge dependency-ordering pain between tangled views; converting a plain view into a materialized one without switching the migration method; templates shared across view kinds.

Understand the failure class

Background: UnsupportedOperationException and "is not supported" errors: when a library deliberately refuses a call — this error's family across 30 libraries.

Related errors


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