pg_trunk 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.github/workflows/ci.yml +87 -0
- data/.gitignore +9 -0
- data/.rspec +4 -0
- data/.rubocop.yml +92 -0
- data/.yardopts +4 -0
- data/CHANGELOG.md +31 -0
- data/CONTRIBUTING.md +17 -0
- data/Gemfile +22 -0
- data/LICENSE.txt +21 -0
- data/README.md +141 -0
- data/Rakefile +16 -0
- data/bin/console +8 -0
- data/bin/rake +19 -0
- data/bin/rspec +19 -0
- data/bin/setup +8 -0
- data/bin/yard +19 -0
- data/lib/pg_trunk/core/adapters/postgres.rb +80 -0
- data/lib/pg_trunk/core/dependencies_resolver.rb +101 -0
- data/lib/pg_trunk/core/generators.rb +140 -0
- data/lib/pg_trunk/core/operation/attributes.rb +78 -0
- data/lib/pg_trunk/core/operation/callbacks.rb +40 -0
- data/lib/pg_trunk/core/operation/generators.rb +51 -0
- data/lib/pg_trunk/core/operation/inversion.rb +70 -0
- data/lib/pg_trunk/core/operation/registration.rb +55 -0
- data/lib/pg_trunk/core/operation/ruby_builder.rb +112 -0
- data/lib/pg_trunk/core/operation/ruby_helpers.rb +99 -0
- data/lib/pg_trunk/core/operation/sql_helpers.rb +44 -0
- data/lib/pg_trunk/core/operation/validations.rb +21 -0
- data/lib/pg_trunk/core/operation.rb +78 -0
- data/lib/pg_trunk/core/qualified_name.rb +165 -0
- data/lib/pg_trunk/core/railtie/command_recorder.rb +30 -0
- data/lib/pg_trunk/core/railtie/custom_types.rb +37 -0
- data/lib/pg_trunk/core/railtie/migration.rb +50 -0
- data/lib/pg_trunk/core/railtie/migrator.rb +22 -0
- data/lib/pg_trunk/core/railtie/schema_dumper.rb +75 -0
- data/lib/pg_trunk/core/railtie/schema_migration.rb +22 -0
- data/lib/pg_trunk/core/railtie/statements.rb +21 -0
- data/lib/pg_trunk/core/railtie.rb +35 -0
- data/lib/pg_trunk/core/registry.rb +159 -0
- data/lib/pg_trunk/core/serializers/array_of_hashes_serializer.rb +28 -0
- data/lib/pg_trunk/core/serializers/array_of_strings_serializer.rb +29 -0
- data/lib/pg_trunk/core/serializers/array_of_symbols_serializer.rb +28 -0
- data/lib/pg_trunk/core/serializers/array_serializer.rb +22 -0
- data/lib/pg_trunk/core/serializers/lowercase_string_serializer.rb +21 -0
- data/lib/pg_trunk/core/serializers/multiline_text_serializer.rb +21 -0
- data/lib/pg_trunk/core/serializers/qualified_name_serializer.rb +27 -0
- data/lib/pg_trunk/core/serializers/symbol_serializer.rb +22 -0
- data/lib/pg_trunk/core/serializers.rb +16 -0
- data/lib/pg_trunk/core/validators/all_items_valid_validator.rb +15 -0
- data/lib/pg_trunk/core/validators/difference_validator.rb +19 -0
- data/lib/pg_trunk/core/validators.rb +10 -0
- data/lib/pg_trunk/core.rb +21 -0
- data/lib/pg_trunk/generators.rb +7 -0
- data/lib/pg_trunk/operations/check_constraints/add_check_constraint.rb +109 -0
- data/lib/pg_trunk/operations/check_constraints/base.rb +69 -0
- data/lib/pg_trunk/operations/check_constraints/drop_check_constraint.rb +60 -0
- data/lib/pg_trunk/operations/check_constraints/rename_check_constraint.rb +54 -0
- data/lib/pg_trunk/operations/check_constraints/validate_check_constraint.rb +39 -0
- data/lib/pg_trunk/operations/check_constraints.rb +14 -0
- data/lib/pg_trunk/operations/composite_types/base.rb +61 -0
- data/lib/pg_trunk/operations/composite_types/change_composite_type.rb +136 -0
- data/lib/pg_trunk/operations/composite_types/column.rb +118 -0
- data/lib/pg_trunk/operations/composite_types/create_composite_type.rb +99 -0
- data/lib/pg_trunk/operations/composite_types/drop_composite_type.rb +67 -0
- data/lib/pg_trunk/operations/composite_types/rename_composite_type.rb +44 -0
- data/lib/pg_trunk/operations/composite_types.rb +15 -0
- data/lib/pg_trunk/operations/domains/base.rb +46 -0
- data/lib/pg_trunk/operations/domains/change_domain.rb +140 -0
- data/lib/pg_trunk/operations/domains/constraint.rb +93 -0
- data/lib/pg_trunk/operations/domains/create_domain.rb +124 -0
- data/lib/pg_trunk/operations/domains/drop_domain.rb +65 -0
- data/lib/pg_trunk/operations/domains/rename_domain.rb +44 -0
- data/lib/pg_trunk/operations/domains.rb +15 -0
- data/lib/pg_trunk/operations/enums/base.rb +47 -0
- data/lib/pg_trunk/operations/enums/change.rb +55 -0
- data/lib/pg_trunk/operations/enums/change_enum.rb +119 -0
- data/lib/pg_trunk/operations/enums/create_enum.rb +83 -0
- data/lib/pg_trunk/operations/enums/drop_enum.rb +63 -0
- data/lib/pg_trunk/operations/enums/rename_enum.rb +44 -0
- data/lib/pg_trunk/operations/enums.rb +15 -0
- data/lib/pg_trunk/operations/foreign_keys/add_foreign_key.rb +174 -0
- data/lib/pg_trunk/operations/foreign_keys/base.rb +155 -0
- data/lib/pg_trunk/operations/foreign_keys/drop_foreign_key.rb +76 -0
- data/lib/pg_trunk/operations/foreign_keys/rename_foreign_key.rb +63 -0
- data/lib/pg_trunk/operations/foreign_keys.rb +16 -0
- data/lib/pg_trunk/operations/functions/base.rb +54 -0
- data/lib/pg_trunk/operations/functions/change_function.rb +108 -0
- data/lib/pg_trunk/operations/functions/create_function.rb +198 -0
- data/lib/pg_trunk/operations/functions/drop_function.rb +88 -0
- data/lib/pg_trunk/operations/functions/rename_function.rb +57 -0
- data/lib/pg_trunk/operations/functions.rb +14 -0
- data/lib/pg_trunk/operations/indexes/add_index.rb +68 -0
- data/lib/pg_trunk/operations/indexes.rb +10 -0
- data/lib/pg_trunk/operations/materialized_views/base.rb +79 -0
- data/lib/pg_trunk/operations/materialized_views/change_materialized_view.rb +139 -0
- data/lib/pg_trunk/operations/materialized_views/column.rb +94 -0
- data/lib/pg_trunk/operations/materialized_views/create_materialized_view.rb +170 -0
- data/lib/pg_trunk/operations/materialized_views/drop_materialized_view.rb +70 -0
- data/lib/pg_trunk/operations/materialized_views/refresh_materialized_view.rb +48 -0
- data/lib/pg_trunk/operations/materialized_views/rename_materialized_view.rb +61 -0
- data/lib/pg_trunk/operations/materialized_views.rb +17 -0
- data/lib/pg_trunk/operations/procedures/base.rb +42 -0
- data/lib/pg_trunk/operations/procedures/change_procedure.rb +107 -0
- data/lib/pg_trunk/operations/procedures/create_procedure.rb +146 -0
- data/lib/pg_trunk/operations/procedures/drop_procedure.rb +66 -0
- data/lib/pg_trunk/operations/procedures/rename_procedure.rb +57 -0
- data/lib/pg_trunk/operations/procedures.rb +14 -0
- data/lib/pg_trunk/operations/statistics/base.rb +94 -0
- data/lib/pg_trunk/operations/statistics/create_statistics.rb +181 -0
- data/lib/pg_trunk/operations/statistics/drop_statistics.rb +75 -0
- data/lib/pg_trunk/operations/statistics/rename_statistics.rb +48 -0
- data/lib/pg_trunk/operations/statistics.rb +13 -0
- data/lib/pg_trunk/operations/tables/create_table.rb +75 -0
- data/lib/pg_trunk/operations/tables.rb +10 -0
- data/lib/pg_trunk/operations/triggers/base.rb +119 -0
- data/lib/pg_trunk/operations/triggers/change_trigger.rb +82 -0
- data/lib/pg_trunk/operations/triggers/create_trigger.rb +208 -0
- data/lib/pg_trunk/operations/triggers/drop_trigger.rb +66 -0
- data/lib/pg_trunk/operations/triggers/rename_trigger.rb +71 -0
- data/lib/pg_trunk/operations/triggers.rb +14 -0
- data/lib/pg_trunk/operations/views/base.rb +38 -0
- data/lib/pg_trunk/operations/views/change_view.rb +90 -0
- data/lib/pg_trunk/operations/views/create_view.rb +115 -0
- data/lib/pg_trunk/operations/views/drop_view.rb +69 -0
- data/lib/pg_trunk/operations/views/rename_view.rb +58 -0
- data/lib/pg_trunk/operations/views.rb +14 -0
- data/lib/pg_trunk/operations.rb +23 -0
- data/lib/pg_trunk/version.rb +6 -0
- data/lib/pg_trunk.rb +27 -0
- data/pg_trunk.gemspec +34 -0
- data/spec/dummy/.gitignore +16 -0
- data/spec/dummy/Rakefile +15 -0
- data/spec/dummy/bin/bundle +6 -0
- data/spec/dummy/bin/rails +6 -0
- data/spec/dummy/bin/rake +6 -0
- data/spec/dummy/config/application.rb +18 -0
- data/spec/dummy/config/boot.rb +7 -0
- data/spec/dummy/config/database.yml +14 -0
- data/spec/dummy/config/environment.rb +7 -0
- data/spec/dummy/config.ru +6 -0
- data/spec/dummy/db/materialized_views/admin_users_v01.sql +1 -0
- data/spec/dummy/db/migrate/.keep +0 -0
- data/spec/dummy/db/schema.rb +18 -0
- data/spec/dummy/db/views/admin_users_v01.sql +1 -0
- data/spec/dummy/db/views/admin_users_v02.sql +1 -0
- data/spec/operations/check_constraints/add_check_constraint_spec.rb +85 -0
- data/spec/operations/check_constraints/drop_check_constraint_spec.rb +111 -0
- data/spec/operations/check_constraints/rename_check_constraint_spec.rb +90 -0
- data/spec/operations/composite_types/change_composite_type_spec.rb +257 -0
- data/spec/operations/composite_types/create_composite_type_spec.rb +55 -0
- data/spec/operations/composite_types/drop_composite_type_spec.rb +109 -0
- data/spec/operations/composite_types/rename_composite_type_spec.rb +74 -0
- data/spec/operations/dependency_resolver_spec.rb +177 -0
- data/spec/operations/domains/change_domain_spec.rb +287 -0
- data/spec/operations/domains/create_domain_spec.rb +69 -0
- data/spec/operations/domains/drop_domain_spec.rb +119 -0
- data/spec/operations/domains/rename_domain_spec.rb +70 -0
- data/spec/operations/enums/change_enum_spec.rb +157 -0
- data/spec/operations/enums/create_enum_spec.rb +40 -0
- data/spec/operations/enums/drop_enum_spec.rb +120 -0
- data/spec/operations/enums/rename_enum_spec.rb +72 -0
- data/spec/operations/foreign_keys/add_foreign_key_spec.rb +208 -0
- data/spec/operations/foreign_keys/drop_foreign_key_spec.rb +167 -0
- data/spec/operations/foreign_keys/rename_foreign_key_spec.rb +101 -0
- data/spec/operations/functions/change_function_spec.rb +166 -0
- data/spec/operations/functions/create_function_spec.rb +192 -0
- data/spec/operations/functions/drop_function_spec.rb +182 -0
- data/spec/operations/functions/rename_function_spec.rb +101 -0
- data/spec/operations/indexes/add_index_spec.rb +94 -0
- data/spec/operations/materialized_views/change_materialized_view_spec.rb +190 -0
- data/spec/operations/materialized_views/create_materialized_view_spec.rb +144 -0
- data/spec/operations/materialized_views/drop_materialized_view_spec.rb +145 -0
- data/spec/operations/materialized_views/refresh_materialized_view_spec.rb +79 -0
- data/spec/operations/materialized_views/rename_materialized_view_spec.rb +88 -0
- data/spec/operations/procedures/change_procedure_spec.rb +175 -0
- data/spec/operations/procedures/create_procedure_spec.rb +151 -0
- data/spec/operations/procedures/drop_procedure_spec.rb +159 -0
- data/spec/operations/procedures/rename_procedure_spec.rb +107 -0
- data/spec/operations/statistics/create_statistics_spec.rb +230 -0
- data/spec/operations/statistics/drop_statistics_spec.rb +106 -0
- data/spec/operations/statistics/rename_statistics_spec.rb +129 -0
- data/spec/operations/tables/create_table_spec.rb +53 -0
- data/spec/operations/tables/rename_table_spec.rb +37 -0
- data/spec/operations/triggers/change_trigger_spec.rb +195 -0
- data/spec/operations/triggers/create_trigger_spec.rb +104 -0
- data/spec/operations/triggers/drop_trigger_spec.rb +124 -0
- data/spec/operations/triggers/rename_trigger_spec.rb +160 -0
- data/spec/operations/views/change_view_spec.rb +144 -0
- data/spec/operations/views/create_view_spec.rb +134 -0
- data/spec/operations/views/drop_view_spec.rb +146 -0
- data/spec/operations/views/rename_view_spec.rb +85 -0
- data/spec/pg_trunk/dependencies_resolver_spec.rb +43 -0
- data/spec/spec_helper.rb +28 -0
- data/spec/support/migrations_helper.rb +376 -0
- 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
|