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,76 @@
1
+ # frozen_string_literal: false
2
+
3
+ # @!method ActiveRecord::Migration#drop_foreign_key(table, reference, **options, &block)
4
+ # Drops 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 [Boolean] :if_exists (false) Suppress the error when the constraint is absent
10
+ # @option [#to_s] :to (nil) The new name for the foreign key
11
+ # @option [Array<#to_s>] :columns ([]) The list of columns of the table
12
+ # @option [#to_s] :column (nil) An alias for :columns for the case of single-column keys
13
+ # @option [Array<#to_s>] :primary_key ([]) The list of columns of the reference table
14
+ # @option [Symbol] :match (:full) Define how to match rows
15
+ # Supported values: :full (default), :partial, :simple
16
+ # @option [Symbol] :on_delete (:restrict)
17
+ # Define how to handle the deletion of the referred row.
18
+ # Supported values: :restrict (default), :cascade, :nullify, :reset
19
+ # @option [Symbol] :on_update (:restrict)
20
+ # Define how to handle the update of the referred row.
21
+ # Supported values: :restrict (default), :cascade, :nullify, :reset
22
+ # @yield [Proc] the block with the key's definition
23
+ # @yieldparam The receiver of methods specifying the foreign key
24
+ #
25
+ # The key can be identified by table/name (not invertible):
26
+ #
27
+ # drop_foreign_key "users", name: "user_roles_fk"
28
+ #
29
+ # To make it invertible use the same options like
30
+ # in the `add_foreign_key` operation.
31
+ #
32
+ # drop_foreign_key do |c|
33
+ # c.table "users"
34
+ # c.reference "roles"
35
+ # c.column "role_id"
36
+ # c.primary_key "id"
37
+ # c.on_update :cascade
38
+ # c.on_delete :cascade
39
+ # c.comment "Phone is 10+ chars long"
40
+ # end
41
+ #
42
+ # Notice that the name can be skipped, in this case we would
43
+ # find it in the database.
44
+ #
45
+ # The operation can be called with `if_exists` option.
46
+ #
47
+ # drop_foreign_key "users", name: "user_roles_fk", if_exists: true
48
+ #
49
+ # In this case the operation is always irreversible due to
50
+ # uncertainty of the previous state of the database.
51
+
52
+ module PGTrunk::Operations::ForeignKeys
53
+ # @private
54
+ class DropForeignKey < Base
55
+ # The name can be looked for in the database.
56
+ # This is necessary because the name generated by `rails` inside the table
57
+ # is different from the name generated by the `add_foreign_key`.
58
+ after_initialize { self.name = current_name if name.blank? }
59
+ # This prevents a validation error in case there's no fk found in the db
60
+ # by reference, columns and primary key.
61
+ after_initialize { self.name = generated_name if if_exists }
62
+
63
+ validates :if_not_exists, absence: true
64
+
65
+ def to_sql(_version)
66
+ sql = "ALTER TABLE #{table.to_sql} DROP CONSTRAINT"
67
+ sql << " IF EXISTS" if if_exists
68
+ sql << " #{name.lean.inspect};"
69
+ end
70
+
71
+ def invert
72
+ irreversible!("if_exists: true") if if_exists
73
+ AddForeignKey.new(**to_h)
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: false
2
+
3
+ # @!method ActiveRecord::Migration#rename_foreign_key(table, reference, **options, &block)
4
+ # Rename a foreign key
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
+ # @yield [Proc] the block with the key's definition
14
+ # @yieldparam The receiver of methods specifying the foreign key
15
+ #
16
+ # You can rename the foreign key constraint identified by its explicit name:
17
+ #
18
+ # rename_foreign_key :users,
19
+ # name: "user_roles_fk",
20
+ # to: "constraints.users_by_roles_fk"
21
+ #
22
+ # The key can also be found in the database by table/reference/columns/pk
23
+ #
24
+ # rename_foreign_key :users, :roles, primary_key: "name", to: "user_roles"
25
+ #
26
+ # If a new name is missed, then the name will be reset to the auto-generated one:
27
+ #
28
+ # rename_foreign_key :users, "user_roles_fk"
29
+ #
30
+ # The operation is always reversible.
31
+
32
+ module PGTrunk::Operations::ForeignKeys
33
+ #
34
+ # Definition for the `rename_foreign_key` operation
35
+ #
36
+ class RenameForeignKey < Base
37
+ # The name can be looked for in the database.
38
+ # This is necessary because the name generated by `rails` inside the table
39
+ # is different from the name generated by the `add_foreign_key`.
40
+ after_initialize { self.name = current_name if name.blank? }
41
+ # Reset the name to default when `to:` option is missed or set to `nil`
42
+ after_initialize { self.new_name = generated_name if new_name.blank? }
43
+
44
+ validates :new_name, presence: true
45
+ validates :if_exists, :if_not_exists, :match, absence: true
46
+
47
+ def to_sql(_version)
48
+ <<~SQL.squish
49
+ ALTER TABLE #{table.to_sql}
50
+ RENAME CONSTRAINT #{name.name.inspect}
51
+ TO #{new_name.name.inspect};
52
+ SQL
53
+ end
54
+
55
+ def invert
56
+ self.class.new(
57
+ **to_h,
58
+ name: (new_name if custom_name?(new_name)),
59
+ to: (name if name != current_name),
60
+ )
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ # nodoc
4
+ module PGTrunk::Operations
5
+ # @private
6
+ # Definitions for foreign keys
7
+ #
8
+ # We overload only add/drop operations to support features
9
+ # like composite keys along with anonymous key deletion.
10
+ module ForeignKeys
11
+ require_relative "foreign_keys/base"
12
+ require_relative "foreign_keys/add_foreign_key"
13
+ require_relative "foreign_keys/drop_foreign_key"
14
+ require_relative "foreign_keys/rename_foreign_key"
15
+ end
16
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: false
2
+
3
+ module PGTrunk::Operations::Functions
4
+ # @abstract
5
+ # @private
6
+ # Base class for operations with functions
7
+ class Base < PGTrunk::Operation
8
+ # All attributes that can be used by function-related commands
9
+ attribute :body, :pg_trunk_multiline_text
10
+ attribute :cost, :float
11
+ attribute :language, :pg_trunk_lowercase_string
12
+ attribute :leakproof, :boolean
13
+ attribute :parallel, :pg_trunk_symbol
14
+ attribute :replace_existing, :boolean
15
+ attribute :rows, :integer
16
+ attribute :security, :pg_trunk_symbol
17
+ attribute :strict, :boolean
18
+ attribute :volatility, :pg_trunk_symbol
19
+ attribute :version, :pg_trunk_multiline_text
20
+
21
+ # Ensure correctness of present values
22
+ validates :if_not_exists, absence: true
23
+ validates :volatility, inclusion: { in: %i[volatile stable immutable] }, allow_nil: true
24
+ validates :security, inclusion: { in: %i[invoker definer] }, allow_nil: true
25
+ validates :parallel, inclusion: { in: %i[safe unsafe] }, allow_nil: true
26
+ validates :cost, numericality: { greater_than: 0 }, allow_nil: true
27
+ validates :rows, numericality: { greater_than_or_equal_to: 0 }, allow_nil: true
28
+ validate do
29
+ errors.add :body, "can't contain SQL injection with $$" if body&.include?("$$")
30
+ end
31
+
32
+ # Use comparison by name from pg_trunk operations base class (default)
33
+ # Support name as the only positional argument (default)
34
+
35
+ ruby_snippet do |s|
36
+ s.ruby_param(name.lean) if name.present?
37
+ s.ruby_param(to: new_name.lean) if new_name.present?
38
+ s.ruby_param(if_exists: true) if if_exists
39
+ s.ruby_param(force: :cascade) if force == :cascade
40
+ s.ruby_param(replace_existing: true) if replace_existing
41
+
42
+ s.ruby_line(:language, language) if language&.!= "sql"
43
+ s.ruby_line(:volatility, volatility, from: from_volatility) if volatility.present?
44
+ s.ruby_line(:leakproof, true) if leakproof
45
+ s.ruby_line(:strict, true) if strict
46
+ s.ruby_line(:security, security) if security.present?
47
+ s.ruby_line(:parallel, parallel, from: from_parallel) unless parallel.nil?
48
+ s.ruby_line(:cost, cost, from: from_cost) if cost.present?
49
+ s.ruby_line(:rows, rows, from: from_rows) if rows.present?
50
+ s.ruby_line(:body, body, from: from_body) if body.present?
51
+ s.ruby_line(:comment, comment, from: from_comment) if comment
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: false
2
+
3
+ # @!method ActiveRecord::Migration#change_function(name, **options, &block)
4
+ # Modify a function
5
+ #
6
+ # @param [#to_s] name (nil) The qualified name of the function
7
+ # @option [Boolean] :if_exists (false) Suppress the error when the function is absent
8
+ # @yield [Proc] the block with the function's definition
9
+ # @yieldparam The receiver of methods specifying the function
10
+ #
11
+ # The operation changes the function without dropping it
12
+ # (which can be necessary when there are other objects
13
+ # using the function and you don't want to change them all).
14
+ #
15
+ # You can change any property except for the name
16
+ # (use `rename_function` instead) and `language`.
17
+ #
18
+ # change_function "math.mult(int, int)" do |f|
19
+ # f.volatility :immutable, from: :stable
20
+ # f.parallel :safe, from: :restricted
21
+ # f.security :invoker
22
+ # f.leakproof true
23
+ # f.strict true
24
+ # f.cost 5.0
25
+ # # f.rows 1 (supported for functions returning sets of rows)
26
+ # SQL
27
+ #
28
+ # The example above is not invertible because of uncertainty
29
+ # about the previous volatility, parallelism, and cost.
30
+ # To define them, use a from options (available in a block syntax only):
31
+ #
32
+ # change_function "math.mult(a int, b int)" do |f|
33
+ # f.body <<~SQL, from: <<~SQL
34
+ # SELECT a * b;
35
+ # SQL
36
+ # SELECT min(a * b, 1);
37
+ # SQL
38
+ # f.volatility :immutable, from: :volatile
39
+ # f.parallel :safe, from: :unsafe
40
+ # f.leakproof true
41
+ # f.strict true
42
+ # f.cost 5.0, from: 100.0
43
+ # # f.rows 1, from: 0
44
+ # SQL
45
+ #
46
+ # Like in the other operations, the function can be
47
+ # identified by a qualified name (with types of arguments).
48
+ # If it has no overloaded implementations, the plain name is supported as well.
49
+
50
+ module PGTrunk::Operations::Functions
51
+ # @private
52
+ class ChangeFunction < Base
53
+ validates :force, :new_name, :language, :replace_existing, absence: true
54
+ validate { errors.add :base, "Changes can't be blank" if changes.blank? }
55
+ validate do
56
+ next if if_exists || name.blank? || create_function.present?
57
+
58
+ errors.add :base, "Function #{name.lean} can't be found"
59
+ end
60
+
61
+ def to_sql(server_version)
62
+ # Use `CREATE OR REPLACE FUNCTION` to make changes
63
+ create_function&.to_sql(server_version)
64
+ end
65
+
66
+ def invert
67
+ irreversible!("if_exists: true") if if_exists
68
+ undefined = inversion.select { |_, v| v.nil? }.keys.join(", ").presence
69
+ raise IrreversibleMigration.new(self, nil, <<~MSG.squish) if undefined
70
+ Undefined values to revert #{undefined}.
71
+ MSG
72
+
73
+ self.class.new(**inversion, name: name)
74
+ end
75
+
76
+ private
77
+
78
+ def create_function
79
+ return if name.blank?
80
+
81
+ @create_function ||= begin
82
+ list = CreateFunction.select { |obj| name.maybe_eq?(obj.name) }
83
+ list.select! { |obj| name == obj.name } if list.size > 1 && name.args
84
+ list.first&.tap do |op|
85
+ op.attributes = { **changes, replace_existing: true }
86
+ end
87
+ end
88
+ end
89
+
90
+ def changes
91
+ @changes ||= to_h.except(:name).reject { |_, v| v.nil? || v == "" }
92
+ end
93
+
94
+ def inversion
95
+ @inversion ||= {
96
+ body: [body, from_body],
97
+ volatility: [volatility, from_volatility],
98
+ parallel: [parallel, from_parallel],
99
+ cost: [cost, from_cost],
100
+ rows: [rows, from_rows],
101
+ comment: [comment, from_comment],
102
+ security: [security, (security == :invoker ? :definer : :invoker)],
103
+ leakproof: [leakproof, !leakproof],
104
+ strict: [strict, !strict],
105
+ }.slice(*changes.keys).transform_values(&:last)
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,198 @@
1
+ # frozen_string_literal: false
2
+
3
+ # @!method ActiveRecord::Migration#create_function(name, **options, &block)
4
+ # Create a function
5
+ #
6
+ # @param [#to_s] name (nil)
7
+ # The qualified name of the function with arguments and returned value type
8
+ # @option [Boolean] :replace_existing (false) If the function should overwrite an existing one
9
+ # @option [#to_s] :language ("sql") The language (like "sql" or "plpgsql")
10
+ # @option [#to_s] :body (nil) The body of the function
11
+ # @option [Symbol] :volatility (:volatile) The volatility of the function.
12
+ # Supported values: :volatile (default), :stable, :immutable
13
+ # @option [Symbol] :parallel (:unsafe) The safety of parallel execution.
14
+ # Supported values: :unsafe (default), :restricted, :safe
15
+ # @option [Symbol] :security (:invoker) Define the role under which the function is invoked
16
+ # Supported values: :invoker (default), :definer
17
+ # @option [Boolean] :leakproof (false) If the function is leakproof
18
+ # @option [Boolean] :strict (false) If the function is strict
19
+ # @option [Float] :cost (nil) The cost estimation for the function
20
+ # @option [Integer] :rows (nil) The number of rows returned by a function
21
+ # @option [#to_s] :comment The description of the function
22
+ # @yield [Proc] the block with the function's definition
23
+ # @yieldparam The receiver of methods specifying the function
24
+ #
25
+ # The function can be created either using inline syntax
26
+ #
27
+ # create_function "math.mult(a int, b int) int",
28
+ # language: :sql,
29
+ # body: "SELECT a * b",
30
+ # volatility: :immutable,
31
+ # leakproof: true,
32
+ # comment: "Multiplies 2 integers"
33
+ #
34
+ # or using a block:
35
+ #
36
+ # create_function "math.mult(a int, b int) int" do |f|
37
+ # f.language "sql" # (default)
38
+ # f.body <<~SQL
39
+ # SELECT a * b;
40
+ # SQL
41
+ # f.volatility :immutable # :stable, :volatile (default)
42
+ # f.parallel :safe # :restricted, :unsafe (default)
43
+ # f.security :invoker # (default), also :definer
44
+ # f.leakproof true
45
+ # f.strict true
46
+ # f.cost 5.0
47
+ # # f.rows 1 (supported for functions returning sets of rows)
48
+ # f.comment "Multiplies 2 integers"
49
+ # SQL
50
+ #
51
+ # With a `replace_existing: true` option,
52
+ # it will be created using the `CREATE OR REPLACE` clause.
53
+ # In this case the migration is irreversible because we
54
+ # don't know if and how to restore its previous definition.
55
+ #
56
+ # create_function "math.mult(a int, b int) int",
57
+ # body: "SELECT a * b",
58
+ # replace_existing: true
59
+ #
60
+ # We presume a function without arguments should have
61
+ # no arguments and return `void` like
62
+ #
63
+ # # the same as "do_something() void"
64
+ # create_function "do_something" do |f|
65
+ # # ...
66
+ # end
67
+
68
+ module PGTrunk::Operations::Functions
69
+ # @private
70
+ class CreateFunction < Base
71
+ # The definition must be either set explicitly
72
+ # or by reading the versioned snippet.
73
+ validate { errors.add :body, :blank if body.blank? && version.blank? }
74
+ validates :if_exists, :force, :new_name, absence: true
75
+
76
+ from_sql do |server_version|
77
+ plain_function = "NOT p.proisagg AND NOT p.proiswindow"
78
+ plain_function = "p.prokind = 'f'" if server_version >= "11"
79
+
80
+ <<~SQL.squish
81
+ SELECT
82
+ p.oid,
83
+ (
84
+ p.pronamespace::regnamespace || '.' || p.proname || '(' || (
85
+ regexp_replace(
86
+ regexp_replace(
87
+ pg_get_function_arguments(p.oid), '^\s*IN\s+', '', 'g'
88
+ ), '[,]\s*IN\s+', ',', 'g'
89
+ )
90
+ ) || ')' || (
91
+ CASE
92
+ WHEN p.prorettype IS NULL THEN ''
93
+ ELSE ' ' || pg_get_function_result(p.oid)
94
+ END
95
+ )
96
+ ) AS name,
97
+ p.prosrc AS body,
98
+ l.lanname AS language,
99
+ (
100
+ CASE
101
+ WHEN p.provolatile = 'i' THEN 'immutable'
102
+ WHEN p.provolatile = 's' THEN 'stable'
103
+ END
104
+ ) AS volatility,
105
+ ( CASE WHEN p.proleakproof THEN true END ) AS leakproof,
106
+ ( CASE WHEN p.proisstrict THEN true END ) AS strict,
107
+ (
108
+ CASE
109
+ WHEN p.proparallel = 's' THEN 'safe'
110
+ WHEN p.proparallel = 'r' THEN 'restricted'
111
+ END
112
+ ) AS parallel,
113
+ ( CASE WHEN p.prosecdef THEN 'definer' END ) AS security,
114
+ ( CASE WHEN p.procost != 100 THEN p.procost END ) AS cost,
115
+ ( CASE WHEN p.prorows != 0 THEN p.prorows END ) AS rows,
116
+ d.description AS comment
117
+ FROM pg_proc p
118
+ JOIN pg_trunk e ON e.oid = p.oid
119
+ JOIN pg_language l ON l.oid = p.prolang
120
+ LEFT JOIN pg_description d ON d.objoid = p.oid
121
+ WHERE e.classid = 'pg_proc'::regclass
122
+ AND #{plain_function};
123
+ SQL
124
+ end
125
+
126
+ def to_sql(version)
127
+ [
128
+ create_function,
129
+ *comment_function,
130
+ register_function(version),
131
+ ].join(" ")
132
+ end
133
+
134
+ def invert
135
+ irreversible!("replace_existing: true") if replace_existing
136
+ DropFunction.new(**to_h)
137
+ end
138
+
139
+ private
140
+
141
+ def create_function
142
+ sql = "CREATE"
143
+ sql << " OR REPLACE" if replace_existing
144
+ sql << " FUNCTION #{name.to_sql(true)}"
145
+ sql << " RETURNS #{name.returns}" if name.returns
146
+ sql << " RETURNS void" if name.returns.blank? && name.args.blank?
147
+ sql << " LANGUAGE #{language || 'sql'}"
148
+ sql << " IMMUTABLE" if volatility == :immutable
149
+ sql << " STABLE" if volatility == :stable
150
+ sql << " VOLATILE" if volatility.blank? || volatility == :volatile
151
+ sql << " LEAKPROOF" if leakproof
152
+ sql << " NOT LEAKPROOF" unless leakproof
153
+ sql << " STRICT" if strict
154
+ sql << " CALLED ON NULL INPUT" if strict == false
155
+ sql << " SECURITY DEFINER" if security == :definer
156
+ sql << " SECURITY INVOKER" if security == :invoker
157
+ sql << " PARALLEL SAFE" if parallel == :safe
158
+ sql << " PARALLEL RESTRICTED" if parallel == :restricted
159
+ sql << " PARALLEL UNSAFE" if parallel.blank? || parallel == :unsafe
160
+ sql << " COST #{cost}" if cost
161
+ sql << " ROWS #{rows}" if rows
162
+ sql << " AS $$#{body}$$;"
163
+ end
164
+
165
+ def comment_function
166
+ <<~SQL
167
+ COMMENT ON FUNCTION #{name.to_sql(true)}
168
+ IS $comment$#{comment}$comment$;
169
+ SQL
170
+ end
171
+
172
+ # Register the most recent `oid` of functions with this schema/name
173
+ # There can be several overloaded definitions, but we're interested
174
+ # in that one we created just now so we can skip checking its args.
175
+ def register_function(version)
176
+ function_only = "NOT proisagg AND NOT proiswindow"
177
+ function_only = "prokind = 'f'" if version >= "11"
178
+
179
+ <<~SQL.squish
180
+ WITH latest AS (
181
+ SELECT
182
+ oid,
183
+ (
184
+ proname = #{name.quoted} AND pronamespace = #{name.namespace}
185
+ ) AS ok
186
+ FROM pg_proc
187
+ WHERE #{function_only}
188
+ ORDER BY oid DESC LIMIT 1
189
+ )
190
+ INSERT INTO pg_trunk (oid, classid)
191
+ SELECT oid, 'pg_proc'::regclass
192
+ FROM latest
193
+ WHERE ok
194
+ ON CONFLICT DO NOTHING;
195
+ SQL
196
+ end
197
+ end
198
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: false
2
+
3
+ # @!method ActiveRecord::Migration#drop_function(name, **options, &block)
4
+ # Drop a function
5
+ #
6
+ # @param [#to_s] name (nil)
7
+ # The qualified name of the function with arguments and returned value type
8
+ # @option [Boolean] :if_exists (false) Suppress the error when the function is absent
9
+ # @option [Symbol] :force (:restrict) How to process dependent objects
10
+ # Supported values: :restrict (default), :cascade
11
+ # @option [#to_s] :language ("sql") The language (like "sql" or "plpgsql")
12
+ # @option [#to_s] :body (nil) The body of the function
13
+ # @option [Symbol] :volatility (:volatile) The volatility of the function.
14
+ # Supported values: :volatile (default), :stable, :immutable
15
+ # @option [Symbol] :parallel (:unsafe) The safety of parallel execution.
16
+ # Supported values: :unsafe (default), :restricted, :safe
17
+ # @option [Symbol] :security (:invoker) Define the role under which the function is invoked
18
+ # Supported values: :invoker (default), :definer
19
+ # @option [Boolean] :leakproof (false) If the function is leakproof
20
+ # @option [Boolean] :strict (false) If the function is strict
21
+ # @option [Float] :cost (nil) The cost estimation for the function
22
+ # @option [Integer] :rows (nil) The number of rows returned by a function
23
+ # @option [#to_s] :comment The description of the function
24
+ # @yield [Proc] the block with the function's definition
25
+ # @yieldparam The receiver of methods specifying the function
26
+ #
27
+ # A function can be dropped by a plain name:
28
+ #
29
+ # drop_function "multiply"
30
+ #
31
+ # If several overloaded functions have the name,
32
+ # then you must specify the signature having
33
+ # types of attributes at least:
34
+ #
35
+ # drop_function "multiply(int, int)"
36
+ #
37
+ # In both cases above the operation is irreversible. To make it
38
+ # inverted you have to provide a full signature along with
39
+ # the body definition. The other options are supported as well:
40
+ #
41
+ # drop_function "math.mult(a int, b int) int" do |f|
42
+ # f.language "sql" # (default)
43
+ # f.body <<~SQL
44
+ # SELECT a * b;
45
+ # SQL
46
+ # f.volatility :immutable # :stable, :volatile (default)
47
+ # f.parallel :safe # :restricted, :unsafe (default)
48
+ # f.security :invoker # (default), also :definer
49
+ # f.leakproof true
50
+ # f.strict true
51
+ # f.cost 5.0
52
+ # # f.rows 1 (supported for functions returning sets of rows)
53
+ # f.comment "Multiplies 2 integers"
54
+ # end
55
+ #
56
+ # The operation can be called with `if_exists` option. In this case
57
+ # it would do nothing when no function existed.
58
+ #
59
+ # drop_function "math.multiply(integer, integer)", if_exists: true
60
+ #
61
+ # Another operation-specific option `force: :cascade` enables
62
+ # to drop silently any object depending on the function.
63
+ #
64
+ # drop_function "math.multiply(integer, integer)", force: :cascade
65
+ #
66
+ # Both options make the operation irreversible because of
67
+ # uncertainty about the previous state of the database.
68
+
69
+ module PGTrunk::Operations::Functions
70
+ # @private
71
+ class DropFunction < Base
72
+ validates :replace_existing, :new_name, absence: true
73
+
74
+ def to_sql(_version)
75
+ sql = "DROP FUNCTION"
76
+ sql << " IF EXISTS" if if_exists
77
+ sql << " #{name.to_sql}"
78
+ sql << " CASCADE" if force == :cascade
79
+ sql << ";"
80
+ end
81
+
82
+ def invert
83
+ irreversible!("if_exists: true") if if_exists
84
+ irreversible!("force: :cascade") if force == :cascade
85
+ CreateFunction.new(**to_h.except(:force))
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: false
2
+
3
+ # @!method ActiveRecord::Migration#rename_function(name, to:)
4
+ # Change the name and/or schema of a function
5
+ #
6
+ # @param [#to_s] :name (nil) The qualified name of the function
7
+ # @option [#to_s] :to (nil) The new qualified name for the function
8
+ #
9
+ # A function can be renamed by changing both the name
10
+ # and the schema (namespace) it belongs to.
11
+ #
12
+ # If there are no overloaded functions, then you can use a plain name:
13
+ #
14
+ # rename_function "math.multiply", to: "public.product"
15
+ #
16
+ # otherwise the types of attributes must be explicitly specified.
17
+ #
18
+ # rename_function "math.multiply(int, int)", to: "public.product"
19
+ #
20
+ # Any specification of attributes or returned values in `to:` option
21
+ # is ignored because they cannot be changed anyway.
22
+ #
23
+ # The operation is always reversible.
24
+
25
+ module PGTrunk::Operations::Functions
26
+ # @private
27
+ class RenameFunction < Base
28
+ validates :new_name, presence: true
29
+ validates :body, :cost, :force, :if_exists, :language, :leakproof,
30
+ :parallel, :replace_existing, :rows, :security, :strict,
31
+ :volatility, absence: true
32
+
33
+ def to_sql(_version)
34
+ [*change_schema, *change_name].join(" ")
35
+ end
36
+
37
+ def invert
38
+ q_new_name = "#{new_name.schema}.#{new_name.routine}(#{name.args}) #{name.returns}"
39
+ self.class.new(**to_h, name: q_new_name.strip, to: name)
40
+ end
41
+
42
+ private
43
+
44
+ def change_schema
45
+ return if name.schema == new_name.schema
46
+
47
+ "ALTER FUNCTION #{name.to_sql} SET SCHEMA #{new_name.schema.inspect};"
48
+ end
49
+
50
+ def change_name
51
+ return if new_name.routine == name.routine
52
+
53
+ changed_name = name.merge(schema: new_name.schema).to_sql
54
+ "ALTER FUNCTION #{changed_name} RENAME TO #{new_name.routine.inspect};"
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ # nodoc
4
+ module PGTrunk::Operations
5
+ # @private
6
+ # Namespace for operations with functions
7
+ module Functions
8
+ require_relative "functions/base"
9
+ require_relative "functions/create_function"
10
+ require_relative "functions/change_function"
11
+ require_relative "functions/drop_function"
12
+ require_relative "functions/rename_function"
13
+ end
14
+ end