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,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
|