pg_trunk 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.github/workflows/ci.yml +87 -0
- data/.gitignore +9 -0
- data/.rspec +4 -0
- data/.rubocop.yml +92 -0
- data/.yardopts +4 -0
- data/CHANGELOG.md +31 -0
- data/CONTRIBUTING.md +17 -0
- data/Gemfile +22 -0
- data/LICENSE.txt +21 -0
- data/README.md +141 -0
- data/Rakefile +16 -0
- data/bin/console +8 -0
- data/bin/rake +19 -0
- data/bin/rspec +19 -0
- data/bin/setup +8 -0
- data/bin/yard +19 -0
- data/lib/pg_trunk/core/adapters/postgres.rb +80 -0
- data/lib/pg_trunk/core/dependencies_resolver.rb +101 -0
- data/lib/pg_trunk/core/generators.rb +140 -0
- data/lib/pg_trunk/core/operation/attributes.rb +78 -0
- data/lib/pg_trunk/core/operation/callbacks.rb +40 -0
- data/lib/pg_trunk/core/operation/generators.rb +51 -0
- data/lib/pg_trunk/core/operation/inversion.rb +70 -0
- data/lib/pg_trunk/core/operation/registration.rb +55 -0
- data/lib/pg_trunk/core/operation/ruby_builder.rb +112 -0
- data/lib/pg_trunk/core/operation/ruby_helpers.rb +99 -0
- data/lib/pg_trunk/core/operation/sql_helpers.rb +44 -0
- data/lib/pg_trunk/core/operation/validations.rb +21 -0
- data/lib/pg_trunk/core/operation.rb +78 -0
- data/lib/pg_trunk/core/qualified_name.rb +165 -0
- data/lib/pg_trunk/core/railtie/command_recorder.rb +30 -0
- data/lib/pg_trunk/core/railtie/custom_types.rb +37 -0
- data/lib/pg_trunk/core/railtie/migration.rb +50 -0
- data/lib/pg_trunk/core/railtie/migrator.rb +22 -0
- data/lib/pg_trunk/core/railtie/schema_dumper.rb +75 -0
- data/lib/pg_trunk/core/railtie/schema_migration.rb +22 -0
- data/lib/pg_trunk/core/railtie/statements.rb +21 -0
- data/lib/pg_trunk/core/railtie.rb +35 -0
- data/lib/pg_trunk/core/registry.rb +159 -0
- data/lib/pg_trunk/core/serializers/array_of_hashes_serializer.rb +28 -0
- data/lib/pg_trunk/core/serializers/array_of_strings_serializer.rb +29 -0
- data/lib/pg_trunk/core/serializers/array_of_symbols_serializer.rb +28 -0
- data/lib/pg_trunk/core/serializers/array_serializer.rb +22 -0
- data/lib/pg_trunk/core/serializers/lowercase_string_serializer.rb +21 -0
- data/lib/pg_trunk/core/serializers/multiline_text_serializer.rb +21 -0
- data/lib/pg_trunk/core/serializers/qualified_name_serializer.rb +27 -0
- data/lib/pg_trunk/core/serializers/symbol_serializer.rb +22 -0
- data/lib/pg_trunk/core/serializers.rb +16 -0
- data/lib/pg_trunk/core/validators/all_items_valid_validator.rb +15 -0
- data/lib/pg_trunk/core/validators/difference_validator.rb +19 -0
- data/lib/pg_trunk/core/validators.rb +10 -0
- data/lib/pg_trunk/core.rb +21 -0
- data/lib/pg_trunk/generators.rb +7 -0
- data/lib/pg_trunk/operations/check_constraints/add_check_constraint.rb +109 -0
- data/lib/pg_trunk/operations/check_constraints/base.rb +69 -0
- data/lib/pg_trunk/operations/check_constraints/drop_check_constraint.rb +60 -0
- data/lib/pg_trunk/operations/check_constraints/rename_check_constraint.rb +54 -0
- data/lib/pg_trunk/operations/check_constraints/validate_check_constraint.rb +39 -0
- data/lib/pg_trunk/operations/check_constraints.rb +14 -0
- data/lib/pg_trunk/operations/composite_types/base.rb +61 -0
- data/lib/pg_trunk/operations/composite_types/change_composite_type.rb +136 -0
- data/lib/pg_trunk/operations/composite_types/column.rb +118 -0
- data/lib/pg_trunk/operations/composite_types/create_composite_type.rb +99 -0
- data/lib/pg_trunk/operations/composite_types/drop_composite_type.rb +67 -0
- data/lib/pg_trunk/operations/composite_types/rename_composite_type.rb +44 -0
- data/lib/pg_trunk/operations/composite_types.rb +15 -0
- data/lib/pg_trunk/operations/domains/base.rb +46 -0
- data/lib/pg_trunk/operations/domains/change_domain.rb +140 -0
- data/lib/pg_trunk/operations/domains/constraint.rb +93 -0
- data/lib/pg_trunk/operations/domains/create_domain.rb +124 -0
- data/lib/pg_trunk/operations/domains/drop_domain.rb +65 -0
- data/lib/pg_trunk/operations/domains/rename_domain.rb +44 -0
- data/lib/pg_trunk/operations/domains.rb +15 -0
- data/lib/pg_trunk/operations/enums/base.rb +47 -0
- data/lib/pg_trunk/operations/enums/change.rb +55 -0
- data/lib/pg_trunk/operations/enums/change_enum.rb +119 -0
- data/lib/pg_trunk/operations/enums/create_enum.rb +83 -0
- data/lib/pg_trunk/operations/enums/drop_enum.rb +63 -0
- data/lib/pg_trunk/operations/enums/rename_enum.rb +44 -0
- data/lib/pg_trunk/operations/enums.rb +15 -0
- data/lib/pg_trunk/operations/foreign_keys/add_foreign_key.rb +174 -0
- data/lib/pg_trunk/operations/foreign_keys/base.rb +155 -0
- data/lib/pg_trunk/operations/foreign_keys/drop_foreign_key.rb +76 -0
- data/lib/pg_trunk/operations/foreign_keys/rename_foreign_key.rb +63 -0
- data/lib/pg_trunk/operations/foreign_keys.rb +16 -0
- data/lib/pg_trunk/operations/functions/base.rb +54 -0
- data/lib/pg_trunk/operations/functions/change_function.rb +108 -0
- data/lib/pg_trunk/operations/functions/create_function.rb +198 -0
- data/lib/pg_trunk/operations/functions/drop_function.rb +88 -0
- data/lib/pg_trunk/operations/functions/rename_function.rb +57 -0
- data/lib/pg_trunk/operations/functions.rb +14 -0
- data/lib/pg_trunk/operations/indexes/add_index.rb +68 -0
- data/lib/pg_trunk/operations/indexes.rb +10 -0
- data/lib/pg_trunk/operations/materialized_views/base.rb +79 -0
- data/lib/pg_trunk/operations/materialized_views/change_materialized_view.rb +139 -0
- data/lib/pg_trunk/operations/materialized_views/column.rb +94 -0
- data/lib/pg_trunk/operations/materialized_views/create_materialized_view.rb +170 -0
- data/lib/pg_trunk/operations/materialized_views/drop_materialized_view.rb +70 -0
- data/lib/pg_trunk/operations/materialized_views/refresh_materialized_view.rb +48 -0
- data/lib/pg_trunk/operations/materialized_views/rename_materialized_view.rb +61 -0
- data/lib/pg_trunk/operations/materialized_views.rb +17 -0
- data/lib/pg_trunk/operations/procedures/base.rb +42 -0
- data/lib/pg_trunk/operations/procedures/change_procedure.rb +107 -0
- data/lib/pg_trunk/operations/procedures/create_procedure.rb +146 -0
- data/lib/pg_trunk/operations/procedures/drop_procedure.rb +66 -0
- data/lib/pg_trunk/operations/procedures/rename_procedure.rb +57 -0
- data/lib/pg_trunk/operations/procedures.rb +14 -0
- data/lib/pg_trunk/operations/statistics/base.rb +94 -0
- data/lib/pg_trunk/operations/statistics/create_statistics.rb +181 -0
- data/lib/pg_trunk/operations/statistics/drop_statistics.rb +75 -0
- data/lib/pg_trunk/operations/statistics/rename_statistics.rb +48 -0
- data/lib/pg_trunk/operations/statistics.rb +13 -0
- data/lib/pg_trunk/operations/tables/create_table.rb +75 -0
- data/lib/pg_trunk/operations/tables.rb +10 -0
- data/lib/pg_trunk/operations/triggers/base.rb +119 -0
- data/lib/pg_trunk/operations/triggers/change_trigger.rb +82 -0
- data/lib/pg_trunk/operations/triggers/create_trigger.rb +208 -0
- data/lib/pg_trunk/operations/triggers/drop_trigger.rb +66 -0
- data/lib/pg_trunk/operations/triggers/rename_trigger.rb +71 -0
- data/lib/pg_trunk/operations/triggers.rb +14 -0
- data/lib/pg_trunk/operations/views/base.rb +38 -0
- data/lib/pg_trunk/operations/views/change_view.rb +90 -0
- data/lib/pg_trunk/operations/views/create_view.rb +115 -0
- data/lib/pg_trunk/operations/views/drop_view.rb +69 -0
- data/lib/pg_trunk/operations/views/rename_view.rb +58 -0
- data/lib/pg_trunk/operations/views.rb +14 -0
- data/lib/pg_trunk/operations.rb +23 -0
- data/lib/pg_trunk/version.rb +6 -0
- data/lib/pg_trunk.rb +27 -0
- data/pg_trunk.gemspec +34 -0
- data/spec/dummy/.gitignore +16 -0
- data/spec/dummy/Rakefile +15 -0
- data/spec/dummy/bin/bundle +6 -0
- data/spec/dummy/bin/rails +6 -0
- data/spec/dummy/bin/rake +6 -0
- data/spec/dummy/config/application.rb +18 -0
- data/spec/dummy/config/boot.rb +7 -0
- data/spec/dummy/config/database.yml +14 -0
- data/spec/dummy/config/environment.rb +7 -0
- data/spec/dummy/config.ru +6 -0
- data/spec/dummy/db/materialized_views/admin_users_v01.sql +1 -0
- data/spec/dummy/db/migrate/.keep +0 -0
- data/spec/dummy/db/schema.rb +18 -0
- data/spec/dummy/db/views/admin_users_v01.sql +1 -0
- data/spec/dummy/db/views/admin_users_v02.sql +1 -0
- data/spec/operations/check_constraints/add_check_constraint_spec.rb +85 -0
- data/spec/operations/check_constraints/drop_check_constraint_spec.rb +111 -0
- data/spec/operations/check_constraints/rename_check_constraint_spec.rb +90 -0
- data/spec/operations/composite_types/change_composite_type_spec.rb +257 -0
- data/spec/operations/composite_types/create_composite_type_spec.rb +55 -0
- data/spec/operations/composite_types/drop_composite_type_spec.rb +109 -0
- data/spec/operations/composite_types/rename_composite_type_spec.rb +74 -0
- data/spec/operations/dependency_resolver_spec.rb +177 -0
- data/spec/operations/domains/change_domain_spec.rb +287 -0
- data/spec/operations/domains/create_domain_spec.rb +69 -0
- data/spec/operations/domains/drop_domain_spec.rb +119 -0
- data/spec/operations/domains/rename_domain_spec.rb +70 -0
- data/spec/operations/enums/change_enum_spec.rb +157 -0
- data/spec/operations/enums/create_enum_spec.rb +40 -0
- data/spec/operations/enums/drop_enum_spec.rb +120 -0
- data/spec/operations/enums/rename_enum_spec.rb +72 -0
- data/spec/operations/foreign_keys/add_foreign_key_spec.rb +208 -0
- data/spec/operations/foreign_keys/drop_foreign_key_spec.rb +167 -0
- data/spec/operations/foreign_keys/rename_foreign_key_spec.rb +101 -0
- data/spec/operations/functions/change_function_spec.rb +166 -0
- data/spec/operations/functions/create_function_spec.rb +192 -0
- data/spec/operations/functions/drop_function_spec.rb +182 -0
- data/spec/operations/functions/rename_function_spec.rb +101 -0
- data/spec/operations/indexes/add_index_spec.rb +94 -0
- data/spec/operations/materialized_views/change_materialized_view_spec.rb +190 -0
- data/spec/operations/materialized_views/create_materialized_view_spec.rb +144 -0
- data/spec/operations/materialized_views/drop_materialized_view_spec.rb +145 -0
- data/spec/operations/materialized_views/refresh_materialized_view_spec.rb +79 -0
- data/spec/operations/materialized_views/rename_materialized_view_spec.rb +88 -0
- data/spec/operations/procedures/change_procedure_spec.rb +175 -0
- data/spec/operations/procedures/create_procedure_spec.rb +151 -0
- data/spec/operations/procedures/drop_procedure_spec.rb +159 -0
- data/spec/operations/procedures/rename_procedure_spec.rb +107 -0
- data/spec/operations/statistics/create_statistics_spec.rb +230 -0
- data/spec/operations/statistics/drop_statistics_spec.rb +106 -0
- data/spec/operations/statistics/rename_statistics_spec.rb +129 -0
- data/spec/operations/tables/create_table_spec.rb +53 -0
- data/spec/operations/tables/rename_table_spec.rb +37 -0
- data/spec/operations/triggers/change_trigger_spec.rb +195 -0
- data/spec/operations/triggers/create_trigger_spec.rb +104 -0
- data/spec/operations/triggers/drop_trigger_spec.rb +124 -0
- data/spec/operations/triggers/rename_trigger_spec.rb +160 -0
- data/spec/operations/views/change_view_spec.rb +144 -0
- data/spec/operations/views/create_view_spec.rb +134 -0
- data/spec/operations/views/drop_view_spec.rb +146 -0
- data/spec/operations/views/rename_view_spec.rb +85 -0
- data/spec/pg_trunk/dependencies_resolver_spec.rb +43 -0
- data/spec/spec_helper.rb +28 -0
- data/spec/support/migrations_helper.rb +376 -0
- metadata +348 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PGTrunk::Operations::Indexes
|
|
4
|
+
# @private
|
|
5
|
+
#
|
|
6
|
+
# PGTrunk excludes indexes from table definitions provided by Rails.
|
|
7
|
+
# That's why we have to fetch and dump indexes separately.
|
|
8
|
+
#
|
|
9
|
+
# We fetch indexes from the database by their names and oids,
|
|
10
|
+
# and then rely on the original method +ActiveRecord::SchemaDumper#add_index+
|
|
11
|
+
#
|
|
12
|
+
# We doesn't overload the method `create_table`, but
|
|
13
|
+
# keep the original implementation unchanged. That's why
|
|
14
|
+
# neither `to_sql`, `invert` or `generates_object` are necessary.
|
|
15
|
+
#
|
|
16
|
+
class AddIndex < PGTrunk::Operation
|
|
17
|
+
attribute :table, :pg_trunk_qualified_name
|
|
18
|
+
|
|
19
|
+
validates :oid, :table, presence: true
|
|
20
|
+
|
|
21
|
+
# Indexes are ordered by table and name
|
|
22
|
+
def <=>(other)
|
|
23
|
+
return unless other.is_a?(self.class)
|
|
24
|
+
|
|
25
|
+
result = table <=> other.table
|
|
26
|
+
result&.zero? ? super : result
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# SQL to fetch table names and oids from the database.
|
|
30
|
+
# We only extract (oid, table, name) for indexes that
|
|
31
|
+
# are not used as primary key constraints.
|
|
32
|
+
#
|
|
33
|
+
# Primary keys are added inside tables because
|
|
34
|
+
# they cannot depend on anything else.
|
|
35
|
+
from_sql do
|
|
36
|
+
<<~SQL
|
|
37
|
+
SELECT
|
|
38
|
+
c.oid,
|
|
39
|
+
(c.relnamespace::regnamespace || '.' || c.relname) AS name,
|
|
40
|
+
(t.relnamespace::regnamespace || '.' || t.relname) AS "table"
|
|
41
|
+
FROM pg_class c
|
|
42
|
+
-- ensure the table was created by a migration
|
|
43
|
+
JOIN pg_trunk p ON p.oid = c.oid
|
|
44
|
+
JOIN pg_index i ON i.indexrelid = c.oid
|
|
45
|
+
JOIN pg_class t ON t.oid = i.indrelid
|
|
46
|
+
-- ignore primary keys
|
|
47
|
+
WHERE NOT i.indisprimary
|
|
48
|
+
SQL
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Instead of defining +ruby_snippet+, we overload
|
|
52
|
+
# the +to_ruby+ to rely on the original implementation.
|
|
53
|
+
#
|
|
54
|
+
# We overloaded the +ActiveRecord::SchemaDumper+
|
|
55
|
+
# method +indexes_in_create+ so that it does nothing
|
|
56
|
+
# to exclude indexes from a table definition.
|
|
57
|
+
#
|
|
58
|
+
# @see +PGTrunk::SchemaDumper+ module (in `core/railtie`).
|
|
59
|
+
def to_ruby
|
|
60
|
+
indexes = PGTrunk.database.send(:indexes, table.lean)
|
|
61
|
+
index = indexes.find { |i| i.name == name.lean }
|
|
62
|
+
return unless index
|
|
63
|
+
|
|
64
|
+
line = PGTrunk.dumper.send(:index_parts, index).join(", ")
|
|
65
|
+
"add_index #{table.lean.inspect}, #{line}"
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: false
|
|
2
|
+
|
|
3
|
+
module PGTrunk::Operations::MaterializedViews
|
|
4
|
+
# @abstract
|
|
5
|
+
# @private
|
|
6
|
+
# Base class for operations with views
|
|
7
|
+
class Base < PGTrunk::Operation
|
|
8
|
+
# All attributes that can be used by view-related commands
|
|
9
|
+
attribute :algorithm, :pg_trunk_symbol
|
|
10
|
+
attribute :cluster_on, :string
|
|
11
|
+
attribute :columns, :pg_trunk_array_of_hashes, default: []
|
|
12
|
+
attribute :sql_definition, :pg_trunk_multiline_text
|
|
13
|
+
attribute :tablespace, :string
|
|
14
|
+
attribute :version, :integer, aliases: :revert_to_version
|
|
15
|
+
attribute :with_data, :boolean
|
|
16
|
+
|
|
17
|
+
def column(name, **opts)
|
|
18
|
+
columns << Column.new(name: name, **opts.except(:new_name))
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Load missed `sql_definition` from the external file
|
|
22
|
+
after_initialize { self.sql_definition ||= read_snippet_from(:materialized_views) }
|
|
23
|
+
after_initialize { columns.map! { |c| Column.build(c) } }
|
|
24
|
+
|
|
25
|
+
# Ensure correctness of present values
|
|
26
|
+
validates :algorithm, inclusion: %i[concurrently], allow_nil: true
|
|
27
|
+
validates :tablespace, exclusion: { in: [UNDEFINED] }, allow_nil: true
|
|
28
|
+
validates :columns, "PGTrunk/all_items_valid": true, allow_nil: true
|
|
29
|
+
|
|
30
|
+
# Use comparison by name from pg_trunk operations base class (default)
|
|
31
|
+
# Support name as the only positional argument (default)
|
|
32
|
+
|
|
33
|
+
ruby_snippet do |s|
|
|
34
|
+
s.ruby_param(name.lean) if name.present?
|
|
35
|
+
s.ruby_param(version: version) if version.present?
|
|
36
|
+
s.ruby_param(to: new_name.lean) if new_name.present?
|
|
37
|
+
s.ruby_param(if_exists: true) if if_exists
|
|
38
|
+
s.ruby_param(if_not_exists: true) if if_not_exists
|
|
39
|
+
s.ruby_param(force: :cascade) if force == :cascade
|
|
40
|
+
|
|
41
|
+
s.ruby_line(:sql_definition, sql_definition) if version.blank?
|
|
42
|
+
s.ruby_line(:tablespace, tablespace) if tablespace.present?
|
|
43
|
+
s.ruby_line(:cluster_on, cluster_on) if cluster_on.present?
|
|
44
|
+
columns.reject(&:new_name).each do |c|
|
|
45
|
+
s.ruby_line(:column, c.name, **c.changes)
|
|
46
|
+
end
|
|
47
|
+
columns.select(&:new_name).each do |c|
|
|
48
|
+
s.ruby_line(:rename_column, c.name, to: c.new_name)
|
|
49
|
+
end
|
|
50
|
+
s.ruby_line(:with_data, false) if with_data == false
|
|
51
|
+
s.ruby_line(:comment, comment, from: from_comment) if comment
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
# A special constant to distinct cluster resetting from nil
|
|
57
|
+
RESET = Object.new.freeze
|
|
58
|
+
|
|
59
|
+
def validate_naming!(name: nil, **)
|
|
60
|
+
errors.add :columns, "has undefined names" if name.blank?
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def validate_definition!(name: nil, **opts)
|
|
64
|
+
return if opts.none? { |_, value| value == UNDEFINED }
|
|
65
|
+
|
|
66
|
+
errors.add :base, "Definition of column #{name} can't be reverted"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def validate_statistics!(name: nil, **opts)
|
|
70
|
+
opts.values_at(*STATISTICS).each do |value|
|
|
71
|
+
next if value.nil? || value == UNDEFINED
|
|
72
|
+
next if value.is_a?(Numeric) && value >= 0
|
|
73
|
+
|
|
74
|
+
errors.add :base, "Column #{name} has invalid statistics #{value}"
|
|
75
|
+
break
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# frozen_string_literal: false
|
|
2
|
+
|
|
3
|
+
# @!method ActiveRecord::Migration#change_materialized_view(name, **options, &block)
|
|
4
|
+
# Modify a materialized view
|
|
5
|
+
#
|
|
6
|
+
# @param [#to_s] name (nil) The qualified name of the view
|
|
7
|
+
# @option [Boolean] :if_exists (false) Suppress the error when the view is absent
|
|
8
|
+
# @yield [Proc] the block with the view's definition
|
|
9
|
+
# @yieldparam The receiver of methods specifying the view
|
|
10
|
+
#
|
|
11
|
+
# The operation enables to alter a view without recreating
|
|
12
|
+
# its from scratch. You can rename columns, change their
|
|
13
|
+
# storage settings (how the column is TOAST-ed), or
|
|
14
|
+
# customize their statistics.
|
|
15
|
+
#
|
|
16
|
+
# change_materialized_view "admin_users" do |v|
|
|
17
|
+
# v.rename_column "name", to: "full_name"
|
|
18
|
+
# v.column "name", storage: "extended", from_storage: "expanded"
|
|
19
|
+
# v.column "admin", n_distinct: 2
|
|
20
|
+
# v.column "role", statistics: 100
|
|
21
|
+
# end
|
|
22
|
+
#
|
|
23
|
+
# Notice that renaming will be done AFTER all changes even
|
|
24
|
+
# though the order of declarations can be different.
|
|
25
|
+
#
|
|
26
|
+
# As in the snippet above, to make the change invertible,
|
|
27
|
+
# you have to define a previous storage via `from_storage` option.
|
|
28
|
+
# The inversion would always reset statistics (set it to 0).
|
|
29
|
+
#
|
|
30
|
+
# In addition to changing columns, the operation enables
|
|
31
|
+
# to set a default clustering by given index:
|
|
32
|
+
#
|
|
33
|
+
# change_materialized_view "admin_users" do |v|
|
|
34
|
+
# v.cluster_on "admin_users_by_names_idx"
|
|
35
|
+
# end
|
|
36
|
+
#
|
|
37
|
+
# The clustering is invertible, but its inversion does nothing,
|
|
38
|
+
# keeping the clustering unchanged.
|
|
39
|
+
#
|
|
40
|
+
# The comment can also be changed:
|
|
41
|
+
#
|
|
42
|
+
# change_materialized_view "admin_users" do |v|
|
|
43
|
+
# v.comment "Admin users", from: "Admin users only"
|
|
44
|
+
# end
|
|
45
|
+
#
|
|
46
|
+
# Notice, that without `from` option the operation is still
|
|
47
|
+
# invertible, but its inversion would delete the comment.
|
|
48
|
+
# It can also be reset to the blank string explicitly:
|
|
49
|
+
#
|
|
50
|
+
# change_materialized_view "admin_users" do |v|
|
|
51
|
+
# v.comment "", from: "Admin users only"
|
|
52
|
+
# end
|
|
53
|
+
#
|
|
54
|
+
# With the `if_exists: true` option, the operation won't fail
|
|
55
|
+
# even when the view wasn't existed. At the same time,
|
|
56
|
+
# this option makes a migration irreversible due to uncertainty
|
|
57
|
+
# of the previous state of the database.
|
|
58
|
+
|
|
59
|
+
module PGTrunk::Operations::MaterializedViews
|
|
60
|
+
# @private
|
|
61
|
+
class ChangeMaterializedView < Base
|
|
62
|
+
# A method to be called in a block
|
|
63
|
+
def rename_column(name, to:)
|
|
64
|
+
columns << Column.new(name: name, new_name: to)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Operation-specific validations
|
|
68
|
+
validate { errors.add :base, "Changes can't be blank" if changes.blank? }
|
|
69
|
+
validates :algorithm, :force, :if_not_exists, :new_name, :sql_definition,
|
|
70
|
+
:tablespace, :version, :with_data, absence: true
|
|
71
|
+
|
|
72
|
+
def to_sql(_version)
|
|
73
|
+
[
|
|
74
|
+
*change_columns,
|
|
75
|
+
*rename_columns,
|
|
76
|
+
*cluster_view,
|
|
77
|
+
*update_comment,
|
|
78
|
+
].join(" ")
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def invert
|
|
82
|
+
irreversible!("if_exists: true") if if_exists
|
|
83
|
+
undefined = inversion.select { |_, v| v.nil? }.keys.join(", ").presence
|
|
84
|
+
raise IrreversibleMigration.new(self, nil, <<~MSG.squish) if undefined
|
|
85
|
+
Undefined values to revert #{undefined}.
|
|
86
|
+
MSG
|
|
87
|
+
|
|
88
|
+
self.class.new(name: name, **inversion) if inversion.any?
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
private
|
|
92
|
+
|
|
93
|
+
def changes
|
|
94
|
+
@changes ||= {
|
|
95
|
+
columns: columns.presence,
|
|
96
|
+
cluster_on: cluster_on,
|
|
97
|
+
comment: comment,
|
|
98
|
+
}.compact
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def inversion
|
|
102
|
+
@inversion ||= {
|
|
103
|
+
columns: columns.map(&:invert).presence,
|
|
104
|
+
comment: from_comment,
|
|
105
|
+
}.slice(*changes.keys)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def alter_view
|
|
109
|
+
@alter_view ||= begin
|
|
110
|
+
sql = "ALTER MATERIALIZED VIEW"
|
|
111
|
+
sql << " IF EXISTS" if if_exists
|
|
112
|
+
sql << " #{name.to_sql}"
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def change_columns
|
|
117
|
+
changes = columns.reject(&:new_name).map(&:to_sql).join(", ")
|
|
118
|
+
"#{alter_view} #{changes};" if changes.present?
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def rename_columns
|
|
122
|
+
changes = columns.select(&:new_name).map(&:to_sql).join(", ")
|
|
123
|
+
"#{alter_view} #{changes};" if changes.present?
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def cluster_view
|
|
127
|
+
"#{alter_view} CLUSTER ON #{cluster_on.inspect};" if cluster_on.present?
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def update_comment
|
|
131
|
+
return if comment.nil?
|
|
132
|
+
|
|
133
|
+
<<~SQL
|
|
134
|
+
COMMENT ON MATERIALIZED VIEW #{name.to_sql}
|
|
135
|
+
IS $comment$#{comment}$comment$;
|
|
136
|
+
SQL
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PGTrunk::Operations::MaterializedViews
|
|
4
|
+
# @private
|
|
5
|
+
# Definition for the column change
|
|
6
|
+
class Column
|
|
7
|
+
include ActiveModel::Model
|
|
8
|
+
include ActiveModel::Attributes
|
|
9
|
+
include ActiveModel::Validations
|
|
10
|
+
|
|
11
|
+
def self.build(data)
|
|
12
|
+
data.is_a?(self) ? data : new(**data)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
attribute :name, :string
|
|
16
|
+
attribute :new_name, :string
|
|
17
|
+
attribute :storage, :pg_trunk_symbol
|
|
18
|
+
attribute :from_storage, :pg_trunk_symbol
|
|
19
|
+
attribute :statistics, :integer
|
|
20
|
+
attribute :n_distinct, :float
|
|
21
|
+
|
|
22
|
+
# Hashify definitions
|
|
23
|
+
|
|
24
|
+
def to_h
|
|
25
|
+
@to_h ||=
|
|
26
|
+
attributes
|
|
27
|
+
.symbolize_keys
|
|
28
|
+
.transform_values(&:presence)
|
|
29
|
+
.compact
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def opts
|
|
33
|
+
to_h.except(:name)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def changes
|
|
37
|
+
opts.except(:new_name)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def invert
|
|
41
|
+
return { name: new_name, new_name: name } if new_name.present?
|
|
42
|
+
|
|
43
|
+
{
|
|
44
|
+
name: name,
|
|
45
|
+
storage: (from_storage || :UNDEFINED if storage.present?),
|
|
46
|
+
statistics: (0 if statistics.present?),
|
|
47
|
+
n_distinct: (0 if n_distinct.present?),
|
|
48
|
+
}.compact
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Ensure if the definition was built properly
|
|
52
|
+
|
|
53
|
+
validates :name, presence: true
|
|
54
|
+
validate { errors.add(:base, :blank) if opts.none? }
|
|
55
|
+
validates :statistics,
|
|
56
|
+
numericality: { greater_than_or_equal_to: 0 },
|
|
57
|
+
allow_nil: true
|
|
58
|
+
validates :n_distinct,
|
|
59
|
+
numericality: { greater_than_or_equal_to: -1 },
|
|
60
|
+
allow_nil: true
|
|
61
|
+
validates :storage, :from_storage,
|
|
62
|
+
inclusion: { in: %i[plain extended external main] },
|
|
63
|
+
allow_nil: true
|
|
64
|
+
validate do
|
|
65
|
+
next unless n_distinct&.positive?
|
|
66
|
+
next if n_distinct.to_i == n_distinct
|
|
67
|
+
|
|
68
|
+
errors.add :n_distinct, "with positive value must be integer"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def error_messages
|
|
72
|
+
validate
|
|
73
|
+
errors&.messages&.flat_map do |k, v|
|
|
74
|
+
v.map do |msg|
|
|
75
|
+
"Column #{name.inspect}: #{k == :base ? msg : "#{k} #{msg}"}"
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Build SQL snippets for the column definition
|
|
81
|
+
# @return [Array<String>]
|
|
82
|
+
def to_sql(_version = "10")
|
|
83
|
+
return ["RENAME COLUMN #{name.inspect} TO #{new_name.inspect}"] if new_name
|
|
84
|
+
|
|
85
|
+
alter = "ALTER COLUMN #{name.inspect}"
|
|
86
|
+
[
|
|
87
|
+
*("#{alter} SET STATISTICS #{statistics}" if statistics),
|
|
88
|
+
*("#{alter} SET (n_distinct = #{n_distinct})" if n_distinct),
|
|
89
|
+
*("#{alter} RESET (n_distinct)" if n_distinct&.zero?),
|
|
90
|
+
*("#{alter} SET STORAGE #{storage.to_s.upcase}" if storage.present?),
|
|
91
|
+
]
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# frozen_string_literal: false
|
|
2
|
+
|
|
3
|
+
# @!method ActiveRecord::Migration#create_materialized_view(name, **options, &block)
|
|
4
|
+
# Create a materialized view
|
|
5
|
+
#
|
|
6
|
+
# @param [#to_s] name (nil) The qualified name of the view
|
|
7
|
+
# @option [Boolean] :if_not_exists (false) Suppress the error when a view has been already created
|
|
8
|
+
# @option [#to_s] :sql_definition (nil) The snippet containing the query
|
|
9
|
+
# @option [#to_i] :version (nil)
|
|
10
|
+
# The alternative way to set sql_definition by referencing to a file containing the snippet
|
|
11
|
+
# @option [#to_s] :tablespace (nil) The tablespace for the view
|
|
12
|
+
# @option [Boolean] :with_data (true) If the view should be populated after creation
|
|
13
|
+
# @option [#to_s] :comment (nil) The comment describing the view
|
|
14
|
+
# @yield [Proc] the block with the view's definition
|
|
15
|
+
# @yieldparam The receiver of methods specifying the view
|
|
16
|
+
#
|
|
17
|
+
# The operation creates the view using its `sql_definition`:
|
|
18
|
+
#
|
|
19
|
+
# create_materialized_view("views.admin_users", sql_definition: <<~SQL)
|
|
20
|
+
# SELECT id, name FROM users WHERE admin;
|
|
21
|
+
# SQL
|
|
22
|
+
#
|
|
23
|
+
# For compatibility to the `scenic` gem, we also support
|
|
24
|
+
# adding a definition via its version:
|
|
25
|
+
#
|
|
26
|
+
# create_materialized_view "admin_users", version: 1
|
|
27
|
+
#
|
|
28
|
+
# It is expected, that a `db/materialized_views/admin_users_v01.sql`
|
|
29
|
+
# to contain the SQL snippet.
|
|
30
|
+
#
|
|
31
|
+
# The tablespace can be specified for the created view.
|
|
32
|
+
# Notice that later it can't be changed (in PostgreSQL all rows
|
|
33
|
+
# can be moved to another tablespace, but we don't support
|
|
34
|
+
# this feature yet).
|
|
35
|
+
#
|
|
36
|
+
# create_materialized_view "admin_users" do |v|
|
|
37
|
+
# v.tablespace "fast_ssd"
|
|
38
|
+
# v.sql_definition <<~SQL
|
|
39
|
+
# SELECT id, name, password, admin, on_duty
|
|
40
|
+
# FROM users
|
|
41
|
+
# WHERE admin
|
|
42
|
+
# SQL
|
|
43
|
+
# end
|
|
44
|
+
#
|
|
45
|
+
# You can also set a comment describing the view,
|
|
46
|
+
# and redefine the storage options for some TOAST-ed columns,
|
|
47
|
+
# as well as their custom statistics:
|
|
48
|
+
#
|
|
49
|
+
# create_materialized_view "admin_users" do |v|
|
|
50
|
+
# v.sql_definition <<~SQL
|
|
51
|
+
# SELECT id, name, password, admin, on_duty
|
|
52
|
+
# FROM users
|
|
53
|
+
# WHERE admin
|
|
54
|
+
# SQL
|
|
55
|
+
#
|
|
56
|
+
# v.column "password", storage: "external" # to avoid compression
|
|
57
|
+
# v.column "password", n_distinct: -1 # linear dependency
|
|
58
|
+
# v.column "admin", n_distinct: 1 # exact number of values
|
|
59
|
+
# v.column "on_duty", statistics: 2 # the total number of values
|
|
60
|
+
#
|
|
61
|
+
# v.comment "Admin users only"
|
|
62
|
+
# end
|
|
63
|
+
#
|
|
64
|
+
# With the `replace_existing: true` option the operation
|
|
65
|
+
# would use `CREATE OR REPLACE VIEW` command, so it
|
|
66
|
+
# can be used to "update" (or reload) the existing view.
|
|
67
|
+
#
|
|
68
|
+
# create_materialized_view "admin_users",
|
|
69
|
+
# version: 1,
|
|
70
|
+
# replace_existing: true
|
|
71
|
+
#
|
|
72
|
+
# This option makes the migration irreversible due to uncertainty
|
|
73
|
+
# of the previous state of the database.
|
|
74
|
+
|
|
75
|
+
module PGTrunk::Operations::MaterializedViews
|
|
76
|
+
# @private
|
|
77
|
+
class CreateMaterializedView < Base
|
|
78
|
+
validates :sql_definition, presence: true
|
|
79
|
+
# Forbid these attributes
|
|
80
|
+
validates :algorithm, :cluster_on, :force, :if_exists, :new_name, absence: true
|
|
81
|
+
|
|
82
|
+
from_sql do |_version|
|
|
83
|
+
<<~SQL
|
|
84
|
+
SELECT
|
|
85
|
+
c.oid,
|
|
86
|
+
(c.relnamespace::regnamespace || '.' || c.relname) AS name,
|
|
87
|
+
t.spcname AS "tablespace",
|
|
88
|
+
replace(pg_get_viewdef(c.oid, 60), ';', '') AS sql_definition,
|
|
89
|
+
(CASE WHEN NOT m.ispopulated THEN false END) AS with_data,
|
|
90
|
+
(
|
|
91
|
+
SELECT
|
|
92
|
+
json_agg(
|
|
93
|
+
json_build_object(
|
|
94
|
+
'name', a.attname,
|
|
95
|
+
'storage', (
|
|
96
|
+
CASE
|
|
97
|
+
WHEN a.attstorage = 'p' THEN 'plain'
|
|
98
|
+
WHEN a.attstorage = 'e' THEN 'external'
|
|
99
|
+
WHEN a.attstorage = 'x' THEN 'extended'
|
|
100
|
+
WHEN a.attstorage = 'm' THEN 'main'
|
|
101
|
+
END
|
|
102
|
+
)
|
|
103
|
+
) ORDER BY a.attnum
|
|
104
|
+
)
|
|
105
|
+
FROM pg_attribute a LEFT JOIN pg_type t ON t.oid = a.atttypid
|
|
106
|
+
WHERE c.oid = a.attrelid AND t.typstorage != a.attstorage
|
|
107
|
+
) AS "columns",
|
|
108
|
+
d.description AS comment
|
|
109
|
+
FROM pg_class c
|
|
110
|
+
JOIN pg_trunk e ON e.oid = c.oid AND e.classid = 'pg_class'::regclass
|
|
111
|
+
JOIN pg_matviews m ON m.matviewname = c.relname
|
|
112
|
+
AND m.schemaname::regnamespace = c.relnamespace::regnamespace
|
|
113
|
+
LEFT JOIN pg_tablespace t ON t.oid = c.reltablespace
|
|
114
|
+
LEFT JOIN pg_description d ON d.objoid = c.oid
|
|
115
|
+
AND d.classoid = 'pg_class'::regclass
|
|
116
|
+
WHERE c.relkind = 'm';
|
|
117
|
+
SQL
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def to_sql(_version)
|
|
121
|
+
[create_view, *alter_columns, *create_comment, register_view].join(" ")
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def invert
|
|
125
|
+
irreversible!("if_not_exists: true") if if_not_exists
|
|
126
|
+
DropMaterializedView.new(name: name)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
private
|
|
130
|
+
|
|
131
|
+
def create_view
|
|
132
|
+
sql = "CREATE MATERIALIZED VIEW"
|
|
133
|
+
sql << " IF NOT EXISTS" if if_not_exists
|
|
134
|
+
sql << " #{name.to_sql}"
|
|
135
|
+
sql << " TABLESPACE #{tablespace.inspect}" if tablespace.present?
|
|
136
|
+
sql << " AS #{sql_definition}"
|
|
137
|
+
sql << " WITH NO DATA" if with_data == false
|
|
138
|
+
sql << ";"
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def alter_columns
|
|
142
|
+
return if columns.blank?
|
|
143
|
+
|
|
144
|
+
sql = "ALTER MATERIALIZED VIEW #{name.to_sql}"
|
|
145
|
+
sql << columns.flat_map(&:to_sql).join(", ")
|
|
146
|
+
sql << ";"
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def create_comment
|
|
150
|
+
return if comment.blank?
|
|
151
|
+
|
|
152
|
+
<<~SQL
|
|
153
|
+
COMMENT ON MATERIALIZED VIEW #{name.to_sql}
|
|
154
|
+
IS $comment$#{comment}$comment$;
|
|
155
|
+
SQL
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def register_view
|
|
159
|
+
<<~SQL.squish
|
|
160
|
+
INSERT INTO pg_trunk (oid, classid)
|
|
161
|
+
SELECT oid, 'pg_class'::regclass
|
|
162
|
+
FROM pg_class
|
|
163
|
+
WHERE relname = #{name.quoted}
|
|
164
|
+
AND relnamespace = #{name.namespace}
|
|
165
|
+
AND relkind = 'm'
|
|
166
|
+
ON CONFLICT DO NOTHING;
|
|
167
|
+
SQL
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: false
|
|
2
|
+
|
|
3
|
+
# @!method ActiveRecord::Migration#drop_materialized_view(name, **options, &block)
|
|
4
|
+
# Drop a materialized view
|
|
5
|
+
#
|
|
6
|
+
# @param [#to_s] name (nil) The qualified name of the view
|
|
7
|
+
# @option [Boolean] :if_exists (false) Suppress the error when the view is absent
|
|
8
|
+
# @option [Symbol] :force (:restrict) How to process dependent objects (`:cascade` or `:restrict`)
|
|
9
|
+
# @option [#to_s] :sql_definition (nil) The snippet containing the query
|
|
10
|
+
# @option [#to_i] :revert_to_version (nil)
|
|
11
|
+
# The alternative way to set sql_definition by referencing to a file containing the snippet
|
|
12
|
+
# @option [#to_s] :tablespace (nil) The tablespace for the view
|
|
13
|
+
# @option [Boolean] :with_data (true) If the view should be populated after creation
|
|
14
|
+
# @option [#to_s] :comment (nil) The comment describing the view
|
|
15
|
+
# @yield [Proc] the block with the view's definition
|
|
16
|
+
# @yieldparam The receiver of methods specifying the view
|
|
17
|
+
#
|
|
18
|
+
# The operation drops a materialized view identified by its
|
|
19
|
+
# qualified name (it can include a schema).
|
|
20
|
+
#
|
|
21
|
+
# drop_materialized_view "views.admin_users"
|
|
22
|
+
#
|
|
23
|
+
# To make the operation invertible, use the same options
|
|
24
|
+
# as in the `create_view` operation.
|
|
25
|
+
#
|
|
26
|
+
# drop_materialized_view "views.admin_users" do |v|
|
|
27
|
+
# v.sql_definition "SELECT name, password FROM users WHERE admin;"
|
|
28
|
+
# v.column "password", storage: "external" # prevent compression
|
|
29
|
+
# v.with_data false
|
|
30
|
+
# v.comment "Admin users only"
|
|
31
|
+
# end
|
|
32
|
+
#
|
|
33
|
+
# You can also use a version-base SQL definition like:
|
|
34
|
+
#
|
|
35
|
+
# drop_materialized_view "admin_users", revert_to_version: 1
|
|
36
|
+
#
|
|
37
|
+
# With the `force: :cascade` option the operation would remove
|
|
38
|
+
# all the objects which depend on the view.
|
|
39
|
+
#
|
|
40
|
+
# drop_materialized_view "admin_users", force: :cascade
|
|
41
|
+
#
|
|
42
|
+
# With the `if_exists: true` option the operation won't fail
|
|
43
|
+
# even when the view was absent in the database.
|
|
44
|
+
#
|
|
45
|
+
# drop_materialized_view "admin_users", if_exists: true
|
|
46
|
+
#
|
|
47
|
+
# Both options make a migration irreversible due to uncertainty
|
|
48
|
+
# of the previous state of the database.
|
|
49
|
+
|
|
50
|
+
module PGTrunk::Operations::MaterializedViews
|
|
51
|
+
# @private
|
|
52
|
+
class DropMaterializedView < Base
|
|
53
|
+
# Forbid these attributes
|
|
54
|
+
validates :algorithm, :cluster_on, :if_not_exists, :new_name, absence: true
|
|
55
|
+
|
|
56
|
+
def to_sql(_version)
|
|
57
|
+
sql = "DROP MATERIALIZED VIEW"
|
|
58
|
+
sql << " IF EXISTS" if if_exists
|
|
59
|
+
sql << " #{name.to_sql}"
|
|
60
|
+
sql << " CASCADE" if force == :cascade
|
|
61
|
+
sql << ";"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def invert
|
|
65
|
+
irreversible!("if_exists: true") if if_exists
|
|
66
|
+
irreversible!("force: :cascade") if force == :cascade
|
|
67
|
+
CreateMaterializedView.new(**to_h.except(:force))
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: false
|
|
2
|
+
|
|
3
|
+
# @!method ActiveRecord::Migration#refresh_materialized_view(name, **options)
|
|
4
|
+
# Refresh a materialized view
|
|
5
|
+
#
|
|
6
|
+
# @param [#to_s] name (nil) The qualified name of the view
|
|
7
|
+
# @option [Boolean] :with_data (true) If the view should be populated after creation
|
|
8
|
+
# @option [Symbol] :algorithm (nil) Makes the operation concurrent when set to :concurrently
|
|
9
|
+
#
|
|
10
|
+
# The operation enables refreshing a materialized view
|
|
11
|
+
# by reloading its underlying SQL query:
|
|
12
|
+
#
|
|
13
|
+
# refresh_materialized_view "admin_users"
|
|
14
|
+
#
|
|
15
|
+
# The option `algorithm: :concurrently` acts exactly
|
|
16
|
+
# like in the `create_index` definition. You should
|
|
17
|
+
# possibly add the `disable_ddl_transaction!` command
|
|
18
|
+
# to the migration as well.
|
|
19
|
+
#
|
|
20
|
+
# With option `with_data: false` the command won't
|
|
21
|
+
# update the data. This option can't be used along with
|
|
22
|
+
# the `:algorithm`.
|
|
23
|
+
#
|
|
24
|
+
# The operation is always reversible, though its
|
|
25
|
+
# inversion does nothing.
|
|
26
|
+
|
|
27
|
+
module PGTrunk::Operations::MaterializedViews
|
|
28
|
+
# @private
|
|
29
|
+
class RefreshMaterializedView < Base
|
|
30
|
+
validate do
|
|
31
|
+
errors.add :algorithm, :present if with_data == false && algorithm
|
|
32
|
+
end
|
|
33
|
+
validates :cluster_on, :columns, :force, :if_exists, :if_not_exists,
|
|
34
|
+
:new_name, :sql_definition, :tablespace, :version, :comment,
|
|
35
|
+
absence: true
|
|
36
|
+
|
|
37
|
+
def to_sql(_version)
|
|
38
|
+
sql = "REFRESH MATERIALIZED VIEW"
|
|
39
|
+
sql << " CONCURRENTLY" if algorithm == :concurrently
|
|
40
|
+
sql << " #{name.to_sql}"
|
|
41
|
+
sql << " WITH NO DATA" if with_data == false
|
|
42
|
+
sql << ";"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# The operation is reversible but its inversion does nothing
|
|
46
|
+
def invert; end
|
|
47
|
+
end
|
|
48
|
+
end
|