module PGTrunk::Operations::MaterializedViews

@private Namespace for operations with materialized views

@!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

@!parse

class ActiveRecord::Migration
  # Create a materialized view
  #
  # @param [#to_s] name (nil) The qualified name of the view
  # @option options [Boolean] :if_not_exists (false) Suppress the error when a view has been already created
  # @option options [#to_s] :sql_definition (nil) The snippet containing the query
  # @option options [#to_i] :version (nil)
  #   The alternative way to set sql_definition by referencing to a file containing the snippet
  # @option options [#to_s] :tablespace (nil) The tablespace for the view
  # @option options [Boolean] :with_data (true) If the view should be populated after creation
  # @option options [#to_s] :comment (nil) The comment describing the view
  # @yield [v] the block with the view's definition
  # @yieldparam Object receiver of methods specifying the view
  # @return [void]
  #
  # The operation creates the view using its `sql_definition`:
  #
  # ```ruby
  # create_materialized_view("views.admin_users", sql_definition: <<~SQL)
  #   SELECT id, name FROM users WHERE admin;
  # SQL
  # ```
  #
  # For compatibility to the `scenic` gem, we also support
  # adding a definition via its version:
  #
  # ```ruby
  # create_materialized_view "admin_users", version: 1
  # ```
  #
  # It is expected, that a `db/materialized_views/admin_users_v01.sql`
  # to contain the SQL snippet.
  #
  # The tablespace can be specified for the created view.
  # Notice that later it can't be changed (in PostgreSQL all rows
  # can be moved to another tablespace, but we don't support
  # this feature yet).
  #
  # ```ruby
  # create_materialized_view "admin_users" do |v|
  #   v.tablespace "fast_ssd"
  #   v.sql_definition <<~SQL
  #     SELECT id, name, password, admin, on_duty
  #     FROM users
  #     WHERE admin
  #   SQL
  # end
  # ```
  #
  # You can also set a comment describing the view,
  # and redefine the storage options for some TOAST-ed columns,
  # as well as their custom statistics:
  #
  # ```ruby
  # create_materialized_view "admin_users" do |v|
  #   v.sql_definition <<~SQL
  #     SELECT id, name, password, admin, on_duty
  #     FROM users
  #     WHERE admin
  #   SQL
  #   v.column "password", storage: "external" # to avoid compression
  #   v.column "password", n_distinct: -1 # linear dependency
  #   v.column "admin", n_distinct: 1 # exact number of values
  #   v.column "on_duty", statistics: 2 # the total number of values
  #   v.comment "Admin users only"
  # end
  # ```
  #
  # With the `replace_existing: true` option the operation
  # would use `CREATE OR REPLACE VIEW` command, so it
  # can be used to "update" (or reload) the existing view.
  #
  # ```ruby
  # create_materialized_view "admin_users",
  #                          version: 1,
  #                          replace_existing: true
  # ```
  #
  # This option makes the migration irreversible due to uncertainty
  # of the previous state of the database.
  def create_materialized_view(name, **options, &block); end
end

@!parse

class ActiveRecord::Migration
  # Drop 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
  # @option options [Symbol] :force (:restrict) How to process dependent objects (`:cascade` or `:restrict`)
  # @option options [#to_s] :sql_definition (nil) The snippet containing the query
  # @option options [#to_i] :revert_to_version (nil)
  #   The alternative way to set sql_definition by referencing to a file containing the snippet
  # @option options [#to_s] :tablespace (nil) The tablespace for the view
  # @option options [Boolean] :with_data (true) If the view should be populated after creation
  # @option options [#to_s] :comment (nil) The comment describing the view
  # @yield [v] the block with the view's definition
  # @yieldparam Object receiver of methods specifying the view
  # @return [void]
  #
  # The operation drops a materialized view identified by its
  # qualified name (it can include a schema).
  #
  # ```ruby
  # drop_materialized_view "views.admin_users"
  # ```
  #
  # To make the operation invertible, use the same options
  # as in the `create_view` operation.
  #
  # ```ruby
  # drop_materialized_view "views.admin_users" do |v|
  #   v.sql_definition "SELECT name, password FROM users WHERE admin;"
  #   v.column "password", storage: "external" # prevent compression
  #   v.with_data false
  #   v.comment "Admin users only"
  # end
  # ```
  #
  # You can also use a version-base SQL definition like:
  #
  # ```ruby
  # drop_materialized_view "admin_users", revert_to_version: 1
  # ```
  #
  # With the `force: :cascade` option the operation would remove
  # all the objects which depend on the view.
  #
  # ```ruby
  # drop_materialized_view "admin_users", force: :cascade
  # ```
  #
  # With the `if_exists: true` option the operation won't fail
  # even when the view was absent in the database.
  #
  # ```ruby
  # drop_materialized_view "admin_users", if_exists: true
  # ```
  #
  # Both options make a migration irreversible due to uncertainty
  # of the previous state of the database.
  def drop_materialized_view(name, **options, &block); end
end

@!parse

class ActiveRecord::Migration
  # Refresh a materialized view
  #
  # @param [#to_s] name (nil) The qualified name of the view
  # @option options [Boolean] :with_data (true) If the view should be populated after creation
  # @option options [Symbol] :algorithm (nil) Makes the operation concurrent when set to :concurrently
  # @return [void]
  #
  # The operation enables refreshing a materialized view
  # by reloading its underlying SQL query:
  #
  # ```ruby
  # refresh_materialized_view "admin_users"
  # ```
  #
  # The option `algorithm: :concurrently` acts exactly
  # like in the `create_index` definition. You should
  # possibly add the `disable_ddl_transaction!` command
  # to the migration as well.
  #
  # With option `with_data: false` the command won't
  # update the data. This option can't be used along with
  # the `:algorithm`.
  #
  # The operation is always reversible, though its
  # inversion does nothing.
  def refresh_materialized_view(name, **options); end
end

@!parse

class ActiveRecord::Migration
  # Change the name and/or schema of a materialized view
  #
  # @param [#to_s] :name (nil) The qualified name of the view
  # @option options [#to_s] :to (nil) The new qualified name for the view
  # @option options [Boolean] :if_exists (false) Suppress the error when the view is absent
  # @return [void]
  #
  # A materialized view can be renamed by changing both the name
  # and the schema (namespace) it belongs to.
  #
  # ```ruby
  # rename_materialized_view "views.admin_users", to: "admins"
  # ```
  #
  # With the `if_exists: true` option, the operation won't fail
  # even when the view wasn't existed.
  #
  # ```ruby
  # rename_materialized_view "admin_users",
  #                          to: "admins",
  #                          if_exists: true
  # ```
  #
  # At the same time, the option makes a migration irreversible
  # due to uncertainty of the previous state of the database.
  def rename_materialized_view(name, **options); end
end