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,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ # nodoc
4
+ module PGTrunk::Operations
5
+ # @private
6
+ # Definitions for composite types
7
+ module CompositeTypes
8
+ require_relative "composite_types/column"
9
+ require_relative "composite_types/base"
10
+ require_relative "composite_types/change_composite_type"
11
+ require_relative "composite_types/create_composite_type"
12
+ require_relative "composite_types/drop_composite_type"
13
+ require_relative "composite_types/rename_composite_type"
14
+ end
15
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: false
2
+
3
+ module PGTrunk::Operations::Domains
4
+ # @abstract
5
+ # @private
6
+ # Base class for operations with domain types
7
+ class Base < PGTrunk::Operation
8
+ # All attributes that can be used by domain-related commands
9
+ attribute :collation, :pg_trunk_qualified_name
10
+ attribute :constraints, :pg_trunk_array_of_hashes, default: []
11
+ attribute :default_sql, :pg_trunk_multiline_text
12
+ attribute :null, :boolean
13
+ attribute :type, :pg_trunk_qualified_name, aliases: :as
14
+
15
+ # Populate constraints from a block
16
+ def constraint(check, name: nil)
17
+ constraints << Constraint.new(check: check, name: name)
18
+ end
19
+
20
+ # Wrap constraint definitions to value objects
21
+ after_initialize { constraints.map! { |c| Constraint.build(c) } }
22
+
23
+ validates :if_not_exists, absence: true
24
+ validates :name, presence: true
25
+ validates :constraints, "PGTrunk/all_items_valid": true, allow_nil: true
26
+
27
+ # Use comparison by name from pg_trunk operations base class (default)
28
+ # Support name as the only positional argument (default)
29
+
30
+ ruby_snippet do |s|
31
+ s.ruby_param(name.lean) if name.present?
32
+ s.ruby_param(as: type.lean) if type.present?
33
+ s.ruby_param(to: new_name) if new_name.present?
34
+ s.ruby_param(if_exists: true) if if_exists
35
+ s.ruby_param(force: :cascade) if force == :cascade
36
+
37
+ s.ruby_line(:collation, collation.lean) if collation.present?
38
+ s.ruby_line(:default_sql, default_sql, from: from_default_sql) if default_sql
39
+ s.ruby_line(:null, false) if null == false
40
+ constraints.sort_by(&:name).each do |c|
41
+ s.ruby_line(:constraint, c.check, **c.opts)
42
+ end
43
+ s.ruby_line(:comment, comment, from: from_comment) if comment
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,140 @@
1
+ # frozen_string_literal: false
2
+
3
+ # @!method ActiveRecord::Migration#change_domain(name, &block)
4
+ # Modify a domain 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 add or remove constraints,
11
+ # modify the default_sql value, or the description of the domain type.
12
+ # Neither the underlying type nor the collation can be changed.
13
+ #
14
+ # change_domain "dict.us_postal_code" do |d|
15
+ # d.null true # from: false
16
+ # # check is added for inversion
17
+ # d.drop_constraint "postal_code_length", check: <<~SQL
18
+ # length(VALUE) > 3 AND length(VALUE) < 6
19
+ # SQL
20
+ # d.add_constraint <<~SQL, name: "postal_code_valid"
21
+ # VALUE ~ '^\d{5}$' OR VALUE ~ '^\d{5}-\d{4}$'
22
+ # SQL
23
+ # d.default_sql "'00000'::text", from: "'0000'::text"
24
+ # d.comment <<~COMMENT, from: <<~COMMENT
25
+ # Supported currencies
26
+ # COMMENT
27
+ # Currencies
28
+ # COMMENT
29
+ # end
30
+ #
31
+ # Use blank string (not a `nil` value) to reset either a default_sql,
32
+ # or the comment. `nil`-s here will be ignored.
33
+ #
34
+ # When dropping a constraint you can use a `check` expression.
35
+ # In the same manner, use `from` option with `comment` or `default_sql`
36
+ # to make the operation invertible.
37
+ #
38
+ # It is irreversible in case any `drop_constraint` clause
39
+ # has `if_exists: true` or `force: :cascade` option -- due to
40
+ # uncertainty of the previous state of the database:
41
+ #
42
+ # # Irreversible change
43
+ # change_domain "dict.us_postal_code", force: :cascade do |d|
44
+ # d.drop_constraint "postal_code_valid" # missed `:check` option
45
+ # d.drop_constraint "postal_code_length"
46
+ # d.drop_constraint "postal_code_format", if_exists: true
47
+ # d.default_sql "'0000'::text" # missed `:from` option
48
+ # d.comment "New comment" # missed `:from` option
49
+ # end
50
+
51
+ module PGTrunk::Operations::Domains
52
+ # @private
53
+ class ChangeDomain < Base
54
+ # Methods to populate `constraints` from the block
55
+
56
+ def add_constraint(check, name: nil, valid: true)
57
+ constraints << Constraint.new(name: name, check: check, valid: valid)
58
+ end
59
+
60
+ def rename_constraint(name, to:)
61
+ constraints << Constraint.new(name: name, new_name: to)
62
+ end
63
+
64
+ def validate_constraint(name)
65
+ constraints << Constraint.new(name: name, valid: true)
66
+ end
67
+
68
+ def drop_constraint(name, check: nil, if_exists: nil)
69
+ constraints << Constraint.new(
70
+ check: check,
71
+ drop: true,
72
+ force: force,
73
+ if_exists: if_exists,
74
+ name: name,
75
+ )
76
+ end
77
+
78
+ validates :if_exists, :new_name, :type, :collation, absence: true
79
+ validate { errors.add :base, "There are no changes" if change.blank? }
80
+
81
+ def to_sql(_version)
82
+ [*change_default, *change_null, *change_constraints, *change_comment]
83
+ .join(" ")
84
+ end
85
+
86
+ def invert
87
+ keys = inversion.select { |_, v| v.nil? }.keys.join(", ").presence
88
+ errors = constraints.map(&:inversion_error).compact
89
+ errors << "Can't invert #{keys}" if keys.present?
90
+ raise IrreversibleMigration.new(self, nil, *errors) if errors.any?
91
+
92
+ self.class.new(**to_h, **inversion)
93
+ end
94
+
95
+ private
96
+
97
+ def change_default
98
+ <<~SQL.squish if default_sql
99
+ ALTER DOMAIN #{name.to_sql}
100
+ #{default_sql.present? ? "SET DEFAULT #{default_sql}" : 'DROP DEFAULT'};
101
+ SQL
102
+ end
103
+
104
+ def change_null
105
+ <<~SQL.squish unless null.nil?
106
+ ALTER DOMAIN #{name.to_sql} #{null ? 'DROP' : 'SET'} NOT NULL;
107
+ SQL
108
+ end
109
+
110
+ def change_constraints
111
+ constraints.map do |c|
112
+ "ALTER DOMAIN #{name.to_sql} #{c.to_sql};"
113
+ end
114
+ end
115
+
116
+ def change_comment
117
+ <<~SQL.squish if comment
118
+ COMMENT ON DOMAIN #{name.to_sql} IS $comment$#{comment}$comment$;
119
+ SQL
120
+ end
121
+
122
+ def change
123
+ @change ||= {
124
+ comment: comment,
125
+ constraints: constraints.map(&:to_h).presence,
126
+ default_sql: default_sql,
127
+ null: null,
128
+ }.compact
129
+ end
130
+
131
+ def inversion
132
+ @inversion ||= {
133
+ comment: from_comment,
134
+ constraints: constraints&.map(&:invert),
135
+ default_sql: from_default_sql,
136
+ null: !null,
137
+ }.slice(*change.keys)
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: false
2
+
3
+ module PGTrunk::Operations::Domains
4
+ # @private
5
+ # Definition for the domain's constraint
6
+ class Constraint
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 :check, :pg_trunk_multiline_text
16
+ attribute :drop, :boolean
17
+ attribute :force, :pg_trunk_symbol
18
+ attribute :if_exists, :boolean
19
+ attribute :name, :string
20
+ attribute :new_name, :string
21
+ attribute :valid, :boolean
22
+
23
+ validates :name, presence: true
24
+ validates :new_name, "PGTrunk/difference": { from: :name }, allow_nil: true
25
+ validates :force, inclusion: { in: %i[force restrict] }, allow_nil: true
26
+
27
+ def to_h
28
+ @to_h ||= attributes.compact.symbolize_keys
29
+ end
30
+
31
+ def opts
32
+ to_h.slice(:name)
33
+ end
34
+
35
+ def invert
36
+ @invert ||= {}.tap do |i|
37
+ i[:name] = new_name.presence || name
38
+ i[:new_name] = name if new_name.present?
39
+ i[:drop] = !drop if new_name.blank?
40
+ i[:check] = check if drop
41
+ end
42
+ end
43
+
44
+ def to_sql
45
+ rename_sql || drop_sql || add_sql || validate_sql
46
+ end
47
+
48
+ def inversion_error
49
+ return <<~MSG.squish if if_exists
50
+ with `if_exists: true` option cannot be inverted
51
+ due to uncertainty of the previous state of the database.
52
+ MSG
53
+
54
+ return <<~MSG.squish if force == :cascade
55
+ with `force: :cascade` option cannot be inverted
56
+ due to uncertainty of the previous state of the database.
57
+ MSG
58
+
59
+ return if check.present?
60
+
61
+ "the constraint `#{name}` is dropped without `check` option."
62
+ end
63
+
64
+ private
65
+
66
+ def rename_sql
67
+ <<~SQL.squish if new_name.present?
68
+ RENAME CONSTRAINT #{name.inspect} TO #{new_name.inspect}
69
+ SQL
70
+ end
71
+
72
+ def drop_sql
73
+ return unless drop
74
+
75
+ sql = "DROP CONSTRAINT"
76
+ sql << " IF EXISTS" if if_exists
77
+ sql << " #{name.inspect}"
78
+ sql << " CASCADE" if force == :cascade
79
+ sql << ";"
80
+ end
81
+
82
+ def add_sql
83
+ <<~SQL.squish if check.present?
84
+ ADD CONSTRAINT #{name.inspect}
85
+ CHECK (#{check})#{' NOT VALID' unless valid}
86
+ SQL
87
+ end
88
+
89
+ def validate_sql
90
+ "VALIDATE CONSTRAINT #{name.inspect}" if valid
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: false
2
+
3
+ # @!method ActiveRecord::Migration#create_domain(name, **options, &block)
4
+ # Create a domain type
5
+ #
6
+ # @param [#to_s] name (nil) The qualified name of the type
7
+ # @option [#to_s] :as (nil) The base type for the domain (alias: :type)
8
+ # @option [#to_s] :collation (nil) The collation
9
+ # @option [Boolean] :null (true) If a value of this type can be NULL
10
+ # @option [#to_s] :default_sql (nil) The snippet for the default value of the domain
11
+ # @option [#to_s] :comment (nil) The comment describing the constraint
12
+ # @yield [Proc] the block with the type's definition
13
+ # @yieldparam The receiver of methods specifying the type
14
+ #
15
+ # @example:
16
+ #
17
+ # create_domain "dict.us_postal_code", as: "text" do |d|
18
+ # d.collation "en_US"
19
+ # d.default_sql "'0000'::text"
20
+ # d.null false
21
+ # d.constraint <<~SQL, name: "code_valid"
22
+ # VALUE ~ '^\d{5}$' OR VALUE ~ '^\d{5}-\d{4}$'
23
+ # SQL
24
+ # d.comment "US postal code"
25
+ # end
26
+ #
27
+ # It is always reversible.
28
+
29
+ module PGTrunk::Operations::Domains
30
+ # @private
31
+ class CreateDomain < Base
32
+ validates :type, presence: true
33
+ validates :force, :if_exists, :new_name, absence: true
34
+
35
+ from_sql do |_version|
36
+ <<~SQL
37
+ SELECT
38
+ t.oid,
39
+ (t.typnamespace::regnamespace || '.' || t.typname) AS name,
40
+ (
41
+ CASE
42
+ WHEN b.typnamespace != 'pg_catalog'::regnamespace
43
+ THEN b.typnamespace::regnamespace || '.' || b.typname
44
+ ELSE b.typname
45
+ END
46
+ ) AS "type",
47
+ (
48
+ CASE
49
+ WHEN c.collnamespace != 'pg_catalog'::regnamespace
50
+ THEN c.collnamespace::regnamespace || '.' || c.collname
51
+ WHEN c.collname != 'default'
52
+ THEN c.collname
53
+ END
54
+ ) AS collation,
55
+ (CASE WHEN t.typnotnull THEN false END) AS null,
56
+ (
57
+ CASE
58
+ WHEN t.typdefaultbin IS NOT NULL
59
+ THEN pg_get_expr(t.typdefaultbin, 0, true)
60
+ END
61
+ ) AS default_sql,
62
+ (
63
+ SELECT json_agg(
64
+ json_build_object(
65
+ 'name', c.conname,
66
+ 'check', pg_get_expr(c.conbin, 0, true)
67
+ )
68
+ )
69
+ FROM pg_constraint c
70
+ WHERE c.contypid = t.oid
71
+ ) AS constraints,
72
+ d.description AS comment
73
+ FROM pg_type t
74
+ JOIN pg_trunk e ON e.oid = t.oid
75
+ AND e.classid = 'pg_type'::regclass
76
+ JOIN pg_type b ON b.oid = t.typbasetype
77
+ LEFT JOIN pg_collation c ON c.oid = t.typcollation
78
+ LEFT JOIN pg_description d ON d.objoid = t.oid
79
+ AND d.classoid = 'pg_type'::regclass
80
+ WHERE t.typtype = 'd';
81
+ SQL
82
+ end
83
+
84
+ def to_sql(_version)
85
+ [create_domain, *create_comment, register_domain].join(" ")
86
+ end
87
+
88
+ def invert
89
+ DropDomain.new(**to_h)
90
+ end
91
+
92
+ private
93
+
94
+ def create_domain
95
+ sql = "CREATE DOMAIN #{name.to_sql} AS #{type.to_sql}"
96
+ sql << " COLLATE #{collation.to_sql}" if collation.present?
97
+ sql << " DEFAULT #{default_sql}" if default_sql.present?
98
+ sql << " NOT NULL" if null == false
99
+ constraints&.each do |c|
100
+ sql << " CONSTRAINT #{c.name.inspect}" if c.name.present?
101
+ sql << " CHECK (#{c.check})"
102
+ end
103
+ sql << ";"
104
+ end
105
+
106
+ def create_comment
107
+ return if comment.blank?
108
+
109
+ "COMMENT ON DOMAIN #{name.to_sql} IS $comment$#{comment}$comment$;"
110
+ end
111
+
112
+ def register_domain
113
+ <<~SQL.squish
114
+ INSERT INTO pg_trunk (oid, classid)
115
+ SELECT oid, 'pg_type'::regclass
116
+ FROM pg_type
117
+ WHERE typname = #{name.quoted}
118
+ AND typnamespace = #{name.namespace}
119
+ AND typtype = 'd'
120
+ ON CONFLICT DO NOTHING;
121
+ SQL
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: false
2
+
3
+ # @!method ActiveRecord::Migration#drop_domain(name, **options, &block)
4
+ # Drop a domain 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 [#to_s] :as (nil) The base type for the domain (alias: :type)
10
+ # @option [#to_s] :collation (nil) The collation
11
+ # @option [#to_s] :default_sql (nil) The snippet for the default value of the domain
12
+ # @option [#to_s] :comment (nil) The comment describing the constraint
13
+ # @yield [Proc] the block with the type's definition
14
+ # @yieldparam The receiver of methods specifying the type
15
+ #
16
+ # @example:
17
+ #
18
+ # drop_domain "dict.us_postal_code"
19
+ #
20
+ # To make the operation invertible, use the same options
21
+ # as in the `create_domain` operation.
22
+ #
23
+ # drop_domain "dict.us_postal_code", as: "string" do |d|
24
+ # d.constraint <<~SQL, name: "code_valid"
25
+ # VALUE ~ '^\d{5}$' OR VALUE ~ '^\d{5}-\d{4}$'
26
+ # SQL
27
+ # d.comment <<~COMMENT
28
+ # US postal code
29
+ # COMMENT
30
+ # end
31
+ #
32
+ # With the `force: :cascade` option the operation would remove
33
+ # all the objects that use the type.
34
+ #
35
+ # drop_domain "dict.us_postal_code", force: :cascade
36
+ #
37
+ # With the `if_exists: true` option the operation won't fail
38
+ # even when the view was absent in the database.
39
+ #
40
+ # drop_domain "dict.us_postal_code", if_exists: true
41
+ #
42
+ # Both options make a migration irreversible due to uncertainty
43
+ # of the previous state of the database.
44
+
45
+ module PGTrunk::Operations::Domains
46
+ # @private
47
+ class DropDomain < Base
48
+ # Forbid these attributes
49
+ validates :new_name, absence: true
50
+
51
+ def to_sql(_version)
52
+ sql = "DROP DOMAIN"
53
+ sql << " IF EXISTS" if if_exists
54
+ sql << " #{name.to_sql}"
55
+ sql << " CASCADE" if force == :cascade
56
+ sql << ";"
57
+ end
58
+
59
+ def invert
60
+ irreversible!("if_exists: true") if if_exists
61
+ irreversible!("force: :cascade") if force == :cascade
62
+ CreateDomain.new(**to_h.except(:force))
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: false
2
+
3
+ # @!method ActiveRecord::Migration#rename_domain(name, to:)
4
+ # Change the name and/or schema of a domain 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
+ # A domain type can be both renamed and moved to another schema.
10
+ #
11
+ # rename_domain "us_code", to: "dict.us_postal_code"
12
+ #
13
+ # The operation is always reversible.
14
+
15
+ module PGTrunk::Operations::Domains
16
+ # @private
17
+ class RenameDomain < Base
18
+ validates :new_name, presence: true
19
+ validates :force, :if_exists, 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 DOMAIN #{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 DOMAIN #{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 Domains
8
+ require_relative "domains/constraint"
9
+ require_relative "domains/base"
10
+ require_relative "domains/change_domain"
11
+ require_relative "domains/create_domain"
12
+ require_relative "domains/drop_domain"
13
+ require_relative "domains/rename_domain"
14
+ end
15
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: false
2
+
3
+ module PGTrunk::Operations::Enums
4
+ # @abstract
5
+ # @private
6
+ # Base class for operations with enumerated types
7
+ class Base < PGTrunk::Operation
8
+ # All attributes that can be used by enum-related commands
9
+ attribute :changes, :pg_trunk_array_of_hashes, default: []
10
+ attribute :values, :pg_trunk_array_of_strings, default: []
11
+
12
+ # populate values one-by-one in a block
13
+ def value(value)
14
+ values << value.to_s
15
+ end
16
+
17
+ # wrap change definitions into value objects
18
+ after_initialize { changes.map! { |change| Change.build(change) } }
19
+
20
+ validates :if_not_exists, absence: true
21
+ validates :name, presence: true
22
+ validates :changes, "PGTrunk/all_items_valid": true, allow_nil: true
23
+
24
+ # Use comparison by name from pg_trunk operations base class (default)
25
+ # Support name as the only positional argument (default)
26
+
27
+ ruby_snippet do |s|
28
+ s.ruby_param(name.lean) if name.present?
29
+ s.ruby_param(to: new_name) if new_name.present?
30
+ s.ruby_param(if_exists: true) if if_exists
31
+ s.ruby_param(force: :cascade) if force == :cascade
32
+
33
+ if values.join(", ").length < 60
34
+ s.ruby_line(:values, *values)
35
+ else
36
+ values.each { |value| s.ruby_line(:value, value) }
37
+ end
38
+ changes.select(&:add?).each do |change|
39
+ s.ruby_line(:add_value, change.name, **change.opts)
40
+ end
41
+ changes.select(&:rename?).each do |change|
42
+ s.ruby_line(:rename_value, change.name, to: change.new_name)
43
+ end
44
+ s.ruby_line(:comment, comment) if comment.present?
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: false
2
+
3
+ module PGTrunk::Operations::Enums
4
+ # @private
5
+ # Definition for the value's change
6
+ class Change
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 :name, :string
16
+ attribute :new_name, :string
17
+ attribute :after, :string
18
+ attribute :before, :string
19
+
20
+ validates :name, presence: true
21
+ validates :new_name, "PGTrunk/difference": { from: :name }, allow_nil: true
22
+ validate { errors.add :after, :present if rename? && after.present? }
23
+ validate { errors.add :before, :present if rename? && before.present? }
24
+ validate { errors.add :before, :present if [after, before].all?(&:present?) }
25
+
26
+ def rename?
27
+ new_name.present?
28
+ end
29
+
30
+ def add?
31
+ new_name.blank?
32
+ end
33
+
34
+ def to_h
35
+ attributes.compact.symbolize_keys
36
+ end
37
+
38
+ def opts
39
+ to_h.slice(:before, :after).compact
40
+ end
41
+
42
+ def invert
43
+ { name: new_name, new_name: name }
44
+ end
45
+
46
+ def to_sql
47
+ return "RENAME VALUE '#{name}' TO '#{new_name}'" if new_name.present?
48
+
49
+ sql = "ADD VALUE IF NOT EXISTS $value$#{name}$value$"
50
+ sql << " BEFORE $value$#{before}$value$" if before.present?
51
+ sql << " AFTER $value$#{after}$value$" if after.present?
52
+ sql
53
+ end
54
+ end
55
+ end