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,61 @@
|
|
|
1
|
+
# frozen_string_literal: false
|
|
2
|
+
|
|
3
|
+
# @!method ActiveRecord::Migration#rename_materialized_view(name, **options)
|
|
4
|
+
# Change the name and/or schema of a materialized view
|
|
5
|
+
#
|
|
6
|
+
# @param [#to_s] :name (nil) The qualified name of the view
|
|
7
|
+
# @option [#to_s] :to (nil) The new qualified name for the view
|
|
8
|
+
# @option [Boolean] :if_exists (false) Suppress the error when the view is absent
|
|
9
|
+
#
|
|
10
|
+
# A materialized view can be renamed by changing both the name
|
|
11
|
+
# and the schema (namespace) it belongs to.
|
|
12
|
+
#
|
|
13
|
+
# rename_materialized_view "views.admin_users", to: "admins"
|
|
14
|
+
#
|
|
15
|
+
# With the `if_exists: true` option, the operation won't fail
|
|
16
|
+
# even when the view wasn't existed.
|
|
17
|
+
#
|
|
18
|
+
# rename_materialized_view "admin_users",
|
|
19
|
+
# to: "admins",
|
|
20
|
+
# if_exists: true
|
|
21
|
+
#
|
|
22
|
+
# At the same time, the option makes a migration irreversible
|
|
23
|
+
# due to uncertainty of the previous state of the database.
|
|
24
|
+
|
|
25
|
+
module PGTrunk::Operations::MaterializedViews
|
|
26
|
+
# @private
|
|
27
|
+
class RenameMaterializedView < Base
|
|
28
|
+
validates :new_name, presence: true
|
|
29
|
+
validates :algorithm, :cluster_on, :columns, :force, :with_data, :comment,
|
|
30
|
+
:if_not_exists, :sql_definition, :tablespace, :version,
|
|
31
|
+
absence: true
|
|
32
|
+
|
|
33
|
+
def to_sql(_version)
|
|
34
|
+
[*change_schema, *change_name].join("; ")
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def invert
|
|
38
|
+
irreversible!("if_exists: true") if if_exists
|
|
39
|
+
self.class.new(**to_h, name: new_name, to: name)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def change_schema
|
|
45
|
+
return if name.schema == new_name.schema
|
|
46
|
+
|
|
47
|
+
sql = "ALTER MATERIALIZED VIEW"
|
|
48
|
+
sql << " IF EXISTS" if if_exists
|
|
49
|
+
sql << " #{name.to_sql} SET SCHEMA #{new_name.schema.inspect};"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def change_name
|
|
53
|
+
return if new_name.name == name.name
|
|
54
|
+
|
|
55
|
+
moved = name.merge(schema: new_name.schema)
|
|
56
|
+
sql = "ALTER MATERIALIZED VIEW"
|
|
57
|
+
sql << " IF EXISTS" if if_exists
|
|
58
|
+
sql << " #{moved.to_sql} RENAME TO #{new_name.name.inspect};"
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# nodoc
|
|
4
|
+
module PGTrunk::Operations
|
|
5
|
+
# @private
|
|
6
|
+
# Namespace for operations with materialized views
|
|
7
|
+
module MaterializedViews
|
|
8
|
+
require_relative "materialized_views/column"
|
|
9
|
+
require_relative "materialized_views/base"
|
|
10
|
+
|
|
11
|
+
require_relative "materialized_views/change_materialized_view"
|
|
12
|
+
require_relative "materialized_views/create_materialized_view"
|
|
13
|
+
require_relative "materialized_views/drop_materialized_view"
|
|
14
|
+
require_relative "materialized_views/refresh_materialized_view"
|
|
15
|
+
require_relative "materialized_views/rename_materialized_view"
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: false
|
|
2
|
+
|
|
3
|
+
module PGTrunk::Operations::Procedures
|
|
4
|
+
# @abstract
|
|
5
|
+
# @private
|
|
6
|
+
# Base class for operations with procedures
|
|
7
|
+
class Base < PGTrunk::Operation
|
|
8
|
+
# All attributes that can be used by procedure-related commands
|
|
9
|
+
attribute :body, :pg_trunk_multiline_text
|
|
10
|
+
attribute :language, :pg_trunk_lowercase_string
|
|
11
|
+
attribute :replace_existing, :boolean
|
|
12
|
+
attribute :security, :pg_trunk_symbol
|
|
13
|
+
|
|
14
|
+
# Ensure correctness of present values
|
|
15
|
+
validates :security, inclusion: { in: %i[invoker definer] }, allow_nil: true
|
|
16
|
+
validates :force, :if_not_exists, absence: true
|
|
17
|
+
validate do
|
|
18
|
+
errors.add :body, "can't contain SQL injection with $$" if body&.include?("$$")
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Use comparison by name from pg_trunk operations base class (default)
|
|
22
|
+
# Support name as the only positional argument (default)
|
|
23
|
+
|
|
24
|
+
ruby_snippet do |s|
|
|
25
|
+
s.ruby_param(name.lean) if name.present?
|
|
26
|
+
s.ruby_param(to: new_name.lean) if new_name.present?
|
|
27
|
+
s.ruby_param(if_exists: true) if if_exists
|
|
28
|
+
s.ruby_param(replace_existing: true) if replace_existing
|
|
29
|
+
|
|
30
|
+
s.ruby_line(:language, language.downcase) if language&.!= "sql"
|
|
31
|
+
s.ruby_line(:security, security) if security&.!= :invoker
|
|
32
|
+
s.ruby_line(:body, body, from: from_body)
|
|
33
|
+
s.ruby_line(:comment, comment, from: from_comment)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def check_version!(version)
|
|
39
|
+
raise "Procedures are supported in PostgreSQL v11+" if version < "11"
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# frozen_string_literal: false
|
|
2
|
+
|
|
3
|
+
# @!method ActiveRecord::Migration#change_procedure(name, **options, &block)
|
|
4
|
+
# Modify a procedure
|
|
5
|
+
#
|
|
6
|
+
# @param [#to_s] name (nil) The qualified name of the procedure
|
|
7
|
+
# @option [Boolean] :if_exists (false) Suppress the error when the procedure is absent
|
|
8
|
+
# @yield [Proc] the block with the procedure's definition
|
|
9
|
+
# @yieldparam The receiver of methods specifying the procedure
|
|
10
|
+
#
|
|
11
|
+
# The operation changes the procedure without dropping it
|
|
12
|
+
# (which is useful when there are other objects
|
|
13
|
+
# using the function and you don't want to change them all).
|
|
14
|
+
#
|
|
15
|
+
# You can change any property except for the name
|
|
16
|
+
# (use `rename_function` instead) and `language`.
|
|
17
|
+
#
|
|
18
|
+
# change_procedure "metadata.set_foo(a int)" do |p|
|
|
19
|
+
# p.body <<~SQL
|
|
20
|
+
# SET foo = a
|
|
21
|
+
# SQL
|
|
22
|
+
# p.security :invoker
|
|
23
|
+
# p.comment "Multiplies 2 integers"
|
|
24
|
+
# SQL
|
|
25
|
+
#
|
|
26
|
+
# The example above is not invertible because of uncertainty
|
|
27
|
+
# about the previous state of body and comment.
|
|
28
|
+
# To define them, use a from options (available in a block syntax only):
|
|
29
|
+
#
|
|
30
|
+
# change_procedure "metadata.set_foo(a int)" do |p|
|
|
31
|
+
# p.body <<~SQL, from: <<~SQL
|
|
32
|
+
# SET foo = a
|
|
33
|
+
# SQL
|
|
34
|
+
# SET foo = -a
|
|
35
|
+
# SQL
|
|
36
|
+
# p.comment <<~MSG, from: <<~MSG
|
|
37
|
+
# Multiplies 2 integers
|
|
38
|
+
# MSG
|
|
39
|
+
# Multiplies ints
|
|
40
|
+
# MSG
|
|
41
|
+
# p.security :invoker
|
|
42
|
+
# SQL
|
|
43
|
+
#
|
|
44
|
+
# Like in the other operations, the procedure can be
|
|
45
|
+
# identified by a qualified name (with types of arguments).
|
|
46
|
+
# If it has no overloaded implementations,
|
|
47
|
+
# the plain name is supported as well.
|
|
48
|
+
|
|
49
|
+
module PGTrunk::Operations::Procedures
|
|
50
|
+
# @private
|
|
51
|
+
class ChangeProcedure < Base
|
|
52
|
+
validates :replace_existing, :language, :new_name, absence: true
|
|
53
|
+
validate { errors.add :base, "Changes can't be blank" if changes.blank? }
|
|
54
|
+
validate do
|
|
55
|
+
next if if_exists || name.blank? || create_procedure.present?
|
|
56
|
+
|
|
57
|
+
errors.add :base, "Procedure #{name.lean} can't be found"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def to_sql(version)
|
|
61
|
+
check_version!(version)
|
|
62
|
+
|
|
63
|
+
# Use `CREATE OR REPLACE FUNCTION` to make changes
|
|
64
|
+
create_procedure&.to_sql(version)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def invert
|
|
68
|
+
irreversible!("if_exists: true") if if_exists
|
|
69
|
+
undefined = inversion.select { |_, v| v.nil? }.keys.join(", ").presence
|
|
70
|
+
raise IrreversibleMigration.new(self, nil, <<~MSG.squish) if undefined
|
|
71
|
+
Undefined values to revert #{undefined}.
|
|
72
|
+
MSG
|
|
73
|
+
|
|
74
|
+
self.class.new(**inversion, name: name)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
def create_procedure
|
|
80
|
+
return if name.blank?
|
|
81
|
+
|
|
82
|
+
@create_procedure ||= begin
|
|
83
|
+
list = CreateProcedure.select { |obj| name.maybe_eq?(obj.name) }
|
|
84
|
+
list.select! { |obj| name == obj.name } if list.size > 1 && name.args
|
|
85
|
+
list.first&.tap do |op|
|
|
86
|
+
op.attributes = { **changes, replace_existing: true }
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def changes
|
|
92
|
+
@changes ||= {
|
|
93
|
+
body: body.presence,
|
|
94
|
+
comment: comment,
|
|
95
|
+
security: security,
|
|
96
|
+
}.compact
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def inversion
|
|
100
|
+
@inversion ||= {
|
|
101
|
+
body: [body, from_body],
|
|
102
|
+
comment: [comment, from_comment],
|
|
103
|
+
security: [security, (%i[invoker definer] - [security]).first],
|
|
104
|
+
}.slice(*changes.keys).transform_values(&:last)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# frozen_string_literal: false
|
|
2
|
+
|
|
3
|
+
# @!method ActiveRecord::Migration#create_procedure(name, **options, &block)
|
|
4
|
+
# Create a procedure
|
|
5
|
+
#
|
|
6
|
+
# @param [#to_s] name (nil)
|
|
7
|
+
# The qualified name of the procedure with arguments and returned value type
|
|
8
|
+
# @option [Boolean] :replace_existing (false) If the procedure should overwrite an existing one
|
|
9
|
+
# @option [#to_s] :language ("sql") The language (like "sql" or "plpgsql")
|
|
10
|
+
# @option [#to_s] :body (nil) The body of the procedure
|
|
11
|
+
# @option [Symbol] :security (:invoker) Define the role under which the procedure is invoked
|
|
12
|
+
# Supported values: :invoker (default), :definer
|
|
13
|
+
# @option [#to_s] :comment The description of the procedure
|
|
14
|
+
# @yield [Proc] the block with the procedure's definition
|
|
15
|
+
# @yieldparam The receiver of methods specifying the procedure
|
|
16
|
+
#
|
|
17
|
+
# The syntax of the operation is the same as for `create_function`,
|
|
18
|
+
# but with only `security` option available. Notice, that
|
|
19
|
+
# procedures cannot return values so you're expected not to
|
|
20
|
+
# define a returned value as well.
|
|
21
|
+
#
|
|
22
|
+
# The procedure can be created either using inline syntax
|
|
23
|
+
#
|
|
24
|
+
# create_procedure "metadata.set_foo(a int)",
|
|
25
|
+
# language: :sql,
|
|
26
|
+
# body: "SET foo = a",
|
|
27
|
+
# comment: "Sets foo value"
|
|
28
|
+
#
|
|
29
|
+
# or using a block:
|
|
30
|
+
#
|
|
31
|
+
# create_procedure "metadata.set_foo(a int)" do |p|
|
|
32
|
+
# p.language "sql" # (default)
|
|
33
|
+
# p.body <<~SQL
|
|
34
|
+
# SET foo = a
|
|
35
|
+
# SQL
|
|
36
|
+
# p.security :invoker # (default), also :definer
|
|
37
|
+
# p.comment "Multiplies 2 integers"
|
|
38
|
+
# SQL
|
|
39
|
+
#
|
|
40
|
+
# With a `replace_existing: true` option,
|
|
41
|
+
# it will be created using the `CREATE OR REPLACE` clause.
|
|
42
|
+
# In this case the migration is irreversible because we
|
|
43
|
+
# don't know if and how to restore its previous definition.
|
|
44
|
+
#
|
|
45
|
+
# create_procedure "set_foo(a int)",
|
|
46
|
+
# body: "SET foo = a",
|
|
47
|
+
# replace_existing: true
|
|
48
|
+
#
|
|
49
|
+
# A procedure without arguments is supported as well
|
|
50
|
+
#
|
|
51
|
+
# # the same as "do_something()"
|
|
52
|
+
# create_procedure "do_something" do |p|
|
|
53
|
+
# # ...
|
|
54
|
+
# end
|
|
55
|
+
|
|
56
|
+
module PGTrunk::Operations::Procedures
|
|
57
|
+
# @private
|
|
58
|
+
class CreateProcedure < Base
|
|
59
|
+
validates :body, presence: true
|
|
60
|
+
validates :if_exists, :new_name, absence: true
|
|
61
|
+
|
|
62
|
+
from_sql do |server_version|
|
|
63
|
+
# Procedures were added to PostgreSQL in v11
|
|
64
|
+
next if server_version < "11"
|
|
65
|
+
|
|
66
|
+
<<~SQL.squish
|
|
67
|
+
SELECT
|
|
68
|
+
p.oid,
|
|
69
|
+
(
|
|
70
|
+
p.pronamespace::regnamespace || '.' || p.proname || '(' ||
|
|
71
|
+
regexp_replace(
|
|
72
|
+
regexp_replace(
|
|
73
|
+
pg_get_function_arguments(p.oid), '^\s*IN\s+', '', 'g'
|
|
74
|
+
), '[,]\s*IN\s+', ',', 'g'
|
|
75
|
+
) || ')'
|
|
76
|
+
) AS name,
|
|
77
|
+
p.prosrc AS body,
|
|
78
|
+
l.lanname AS language,
|
|
79
|
+
(
|
|
80
|
+
CASE
|
|
81
|
+
WHEN p.prosecdef THEN 'definer'
|
|
82
|
+
ELSE 'invoker'
|
|
83
|
+
END
|
|
84
|
+
) AS security,
|
|
85
|
+
d.description AS comment
|
|
86
|
+
FROM pg_proc p
|
|
87
|
+
JOIN pg_trunk e ON e.oid = p.oid
|
|
88
|
+
JOIN pg_language l ON l.oid = p.prolang
|
|
89
|
+
LEFT JOIN pg_description d ON d.objoid = p.oid
|
|
90
|
+
WHERE e.classid = 'pg_proc'::regclass
|
|
91
|
+
AND p.prokind = 'p';
|
|
92
|
+
SQL
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def to_sql(version)
|
|
96
|
+
# Procedures were added to PostgreSQL in v11
|
|
97
|
+
check_version!(version)
|
|
98
|
+
|
|
99
|
+
[create_proc, *comment_proc, register_proc].join(" ")
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def invert
|
|
103
|
+
irreversible!("replace_existing: true") if replace_existing
|
|
104
|
+
DropProcedure.new(**to_h)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
private
|
|
108
|
+
|
|
109
|
+
def create_proc
|
|
110
|
+
sql = "CREATE"
|
|
111
|
+
sql << " OR REPLACE" if replace_existing
|
|
112
|
+
sql << " PROCEDURE #{name.to_sql(true)}"
|
|
113
|
+
sql << " LANGUAGE #{language&.downcase || 'sql'}"
|
|
114
|
+
sql << " SECURITY DEFINER" if security == :definer
|
|
115
|
+
sql << " AS $$#{body}$$;"
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def comment_proc
|
|
119
|
+
<<~SQL
|
|
120
|
+
COMMENT ON PROCEDURE #{name.to_sql(true)}
|
|
121
|
+
IS $comment$#{comment}$comment$;
|
|
122
|
+
SQL
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Register the most recent `oid` of procedures with this schema/name
|
|
126
|
+
# There can be several overloaded definitions, but we're interested
|
|
127
|
+
# in that one we created just now so we can skip checking its args.
|
|
128
|
+
def register_proc
|
|
129
|
+
<<~SQL.squish
|
|
130
|
+
WITH latest AS (
|
|
131
|
+
SELECT
|
|
132
|
+
oid,
|
|
133
|
+
(proname = #{name.quoted} AND pronamespace = #{name.namespace}) AS ok
|
|
134
|
+
FROM pg_proc
|
|
135
|
+
WHERE prokind = 'p'
|
|
136
|
+
ORDER BY oid DESC LIMIT 1
|
|
137
|
+
)
|
|
138
|
+
INSERT INTO pg_trunk (oid, classid)
|
|
139
|
+
SELECT oid, 'pg_proc'::regclass
|
|
140
|
+
FROM latest
|
|
141
|
+
WHERE ok
|
|
142
|
+
ON CONFLICT DO NOTHING;
|
|
143
|
+
SQL
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: false
|
|
2
|
+
|
|
3
|
+
# @!method ActiveRecord::Migration#drop_procedure(name, **options, &block)
|
|
4
|
+
# Drop a procedure
|
|
5
|
+
#
|
|
6
|
+
# @param [#to_s] name (nil)
|
|
7
|
+
# The qualified name of the procedure with arguments and returned value type
|
|
8
|
+
# @option [Boolean] :if_exists (false) Suppress the error when the procedure is absent
|
|
9
|
+
# @option [#to_s] :language ("sql") The language (like "sql" or "plpgsql")
|
|
10
|
+
# @option [#to_s] :body (nil) The body of the procedure
|
|
11
|
+
# @option [Symbol] :security (:invoker) Define the role under which the procedure is invoked
|
|
12
|
+
# Supported values: :invoker (default), :definer
|
|
13
|
+
# @option [#to_s] :comment The description of the procedure
|
|
14
|
+
# @yield [Proc] the block with the procedure's definition
|
|
15
|
+
# @yieldparam The receiver of methods specifying the procedure
|
|
16
|
+
#
|
|
17
|
+
# A procedure can be dropped by a plain name:
|
|
18
|
+
#
|
|
19
|
+
# drop_procedure "set_foo"
|
|
20
|
+
#
|
|
21
|
+
# If several overloaded procedures have the name,
|
|
22
|
+
# then you must specify the signature having
|
|
23
|
+
# types of attributes at least:
|
|
24
|
+
#
|
|
25
|
+
# drop_procedure "set_foo(int)"
|
|
26
|
+
#
|
|
27
|
+
# In both cases above the operation is irreversible. To make it
|
|
28
|
+
# inverted you have to provide a full signature along with
|
|
29
|
+
# the body definition. The other options are supported as well:
|
|
30
|
+
#
|
|
31
|
+
# drop_procedure "metadata.set_foo(a int)" do |p|
|
|
32
|
+
# p.language "sql" # (default)
|
|
33
|
+
# p.body <<~SQL
|
|
34
|
+
# SET foo = a
|
|
35
|
+
# SQL
|
|
36
|
+
# p.security :invoker # (default), also :definer
|
|
37
|
+
# p.comment "Multiplies 2 integers"
|
|
38
|
+
# SQL
|
|
39
|
+
#
|
|
40
|
+
# The operation can be called with `if_exists` option. In this case
|
|
41
|
+
# it would do nothing when no procedure existed.
|
|
42
|
+
#
|
|
43
|
+
# drop_procedure "metadata.set_foo(a int)", if_exists: true
|
|
44
|
+
#
|
|
45
|
+
# Notice, that this option make the operation irreversible because of
|
|
46
|
+
# uncertainty about the previous state of the database.
|
|
47
|
+
|
|
48
|
+
module PGTrunk::Operations::Procedures
|
|
49
|
+
# @private
|
|
50
|
+
class DropProcedure < Base
|
|
51
|
+
validates :replace_existing, :new_name, absence: true
|
|
52
|
+
|
|
53
|
+
def to_sql(version)
|
|
54
|
+
check_version!(version)
|
|
55
|
+
|
|
56
|
+
sql = "DROP PROCEDURE"
|
|
57
|
+
sql << " IF EXISTS" if if_exists
|
|
58
|
+
sql << " #{name.to_sql};"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def invert
|
|
62
|
+
irreversible!("if_exists: true") if if_exists
|
|
63
|
+
CreateProcedure.new(**to_h)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: false
|
|
2
|
+
|
|
3
|
+
# @!method ActiveRecord::Migration#rename_procedure(name, to:)
|
|
4
|
+
# Change the name and/or schema of a procedure
|
|
5
|
+
#
|
|
6
|
+
# @param [#to_s] :name (nil) The qualified name of the procedure
|
|
7
|
+
# @option [#to_s] :to (nil) The new qualified name for the procedure
|
|
8
|
+
#
|
|
9
|
+
# A procedure can be renamed by changing both the name
|
|
10
|
+
# and the schema (namespace) it belongs to.
|
|
11
|
+
#
|
|
12
|
+
# If there are no overloaded procedures, then you can use a plain name:
|
|
13
|
+
#
|
|
14
|
+
# rename_procedure "math.set_foo", to: "public.foo_setup"
|
|
15
|
+
#
|
|
16
|
+
# otherwise the types of attributes must be explicitly specified.
|
|
17
|
+
#
|
|
18
|
+
# rename_procedure "math.set_foo(int)", to: "public.foo_setup"
|
|
19
|
+
#
|
|
20
|
+
# Any specification of attributes in `to:` option
|
|
21
|
+
# is ignored because they cannot be changed anyway.
|
|
22
|
+
#
|
|
23
|
+
# The operation is always reversible.
|
|
24
|
+
|
|
25
|
+
module PGTrunk::Operations::Procedures
|
|
26
|
+
# @private
|
|
27
|
+
class RenameProcedure < Base
|
|
28
|
+
validates :new_name, presence: true
|
|
29
|
+
validates :body, :if_exists, :replace_existing, :language, :security, absence: true
|
|
30
|
+
|
|
31
|
+
def to_sql(version)
|
|
32
|
+
check_version!(version)
|
|
33
|
+
|
|
34
|
+
[*change_schema, *change_name].join("; ")
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def invert
|
|
38
|
+
q_new_name = "#{new_name.schema}.#{new_name.routine}(#{name.args}) #{name.returns}"
|
|
39
|
+
self.class.new(**to_h, name: q_new_name.strip, to: name)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def change_schema
|
|
45
|
+
return if name.schema == new_name.schema
|
|
46
|
+
|
|
47
|
+
"ALTER PROCEDURE #{name.to_sql} SET SCHEMA #{new_name.schema.inspect};"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def change_name
|
|
51
|
+
return if new_name.routine == name.routine
|
|
52
|
+
|
|
53
|
+
changed_name = name.merge(schema: new_name.schema).to_sql
|
|
54
|
+
"ALTER PROCEDURE #{changed_name} RENAME TO #{new_name.routine.inspect};"
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# nodoc
|
|
4
|
+
module PGTrunk::Operations
|
|
5
|
+
# @private
|
|
6
|
+
# Namespace for operations with procedures
|
|
7
|
+
module Procedures
|
|
8
|
+
require_relative "procedures/base"
|
|
9
|
+
require_relative "procedures/change_procedure"
|
|
10
|
+
require_relative "procedures/create_procedure"
|
|
11
|
+
require_relative "procedures/drop_procedure"
|
|
12
|
+
require_relative "procedures/rename_procedure"
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: false
|
|
2
|
+
|
|
3
|
+
module PGTrunk::Operations::Statistics
|
|
4
|
+
# @abstract
|
|
5
|
+
# @private
|
|
6
|
+
# Base class for operations with check constraints
|
|
7
|
+
class Base < PGTrunk::Operation
|
|
8
|
+
# All attributes that can be used by statistics-related commands
|
|
9
|
+
attribute :columns, :pg_trunk_array_of_strings, default: []
|
|
10
|
+
attribute :expressions, :pg_trunk_array_of_strings, default: []
|
|
11
|
+
attribute :kinds, :pg_trunk_array_of_symbols, default: []
|
|
12
|
+
attribute :table, :pg_trunk_qualified_name
|
|
13
|
+
|
|
14
|
+
# Methods to populate multivariable attributes from a block
|
|
15
|
+
|
|
16
|
+
def expression(text)
|
|
17
|
+
expressions << text.strip
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def column(name)
|
|
21
|
+
columns << name.strip
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Generate missed name from table & expression
|
|
25
|
+
after_initialize { self.name ||= generated_name }
|
|
26
|
+
after_initialize { expressions&.map!(&:strip) }
|
|
27
|
+
|
|
28
|
+
# Ensure correctness of present values
|
|
29
|
+
# The table must be defined because the name only
|
|
30
|
+
# is not enough to identify the constraint.
|
|
31
|
+
validates :name, presence: true
|
|
32
|
+
validate do
|
|
33
|
+
next if (kinds - %i[ndistinct dependencies mcv]).none?
|
|
34
|
+
|
|
35
|
+
errors.add :kinds, :invalid
|
|
36
|
+
end
|
|
37
|
+
validate do
|
|
38
|
+
next unless columns.blank? && expressions.size == 1
|
|
39
|
+
|
|
40
|
+
errors.add :kinds, :present if kinds.present?
|
|
41
|
+
end
|
|
42
|
+
validate do
|
|
43
|
+
next if expressions.present?
|
|
44
|
+
|
|
45
|
+
errors.add :base, "Add more columns or expressions" if columns.size == 1
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# By default foreign keys are sorted by names.
|
|
49
|
+
# Support `table` only in positional arguments.
|
|
50
|
+
|
|
51
|
+
# Snippet to be used in all operations with check constraints
|
|
52
|
+
ruby_snippet do |s|
|
|
53
|
+
s.ruby_param(name.lean) if custom_name?
|
|
54
|
+
s.ruby_param(to: new_name.lean) if new_name.present?
|
|
55
|
+
s.ruby_param(if_not_exists: true) if if_not_exists
|
|
56
|
+
s.ruby_param(if_exists: true) if if_exists
|
|
57
|
+
s.ruby_param(force: :custom) if force == :custom
|
|
58
|
+
|
|
59
|
+
s.ruby_line(:table, table.lean) if table.present?
|
|
60
|
+
if columns.size > 3
|
|
61
|
+
columns.sort.each { |column| s.ruby_line(:column, column) }
|
|
62
|
+
elsif columns.present?
|
|
63
|
+
s.ruby_line(:columns, *columns.sort)
|
|
64
|
+
end
|
|
65
|
+
expressions.sort.each { |value| s.ruby_line(:expression, value) }
|
|
66
|
+
s.ruby_line(:kinds, *kinds.sort) if kinds.present?
|
|
67
|
+
s.ruby_line(:comment, comment) if comment.present?
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
def generated_name
|
|
73
|
+
return if table.blank? || parts.blank?
|
|
74
|
+
|
|
75
|
+
@generated_name ||= begin
|
|
76
|
+
key_options = { kinds: kinds, parts: parts }
|
|
77
|
+
identifier = "#{table.lean}_#{key_options}_stat"
|
|
78
|
+
hashed_identifier = Digest::SHA256.hexdigest(identifier).first(10)
|
|
79
|
+
PGTrunk::QualifiedName.wrap("stat_rails_#{hashed_identifier}")
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def custom_name?(qname = name)
|
|
84
|
+
qname&.differs_from?(/^stat_rails_\w+$/)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def parts
|
|
88
|
+
@parts ||= [
|
|
89
|
+
*columns.reject(&:blank?).map(&:inspect),
|
|
90
|
+
*expressions.reject(&:blank?).map { |ex| "(#{ex})" },
|
|
91
|
+
]
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|