pg_trunk 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|