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,119 @@
|
|
1
|
+
# frozen_string_literal: false
|
2
|
+
|
3
|
+
# @!method ActiveRecord::Migration#change_enum(name, &block)
|
4
|
+
# Modify an enumerated type
|
5
|
+
#
|
6
|
+
# @param [#to_s] name (nil) The qualified name of the type
|
7
|
+
# @yield [Proc] the block with the type's definition
|
8
|
+
# @yieldparam The receiver of methods specifying the type
|
9
|
+
#
|
10
|
+
# The operation can be used to rename or add values to the
|
11
|
+
# enumerated type. The commend can be changed as well.
|
12
|
+
#
|
13
|
+
# change_enum "currencies" do |e|
|
14
|
+
# e.add_value "EUR", after: "BTC"
|
15
|
+
# e.add_value "GBP", before: "usd"
|
16
|
+
# e.add_value "JPY" # to the end of the list
|
17
|
+
# e.rename_value "usd", to: "USD"
|
18
|
+
# e.comment <<~COMMENT, from: <<~COMMENT
|
19
|
+
# Supported currencies
|
20
|
+
# COMMENT
|
21
|
+
# Currencies
|
22
|
+
# COMMENT
|
23
|
+
# end
|
24
|
+
#
|
25
|
+
# Please, keep in mind that all values will be added before
|
26
|
+
# the first rename. That's why you should use old values
|
27
|
+
# (like the `usd` instead of the `USD` in the example above)
|
28
|
+
# in `before` and `after` options.
|
29
|
+
#
|
30
|
+
# Also notice that PostgreSQL doesn't support value deletion,
|
31
|
+
# that's why adding any value makes the migration irreversible.
|
32
|
+
#
|
33
|
+
# It is also irreversible if you changed the comment, but
|
34
|
+
# not defined its previous value.
|
35
|
+
|
36
|
+
module PGTrunk::Operations::Enums
|
37
|
+
# @private
|
38
|
+
class ChangeEnum < Base
|
39
|
+
# Add new value (irreversible!)
|
40
|
+
# If neither option is specified, the value will be added
|
41
|
+
# to the very end of the array.
|
42
|
+
# Notice, that all add-ons are made BEFORE renames.
|
43
|
+
def add_value(name, after: nil, before: nil)
|
44
|
+
changes << Change.new(name: name, after: after, before: before)
|
45
|
+
end
|
46
|
+
|
47
|
+
# Rename the value to new unique name (reversible)
|
48
|
+
def rename_value(name, to: nil)
|
49
|
+
changes << Change.new(name: name, new_name: to)
|
50
|
+
end
|
51
|
+
|
52
|
+
validates :if_exists, :force, :values, :new_name, absence: true
|
53
|
+
validate do
|
54
|
+
next if comment.present? || changes.present?
|
55
|
+
|
56
|
+
errors.add :base, "There are no changes"
|
57
|
+
end
|
58
|
+
|
59
|
+
def to_sql(version)
|
60
|
+
raise <<~MSG.squish if version < "12" && changes.any?(&:add?)
|
61
|
+
Adding new values to enumerable types inside a migration
|
62
|
+
is supported in PostgreSQL v12+.
|
63
|
+
MSG
|
64
|
+
|
65
|
+
[*add_values, *rename_values, *change_comment].join(" ")
|
66
|
+
end
|
67
|
+
|
68
|
+
def invert
|
69
|
+
values_added = changes.any?(&:add?)
|
70
|
+
raise IrreversibleMigration.new(self, nil, <<~MSG.squish) if values_added
|
71
|
+
Removal of values from enumerated type is not supported by PostgreSQL,
|
72
|
+
that's why adding new values can't be reverted.
|
73
|
+
MSG
|
74
|
+
|
75
|
+
undefined = inversion.select { |_, v| v.nil? }.keys.join(", ").presence
|
76
|
+
raise IrreversibleMigration.new(self, nil, <<~MSG.squish) if undefined
|
77
|
+
Undefined values to revert #{undefined}.
|
78
|
+
MSG
|
79
|
+
|
80
|
+
self.class.new(**to_h, **inversion)
|
81
|
+
end
|
82
|
+
|
83
|
+
def to_h
|
84
|
+
super.tap { |data| data[:changes]&.map!(&:to_h) }
|
85
|
+
end
|
86
|
+
|
87
|
+
private
|
88
|
+
|
89
|
+
def add_values
|
90
|
+
changes.select(&:add?).map do |change|
|
91
|
+
"ALTER TYPE #{name.to_sql} #{change.to_sql};"
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def rename_values
|
96
|
+
changes.select(&:rename?).map do |change|
|
97
|
+
"ALTER TYPE #{name.to_sql} #{change.to_sql};"
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def change_comment
|
102
|
+
return unless comment # empty string is processed
|
103
|
+
|
104
|
+
"COMMENT ON TYPE #{name.to_sql} IS $comment$#{comment}$comment$;"
|
105
|
+
end
|
106
|
+
|
107
|
+
def change
|
108
|
+
@change ||= {
|
109
|
+
changes: changes.map(&:to_h).presence, comment: comment,
|
110
|
+
}.compact
|
111
|
+
end
|
112
|
+
|
113
|
+
def inversion
|
114
|
+
@inversion ||= {
|
115
|
+
changes: changes.reverse.map(&:invert).presence, comment: from_comment,
|
116
|
+
}.slice(*change.keys)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
# frozen_string_literal: false
|
2
|
+
|
3
|
+
# @!method ActiveRecord::Migration#create_enum(name, **options, &block)
|
4
|
+
# Create an enumerated type by qualified name
|
5
|
+
#
|
6
|
+
# @param [#to_s] name (nil) The qualified name of the type
|
7
|
+
# @option [Array<#to_s>] :values ([]) The list of values
|
8
|
+
# @option [#to_s] :comment (nil) The comment describing the constraint
|
9
|
+
# @yield [Proc] the block with the type's definition
|
10
|
+
# @yieldparam The receiver of methods specifying the type
|
11
|
+
#
|
12
|
+
# @example
|
13
|
+
#
|
14
|
+
# create_enum "finances.currency" do |e|
|
15
|
+
# e.values "BTC", "EUR", "GBP", "USD"
|
16
|
+
# e.value "JPY" # the alternative way to add a value to the tail
|
17
|
+
# e.comment <<~COMMENT
|
18
|
+
# The list of values for supported currencies.
|
19
|
+
# COMMENT
|
20
|
+
# end
|
21
|
+
#
|
22
|
+
# It is always reversible.
|
23
|
+
|
24
|
+
module PGTrunk::Operations::Enums
|
25
|
+
# @private
|
26
|
+
class CreateEnum < Base
|
27
|
+
validates :values, presence: true
|
28
|
+
validates :changes, :force, :if_exists, :new_name, absence: true
|
29
|
+
|
30
|
+
from_sql do |_version|
|
31
|
+
<<~SQL
|
32
|
+
SELECT
|
33
|
+
t.oid,
|
34
|
+
(t.typnamespace::regnamespace || '.' || t.typname) AS name,
|
35
|
+
array_agg(n.enumlabel ORDER BY n.enumsortorder) AS values,
|
36
|
+
d.description AS comment
|
37
|
+
FROM pg_type t
|
38
|
+
JOIN pg_trunk e ON e.oid = t.oid AND e.classid = 'pg_type'::regclass
|
39
|
+
LEFT JOIN pg_enum n ON n.enumtypid = t.oid
|
40
|
+
LEFT JOIN pg_description d ON d.objoid = t.oid
|
41
|
+
AND d.classoid = 'pg_type'::regclass
|
42
|
+
WHERE t.typtype = 'e'
|
43
|
+
GROUP BY t.oid, t.typnamespace, t.typname, d.description
|
44
|
+
SQL
|
45
|
+
end
|
46
|
+
|
47
|
+
def to_sql(_version)
|
48
|
+
[create_enum, *create_comment, register_enum].join(" ")
|
49
|
+
end
|
50
|
+
|
51
|
+
def invert
|
52
|
+
DropEnum.new(**to_h)
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
def create_enum
|
58
|
+
<<~SQL.squish
|
59
|
+
CREATE TYPE #{name.to_sql} AS ENUM (
|
60
|
+
#{values.map { |value| "'#{value}'" }.join(', ')}
|
61
|
+
);
|
62
|
+
SQL
|
63
|
+
end
|
64
|
+
|
65
|
+
def create_comment
|
66
|
+
return if comment.blank?
|
67
|
+
|
68
|
+
"COMMENT ON TYPE #{name.to_sql} IS $comment$#{comment}$comment$;"
|
69
|
+
end
|
70
|
+
|
71
|
+
def register_enum
|
72
|
+
<<~SQL.squish
|
73
|
+
INSERT INTO pg_trunk (oid, classid)
|
74
|
+
SELECT oid, 'pg_type'::regclass
|
75
|
+
FROM pg_type
|
76
|
+
WHERE typname = #{name.quoted}
|
77
|
+
AND typnamespace = #{name.namespace}
|
78
|
+
AND typtype = 'e'
|
79
|
+
ON CONFLICT DO NOTHING;
|
80
|
+
SQL
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: false
|
2
|
+
|
3
|
+
# @!method ActiveRecord::Migration#drop_enum(name, **options, &block)
|
4
|
+
# Drop an enumerated type by qualified name
|
5
|
+
#
|
6
|
+
# @param [#to_s] name (nil) The qualified name of the type
|
7
|
+
# @option [Boolean] :if_exists (false) Suppress the error when the type is absent
|
8
|
+
# @option [Symbol] :force (:restrict) How to process dependent objects (`:cascade` or `:restrict`)
|
9
|
+
# @option [Array<#to_s>] :values ([]) The list of values
|
10
|
+
# @option [#to_s] :comment (nil) The comment describing the constraint
|
11
|
+
# @yield [Proc] the block with the type's definition
|
12
|
+
# @yieldparam The receiver of methods specifying the type
|
13
|
+
#
|
14
|
+
# The operation drops a enumerated type identified by its
|
15
|
+
# qualified name (it can include a schema).
|
16
|
+
#
|
17
|
+
# drop_enum "finances.currency"
|
18
|
+
#
|
19
|
+
# To make the operation invertible, use the same options
|
20
|
+
# as in the `create_enum` operation.
|
21
|
+
#
|
22
|
+
# drop_enum "finances.currency" do |e|
|
23
|
+
# e.values "BTC", "EUR", "GBP", "USD"
|
24
|
+
# e.value "JPY" # the alternative way to add a value
|
25
|
+
# e.comment <<~COMMENT
|
26
|
+
# The list of values for supported currencies.
|
27
|
+
# COMMENT
|
28
|
+
# end
|
29
|
+
#
|
30
|
+
# With the `force: :cascade` option the operation would remove
|
31
|
+
# all the objects that use the type.
|
32
|
+
#
|
33
|
+
# drop_enum "finances.currency", force: :cascade
|
34
|
+
#
|
35
|
+
# With the `if_exists: true` option the operation won't fail
|
36
|
+
# even when the view was absent in the database.
|
37
|
+
#
|
38
|
+
# drop_enum "finances.currency", if_exists: true
|
39
|
+
#
|
40
|
+
# Both options make a migration irreversible due to uncertainty
|
41
|
+
# of the previous state of the database.
|
42
|
+
|
43
|
+
module PGTrunk::Operations::Enums
|
44
|
+
# @private
|
45
|
+
class DropEnum < Base
|
46
|
+
# Forbid these attributes
|
47
|
+
validates :changes, :new_name, absence: true
|
48
|
+
|
49
|
+
def to_sql(_version)
|
50
|
+
sql = "DROP TYPE"
|
51
|
+
sql << " IF EXISTS" if if_exists
|
52
|
+
sql << " #{name.to_sql}"
|
53
|
+
sql << " CASCADE" if force == :cascade
|
54
|
+
sql << ";"
|
55
|
+
end
|
56
|
+
|
57
|
+
def invert
|
58
|
+
irreversible!("if_exists: true") if if_exists
|
59
|
+
irreversible!("force: :cascade") if force == :cascade
|
60
|
+
CreateEnum.new(**to_h.except(:force))
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: false
|
2
|
+
|
3
|
+
# @!method ActiveRecord::Migration#rename_enum(name, to:)
|
4
|
+
# Change the name and/or schema of an enumerated type
|
5
|
+
#
|
6
|
+
# @param [#to_s] :name (nil) The qualified name of the type
|
7
|
+
# @option [#to_s] :to (nil) The new qualified name for the type
|
8
|
+
#
|
9
|
+
# @example:
|
10
|
+
#
|
11
|
+
# rename_enum "currencies", to: "finances.currency"
|
12
|
+
#
|
13
|
+
# The operation is always reversible.
|
14
|
+
|
15
|
+
module PGTrunk::Operations::Enums
|
16
|
+
# @private
|
17
|
+
class RenameEnum < Base
|
18
|
+
validates :new_name, presence: true
|
19
|
+
validates :force, :if_exists, :values, :changes, absence: true
|
20
|
+
|
21
|
+
def to_sql(_version)
|
22
|
+
[*change_schema, *change_name].join("; ")
|
23
|
+
end
|
24
|
+
|
25
|
+
def invert
|
26
|
+
self.class.new(**to_h, name: new_name, to: name)
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def change_schema
|
32
|
+
return if name.schema == new_name.schema
|
33
|
+
|
34
|
+
"ALTER TYPE #{name.to_sql} SET SCHEMA #{new_name.schema.inspect};"
|
35
|
+
end
|
36
|
+
|
37
|
+
def change_name
|
38
|
+
return if new_name.name == name.name
|
39
|
+
|
40
|
+
moved = name.merge(schema: new_name.schema)
|
41
|
+
"ALTER TYPE #{moved.to_sql} RENAME TO #{new_name.name.inspect};"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# nodoc
|
4
|
+
module PGTrunk::Operations
|
5
|
+
# @private
|
6
|
+
# Namespace for operations with functions
|
7
|
+
module Enums
|
8
|
+
require_relative "enums/change"
|
9
|
+
require_relative "enums/base"
|
10
|
+
require_relative "enums/change_enum"
|
11
|
+
require_relative "enums/create_enum"
|
12
|
+
require_relative "enums/drop_enum"
|
13
|
+
require_relative "enums/rename_enum"
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,174 @@
|
|
1
|
+
# frozen_string_literal: false
|
2
|
+
|
3
|
+
# @!method ActiveRecord::Migration#add_foreign_key(table, reference, **options, &block)
|
4
|
+
# Create a foreign key constraint
|
5
|
+
#
|
6
|
+
# @param [#to_s] table (nil) The qualified name of the table
|
7
|
+
# @param [#to_s] reference (nil) The qualified name of the reference table
|
8
|
+
# @option [#to_s] :name (nil) The current name of the foreign key
|
9
|
+
# @option [#to_s] :to (nil) The new name for the foreign key
|
10
|
+
# @option [Array<#to_s>] :columns ([]) The list of columns of the table
|
11
|
+
# @option [#to_s] :column (nil) An alias for :columns for the case of single-column keys
|
12
|
+
# @option [Array<#to_s>] :primary_key ([]) The list of columns of the reference table
|
13
|
+
# @option [Symbol] :match (:full) Define how to match rows
|
14
|
+
# Supported values: :full (default), :partial, :simple
|
15
|
+
# @option [Symbol] :on_delete (:restrict)
|
16
|
+
# Define how to handle the deletion of the referred row.
|
17
|
+
# Supported values: :restrict (default), :cascade, :nullify, :reset
|
18
|
+
# @option [Symbol] :on_update (:restrict)
|
19
|
+
# Define how to handle the update of the referred row.
|
20
|
+
# Supported values: :restrict (default), :cascade, :nullify, :reset
|
21
|
+
# @yield [Proc] the block with the key's definition
|
22
|
+
# @yieldparam The receiver of methods specifying the foreign key
|
23
|
+
#
|
24
|
+
# The table and reference of the new key must be set explicitly.
|
25
|
+
# All the rest (including the name) can be generated by default:
|
26
|
+
#
|
27
|
+
# # same as `..., column: 'role_id', primary_key: 'id'`
|
28
|
+
# add_foreign_key :users, :roles
|
29
|
+
#
|
30
|
+
# The block syntax can be used for any argument:
|
31
|
+
#
|
32
|
+
# add_foreign_key do |c|
|
33
|
+
# c.table "users"
|
34
|
+
# c.reference "roles"
|
35
|
+
# c.column "role_id" # (generated by default from reference and pk)
|
36
|
+
# c.primary_key "id" # (default)
|
37
|
+
# c.on_update :cascade # :restrict (default)
|
38
|
+
# c.on_delete :cascade # :restrict (default)
|
39
|
+
# c.name "user_roles_fk" # can be generated
|
40
|
+
# c.comment "Phone is 10+ chars long"
|
41
|
+
# end
|
42
|
+
#
|
43
|
+
# Composite foreign keys are supported as well:
|
44
|
+
#
|
45
|
+
# add_foreign_key "users", "roles" do |c|
|
46
|
+
# c.columns %w[role_name role_id]
|
47
|
+
# c.primary_key %w[name id] # Requires unique index
|
48
|
+
# c.match :full # :partial, :simple (default)
|
49
|
+
# end
|
50
|
+
#
|
51
|
+
# The operation is always invertible.
|
52
|
+
|
53
|
+
module PGTrunk::Operations::ForeignKeys
|
54
|
+
# @private
|
55
|
+
class AddForeignKey < Base
|
56
|
+
# The operation used by the generator `rails g foreign_key`
|
57
|
+
generates_object :foreign_key
|
58
|
+
|
59
|
+
# New name is generated from the full signature
|
60
|
+
# including table, reference, columns and primary_key.
|
61
|
+
after_initialize { self.name = generated_name if name.blank? }
|
62
|
+
|
63
|
+
validates :reference, presence: true
|
64
|
+
validates :if_exists, :new_name, absence: true
|
65
|
+
|
66
|
+
from_sql do
|
67
|
+
<<~SQL
|
68
|
+
SELECT
|
69
|
+
c.oid,
|
70
|
+
c.conname AS name,
|
71
|
+
c.connamespace::regnamespace AS schema,
|
72
|
+
(t.relnamespace::regnamespace || '.' || t.relname) AS "table",
|
73
|
+
(r.relnamespace::regnamespace || '.' || r.relname) AS "reference",
|
74
|
+
(
|
75
|
+
SELECT array_agg(attname)
|
76
|
+
FROM (
|
77
|
+
SELECT a.attname
|
78
|
+
FROM unnest(c.conkey) b(i) JOIN pg_attribute a ON a.attnum = b.i
|
79
|
+
WHERE a.attrelid = c.conrelid
|
80
|
+
ORDER BY array_position(c.conkey, b.i)
|
81
|
+
) list
|
82
|
+
) AS columns,
|
83
|
+
(
|
84
|
+
SELECT array_agg(attname)
|
85
|
+
FROM (
|
86
|
+
SELECT a.attname
|
87
|
+
FROM unnest(c.confkey) b(i) JOIN pg_attribute a ON a.attnum = b.i
|
88
|
+
WHERE a.attrelid = c.confrelid
|
89
|
+
ORDER BY array_position(c.confkey, b.i)
|
90
|
+
) list
|
91
|
+
) AS primary_key,
|
92
|
+
(
|
93
|
+
CASE
|
94
|
+
WHEN c.confupdtype = 'r' THEN 'restrict'
|
95
|
+
WHEN c.confupdtype = 'c' THEN 'cascade'
|
96
|
+
WHEN c.confupdtype = 'n' THEN 'nullify'
|
97
|
+
WHEN c.confupdtype = 'd' THEN 'reset'
|
98
|
+
END
|
99
|
+
) AS on_update,
|
100
|
+
(
|
101
|
+
CASE
|
102
|
+
WHEN c.confdeltype = 'r' THEN 'restrict'
|
103
|
+
WHEN c.confdeltype = 'c' THEN 'cascade'
|
104
|
+
WHEN c.confdeltype = 'n' THEN 'nullify'
|
105
|
+
WHEN c.confdeltype = 'd' THEN 'reset'
|
106
|
+
END
|
107
|
+
) AS on_delete,
|
108
|
+
(
|
109
|
+
CASE
|
110
|
+
WHEN c.confmatchtype = 's' THEN 'simple'
|
111
|
+
WHEN c.confmatchtype = 'f' THEN 'full'
|
112
|
+
WHEN c.confmatchtype = 'p' THEN 'partial'
|
113
|
+
END
|
114
|
+
) AS match,
|
115
|
+
c.convalidated AS validate,
|
116
|
+
d.description AS comment
|
117
|
+
FROM pg_constraint c
|
118
|
+
JOIN pg_class t ON t.oid = c.conrelid
|
119
|
+
JOIN pg_class r ON r.oid = c.confrelid
|
120
|
+
LEFT JOIN pg_description d ON d.objoid = c.oid
|
121
|
+
WHERE c.contype = 'f';
|
122
|
+
SQL
|
123
|
+
end
|
124
|
+
|
125
|
+
def to_sql(_version)
|
126
|
+
# Notice that in Rails the key `if_not_exists: true` means
|
127
|
+
# the constraint should not be created if the table has ANY other
|
128
|
+
# foreign key with the same reference <table>.
|
129
|
+
return if if_not_exists && added?
|
130
|
+
|
131
|
+
[add_constraint, create_comment, register_fk].join(" ")
|
132
|
+
end
|
133
|
+
|
134
|
+
def invert
|
135
|
+
irreversible!("if_not_exists: true") if if_not_exists
|
136
|
+
DropForeignKey.new(**to_h)
|
137
|
+
end
|
138
|
+
|
139
|
+
private
|
140
|
+
|
141
|
+
def add_constraint
|
142
|
+
sql = "ALTER TABLE #{table.to_sql} ADD CONSTRAINT #{name.lean.inspect}"
|
143
|
+
sql << " FOREIGN KEY (#{columns.map(&:inspect).join(', ')})"
|
144
|
+
sql << " REFERENCES #{reference.to_sql} (#{primary_key.map(&:inspect).join(', ')})"
|
145
|
+
sql << " MATCH #{match.to_s.upcase}" if match&.!= :simple
|
146
|
+
sql << " ON DELETE #{sql_action(on_delete)}"
|
147
|
+
sql << " ON UPDATE #{sql_action(on_update)}"
|
148
|
+
sql << " NOT VALID" unless validate
|
149
|
+
sql << ";"
|
150
|
+
end
|
151
|
+
|
152
|
+
def create_comment
|
153
|
+
return if comment.blank?
|
154
|
+
|
155
|
+
<<~SQL
|
156
|
+
COMMENT ON CONSTRAINT #{name.lean.inspect} ON #{table.to_sql}
|
157
|
+
IS $comment$#{comment}$comment$;
|
158
|
+
SQL
|
159
|
+
end
|
160
|
+
|
161
|
+
# Rely on the fact the (schema.table, schema.name) is unique
|
162
|
+
def register_fk
|
163
|
+
<<~SQL
|
164
|
+
INSERT INTO pg_trunk (oid, classid)
|
165
|
+
SELECT c.oid, 'pg_constraint'::regclass
|
166
|
+
FROM pg_constraint c JOIN pg_class r ON r.oid = c.conrelid
|
167
|
+
WHERE r.relname = #{table.quoted}
|
168
|
+
AND r.relnamespace = #{table.namespace}
|
169
|
+
AND c.conname = #{name.quoted}
|
170
|
+
ON CONFLICT DO NOTHING;
|
171
|
+
SQL
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
@@ -0,0 +1,155 @@
|
|
1
|
+
# frozen_string_literal: false
|
2
|
+
|
3
|
+
module PGTrunk::Operations::ForeignKeys
|
4
|
+
# @abstract
|
5
|
+
# @private
|
6
|
+
# Base class for operations with foreign keys
|
7
|
+
class Base < PGTrunk::Operation
|
8
|
+
# All attributes that can be used by fk-related commands
|
9
|
+
attribute :columns, :pg_trunk_array_of_strings, aliases: :column, default: []
|
10
|
+
attribute :match, :pg_trunk_symbol
|
11
|
+
attribute :on_delete, :pg_trunk_symbol
|
12
|
+
attribute :on_update, :pg_trunk_symbol
|
13
|
+
attribute :primary_key, :pg_trunk_array_of_strings, default: %(id)
|
14
|
+
attribute :reference, :pg_trunk_qualified_name
|
15
|
+
attribute :table, :pg_trunk_qualified_name
|
16
|
+
attribute :validate, :boolean, default: true
|
17
|
+
|
18
|
+
# Generate missed columns from the reference ant its foreign keys.
|
19
|
+
# The generation of the missed name is operation-specific.
|
20
|
+
after_initialize { self.columns = generated_columns if columns.blank? }
|
21
|
+
|
22
|
+
# Ensure correctness of present values
|
23
|
+
# The table must be defined because the name only
|
24
|
+
# is not enough to identify the key.
|
25
|
+
validates :table, presence: true
|
26
|
+
validates :force, absence: true
|
27
|
+
validates :match, inclusion: { in: %i[full partial simple] }, allow_nil: true
|
28
|
+
validates :on_update, :on_delete,
|
29
|
+
inclusion: { in: %i[restrict cascade nullify reset] },
|
30
|
+
allow_nil: true
|
31
|
+
|
32
|
+
# By default foreign keys are sorted by tables and names.
|
33
|
+
def <=>(other)
|
34
|
+
return unless other.is_a?(self.class)
|
35
|
+
|
36
|
+
result = table <=> other.table
|
37
|
+
result.zero? ? super : result
|
38
|
+
end
|
39
|
+
|
40
|
+
# Support `table` and `reference` in positional arguments.
|
41
|
+
# @example
|
42
|
+
# add_foreign_key :users, :roles, **opts
|
43
|
+
ruby_params :table, :reference
|
44
|
+
|
45
|
+
ruby_snippet do |s|
|
46
|
+
s.ruby_param(table.lean) if table.present?
|
47
|
+
s.ruby_param(reference.lean) if reference.present?
|
48
|
+
unless default_columns?
|
49
|
+
s.ruby_param(column: columns.first) if columns.size == 1
|
50
|
+
s.ruby_param(columns: columns) if columns.size > 1
|
51
|
+
end
|
52
|
+
unless default_pkey?
|
53
|
+
s.ruby_param(primary_key: primary_key.first) if primary_key.size == 1
|
54
|
+
s.ruby_param(primary_key: primary_key) if primary_key.size > 1
|
55
|
+
end
|
56
|
+
s.ruby_param(match: match) if match&.!= :simple
|
57
|
+
s.ruby_param(on_update: on_update) if on_update
|
58
|
+
s.ruby_param(on_delete: on_delete) if on_delete
|
59
|
+
s.ruby_param(name: name.lean) if custom_name?
|
60
|
+
s.ruby_param(to: new_name.lean) if custom_name?(new_name)
|
61
|
+
s.ruby_param(if_exists: true) if if_exists
|
62
|
+
s.ruby_param(comment: comment) if comment.present?
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
# ***********************************************************************
|
68
|
+
# Helpers for operation definitions
|
69
|
+
# ***********************************************************************
|
70
|
+
|
71
|
+
# @param [#reference] operation
|
72
|
+
# @return [Array<String>]
|
73
|
+
def generated_columns
|
74
|
+
return @generated_columns if instance_variable_defined?(:@generated_columns)
|
75
|
+
|
76
|
+
@generated_columns = begin
|
77
|
+
return if reference.blank? || primary_key.blank?
|
78
|
+
|
79
|
+
prefix =
|
80
|
+
PGTrunk
|
81
|
+
.database
|
82
|
+
.strip_table_name(reference.name)
|
83
|
+
.to_s
|
84
|
+
.singularize
|
85
|
+
|
86
|
+
primary_key.map { |pk| "#{prefix}_#{pk}" }
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# Generate the name for the foreign key using the essential options
|
91
|
+
# @return [PGTrunk::QualifiedName]
|
92
|
+
def generated_name
|
93
|
+
return @generated_name if instance_variable_defined?(:@generated_name)
|
94
|
+
|
95
|
+
@generated_name = begin
|
96
|
+
return if table.blank? || reference.blank?
|
97
|
+
return if primary_key.blank? || columns.blank?
|
98
|
+
|
99
|
+
key_options = to_h.slice(:reference, :columns, :primary_key)
|
100
|
+
identifier = "#{table.lean}_#{key_options}_fk"
|
101
|
+
hashed_identifier = Digest::SHA256.hexdigest(identifier).first(10)
|
102
|
+
PGTrunk::QualifiedName.wrap("fk_rails_#{hashed_identifier}")
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
# Check if the fk name wasn't generated by Rails
|
107
|
+
# @param [#name] operation The operation
|
108
|
+
# @return [Boolean]
|
109
|
+
def custom_name?(qname = name)
|
110
|
+
qname&.differs_from?(/^fk_rails_\w+$/)
|
111
|
+
end
|
112
|
+
|
113
|
+
# Check if the only column is custom
|
114
|
+
# @param [#table, #reference, #columns, #primary_key] operation
|
115
|
+
def default_columns?
|
116
|
+
columns == generated_columns
|
117
|
+
end
|
118
|
+
|
119
|
+
# Check if columns are default for the reference
|
120
|
+
# @param [#table, #reference, #columns, #primary_key] operation
|
121
|
+
def default_pkey?
|
122
|
+
primary_key == %w[id]
|
123
|
+
end
|
124
|
+
|
125
|
+
# Notice that in Rails the key `if_not_exists: true` means that
|
126
|
+
# the constraint should not be created if the table has ANY
|
127
|
+
# fk reference to the other table (even though the keys differ).
|
128
|
+
#
|
129
|
+
# @param [#table, #reference] operation
|
130
|
+
# @return [Boolean]
|
131
|
+
def added?
|
132
|
+
PGTrunk.database.foreign_key_exists?(table.name, reference.name)
|
133
|
+
end
|
134
|
+
|
135
|
+
# Add a name of the existing foreign key
|
136
|
+
def current_name
|
137
|
+
@current_name ||= AddForeignKey.find do |fk|
|
138
|
+
fk.table == table &&
|
139
|
+
fk.columns == columns &&
|
140
|
+
fk.reference == reference &&
|
141
|
+
fk.primary_key == primary_key
|
142
|
+
end&.name
|
143
|
+
end
|
144
|
+
|
145
|
+
def sql_action(key)
|
146
|
+
case key
|
147
|
+
when :nullify then "SET NULL"
|
148
|
+
when :reset then "SET DEFAULT"
|
149
|
+
when :restrict then "RESTRICT"
|
150
|
+
when :cascade then "CASCADE"
|
151
|
+
else "NO ACTION"
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|