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,61 @@
1
+ # frozen_string_literal: false
2
+
3
+ # @!method ActiveRecord::Migration#rename_materialized_view(name, **options)
4
+ # Change the name and/or schema of a materialized view
5
+ #
6
+ # @param [#to_s] :name (nil) The qualified name of the view
7
+ # @option [#to_s] :to (nil) The new qualified name for the view
8
+ # @option [Boolean] :if_exists (false) Suppress the error when the view is absent
9
+ #
10
+ # A materialized view can be renamed by changing both the name
11
+ # and the schema (namespace) it belongs to.
12
+ #
13
+ # rename_materialized_view "views.admin_users", to: "admins"
14
+ #
15
+ # With the `if_exists: true` option, the operation won't fail
16
+ # even when the view wasn't existed.
17
+ #
18
+ # rename_materialized_view "admin_users",
19
+ # to: "admins",
20
+ # if_exists: true
21
+ #
22
+ # At the same time, the option makes a migration irreversible
23
+ # due to uncertainty of the previous state of the database.
24
+
25
+ module PGTrunk::Operations::MaterializedViews
26
+ # @private
27
+ class RenameMaterializedView < Base
28
+ validates :new_name, presence: true
29
+ validates :algorithm, :cluster_on, :columns, :force, :with_data, :comment,
30
+ :if_not_exists, :sql_definition, :tablespace, :version,
31
+ absence: true
32
+
33
+ def to_sql(_version)
34
+ [*change_schema, *change_name].join("; ")
35
+ end
36
+
37
+ def invert
38
+ irreversible!("if_exists: true") if if_exists
39
+ self.class.new(**to_h, name: new_name, to: name)
40
+ end
41
+
42
+ private
43
+
44
+ def change_schema
45
+ return if name.schema == new_name.schema
46
+
47
+ sql = "ALTER MATERIALIZED VIEW"
48
+ sql << " IF EXISTS" if if_exists
49
+ sql << " #{name.to_sql} SET SCHEMA #{new_name.schema.inspect};"
50
+ end
51
+
52
+ def change_name
53
+ return if new_name.name == name.name
54
+
55
+ moved = name.merge(schema: new_name.schema)
56
+ sql = "ALTER MATERIALIZED VIEW"
57
+ sql << " IF EXISTS" if if_exists
58
+ sql << " #{moved.to_sql} RENAME TO #{new_name.name.inspect};"
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ # nodoc
4
+ module PGTrunk::Operations
5
+ # @private
6
+ # Namespace for operations with materialized views
7
+ module MaterializedViews
8
+ require_relative "materialized_views/column"
9
+ require_relative "materialized_views/base"
10
+
11
+ require_relative "materialized_views/change_materialized_view"
12
+ require_relative "materialized_views/create_materialized_view"
13
+ require_relative "materialized_views/drop_materialized_view"
14
+ require_relative "materialized_views/refresh_materialized_view"
15
+ require_relative "materialized_views/rename_materialized_view"
16
+ end
17
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: false
2
+
3
+ module PGTrunk::Operations::Procedures
4
+ # @abstract
5
+ # @private
6
+ # Base class for operations with procedures
7
+ class Base < PGTrunk::Operation
8
+ # All attributes that can be used by procedure-related commands
9
+ attribute :body, :pg_trunk_multiline_text
10
+ attribute :language, :pg_trunk_lowercase_string
11
+ attribute :replace_existing, :boolean
12
+ attribute :security, :pg_trunk_symbol
13
+
14
+ # Ensure correctness of present values
15
+ validates :security, inclusion: { in: %i[invoker definer] }, allow_nil: true
16
+ validates :force, :if_not_exists, absence: true
17
+ validate do
18
+ errors.add :body, "can't contain SQL injection with $$" if body&.include?("$$")
19
+ end
20
+
21
+ # Use comparison by name from pg_trunk operations base class (default)
22
+ # Support name as the only positional argument (default)
23
+
24
+ ruby_snippet do |s|
25
+ s.ruby_param(name.lean) if name.present?
26
+ s.ruby_param(to: new_name.lean) if new_name.present?
27
+ s.ruby_param(if_exists: true) if if_exists
28
+ s.ruby_param(replace_existing: true) if replace_existing
29
+
30
+ s.ruby_line(:language, language.downcase) if language&.!= "sql"
31
+ s.ruby_line(:security, security) if security&.!= :invoker
32
+ s.ruby_line(:body, body, from: from_body)
33
+ s.ruby_line(:comment, comment, from: from_comment)
34
+ end
35
+
36
+ private
37
+
38
+ def check_version!(version)
39
+ raise "Procedures are supported in PostgreSQL v11+" if version < "11"
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: false
2
+
3
+ # @!method ActiveRecord::Migration#change_procedure(name, **options, &block)
4
+ # Modify a procedure
5
+ #
6
+ # @param [#to_s] name (nil) The qualified name of the procedure
7
+ # @option [Boolean] :if_exists (false) Suppress the error when the procedure is absent
8
+ # @yield [Proc] the block with the procedure's definition
9
+ # @yieldparam The receiver of methods specifying the procedure
10
+ #
11
+ # The operation changes the procedure without dropping it
12
+ # (which is useful 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_procedure "metadata.set_foo(a int)" do |p|
19
+ # p.body <<~SQL
20
+ # SET foo = a
21
+ # SQL
22
+ # p.security :invoker
23
+ # p.comment "Multiplies 2 integers"
24
+ # SQL
25
+ #
26
+ # The example above is not invertible because of uncertainty
27
+ # about the previous state of body and comment.
28
+ # To define them, use a from options (available in a block syntax only):
29
+ #
30
+ # change_procedure "metadata.set_foo(a int)" do |p|
31
+ # p.body <<~SQL, from: <<~SQL
32
+ # SET foo = a
33
+ # SQL
34
+ # SET foo = -a
35
+ # SQL
36
+ # p.comment <<~MSG, from: <<~MSG
37
+ # Multiplies 2 integers
38
+ # MSG
39
+ # Multiplies ints
40
+ # MSG
41
+ # p.security :invoker
42
+ # SQL
43
+ #
44
+ # Like in the other operations, the procedure can be
45
+ # identified by a qualified name (with types of arguments).
46
+ # If it has no overloaded implementations,
47
+ # the plain name is supported as well.
48
+
49
+ module PGTrunk::Operations::Procedures
50
+ # @private
51
+ class ChangeProcedure < Base
52
+ validates :replace_existing, :language, :new_name, absence: true
53
+ validate { errors.add :base, "Changes can't be blank" if changes.blank? }
54
+ validate do
55
+ next if if_exists || name.blank? || create_procedure.present?
56
+
57
+ errors.add :base, "Procedure #{name.lean} can't be found"
58
+ end
59
+
60
+ def to_sql(version)
61
+ check_version!(version)
62
+
63
+ # Use `CREATE OR REPLACE FUNCTION` to make changes
64
+ create_procedure&.to_sql(version)
65
+ end
66
+
67
+ def invert
68
+ irreversible!("if_exists: true") if if_exists
69
+ undefined = inversion.select { |_, v| v.nil? }.keys.join(", ").presence
70
+ raise IrreversibleMigration.new(self, nil, <<~MSG.squish) if undefined
71
+ Undefined values to revert #{undefined}.
72
+ MSG
73
+
74
+ self.class.new(**inversion, name: name)
75
+ end
76
+
77
+ private
78
+
79
+ def create_procedure
80
+ return if name.blank?
81
+
82
+ @create_procedure ||= begin
83
+ list = CreateProcedure.select { |obj| name.maybe_eq?(obj.name) }
84
+ list.select! { |obj| name == obj.name } if list.size > 1 && name.args
85
+ list.first&.tap do |op|
86
+ op.attributes = { **changes, replace_existing: true }
87
+ end
88
+ end
89
+ end
90
+
91
+ def changes
92
+ @changes ||= {
93
+ body: body.presence,
94
+ comment: comment,
95
+ security: security,
96
+ }.compact
97
+ end
98
+
99
+ def inversion
100
+ @inversion ||= {
101
+ body: [body, from_body],
102
+ comment: [comment, from_comment],
103
+ security: [security, (%i[invoker definer] - [security]).first],
104
+ }.slice(*changes.keys).transform_values(&:last)
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,146 @@
1
+ # frozen_string_literal: false
2
+
3
+ # @!method ActiveRecord::Migration#create_procedure(name, **options, &block)
4
+ # Create a procedure
5
+ #
6
+ # @param [#to_s] name (nil)
7
+ # The qualified name of the procedure with arguments and returned value type
8
+ # @option [Boolean] :replace_existing (false) If the procedure 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 procedure
11
+ # @option [Symbol] :security (:invoker) Define the role under which the procedure is invoked
12
+ # Supported values: :invoker (default), :definer
13
+ # @option [#to_s] :comment The description of the procedure
14
+ # @yield [Proc] the block with the procedure's definition
15
+ # @yieldparam The receiver of methods specifying the procedure
16
+ #
17
+ # The syntax of the operation is the same as for `create_function`,
18
+ # but with only `security` option available. Notice, that
19
+ # procedures cannot return values so you're expected not to
20
+ # define a returned value as well.
21
+ #
22
+ # The procedure can be created either using inline syntax
23
+ #
24
+ # create_procedure "metadata.set_foo(a int)",
25
+ # language: :sql,
26
+ # body: "SET foo = a",
27
+ # comment: "Sets foo value"
28
+ #
29
+ # or using a block:
30
+ #
31
+ # create_procedure "metadata.set_foo(a int)" do |p|
32
+ # p.language "sql" # (default)
33
+ # p.body <<~SQL
34
+ # SET foo = a
35
+ # SQL
36
+ # p.security :invoker # (default), also :definer
37
+ # p.comment "Multiplies 2 integers"
38
+ # SQL
39
+ #
40
+ # With a `replace_existing: true` option,
41
+ # it will be created using the `CREATE OR REPLACE` clause.
42
+ # In this case the migration is irreversible because we
43
+ # don't know if and how to restore its previous definition.
44
+ #
45
+ # create_procedure "set_foo(a int)",
46
+ # body: "SET foo = a",
47
+ # replace_existing: true
48
+ #
49
+ # A procedure without arguments is supported as well
50
+ #
51
+ # # the same as "do_something()"
52
+ # create_procedure "do_something" do |p|
53
+ # # ...
54
+ # end
55
+
56
+ module PGTrunk::Operations::Procedures
57
+ # @private
58
+ class CreateProcedure < Base
59
+ validates :body, presence: true
60
+ validates :if_exists, :new_name, absence: true
61
+
62
+ from_sql do |server_version|
63
+ # Procedures were added to PostgreSQL in v11
64
+ next if server_version < "11"
65
+
66
+ <<~SQL.squish
67
+ SELECT
68
+ p.oid,
69
+ (
70
+ p.pronamespace::regnamespace || '.' || p.proname || '(' ||
71
+ regexp_replace(
72
+ regexp_replace(
73
+ pg_get_function_arguments(p.oid), '^\s*IN\s+', '', 'g'
74
+ ), '[,]\s*IN\s+', ',', 'g'
75
+ ) || ')'
76
+ ) AS name,
77
+ p.prosrc AS body,
78
+ l.lanname AS language,
79
+ (
80
+ CASE
81
+ WHEN p.prosecdef THEN 'definer'
82
+ ELSE 'invoker'
83
+ END
84
+ ) AS security,
85
+ d.description AS comment
86
+ FROM pg_proc p
87
+ JOIN pg_trunk e ON e.oid = p.oid
88
+ JOIN pg_language l ON l.oid = p.prolang
89
+ LEFT JOIN pg_description d ON d.objoid = p.oid
90
+ WHERE e.classid = 'pg_proc'::regclass
91
+ AND p.prokind = 'p';
92
+ SQL
93
+ end
94
+
95
+ def to_sql(version)
96
+ # Procedures were added to PostgreSQL in v11
97
+ check_version!(version)
98
+
99
+ [create_proc, *comment_proc, register_proc].join(" ")
100
+ end
101
+
102
+ def invert
103
+ irreversible!("replace_existing: true") if replace_existing
104
+ DropProcedure.new(**to_h)
105
+ end
106
+
107
+ private
108
+
109
+ def create_proc
110
+ sql = "CREATE"
111
+ sql << " OR REPLACE" if replace_existing
112
+ sql << " PROCEDURE #{name.to_sql(true)}"
113
+ sql << " LANGUAGE #{language&.downcase || 'sql'}"
114
+ sql << " SECURITY DEFINER" if security == :definer
115
+ sql << " AS $$#{body}$$;"
116
+ end
117
+
118
+ def comment_proc
119
+ <<~SQL
120
+ COMMENT ON PROCEDURE #{name.to_sql(true)}
121
+ IS $comment$#{comment}$comment$;
122
+ SQL
123
+ end
124
+
125
+ # Register the most recent `oid` of procedures with this schema/name
126
+ # There can be several overloaded definitions, but we're interested
127
+ # in that one we created just now so we can skip checking its args.
128
+ def register_proc
129
+ <<~SQL.squish
130
+ WITH latest AS (
131
+ SELECT
132
+ oid,
133
+ (proname = #{name.quoted} AND pronamespace = #{name.namespace}) AS ok
134
+ FROM pg_proc
135
+ WHERE prokind = 'p'
136
+ ORDER BY oid DESC LIMIT 1
137
+ )
138
+ INSERT INTO pg_trunk (oid, classid)
139
+ SELECT oid, 'pg_proc'::regclass
140
+ FROM latest
141
+ WHERE ok
142
+ ON CONFLICT DO NOTHING;
143
+ SQL
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: false
2
+
3
+ # @!method ActiveRecord::Migration#drop_procedure(name, **options, &block)
4
+ # Drop a procedure
5
+ #
6
+ # @param [#to_s] name (nil)
7
+ # The qualified name of the procedure with arguments and returned value type
8
+ # @option [Boolean] :if_exists (false) Suppress the error when the procedure is absent
9
+ # @option [#to_s] :language ("sql") The language (like "sql" or "plpgsql")
10
+ # @option [#to_s] :body (nil) The body of the procedure
11
+ # @option [Symbol] :security (:invoker) Define the role under which the procedure is invoked
12
+ # Supported values: :invoker (default), :definer
13
+ # @option [#to_s] :comment The description of the procedure
14
+ # @yield [Proc] the block with the procedure's definition
15
+ # @yieldparam The receiver of methods specifying the procedure
16
+ #
17
+ # A procedure can be dropped by a plain name:
18
+ #
19
+ # drop_procedure "set_foo"
20
+ #
21
+ # If several overloaded procedures have the name,
22
+ # then you must specify the signature having
23
+ # types of attributes at least:
24
+ #
25
+ # drop_procedure "set_foo(int)"
26
+ #
27
+ # In both cases above the operation is irreversible. To make it
28
+ # inverted you have to provide a full signature along with
29
+ # the body definition. The other options are supported as well:
30
+ #
31
+ # drop_procedure "metadata.set_foo(a int)" do |p|
32
+ # p.language "sql" # (default)
33
+ # p.body <<~SQL
34
+ # SET foo = a
35
+ # SQL
36
+ # p.security :invoker # (default), also :definer
37
+ # p.comment "Multiplies 2 integers"
38
+ # SQL
39
+ #
40
+ # The operation can be called with `if_exists` option. In this case
41
+ # it would do nothing when no procedure existed.
42
+ #
43
+ # drop_procedure "metadata.set_foo(a int)", if_exists: true
44
+ #
45
+ # Notice, that this option make the operation irreversible because of
46
+ # uncertainty about the previous state of the database.
47
+
48
+ module PGTrunk::Operations::Procedures
49
+ # @private
50
+ class DropProcedure < Base
51
+ validates :replace_existing, :new_name, absence: true
52
+
53
+ def to_sql(version)
54
+ check_version!(version)
55
+
56
+ sql = "DROP PROCEDURE"
57
+ sql << " IF EXISTS" if if_exists
58
+ sql << " #{name.to_sql};"
59
+ end
60
+
61
+ def invert
62
+ irreversible!("if_exists: true") if if_exists
63
+ CreateProcedure.new(**to_h)
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: false
2
+
3
+ # @!method ActiveRecord::Migration#rename_procedure(name, to:)
4
+ # Change the name and/or schema of a procedure
5
+ #
6
+ # @param [#to_s] :name (nil) The qualified name of the procedure
7
+ # @option [#to_s] :to (nil) The new qualified name for the procedure
8
+ #
9
+ # A procedure can be renamed by changing both the name
10
+ # and the schema (namespace) it belongs to.
11
+ #
12
+ # If there are no overloaded procedures, then you can use a plain name:
13
+ #
14
+ # rename_procedure "math.set_foo", to: "public.foo_setup"
15
+ #
16
+ # otherwise the types of attributes must be explicitly specified.
17
+ #
18
+ # rename_procedure "math.set_foo(int)", to: "public.foo_setup"
19
+ #
20
+ # Any specification of attributes in `to:` option
21
+ # is ignored because they cannot be changed anyway.
22
+ #
23
+ # The operation is always reversible.
24
+
25
+ module PGTrunk::Operations::Procedures
26
+ # @private
27
+ class RenameProcedure < Base
28
+ validates :new_name, presence: true
29
+ validates :body, :if_exists, :replace_existing, :language, :security, absence: true
30
+
31
+ def to_sql(version)
32
+ check_version!(version)
33
+
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 PROCEDURE #{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 PROCEDURE #{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 procedures
7
+ module Procedures
8
+ require_relative "procedures/base"
9
+ require_relative "procedures/change_procedure"
10
+ require_relative "procedures/create_procedure"
11
+ require_relative "procedures/drop_procedure"
12
+ require_relative "procedures/rename_procedure"
13
+ end
14
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: false
2
+
3
+ module PGTrunk::Operations::Statistics
4
+ # @abstract
5
+ # @private
6
+ # Base class for operations with check constraints
7
+ class Base < PGTrunk::Operation
8
+ # All attributes that can be used by statistics-related commands
9
+ attribute :columns, :pg_trunk_array_of_strings, default: []
10
+ attribute :expressions, :pg_trunk_array_of_strings, default: []
11
+ attribute :kinds, :pg_trunk_array_of_symbols, default: []
12
+ attribute :table, :pg_trunk_qualified_name
13
+
14
+ # Methods to populate multivariable attributes from a block
15
+
16
+ def expression(text)
17
+ expressions << text.strip
18
+ end
19
+
20
+ def column(name)
21
+ columns << name.strip
22
+ end
23
+
24
+ # Generate missed name from table & expression
25
+ after_initialize { self.name ||= generated_name }
26
+ after_initialize { expressions&.map!(&:strip) }
27
+
28
+ # Ensure correctness of present values
29
+ # The table must be defined because the name only
30
+ # is not enough to identify the constraint.
31
+ validates :name, presence: true
32
+ validate do
33
+ next if (kinds - %i[ndistinct dependencies mcv]).none?
34
+
35
+ errors.add :kinds, :invalid
36
+ end
37
+ validate do
38
+ next unless columns.blank? && expressions.size == 1
39
+
40
+ errors.add :kinds, :present if kinds.present?
41
+ end
42
+ validate do
43
+ next if expressions.present?
44
+
45
+ errors.add :base, "Add more columns or expressions" if columns.size == 1
46
+ end
47
+
48
+ # By default foreign keys are sorted by names.
49
+ # Support `table` only in positional arguments.
50
+
51
+ # Snippet to be used in all operations with check constraints
52
+ ruby_snippet do |s|
53
+ s.ruby_param(name.lean) if custom_name?
54
+ s.ruby_param(to: new_name.lean) if new_name.present?
55
+ s.ruby_param(if_not_exists: true) if if_not_exists
56
+ s.ruby_param(if_exists: true) if if_exists
57
+ s.ruby_param(force: :custom) if force == :custom
58
+
59
+ s.ruby_line(:table, table.lean) if table.present?
60
+ if columns.size > 3
61
+ columns.sort.each { |column| s.ruby_line(:column, column) }
62
+ elsif columns.present?
63
+ s.ruby_line(:columns, *columns.sort)
64
+ end
65
+ expressions.sort.each { |value| s.ruby_line(:expression, value) }
66
+ s.ruby_line(:kinds, *kinds.sort) if kinds.present?
67
+ s.ruby_line(:comment, comment) if comment.present?
68
+ end
69
+
70
+ private
71
+
72
+ def generated_name
73
+ return if table.blank? || parts.blank?
74
+
75
+ @generated_name ||= begin
76
+ key_options = { kinds: kinds, parts: parts }
77
+ identifier = "#{table.lean}_#{key_options}_stat"
78
+ hashed_identifier = Digest::SHA256.hexdigest(identifier).first(10)
79
+ PGTrunk::QualifiedName.wrap("stat_rails_#{hashed_identifier}")
80
+ end
81
+ end
82
+
83
+ def custom_name?(qname = name)
84
+ qname&.differs_from?(/^stat_rails_\w+$/)
85
+ end
86
+
87
+ def parts
88
+ @parts ||= [
89
+ *columns.reject(&:blank?).map(&:inspect),
90
+ *expressions.reject(&:blank?).map { |ex| "(#{ex})" },
91
+ ]
92
+ end
93
+ end
94
+ end