# frozen_string_literal: false # @!parse # class ActiveRecord::Migration # # Modify a materialized view # # # # @param [#to_s] name (nil) The qualified name of the view # # @option options [Boolean] :if_exists (false) Suppress the error when the view is absent # # @yield [v] the block with the view's definition # # @yieldparam Object receiver of methods specifying the view # # @return [void] # # # # The operation enables to alter a view without recreating # # its from scratch. You can rename columns, change their # # storage settings (how the column is TOAST-ed), or customize their statistics. # # # # ```ruby # # change_materialized_view "admin_users" do |v| # # v.rename_column "name", to: "full_name" # # v.column "name", storage: "extended", from_storage: "expanded" # # v.column "admin", n_distinct: 2 # # v.column "role", statistics: 100 # # end # # ``` # # # # Notice that renaming will be done AFTER all changes even # # though the order of declarations can be different. # # # # As in the snippet above, to make the change invertible, # # you have to define a previous storage via `from_storage` option. # # The inversion would always reset statistics (set it to 0). # # # # In addition to changing columns, the operation enables # # to set a default clustering by given index: # # # # ```ruby # # change_materialized_view "admin_users" do |v| # # v.cluster_on "admin_users_by_names_idx" # # end # # ``` # # # # The clustering is invertible, but its inversion does nothing, # # keeping the clustering unchanged. # # # # The comment can also be changed: # # # # ```ruby # # change_materialized_view "admin_users" do |v| # # v.comment "Admin users", from: "Admin users only" # # end # # ``` # # # # Notice, that without `from` option the operation is still # # invertible, but its inversion would delete the comment. # # It can also be reset to the blank string explicitly: # # # # ```ruby # # change_materialized_view "admin_users" do |v| # # v.comment "", from: "Admin users only" # # end # # ``` # # # # With the `if_exists: true` option, the operation won't fail # # even when the view wasn't existed. At the same time, # # this option makes a migration irreversible due to uncertainty # # of the previous state of the database. # def change_materialized_view(name, **options, &block); end # end module PGTrunk::Operations::MaterializedViews # @private class ChangeMaterializedView < Base # A method to be called in a block def rename_column(name, to:) columns << Column.new(name: name, new_name: to) end # Operation-specific validations validate { errors.add :base, "Changes can't be blank" if changes.blank? } validates :algorithm, :force, :if_not_exists, :new_name, :sql_definition, :tablespace, :version, :with_data, absence: true def to_sql(_version) [ *change_columns, *rename_columns, *cluster_view, *update_comment, ].join(" ") end def invert irreversible!("if_exists: true") if if_exists undefined = inversion.select { |_, v| v.nil? }.keys.join(", ").presence raise IrreversibleMigration.new(self, nil, <<~MSG.squish) if undefined Undefined values to revert #{undefined}. MSG self.class.new(name: name, **inversion) if inversion.any? end private def changes @changes ||= { columns: columns.presence, cluster_on: cluster_on, comment: comment, }.compact end def inversion @inversion ||= { columns: columns.map(&:invert).presence, comment: from_comment, }.slice(*changes.keys) end def alter_view @alter_view ||= begin sql = "ALTER MATERIALIZED VIEW" sql << " IF EXISTS" if if_exists sql << " #{name.to_sql}" end end def change_columns changes = columns.reject(&:new_name).map(&:to_sql).join(", ") "#{alter_view} #{changes};" if changes.present? end def rename_columns changes = columns.select(&:new_name).map(&:to_sql).join(", ") "#{alter_view} #{changes};" if changes.present? end def cluster_view "#{alter_view} CLUSTER ON #{cluster_on.inspect};" if cluster_on.present? end def update_comment return if comment.nil? <<~SQL COMMENT ON MATERIALIZED VIEW #{name.to_sql} IS $comment$#{comment}$comment$; SQL end end end