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.
Files changed (196) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/ci.yml +87 -0
  3. data/.gitignore +9 -0
  4. data/.rspec +4 -0
  5. data/.rubocop.yml +92 -0
  6. data/.yardopts +4 -0
  7. data/CHANGELOG.md +31 -0
  8. data/CONTRIBUTING.md +17 -0
  9. data/Gemfile +22 -0
  10. data/LICENSE.txt +21 -0
  11. data/README.md +141 -0
  12. data/Rakefile +16 -0
  13. data/bin/console +8 -0
  14. data/bin/rake +19 -0
  15. data/bin/rspec +19 -0
  16. data/bin/setup +8 -0
  17. data/bin/yard +19 -0
  18. data/lib/pg_trunk/core/adapters/postgres.rb +80 -0
  19. data/lib/pg_trunk/core/dependencies_resolver.rb +101 -0
  20. data/lib/pg_trunk/core/generators.rb +140 -0
  21. data/lib/pg_trunk/core/operation/attributes.rb +78 -0
  22. data/lib/pg_trunk/core/operation/callbacks.rb +40 -0
  23. data/lib/pg_trunk/core/operation/generators.rb +51 -0
  24. data/lib/pg_trunk/core/operation/inversion.rb +70 -0
  25. data/lib/pg_trunk/core/operation/registration.rb +55 -0
  26. data/lib/pg_trunk/core/operation/ruby_builder.rb +112 -0
  27. data/lib/pg_trunk/core/operation/ruby_helpers.rb +99 -0
  28. data/lib/pg_trunk/core/operation/sql_helpers.rb +44 -0
  29. data/lib/pg_trunk/core/operation/validations.rb +21 -0
  30. data/lib/pg_trunk/core/operation.rb +78 -0
  31. data/lib/pg_trunk/core/qualified_name.rb +165 -0
  32. data/lib/pg_trunk/core/railtie/command_recorder.rb +30 -0
  33. data/lib/pg_trunk/core/railtie/custom_types.rb +37 -0
  34. data/lib/pg_trunk/core/railtie/migration.rb +50 -0
  35. data/lib/pg_trunk/core/railtie/migrator.rb +22 -0
  36. data/lib/pg_trunk/core/railtie/schema_dumper.rb +75 -0
  37. data/lib/pg_trunk/core/railtie/schema_migration.rb +22 -0
  38. data/lib/pg_trunk/core/railtie/statements.rb +21 -0
  39. data/lib/pg_trunk/core/railtie.rb +35 -0
  40. data/lib/pg_trunk/core/registry.rb +159 -0
  41. data/lib/pg_trunk/core/serializers/array_of_hashes_serializer.rb +28 -0
  42. data/lib/pg_trunk/core/serializers/array_of_strings_serializer.rb +29 -0
  43. data/lib/pg_trunk/core/serializers/array_of_symbols_serializer.rb +28 -0
  44. data/lib/pg_trunk/core/serializers/array_serializer.rb +22 -0
  45. data/lib/pg_trunk/core/serializers/lowercase_string_serializer.rb +21 -0
  46. data/lib/pg_trunk/core/serializers/multiline_text_serializer.rb +21 -0
  47. data/lib/pg_trunk/core/serializers/qualified_name_serializer.rb +27 -0
  48. data/lib/pg_trunk/core/serializers/symbol_serializer.rb +22 -0
  49. data/lib/pg_trunk/core/serializers.rb +16 -0
  50. data/lib/pg_trunk/core/validators/all_items_valid_validator.rb +15 -0
  51. data/lib/pg_trunk/core/validators/difference_validator.rb +19 -0
  52. data/lib/pg_trunk/core/validators.rb +10 -0
  53. data/lib/pg_trunk/core.rb +21 -0
  54. data/lib/pg_trunk/generators.rb +7 -0
  55. data/lib/pg_trunk/operations/check_constraints/add_check_constraint.rb +109 -0
  56. data/lib/pg_trunk/operations/check_constraints/base.rb +69 -0
  57. data/lib/pg_trunk/operations/check_constraints/drop_check_constraint.rb +60 -0
  58. data/lib/pg_trunk/operations/check_constraints/rename_check_constraint.rb +54 -0
  59. data/lib/pg_trunk/operations/check_constraints/validate_check_constraint.rb +39 -0
  60. data/lib/pg_trunk/operations/check_constraints.rb +14 -0
  61. data/lib/pg_trunk/operations/composite_types/base.rb +61 -0
  62. data/lib/pg_trunk/operations/composite_types/change_composite_type.rb +136 -0
  63. data/lib/pg_trunk/operations/composite_types/column.rb +118 -0
  64. data/lib/pg_trunk/operations/composite_types/create_composite_type.rb +99 -0
  65. data/lib/pg_trunk/operations/composite_types/drop_composite_type.rb +67 -0
  66. data/lib/pg_trunk/operations/composite_types/rename_composite_type.rb +44 -0
  67. data/lib/pg_trunk/operations/composite_types.rb +15 -0
  68. data/lib/pg_trunk/operations/domains/base.rb +46 -0
  69. data/lib/pg_trunk/operations/domains/change_domain.rb +140 -0
  70. data/lib/pg_trunk/operations/domains/constraint.rb +93 -0
  71. data/lib/pg_trunk/operations/domains/create_domain.rb +124 -0
  72. data/lib/pg_trunk/operations/domains/drop_domain.rb +65 -0
  73. data/lib/pg_trunk/operations/domains/rename_domain.rb +44 -0
  74. data/lib/pg_trunk/operations/domains.rb +15 -0
  75. data/lib/pg_trunk/operations/enums/base.rb +47 -0
  76. data/lib/pg_trunk/operations/enums/change.rb +55 -0
  77. data/lib/pg_trunk/operations/enums/change_enum.rb +119 -0
  78. data/lib/pg_trunk/operations/enums/create_enum.rb +83 -0
  79. data/lib/pg_trunk/operations/enums/drop_enum.rb +63 -0
  80. data/lib/pg_trunk/operations/enums/rename_enum.rb +44 -0
  81. data/lib/pg_trunk/operations/enums.rb +15 -0
  82. data/lib/pg_trunk/operations/foreign_keys/add_foreign_key.rb +174 -0
  83. data/lib/pg_trunk/operations/foreign_keys/base.rb +155 -0
  84. data/lib/pg_trunk/operations/foreign_keys/drop_foreign_key.rb +76 -0
  85. data/lib/pg_trunk/operations/foreign_keys/rename_foreign_key.rb +63 -0
  86. data/lib/pg_trunk/operations/foreign_keys.rb +16 -0
  87. data/lib/pg_trunk/operations/functions/base.rb +54 -0
  88. data/lib/pg_trunk/operations/functions/change_function.rb +108 -0
  89. data/lib/pg_trunk/operations/functions/create_function.rb +198 -0
  90. data/lib/pg_trunk/operations/functions/drop_function.rb +88 -0
  91. data/lib/pg_trunk/operations/functions/rename_function.rb +57 -0
  92. data/lib/pg_trunk/operations/functions.rb +14 -0
  93. data/lib/pg_trunk/operations/indexes/add_index.rb +68 -0
  94. data/lib/pg_trunk/operations/indexes.rb +10 -0
  95. data/lib/pg_trunk/operations/materialized_views/base.rb +79 -0
  96. data/lib/pg_trunk/operations/materialized_views/change_materialized_view.rb +139 -0
  97. data/lib/pg_trunk/operations/materialized_views/column.rb +94 -0
  98. data/lib/pg_trunk/operations/materialized_views/create_materialized_view.rb +170 -0
  99. data/lib/pg_trunk/operations/materialized_views/drop_materialized_view.rb +70 -0
  100. data/lib/pg_trunk/operations/materialized_views/refresh_materialized_view.rb +48 -0
  101. data/lib/pg_trunk/operations/materialized_views/rename_materialized_view.rb +61 -0
  102. data/lib/pg_trunk/operations/materialized_views.rb +17 -0
  103. data/lib/pg_trunk/operations/procedures/base.rb +42 -0
  104. data/lib/pg_trunk/operations/procedures/change_procedure.rb +107 -0
  105. data/lib/pg_trunk/operations/procedures/create_procedure.rb +146 -0
  106. data/lib/pg_trunk/operations/procedures/drop_procedure.rb +66 -0
  107. data/lib/pg_trunk/operations/procedures/rename_procedure.rb +57 -0
  108. data/lib/pg_trunk/operations/procedures.rb +14 -0
  109. data/lib/pg_trunk/operations/statistics/base.rb +94 -0
  110. data/lib/pg_trunk/operations/statistics/create_statistics.rb +181 -0
  111. data/lib/pg_trunk/operations/statistics/drop_statistics.rb +75 -0
  112. data/lib/pg_trunk/operations/statistics/rename_statistics.rb +48 -0
  113. data/lib/pg_trunk/operations/statistics.rb +13 -0
  114. data/lib/pg_trunk/operations/tables/create_table.rb +75 -0
  115. data/lib/pg_trunk/operations/tables.rb +10 -0
  116. data/lib/pg_trunk/operations/triggers/base.rb +119 -0
  117. data/lib/pg_trunk/operations/triggers/change_trigger.rb +82 -0
  118. data/lib/pg_trunk/operations/triggers/create_trigger.rb +208 -0
  119. data/lib/pg_trunk/operations/triggers/drop_trigger.rb +66 -0
  120. data/lib/pg_trunk/operations/triggers/rename_trigger.rb +71 -0
  121. data/lib/pg_trunk/operations/triggers.rb +14 -0
  122. data/lib/pg_trunk/operations/views/base.rb +38 -0
  123. data/lib/pg_trunk/operations/views/change_view.rb +90 -0
  124. data/lib/pg_trunk/operations/views/create_view.rb +115 -0
  125. data/lib/pg_trunk/operations/views/drop_view.rb +69 -0
  126. data/lib/pg_trunk/operations/views/rename_view.rb +58 -0
  127. data/lib/pg_trunk/operations/views.rb +14 -0
  128. data/lib/pg_trunk/operations.rb +23 -0
  129. data/lib/pg_trunk/version.rb +6 -0
  130. data/lib/pg_trunk.rb +27 -0
  131. data/pg_trunk.gemspec +34 -0
  132. data/spec/dummy/.gitignore +16 -0
  133. data/spec/dummy/Rakefile +15 -0
  134. data/spec/dummy/bin/bundle +6 -0
  135. data/spec/dummy/bin/rails +6 -0
  136. data/spec/dummy/bin/rake +6 -0
  137. data/spec/dummy/config/application.rb +18 -0
  138. data/spec/dummy/config/boot.rb +7 -0
  139. data/spec/dummy/config/database.yml +14 -0
  140. data/spec/dummy/config/environment.rb +7 -0
  141. data/spec/dummy/config.ru +6 -0
  142. data/spec/dummy/db/materialized_views/admin_users_v01.sql +1 -0
  143. data/spec/dummy/db/migrate/.keep +0 -0
  144. data/spec/dummy/db/schema.rb +18 -0
  145. data/spec/dummy/db/views/admin_users_v01.sql +1 -0
  146. data/spec/dummy/db/views/admin_users_v02.sql +1 -0
  147. data/spec/operations/check_constraints/add_check_constraint_spec.rb +85 -0
  148. data/spec/operations/check_constraints/drop_check_constraint_spec.rb +111 -0
  149. data/spec/operations/check_constraints/rename_check_constraint_spec.rb +90 -0
  150. data/spec/operations/composite_types/change_composite_type_spec.rb +257 -0
  151. data/spec/operations/composite_types/create_composite_type_spec.rb +55 -0
  152. data/spec/operations/composite_types/drop_composite_type_spec.rb +109 -0
  153. data/spec/operations/composite_types/rename_composite_type_spec.rb +74 -0
  154. data/spec/operations/dependency_resolver_spec.rb +177 -0
  155. data/spec/operations/domains/change_domain_spec.rb +287 -0
  156. data/spec/operations/domains/create_domain_spec.rb +69 -0
  157. data/spec/operations/domains/drop_domain_spec.rb +119 -0
  158. data/spec/operations/domains/rename_domain_spec.rb +70 -0
  159. data/spec/operations/enums/change_enum_spec.rb +157 -0
  160. data/spec/operations/enums/create_enum_spec.rb +40 -0
  161. data/spec/operations/enums/drop_enum_spec.rb +120 -0
  162. data/spec/operations/enums/rename_enum_spec.rb +72 -0
  163. data/spec/operations/foreign_keys/add_foreign_key_spec.rb +208 -0
  164. data/spec/operations/foreign_keys/drop_foreign_key_spec.rb +167 -0
  165. data/spec/operations/foreign_keys/rename_foreign_key_spec.rb +101 -0
  166. data/spec/operations/functions/change_function_spec.rb +166 -0
  167. data/spec/operations/functions/create_function_spec.rb +192 -0
  168. data/spec/operations/functions/drop_function_spec.rb +182 -0
  169. data/spec/operations/functions/rename_function_spec.rb +101 -0
  170. data/spec/operations/indexes/add_index_spec.rb +94 -0
  171. data/spec/operations/materialized_views/change_materialized_view_spec.rb +190 -0
  172. data/spec/operations/materialized_views/create_materialized_view_spec.rb +144 -0
  173. data/spec/operations/materialized_views/drop_materialized_view_spec.rb +145 -0
  174. data/spec/operations/materialized_views/refresh_materialized_view_spec.rb +79 -0
  175. data/spec/operations/materialized_views/rename_materialized_view_spec.rb +88 -0
  176. data/spec/operations/procedures/change_procedure_spec.rb +175 -0
  177. data/spec/operations/procedures/create_procedure_spec.rb +151 -0
  178. data/spec/operations/procedures/drop_procedure_spec.rb +159 -0
  179. data/spec/operations/procedures/rename_procedure_spec.rb +107 -0
  180. data/spec/operations/statistics/create_statistics_spec.rb +230 -0
  181. data/spec/operations/statistics/drop_statistics_spec.rb +106 -0
  182. data/spec/operations/statistics/rename_statistics_spec.rb +129 -0
  183. data/spec/operations/tables/create_table_spec.rb +53 -0
  184. data/spec/operations/tables/rename_table_spec.rb +37 -0
  185. data/spec/operations/triggers/change_trigger_spec.rb +195 -0
  186. data/spec/operations/triggers/create_trigger_spec.rb +104 -0
  187. data/spec/operations/triggers/drop_trigger_spec.rb +124 -0
  188. data/spec/operations/triggers/rename_trigger_spec.rb +160 -0
  189. data/spec/operations/views/change_view_spec.rb +144 -0
  190. data/spec/operations/views/create_view_spec.rb +134 -0
  191. data/spec/operations/views/drop_view_spec.rb +146 -0
  192. data/spec/operations/views/rename_view_spec.rb +85 -0
  193. data/spec/pg_trunk/dependencies_resolver_spec.rb +43 -0
  194. data/spec/spec_helper.rb +28 -0
  195. data/spec/support/migrations_helper.rb +376 -0
  196. 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