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,208 @@
1
+ # frozen_string_literal: false
2
+
3
+ # @!method ActiveRecord::Migration#create_trigger(table, name = nil, **options, &block)
4
+ # Create a trigger for a table
5
+ #
6
+ # @param [#to_s] table (nil) The qualified name of the table
7
+ # @param [#to_s] name (nil) The name of the trigger
8
+ # @option [Boolean] :replace_existing (false) If the trigger should overwrite an existing one
9
+ # @option [#to_s] :function (nil) The qualified name of the function to be called
10
+ # @option [Symbol] :type (nil) When the trigger should be run
11
+ # Supported values: :before, :after, :instead_of
12
+ # @option [Array<Symbol>] :events List of events running the trigger
13
+ # Supported values in the array: :insert, :update, :delete, :truncate
14
+ # @option [Boolean] :constraint (false) If the trigger is a constraint
15
+ # @option [Symbol] :initially (:immediate) If the constraint check should be deferred
16
+ # Supported values: :immediate (default), :deferred
17
+ # @option [#to_s] :when (nil) The SQL snippet definiing a condition for the trigger
18
+ # @option [Symbol] :for_each (:statement) Define if a trigger should be run for every row
19
+ # Supported values: :statement (default), :row
20
+ # @option [#to_s] :comment (nil) The commend describing the trigger
21
+ # @yield [Proc] the block with the trigger's definition
22
+ # @yieldparam The receiver of methods specifying the trigger
23
+ #
24
+ # The trigger can be created either using inline syntax
25
+ #
26
+ # create_trigger "users", "do_something",
27
+ # function: "do_something()",
28
+ # for_each: :row,
29
+ # type: :after,
30
+ # events: %i[insert update],
31
+ # comment: "Does something useful"
32
+ #
33
+ # or using a block:
34
+ #
35
+ # create_trigger do |t|
36
+ # t.table "users"
37
+ # t.name "do_something"
38
+ # t.function "do_something()"
39
+ # t.for_each :row
40
+ # t.type :after
41
+ # t.events %i[insert update]
42
+ # t.comment "Does something useful"
43
+ # end
44
+ #
45
+ # With a `replace_existing: true` option,
46
+ # it will be created using the `CREATE OR REPLACE` clause.
47
+ # (Available in PostgreSQL v14+).
48
+ #
49
+ # create_trigger "users", "do_something",
50
+ # function: "do_something()",
51
+ # type: :after,
52
+ # events: %i[insert update],
53
+ # replace_previous: true
54
+ #
55
+ # In this case the migration is irreversible because we
56
+ # don't know if and how to restore its previous definition.
57
+
58
+ module PGTrunk::Operations::Triggers
59
+ # @private
60
+ class CreateTrigger < Base
61
+ validates :function, :type, :events, presence: true
62
+ validates :if_exists, :new_name, absence: true
63
+
64
+ from_sql do |_version|
65
+ <<~SQL
66
+ WITH t AS (
67
+ SELECT
68
+ t.oid,
69
+ t.tgname AS name,
70
+ (
71
+ CASE WHEN t.tgconstraint != 0 THEN true END
72
+ ) AS constraint,
73
+ (
74
+ CASE
75
+ WHEN t.tgdeferrable AND t.tginitdeferred THEN 'deferred'
76
+ WHEN t.tgdeferrable AND NOT t.tginitdeferred THEN 'immediate'
77
+ END
78
+ ) AS "initially",
79
+ pg_get_triggerdef(t.oid, true) AS snippet,
80
+ (
81
+ CASE
82
+ WHEN (t.tgtype::int::bit(7) & b'0000001')::int = 0 THEN 'statement'
83
+ ELSE 'row'
84
+ END
85
+ ) AS for_each,
86
+ (
87
+ SELECT array_agg(attname)
88
+ FROM (
89
+ SELECT a.attname
90
+ FROM unnest(t.tgattr) col(num)
91
+ JOIN pg_attribute a ON a.attnum = col.num
92
+ WHERE a.attrelid = t.tgrelid
93
+ ) list
94
+ ) AS columns,
95
+ (
96
+ CASE
97
+ WHEN ((tgtype::int::bit(7) & b'0000010')::int != 0) THEN 'before'
98
+ WHEN ((tgtype::int::bit(7) & b'0000010')::int = 0) THEN 'after'
99
+ ELSE 'instead_of'
100
+ END
101
+ ) AS type,
102
+ array_remove(
103
+ ARRAY[
104
+ (CASE WHEN (tgtype::int::bit(7) & b'0000100')::int != 0 THEN 'insert' END),
105
+ (CASE WHEN (tgtype::int::bit(7) & b'0001000')::int != 0 THEN 'delete' END),
106
+ (CASE WHEN (tgtype::int::bit(7) & b'0010000')::int != 0 THEN 'update' END),
107
+ (CASE WHEN (tgtype::int::bit(7) & b'0100000')::int != 0 THEN 'truncate' END)
108
+ ]::text[],
109
+ NULL
110
+ ) AS events,
111
+ (c.relnamespace::regnamespace || '.' || c.relname) AS "table",
112
+ (f.pronamespace::regnamespace || '.' || f.proname || '()') AS function,
113
+ d.description AS comment
114
+ FROM pg_trigger t
115
+ JOIN pg_proc f ON f.oid = t.tgfoid
116
+ JOIN pg_class c ON c.oid = t.tgrelid
117
+ LEFT JOIN pg_description d ON d.objoid = t.oid
118
+ )
119
+ SELECT
120
+ oid,
121
+ name,
122
+ "table",
123
+ function,
124
+ "constraint",
125
+ "initially",
126
+ for_each,
127
+ (
128
+ CASE
129
+ WHEN regexp_match(snippet, 'WHEN') IS NOT NULL
130
+ THEN
131
+ regexp_replace(
132
+ regexp_replace(snippet, '^.+WHEN [(]', ''),
133
+ '[)] EXECUTE.+',
134
+ ''
135
+ )
136
+ END
137
+ ) AS "when",
138
+ type,
139
+ events,
140
+ columns,
141
+ comment
142
+ FROM t
143
+ SQL
144
+ end
145
+
146
+ def to_sql(version)
147
+ [
148
+ create_trigger(version),
149
+ *create_comment,
150
+ register_trigger,
151
+ ].join(" ")
152
+ end
153
+
154
+ def invert
155
+ irreversible!("replace_existing: true") if replace_existing
156
+ DropTrigger.new(**to_h)
157
+ end
158
+
159
+ private
160
+
161
+ def create_trigger(version)
162
+ sql = "CREATE"
163
+ sql << " OR REPLACE" if replace_existing && version >= "14"
164
+ sql << " CONSTRAINT" if constraint
165
+ sql << " TRIGGER #{name.name.inspect}"
166
+ sql << " BEFORE #{events_sql}" if type == :before
167
+ sql << " AFTER #{events_sql}" if type == :after
168
+ sql << " INSTEAD OF #{events_sql}" if type == :instead_of
169
+ sql << " ON #{table.to_sql}"
170
+ sql << " DEFERRABLE" if initially.present?
171
+ sql << " INITIALLY DEFERRED" if initially == :deferred
172
+ sql << " FOR EACH ROW" if for_each&.== :row
173
+ sql << " WHEN (#{self.when})" if self.when.present?
174
+ sql << " EXECUTE PROCEDURE #{function.to_sql(true)};"
175
+ end
176
+
177
+ def create_comment
178
+ return unless comment
179
+
180
+ <<~SQL.squish
181
+ COMMENT ON TRIGGER #{name.name.inspect} ON #{table.to_sql}
182
+ IS $comment$#{comment}$comment$;
183
+ SQL
184
+ end
185
+
186
+ def register_trigger
187
+ <<~SQL.squish
188
+ INSERT INTO pg_trunk (oid, classid)
189
+ SELECT t.oid, 'pg_trigger'::regclass
190
+ FROM pg_trigger t JOIN pg_class c ON t.tgrelid = c.oid
191
+ WHERE c.relname = #{table.quoted}
192
+ AND c.relnamespace = #{table.namespace}
193
+ AND t.tgname = #{name.quoted}
194
+ ON CONFLICT DO NOTHING;
195
+ SQL
196
+ end
197
+
198
+ def events_sql
199
+ events.map do |event|
200
+ if event == :update && columns.present?
201
+ "UPDATE OF #{columns.join(', ')}"
202
+ else
203
+ event.to_s.upcase
204
+ end
205
+ end.join(" OR ")
206
+ end
207
+ end
208
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: false
2
+
3
+ # @!method ActiveRecord::Migration#drop_trigger(table, name = nil, **options, &block)
4
+ # Drop a trigger for a table
5
+ #
6
+ # @param [#to_s] table (nil) The qualified name of the table
7
+ # @param [#to_s] name (nil) The name of the trigger
8
+ # @option [Boolean] :if_exists (false) Suppress the error when the trigger is absent
9
+ # @option [#to_s] :function (nil) The qualified name of the function to be called
10
+ # @option [Symbol] :type (nil) When the trigger should be run
11
+ # Supported values: :before, :after, :instead_of
12
+ # @option [Array<Symbol>] :events List of events running the trigger
13
+ # Supported values in the array: :insert, :update, :delete, :truncate
14
+ # @option [Boolean] :constraint (false) If the trigger is a constraint
15
+ # @option [Symbol] :initially (:immediate) If the constraint check should be deferred
16
+ # Supported values: :immediate (default), :deferred
17
+ # @option [#to_s] :when (nil) The SQL snippet definiing a condition for the trigger
18
+ # @option [Symbol] :for_each (:statement) Define if a trigger should be run for every row
19
+ # Supported values: :statement (default), :row
20
+ # @option [#to_s] :comment (nil) The commend describing the trigger
21
+ # @yield [Proc] the block with the trigger's definition
22
+ # @yieldparam The receiver of methods specifying the trigger
23
+ #
24
+ # A trigger can be dropped by a table and name:
25
+ #
26
+ # drop_trigger "users", "do_something"
27
+ #
28
+ # the default name can be restored from its attributes as well.
29
+ #
30
+ # drop_trigger "users" do |t|
31
+ # t.function "send_notifications()"
32
+ # t.for_each :row
33
+ # t.type :after
34
+ # t.events %i[update]
35
+ # t.columns %w[email phone]
36
+ # t.comment "Does something"
37
+ # end
38
+ #
39
+ # Notice, that you have to specify all attributes to make
40
+ # the operation reversible.
41
+ #
42
+ # The operation can be called with `if_exists` option. In this case
43
+ # it would do nothing when no trigger existed.
44
+ #
45
+ # drop_trigger "users", "unknown_trigger", if_exists: true
46
+ #
47
+ # This option, though, makes the operation irreversible because of
48
+ # uncertainty of the previous state of the database.
49
+
50
+ module PGTrunk::Operations::Triggers
51
+ # @private
52
+ class DropTrigger < Base
53
+ validates :replace_existing, :new_name, absence: true
54
+
55
+ def to_sql(_version)
56
+ sql = "DROP TRIGGER"
57
+ sql << " IF EXISTS" if if_exists
58
+ sql << " #{name.name.inspect} ON #{table.to_sql};"
59
+ end
60
+
61
+ def invert
62
+ irreversible!("if_exists: true") if if_exists
63
+ CreateTrigger.new(**to_h)
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: false
2
+
3
+ # @!method ActiveRecord::Migration#rename_trigger(table, name = nil, **options, &block)
4
+ # Rename a trigger
5
+ #
6
+ # @param [#to_s] table (nil) The qualified name of the table
7
+ # @param [#to_s] name (nil) The name of the trigger
8
+ # @option [#to_s] :to (nil) The new name of the trigger
9
+ # @param [#to_s] table (nil) The qualified name of the table
10
+ # @param [#to_s] name (nil) The current name of the trigger
11
+ # @option [#to_s] :to (nil) The new name for the trigger
12
+ # @option [#to_s] :function (nil) The qualified name of the function to be called
13
+ # @option [Symbol] :type (nil) When the trigger should be run
14
+ # Supported values: :before, :after, :instead_of
15
+ # @option [Array<Symbol>] :events List of events running the trigger
16
+ # Supported values in the array: :insert, :update, :delete, :truncate
17
+ # @option [Symbol] :for_each (:statement) Define if a trigger should be run for every row
18
+ # Supported values: :statement (default), :row
19
+ # @yield [Proc] the block with the trigger's definition
20
+ # @yieldparam The receiver of methods specifying the trigger
21
+ #
22
+ # A trigger can be renamed by either setting a new name explicitly
23
+ #
24
+ # rename_trigger "users", "do_something", to: "do_something_different"
25
+ #
26
+ # or resetting it to the default (generated) value.
27
+ #
28
+ # rename_trigger "users", "do_something"
29
+ #
30
+ # The previously generated name of the trigger can be get
31
+ # from its parameters. In this case all the essentials
32
+ # parameters must be specified:
33
+ #
34
+ # rename_trigger "users", to: "do_something_different" do |t|
35
+ # t.function "do_something()"
36
+ # t.for_each :row
37
+ # t.type :after
38
+ # t.events %i[insert update]
39
+ # end
40
+ #
41
+ # In the same way, when you reset the name to default,
42
+ # all the essential parameters must be got to make the trigger
43
+ # invertible.
44
+ #
45
+ # rename_trigger "users", "do_something" do |t|
46
+ # t.function "do_something()"
47
+ # t.for_each :row
48
+ # t.type :after
49
+ # t.events %i[insert update]
50
+ # end
51
+
52
+ module PGTrunk::Operations::Triggers
53
+ # @private
54
+ class RenameTrigger < Base
55
+ after_initialize { self.new_name = generated_name if new_name.blank? }
56
+
57
+ validates :if_exists, :constraint, :initially, :when, :replace_existing, absence: true
58
+ validates :new_name, presence: true
59
+
60
+ def to_sql(_version)
61
+ <<~SQL.squish
62
+ ALTER TRIGGER #{name.name.inspect} ON #{table.to_sql}
63
+ RENAME TO #{new_name.name.inspect};
64
+ SQL
65
+ end
66
+
67
+ def invert
68
+ self.class.new(**to_h, name: new_name, to: name)
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ # nodoc
4
+ module PGTrunk::Operations
5
+ # @private
6
+ # Namespace for operations with triggers
7
+ module Triggers
8
+ require_relative "triggers/base"
9
+ require_relative "triggers/change_trigger"
10
+ require_relative "triggers/create_trigger"
11
+ require_relative "triggers/drop_trigger"
12
+ require_relative "triggers/rename_trigger"
13
+ end
14
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: false
2
+
3
+ module PGTrunk::Operations::Views
4
+ # @abstract
5
+ # @private
6
+ # Base class for operations with views
7
+ class Base < PGTrunk::Operation
8
+ # All attributes that can be used by view-related commands
9
+ attribute :check, :pg_trunk_symbol
10
+ attribute :force, :pg_trunk_symbol
11
+ attribute :replace_existing, :boolean
12
+ attribute :sql_definition, :pg_trunk_multiline_text
13
+ attribute :version, :integer, aliases: :revert_to_version
14
+
15
+ # Load missed `sql_definition` from the external file
16
+ after_initialize { self.sql_definition ||= read_snippet_from(:views) }
17
+
18
+ # Ensure correctness of present values
19
+ validates :check, inclusion: %i[local cascaded], allow_nil: true
20
+ validates :force, inclusion: %i[cascade restrict], allow_nil: true
21
+
22
+ # Use comparison by name from pg_trunk operations base class (default)
23
+ # Support name as the only positional argument (default)
24
+
25
+ ruby_snippet do |s|
26
+ s.ruby_param(name.lean) if name.present?
27
+ s.ruby_param(to: new_name.lean) if new_name.present?
28
+ s.ruby_param(replace_existing: true) if replace_existing
29
+ s.ruby_param(if_exists: true) if if_exists
30
+ s.ruby_param(force: :cascade) if force == :cascade
31
+
32
+ s.ruby_line(:version, version, from: from_version)
33
+ s.ruby_line(:sql_definition, sql_definition, from: from_sql_definition)
34
+ s.ruby_line(:check, check, from: from_check)
35
+ s.ruby_line(:comment, comment, from: from_comment)
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: false
2
+
3
+ # @!method ActiveRecord::Migration#change_view(name, **options, &block)
4
+ # Modify a view
5
+ #
6
+ # @param [#to_s] name (nil) The qualified name of the view
7
+ # @option [Boolean] :if_exists (false) Suppress the error when the view is absent
8
+ # @yield [Proc] the block with the view's definition
9
+ # @yieldparam The receiver of methods specifying the view
10
+ #
11
+ # The operation replaces the view with a new definition(s):
12
+ #
13
+ # change_view "admin_users" do |v|
14
+ # v.sql_definition: <<~SQL, from: <<~SQL
15
+ # SELECT id, name FROM users WHERE admin;
16
+ # SQL
17
+ # SELECT * FROM users WHERE admin;
18
+ # SQL
19
+ # end
20
+ #
21
+ # For some compatibility to the `scenic` gem, we also support
22
+ # adding a definition via its version:
23
+ #
24
+ # change_view "admin_users" do |v|
25
+ # v.version 2, from: 1
26
+ # end
27
+ #
28
+ # It is expected, that both `db/views/admin_users_v01.sql`
29
+ # and `db/views/admin_users_v02.sql` to contain SQL snippets.
30
+ #
31
+ # Please, notice that neither deletion of columns,
32
+ # nor changing their types is supported by the PostgreSQL.
33
+ #
34
+ # You can also (re)set a comment describing the view,
35
+ # and the check option (either `:local` or `:cascaded`):
36
+ #
37
+ # change_view "admin_users" do |v|
38
+ # v.check :local, from: :cascaded
39
+ # v.comment "Admin users only", from: ""
40
+ # end
41
+
42
+ module PGTrunk::Operations::Views
43
+ # @private
44
+ class ChangeView < Base
45
+ validates :replace_existing, :force, :new_name, absence: true
46
+ validate { errors.add :base, "Changes can't be blank" if changes.blank? }
47
+ validate do
48
+ next if if_exists || name.blank?
49
+
50
+ errors.add :base, "Can't find the view #{name.lean}" unless create_view
51
+ end
52
+
53
+ def to_sql(server_version)
54
+ create_view&.to_sql(server_version)
55
+ end
56
+
57
+ def invert
58
+ irreversible!("if_exists: true") if if_exists
59
+ undefined = inversion.select { |_, v| v.nil? }.keys.join(", ").presence
60
+ raise IrreversibleMigration.new(self, nil, <<~MSG.squish) if undefined
61
+ Undefined values to revert #{undefined}.
62
+ MSG
63
+
64
+ self.class.new(**inversion, name: name)
65
+ end
66
+
67
+ private
68
+
69
+ def changes
70
+ @changes ||= to_h.slice(:sql_definition, :check, :comment).compact
71
+ end
72
+
73
+ def inversion
74
+ @inversion ||= {}.tap do |inv|
75
+ inv[:version] = from_version if version
76
+ inv[:sql_definition] = from_sql_definition unless version
77
+ inv[:check] = from_check if check
78
+ inv[:comment] = from_comment if comment
79
+ end
80
+ end
81
+
82
+ def create_view
83
+ return if name.blank?
84
+
85
+ @create_view ||= CreateView.find { |o| o.name == name }&.tap do |op|
86
+ op.attributes = { **changes, replace_existing: true }
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: false
2
+
3
+ # @!method ActiveRecord::Migration#create_view(name, **options, &block)
4
+ # Create a view
5
+ #
6
+ # @param [#to_s] name (nil) The qualified name of the view
7
+ # @option [Boolean] :replace_existing (false) If the view should overwrite an existing one
8
+ # @option [#to_s] :sql_definition (nil) The snippet containing the query
9
+ # @option [#to_i] :version (nil)
10
+ # The alternative way to set sql_definition by referencing to a file containing the snippet
11
+ # @option [#to_s] :check (nil) Controls the behavior of automatically updatable views
12
+ # Supported values: :local, :cascaded
13
+ # @option [#to_s] :comment (nil) The comment describing the view
14
+ # @yield [Proc] the block with the view's definition
15
+ # @yieldparam The receiver of methods specifying the view
16
+ #
17
+ # The operation creates the view using its `sql_definition`:
18
+ #
19
+ # create_view("views.admin_users", sql_definition: <<~SQL)
20
+ # SELECT id, name FROM users WHERE admin;
21
+ # SQL
22
+ #
23
+ # For compatibility to the `scenic` gem, we also support
24
+ # adding a definition via its version:
25
+ #
26
+ # create_view "admin_users", version: 1
27
+ #
28
+ # It is expected, that a `db/views/admin_users_v01.sql`
29
+ # to contain the SQL snippet.
30
+ #
31
+ # You can also set a comment describing the view, and the check option
32
+ # (either `:local` or `:cascaded`):
33
+ #
34
+ # create_view "admin_users" do |v|
35
+ # v.sql_definition "SELECT id, name FROM users WHERE admin;"
36
+ # v.check :local
37
+ # v.comment "Admin users only"
38
+ # end
39
+ #
40
+ # With the `replace_existing: true` option the operation
41
+ # would use `CREATE OR REPLACE VIEW` command, so it
42
+ # can be used to "update" (or reload) the existing view.
43
+ #
44
+ # create_view "admin_users", version: 1, replace_existing: true
45
+ #
46
+ # This option makes an operation irreversible due to uncertainty
47
+ # of the previous state of the database.
48
+
49
+ module PGTrunk::Operations::Views
50
+ # @private
51
+ class CreateView < Base
52
+ validates :sql_definition, presence: true
53
+ validates :if_exists, :force, :new_name, absence: true
54
+
55
+ from_sql do |_version|
56
+ <<~SQL
57
+ SELECT
58
+ c.oid,
59
+ (c.relnamespace::regnamespace || '.' || c.relname) AS name,
60
+ replace(pg_get_viewdef(c.oid, 60), ';', '') AS sql_definition,
61
+ (
62
+ SELECT option_value
63
+ FROM pg_options_to_table(c.reloptions)
64
+ WHERE option_name = 'check_option'
65
+ LIMIT 1
66
+ ) AS check,
67
+ d.description AS comment
68
+ FROM pg_class c
69
+ JOIN pg_trunk e ON e.oid = c.oid
70
+ AND e.classid = 'pg_class'::regclass
71
+ LEFT JOIN pg_description d ON d.objoid = c.oid
72
+ AND d.classoid = 'pg_class'::regclass
73
+ WHERE c.relkind = 'v';
74
+ SQL
75
+ end
76
+
77
+ def to_sql(_version)
78
+ [create_view, *create_comment, register_view].join(" ")
79
+ end
80
+
81
+ def invert
82
+ irreversible!("replace_existing: true") if replace_existing
83
+ DropView.new(**to_h)
84
+ end
85
+
86
+ private
87
+
88
+ def create_view
89
+ sql = "CREATE"
90
+ sql << " OR REPLACE" if replace_existing
91
+ sql << " VIEW #{name.to_sql}"
92
+ sql << " AS (#{sql_definition})"
93
+ sql << " WITH #{check.to_s.upcase} CHECK OPTION" if check.present?
94
+ sql << ";"
95
+ end
96
+
97
+ def create_comment
98
+ return if comment.blank?
99
+
100
+ "COMMENT ON VIEW #{name.to_sql} IS $comment$#{comment}$comment$;"
101
+ end
102
+
103
+ def register_view
104
+ <<~SQL.squish
105
+ INSERT INTO pg_trunk (oid, classid)
106
+ SELECT oid, 'pg_class'::regclass
107
+ FROM pg_class
108
+ WHERE relname = #{name.quoted}
109
+ AND relnamespace = #{name.namespace}
110
+ AND relkind = 'v'
111
+ ON CONFLICT DO NOTHING;
112
+ SQL
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: false
2
+
3
+ # @!method ActiveRecord::Migration#drop_view(name, **options, &block)
4
+ # Drop a view
5
+ #
6
+ # @param [#to_s] name (nil) The qualified name of the view
7
+ # @option [Boolean] :replace_existing (false) If the view should overwrite an existing one
8
+ # @option [Boolean] :if_exists (false) Suppress the error when the view is absent
9
+ # @option [Symbol] :force (:restrict) How to process dependent objects (`:cascade` or `:restrict`)
10
+ # @option [#to_s] :sql_definition (nil) The snippet containing the query
11
+ # @option [#to_i] :revert_to_version (nil)
12
+ # The alternative way to set sql_definition by referencing to a file containing the snippet
13
+ # @option [#to_s] :check (nil) Controls the behavior of automatically updatable views
14
+ # Supported values: :local, :cascaded
15
+ # @option [#to_s] :comment (nil) The comment describing the view
16
+ # @yield [Proc] the block with the view's definition
17
+ # @yieldparam The receiver of methods specifying the view
18
+ #
19
+ # The operation drops the existing view identified by its
20
+ # qualified name (it can include a schema).
21
+ #
22
+ # drop_view "views.admin_users"
23
+ #
24
+ # To make the operation invertible, use the same options
25
+ # as in the `create_view` operation.
26
+ #
27
+ # drop_view "views.admin_users" do |v|
28
+ # v.sql_definition "SELECT name, email FROM users WHERE admin;"
29
+ # v.check :local
30
+ # v.comment "Admin users only"
31
+ # end
32
+ #
33
+ # You can also use a version-base SQL definition like:
34
+ #
35
+ # drop_view "views.admin_users", revert_to_version: 1
36
+ #
37
+ # With the `force: :cascade` option the operation would remove
38
+ # all the objects which depend on the view.
39
+ #
40
+ # drop_view "views.admin_users", force: :cascade
41
+ #
42
+ # With the `if_exists: true` option the operation won't fail
43
+ # even when the view was absent in the database.
44
+ #
45
+ # drop_view "views.admin_users", if_exists: true
46
+ #
47
+ # Both options make an operation irreversible due to uncertainty
48
+ # of the previous state of the database.
49
+
50
+ module PGTrunk::Operations::Views
51
+ # @private
52
+ class DropView < Base
53
+ validates :replace_existing, :new_name, absence: true
54
+
55
+ def to_sql(_version)
56
+ sql = "DROP VIEW"
57
+ sql << " IF EXISTS" if if_exists
58
+ sql << " #{name.to_sql}"
59
+ sql << " CASCADE" if force == :cascade
60
+ sql << ";"
61
+ end
62
+
63
+ def invert
64
+ irreversible!("if_exists: true") if if_exists
65
+ irreversible!("force: :cascade") if force == :cascade
66
+ CreateView.new(**to_h.except(:force))
67
+ end
68
+ end
69
+ end