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,60 @@
1
+ # frozen_string_literal: false
2
+
3
+ # @!method ActiveRecord::Migration#drop_check_constraint(table, expression = nil, **options, &block)
4
+ # Remove a check constraint from the table
5
+ #
6
+ # @param [#to_s] table (nil) The qualified name of the table
7
+ # @param [#to_s] expression (nil) The SQL expression
8
+ # @option [Boolean] :if_exists (false) Suppress the error when the constraint is absent
9
+ # @option [#to_s] :name (nil) The optional name of the constraint
10
+ # @option [Boolean] :inherit (true) If the constraint should be inherited by subtables
11
+ # @option [#to_s] :comment (nil) The comment describing the constraint
12
+ # @yield [Proc] the block with the constraint's definition
13
+ # @yieldparam The receiver of methods specifying the constraint
14
+ #
15
+ # Definition for the `drop_check_constraint` operation
16
+ #
17
+ # The constraint can be identified by the table and explicit name
18
+ #
19
+ # drop_check_constraint :users, name: "phone_is_long_enough"
20
+ #
21
+ # Alternatively the name can be got from the expression.
22
+ # Be careful! the expression must have exactly the same form
23
+ # as stored in the database:
24
+ #
25
+ # drop_check_constraint :users, "length((phone::text) > 10)"
26
+ #
27
+ # To made operation reversible the expression must be provided:
28
+ #
29
+ # drop_check_constraint "users" do |c|
30
+ # c.expression "length((phone::text) > 10)"
31
+ # c.inherit false
32
+ # c.comment "The phone is 10+ chars long"
33
+ # end
34
+ #
35
+ # The operation can be called with `if_exists` option.
36
+ #
37
+ # drop_check_constraint :users,
38
+ # name: "phone_is_long_enough",
39
+ # if_exists: true
40
+ #
41
+ # In this case the operation is always irreversible due to
42
+ # uncertainty of the previous state of the database.
43
+
44
+ module PGTrunk::Operations::CheckConstraints
45
+ # @private
46
+ class DropCheckConstraint < Base
47
+ validates :new_name, absence: true
48
+
49
+ def to_sql(_version)
50
+ sql = "ALTER TABLE #{table.to_sql} DROP CONSTRAINT"
51
+ sql << " IF EXISTS" if if_exists
52
+ sql << " #{name.name.inspect};"
53
+ end
54
+
55
+ def invert
56
+ irreversible!("if_exists: true") if if_exists
57
+ AddCheckConstraint.new(**to_h)
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: false
2
+
3
+ # @!method ActiveRecord::Migration#rename_check_constraint(table, expression = nil, **options, &block)
4
+ # Rename a check constraint
5
+ #
6
+ # @param [#to_s] table (nil) The qualified name of the table
7
+ # @param [#to_s] expression (nil) The SQL expression
8
+ # @option [#to_s] :name (nil) The current name of the constraint
9
+ # @option [#to_s] :to (nil) The new name for the constraint
10
+ # @yield [Proc] the block with the constraint's definition
11
+ # @yieldparam The receiver of methods specifying the constraint
12
+ #
13
+ # A constraint can be identified by the table and explicit name
14
+ #
15
+ # rename_check_constraint :users,
16
+ # name: "phone_is_long_enough",
17
+ # to: "phones.long_enough"
18
+ #
19
+ # Alternatively the name can be got from the expression.
20
+ # Be careful! the expression must have exactly the same form
21
+ # as stored in the database:
22
+ #
23
+ # rename_check_constraint :users, "length((phone::text) > 10)",
24
+ # to: "long_enough"
25
+ #
26
+ # The name can be reset to auto-generated when
27
+ # the `:to` option is missed or blank:
28
+ #
29
+ # rename_check_constraint :users, "phone_is_long_enough"
30
+ #
31
+ # The operation is always reversible.
32
+
33
+ module PGTrunk::Operations::CheckConstraints
34
+ # @private
35
+ class RenameCheckConstraint < Base
36
+ # Reset the name to default when `to:` option is missed or set to `nil`
37
+ after_initialize { self.new_name = generated_name if new_name.blank? }
38
+
39
+ validates :new_name, presence: true
40
+ validates :if_exists, :force, :comment, absence: true
41
+
42
+ def to_sql(_version)
43
+ <<~SQL.squish
44
+ ALTER TABLE #{table.to_sql}
45
+ RENAME CONSTRAINT #{name.name.inspect}
46
+ TO #{new_name.name.inspect};
47
+ SQL
48
+ end
49
+
50
+ def invert
51
+ self.class.new(**to_h, name: new_name, to: name)
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: false
2
+
3
+ # @!method ActiveRecord::Migration#validate_check_constraint(table, expression = nil, **options, &block)
4
+ # Validate an invalid check constraint
5
+ #
6
+ # @param [#to_s] table (nil) The qualified name of the table
7
+ # @param [#to_s] expression (nil) The SQL expression
8
+ # @option [#to_s] :name (nil) The optional name of the constraint
9
+ # @yield [Proc] the block with the constraint's definition
10
+ # @yieldparam The receiver of methods specifying the constraint
11
+ #
12
+ # The invalid constraint can be identified by table and explicit name:
13
+ #
14
+ # validate_check_constraint :users, name: "phone_is_long_enough"
15
+ #
16
+ # Alternatively it can be specified by expression. In this case
17
+ # you must ensure the expression has the same form as it is stored
18
+ # in the database (after parsing the source).
19
+ #
20
+ # validate_check_constraint :users, "length((phone::text) > 10)"
21
+ #
22
+ # Notice that it is invertible but the inverted operation does nothing.
23
+
24
+ module PGTrunk::Operations::CheckConstraints
25
+ # @private
26
+ class ValidateCheckConstraint < Base
27
+ validates :if_exists, :force, :new_name, :comment, :new_name, :inherit,
28
+ absence: true
29
+
30
+ def to_sql(_version)
31
+ <<~SQL.squish
32
+ ALTER TABLE #{table.to_sql} VALIDATE CONSTRAINT #{name.name.inspect};
33
+ SQL
34
+ end
35
+
36
+ # The operation is invertible but the inversion does nothing
37
+ def invert; end
38
+ end
39
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ # nodoc
4
+ module PGTrunk::Operations
5
+ # @private
6
+ # Definitions for check constraints
7
+ module CheckConstraints
8
+ require_relative "check_constraints/base"
9
+ require_relative "check_constraints/add_check_constraint"
10
+ require_relative "check_constraints/drop_check_constraint"
11
+ require_relative "check_constraints/rename_check_constraint"
12
+ require_relative "check_constraints/validate_check_constraint"
13
+ end
14
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: false
2
+
3
+ module PGTrunk::Operations::CompositeTypes
4
+ # @abstract
5
+ # @private
6
+ # Base class for operations with composite types
7
+ class Base < PGTrunk::Operation
8
+ # All columns that can be used by type-related commands
9
+ attribute :columns, :pg_trunk_array_of_hashes, default: []
10
+
11
+ # Populate columns from a block
12
+ def column(name, type, collation: nil)
13
+ columns << Column.new(name: name, type: type, collation: collation)
14
+ end
15
+
16
+ # Wrap column definitions to value objects
17
+ after_initialize { columns.map! { |a| Column.build(a) } }
18
+
19
+ validates :if_not_exists, absence: true
20
+ validates :name, presence: true
21
+ validates :columns, "PGTrunk/all_items_valid": true, allow_nil: true
22
+
23
+ # Use comparison by name from pg_trunk operations base class (default)
24
+ # Support name as the only positional argument (default)
25
+
26
+ ruby_snippet do |s|
27
+ s.ruby_param(name.lean) if name.present?
28
+ s.ruby_param(to: new_name.lean) if new_name.present?
29
+ s.ruby_param(if_exists: true) if if_exists
30
+ s.ruby_param(force: :cascade) if force == :cascade
31
+
32
+ columns.reject(&:change).each do |c|
33
+ s.ruby_line(
34
+ :column, c.name, c.type&.lean, collation: c.collation&.lean,
35
+ )
36
+ end
37
+ columns.select { |c| c.change == :add }.each do |c|
38
+ s.ruby_line(
39
+ :add_column, c.name, c.type&.lean, collation: c.collation&.lean,
40
+ )
41
+ end
42
+ columns.select { |c| c.change == :rename }.each do |c|
43
+ s.ruby_line(:rename_column, c.name, to: c.new_name)
44
+ end
45
+ columns.select { |c| c.change == :alter }.each do |c|
46
+ s.ruby_line(
47
+ :change_column, c.name, c.type&.lean,
48
+ collation: c.collation&.lean,
49
+ from_type: c.from_type&.lean,
50
+ from_collation: c.from_collation&.lean,
51
+ )
52
+ end
53
+ columns.select { |c| c.change == :drop }.each do |c|
54
+ s.ruby_line(
55
+ :drop_column, c.name, *c.type&.lean, collation: c.collation&.lean,
56
+ )
57
+ end
58
+ s.ruby_line(:comment, comment, from: from_comment) if comment
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,136 @@
1
+ # frozen_string_literal: false
2
+
3
+ # @!method ActiveRecord::Migration#change_composite_type(name, **options, &block)
4
+ # Modify a composite type
5
+ #
6
+ # @param [#to_s] name (nil) The qualified name of the type
7
+ # @option [Symbol] :force (:restrict) How to process dependent objects (`:cascade` or `:restrict`)
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
+ # The operation can be used to add, drop, rename or change columns.
13
+ # The comment can be changed as well.
14
+ #
15
+ # Providing a type "paint.colored_point":
16
+ #
17
+ # create_composite_type "paint.colored_point" do |t|
18
+ # t.column "color", "text", collation: "en_US"
19
+ # t.column "x", "integer"
20
+ # t.column "z", "integer"
21
+ # end
22
+ #
23
+ # After the following change:
24
+ #
25
+ # change_composite_type "paint.colored_point" do |t|
26
+ # t.change_column "color", "text", collation: "ru_RU", from_collation: "en_US"
27
+ # t.change_column "x", "bigint", from_type: "integer"
28
+ # t.drop_column "z", "integer"
29
+ # t.add_column "Y", "bigint"
30
+ # t.rename_column "x", to: "X"
31
+ # t.comment "2D point with a color", from: "2D point"
32
+ # end
33
+ #
34
+ # The definition became:
35
+ #
36
+ # create_composite_type "paint.colored_point" do |t|
37
+ # t.column "color", "text", collation: "ru_RU"
38
+ # t.column "X", "bigint"
39
+ # t.column "Y", "integer"
40
+ # end
41
+ #
42
+ # Notice, that all renames will be done AFTER other changes,
43
+ # so in `change_column` you should use the old names.
44
+ #
45
+ # In several cases the operation is not invertible:
46
+ #
47
+ # - when a column was dropped
48
+ # - when `force: :cascade` option is used (to update
49
+ # objects that use the type)
50
+ # - when `if_exists: true` is added to the `drop_column` clause
51
+ # - when a previous state of the column type, collation or comment
52
+ # is not specified.
53
+
54
+ module PGTrunk::Operations::CompositeTypes
55
+ # @private
56
+ class ChangeCompositeType < Base
57
+ # Methods to populate `columns` from the block
58
+ def add_column(name, type, collation: nil)
59
+ columns << Column.new(
60
+ name: name, type: type, collation: collation, change: :add, force: force,
61
+ )
62
+ end
63
+
64
+ def drop_column(name, type = nil, **opts)
65
+ opts = opts.slice(:if_exists, :collation)
66
+ columns << Column.new(
67
+ name: name, type: type, force: force, **opts, change: :drop,
68
+ )
69
+ end
70
+
71
+ def change_column(name, type, **opts)
72
+ opts = opts.slice(:collation, :from_type, :from_collation)
73
+ columns << Column.new(
74
+ name: name, type: type, force: force, change: :alter, **opts,
75
+ )
76
+ end
77
+
78
+ def rename_column(name, to:)
79
+ columns << Column.new(
80
+ name: name, new_name: to, force: force, change: :rename,
81
+ )
82
+ end
83
+
84
+ validates :if_exists, :new_name, absence: true
85
+ validate { errors.add :base, "There are no changes" if change.blank? }
86
+
87
+ def to_sql(_version)
88
+ [*change_columns, *rename_columns, *change_comment].join(" ")
89
+ end
90
+
91
+ def invert
92
+ keys = inversion.select { |_, v| v.nil? }.keys.join(", ").presence
93
+ errors = columns.map(&:inversion_error).compact
94
+ errors << "Can't invert #{keys}" if keys.present?
95
+ errors << "Can't invert dropped columns" if columns.any? { |c| c.change == :drop }
96
+ raise IrreversibleMigration.new(self, nil, *errors) if errors.any?
97
+
98
+ self.class.new(**to_h, **inversion)
99
+ end
100
+
101
+ private
102
+
103
+ def change_columns
104
+ list = columns.select { |c| c.change&.!= :rename }
105
+ return if list.blank?
106
+
107
+ "ALTER TYPE #{name.to_sql} #{list.map(&:to_sql).join(', ')};"
108
+ end
109
+
110
+ def rename_columns
111
+ columns.select { |c| c.change == :rename }.map do |c|
112
+ "ALTER TYPE #{name.to_sql} #{c.to_sql};"
113
+ end
114
+ end
115
+
116
+ def change_comment
117
+ <<~SQL.squish if comment
118
+ COMMENT ON TYPE #{name.to_sql} IS $comment$#{comment}$comment$;
119
+ SQL
120
+ end
121
+
122
+ def change
123
+ @change ||= {
124
+ comment: comment,
125
+ columns: columns.select(&:change).map(&:to_h).presence,
126
+ }.compact
127
+ end
128
+
129
+ def inversion
130
+ @inversion ||= {
131
+ comment: from_comment,
132
+ columns: columns.reverse.map(&:invert).presence,
133
+ }.slice(*change.keys)
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: false
2
+
3
+ module PGTrunk::Operations::CompositeTypes
4
+ # @private
5
+ # Definition for an column of a composite type
6
+ class Column
7
+ include ActiveModel::Model
8
+ include ActiveModel::Attributes
9
+ include ActiveModel::Validations
10
+
11
+ def self.build(data)
12
+ data.is_a?(self) ? data : new(**data)
13
+ end
14
+
15
+ attribute :change, :pg_trunk_symbol
16
+ attribute :collation, :pg_trunk_qualified_name
17
+ attribute :force, :pg_trunk_symbol
18
+ attribute :from_collation, :pg_trunk_qualified_name
19
+ attribute :from_type, :pg_trunk_qualified_name
20
+ attribute :if_exists, :boolean
21
+ attribute :name, :string
22
+ attribute :new_name, :string
23
+ attribute :type, :pg_trunk_qualified_name
24
+
25
+ validates :name, presence: true
26
+ validates :new_name, "PGTrunk/difference": { from: :name }, allow_nil: true
27
+ validates :change, inclusion: { in: %i[add alter drop rename] }, allow_nil: true
28
+ validates :force, inclusion: { in: %i[force restrict] }, allow_nil: true
29
+
30
+ def to_h
31
+ @to_h ||= attributes.compact.symbolize_keys
32
+ end
33
+
34
+ INVERTED = {
35
+ add: :drop, drop: :add, rename: :rename, alter: :alter,
36
+ }.freeze
37
+
38
+ def invert
39
+ @invert ||= {}.tap do |i|
40
+ i[:change] = INVERTED[change]
41
+ i[:name] = new_name.presence || name
42
+ i[:new_name] = name if new_name.present?
43
+ i[:type] = change == :add ? type : from_type
44
+ i[:collation] = change == :add ? collation : from_collation
45
+ end
46
+ end
47
+
48
+ def to_sql
49
+ case change
50
+ when :add then add_sql
51
+ when :alter then alter_sql
52
+ when :drop then drop_sql
53
+ when :rename then rename_sql
54
+ else sql
55
+ end
56
+ end
57
+
58
+ def inversion_error
59
+ return <<~MSG.squish if if_exists
60
+ with `if_exists: true` option cannot be inverted
61
+ due to uncertainty of the previous state of the database.
62
+ MSG
63
+
64
+ return <<~MSG.squish if force == :cascade
65
+ with `force: :cascade` option cannot be inverted
66
+ due to uncertainty of the previous state of the database.
67
+ MSG
68
+
69
+ return <<~MSG.squish if change == :drop && type.blank?
70
+ undefined type of the dropped column #{name}
71
+ MSG
72
+
73
+ return <<~MSG.squish if change == :alter && type && !from_type
74
+ undefined a previous state of the type for column #{name}
75
+ MSG
76
+
77
+ return <<~MSG.squish if change == :alter && collation && !from_collation
78
+ undefined a previous state of the collation for column #{name}
79
+ MSG
80
+ end
81
+
82
+ private
83
+
84
+ def rename_sql
85
+ "RENAME ATTRIBUTE #{name.inspect} TO #{new_name.inspect}".tap do |sql|
86
+ sql << " CASCADE" if force == :cascade
87
+ end
88
+ end
89
+
90
+ def drop_sql
91
+ "DROP ATTRIBUTE".tap do |sql|
92
+ sql << " IF EXISTS" if if_exists
93
+ sql << " #{name.inspect}"
94
+ sql << " CASCADE" if force == :cascade
95
+ end
96
+ end
97
+
98
+ def add_sql
99
+ "ADD ATTRIBUTE #{name.inspect} #{type.lean}".tap do |sql|
100
+ sql << " COLLATE #{collation.to_sql}" if collation.present?
101
+ sql << " CASCADE" if force == :cascade
102
+ end
103
+ end
104
+
105
+ def alter_sql
106
+ "ALTER ATTRIBUTE #{name.inspect} SET DATA TYPE #{type.lean}".tap do |sql|
107
+ sql << " COLLATE #{collation.to_sql}" if collation.present?
108
+ sql << " CASCADE" if force == :cascade
109
+ end
110
+ end
111
+
112
+ def sql
113
+ "#{name.inspect} #{type.lean}".tap do |sql|
114
+ sql << " COLLATE #{collation.to_sql}" if collation
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: false
2
+
3
+ # @!method ActiveRecord::Migration#create_composite_type(name, **options, &block)
4
+ # Create a composite type
5
+ #
6
+ # @param [#to_s] name (nil) The qualified name of the type
7
+ # @option [#to_s] :comment (nil) The comment describing the constraint
8
+ # @yield [Proc] the block with the type's definition
9
+ # @yieldparam The receiver of methods specifying the type
10
+ #
11
+ # @example
12
+ # create_composite_type "paint.colored_point" do |d|
13
+ # d.column "x", "integer"
14
+ # d.column "y", "integer"
15
+ # d.column "color", "text", collation: "en_US"
16
+ # d.comment <<~COMMENT
17
+ # 2D point with color
18
+ # COMMENT
19
+ # end
20
+ #
21
+ # It is always reversible.
22
+
23
+ module PGTrunk::Operations::CompositeTypes
24
+ # @private
25
+ class CreateCompositeType < Base
26
+ validates :force, :if_exists, :new_name, absence: true
27
+
28
+ from_sql do |_version|
29
+ <<~SQL
30
+ SELECT
31
+ t.oid,
32
+ (t.typnamespace::regnamespace || '.' || t.typname) AS name,
33
+ (
34
+ SELECT
35
+ json_agg(
36
+ json_build_object(
37
+ 'name', a.attname,
38
+ 'type', format_type(a.atttypid, a.atttypmod),
39
+ 'collation', (
40
+ CASE
41
+ WHEN c.collnamespace != 'pg_catalog'::regnamespace
42
+ THEN c.collnamespace::regnamespace || '.' || c.collname
43
+ WHEN c.collname != 'default'
44
+ THEN c.collname
45
+ END
46
+ )
47
+ ) ORDER BY a.attnum
48
+ )
49
+ FROM pg_attribute a
50
+ LEFT JOIN pg_collation c ON c.oid = a.attcollation
51
+ WHERE a.attrelid = t.typrelid
52
+ AND EXISTS (SELECT FROM pg_type WHERE a.atttypid = pg_type.oid)
53
+ ) AS columns,
54
+ d.description AS comment
55
+ FROM pg_type t
56
+ JOIN pg_trunk e ON e.oid = t.oid
57
+ AND e.classid = 'pg_type'::regclass
58
+ LEFT JOIN pg_description d ON d.objoid = t.oid
59
+ AND d.classoid = 'pg_type'::regclass
60
+ WHERE t.typtype = 'c';
61
+ SQL
62
+ end
63
+
64
+ def to_sql(_version)
65
+ [create_type, *create_comment, register_type].join(" ")
66
+ end
67
+
68
+ def invert
69
+ DropCompositeType.new(**to_h)
70
+ end
71
+
72
+ private
73
+
74
+ def create_type
75
+ <<~SQL.squish
76
+ CREATE TYPE #{name.to_sql}
77
+ AS (#{columns.reject(&:change).map(&:to_sql).join(',')});
78
+ SQL
79
+ end
80
+
81
+ def create_comment
82
+ return if comment.blank?
83
+
84
+ "COMMENT ON TYPE #{name.to_sql} IS $comment$#{comment}$comment$;"
85
+ end
86
+
87
+ def register_type
88
+ <<~SQL.squish
89
+ INSERT INTO pg_trunk (oid, classid)
90
+ SELECT oid, 'pg_type'::regclass
91
+ FROM pg_type
92
+ WHERE typname = #{name.quoted}
93
+ AND typnamespace = #{name.namespace}
94
+ AND typtype = 'c'
95
+ ON CONFLICT DO NOTHING;
96
+ SQL
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: false
2
+
3
+ # @!method ActiveRecord::Migration#drop_composite_type(name, **options, &block)
4
+ # Drop a composite type
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 [#to_s] :comment (nil) The comment describing the constraint
10
+ # @yield [Proc] the block with the type's definition
11
+ # @yieldparam The receiver of methods specifying the type
12
+ #
13
+ # The operation drops a composite_type type identified by its
14
+ # qualified name (it can include a schema).
15
+ #
16
+ # For inversion use the same options as in the
17
+ # `create_composite_type` operation.
18
+ #
19
+ # drop_composite_type "paint.colored_point" do |d|
20
+ # d.column "x", "integer"
21
+ # d.column "y", "integer"
22
+ # d.column "color", "text", collation: "en_US"
23
+ # d.comment <<~COMMENT
24
+ # 2D point with color
25
+ # COMMENT
26
+ # end
27
+ #
28
+ # Notice, that the composite type creation can use no attributes.
29
+ # That's why dropping it is always reversible; though the reversion
30
+ # would provide a type without columns:
31
+ #
32
+ # drop_composite_type "paint.colored_point"
33
+ #
34
+ # With the `force: :cascade` option the operation would remove
35
+ # all objects using the type.
36
+ #
37
+ # drop_composite_type "paint.colored_point", force: :cascade
38
+ #
39
+ # With the `if_exists: true` option the operation won't fail
40
+ # even when the view was absent in the database.
41
+ #
42
+ # drop_composite_type "paint.colored_point", if_exists: true
43
+ #
44
+ # Both options make a migration irreversible due to uncertainty
45
+ # of the previous state of the database.
46
+
47
+ module PGTrunk::Operations::CompositeTypes
48
+ # @private
49
+ class DropCompositeType < Base
50
+ # Forbid these columns
51
+ validates :new_name, absence: true
52
+
53
+ def to_sql(_version)
54
+ sql = "DROP TYPE"
55
+ sql << " IF EXISTS" if if_exists
56
+ sql << " #{name.to_sql}"
57
+ sql << " CASCADE" if force == :cascade
58
+ sql << ";"
59
+ end
60
+
61
+ def invert
62
+ irreversible!("if_exists: true") if if_exists
63
+ irreversible!("force: :cascade") if force == :cascade
64
+ CreateCompositeType.new(**to_h.except(:force, :if_exists))
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: false
2
+
3
+ # @!method ActiveRecord::Migration#rename_composite_type(name, to:)
4
+ # Change the name and/or schema of a composite 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_composite_type "point", to: "paint.colored_point"
12
+ #
13
+ # The operation is always reversible.
14
+
15
+ module PGTrunk::Operations::CompositeTypes
16
+ # @private
17
+ class RenameCompositeType < Base
18
+ validates :new_name, presence: true
19
+ validates :force, :if_exists, :columns, 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