pg_sql_triggers 1.3.0 → 1.5.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.
- checksums.yaml +4 -4
- data/.erb_lint.yml +0 -0
- data/.rspec +0 -0
- data/.rubocop.yml +6 -16
- data/AGENTS.md +8 -0
- data/CHANGELOG.md +354 -0
- data/COVERAGE.md +39 -41
- data/LICENSE +0 -0
- data/README.md +44 -26
- data/RELEASE.md +0 -0
- data/Rakefile +5 -0
- data/app/assets/javascripts/pg_sql_triggers/application.js +0 -0
- data/app/assets/javascripts/pg_sql_triggers/trigger_actions.js +0 -0
- data/app/assets/stylesheets/pg_sql_triggers/application.css +0 -0
- data/app/controllers/concerns/pg_sql_triggers/error_handling.rb +0 -0
- data/app/controllers/concerns/pg_sql_triggers/kill_switch_protection.rb +0 -0
- data/app/controllers/concerns/pg_sql_triggers/permission_checking.rb +6 -5
- data/app/controllers/pg_sql_triggers/application_controller.rb +0 -0
- data/app/controllers/pg_sql_triggers/audit_logs_controller.rb +81 -64
- data/app/controllers/pg_sql_triggers/dashboard_controller.rb +111 -34
- data/app/controllers/pg_sql_triggers/migrations_controller.rb +13 -14
- data/app/controllers/pg_sql_triggers/tables_controller.rb +8 -0
- data/app/controllers/pg_sql_triggers/triggers_controller.rb +1 -0
- data/app/helpers/pg_sql_triggers/dashboard_helper.rb +19 -0
- data/app/helpers/pg_sql_triggers/permissions_helper.rb +3 -2
- data/app/models/pg_sql_triggers/application_record.rb +0 -0
- data/app/models/pg_sql_triggers/audit_log.rb +29 -47
- data/app/models/pg_sql_triggers/trigger_registry.rb +137 -74
- data/app/views/layouts/pg_sql_triggers/application.html.erb +0 -1
- data/app/views/pg_sql_triggers/audit_logs/index.html.erb +9 -5
- data/app/views/pg_sql_triggers/dashboard/index.html.erb +107 -27
- data/app/views/pg_sql_triggers/shared/_confirmation_modal.html.erb +0 -0
- data/app/views/pg_sql_triggers/shared/_kill_switch_status.html.erb +0 -0
- data/app/views/pg_sql_triggers/tables/index.html.erb +27 -18
- data/app/views/pg_sql_triggers/tables/show.html.erb +0 -2
- data/app/views/pg_sql_triggers/triggers/_drop_modal.html.erb +0 -0
- data/app/views/pg_sql_triggers/triggers/_re_execute_modal.html.erb +0 -0
- data/app/views/pg_sql_triggers/triggers/show.html.erb +33 -0
- data/config/initializers/pg_sql_triggers.rb +0 -0
- data/config/routes.rb +0 -14
- data/db/migrate/{20251222000001_create_pg_sql_triggers_tables.rb → 20251222104327_create_pg_sql_triggers_tables.rb} +0 -0
- data/db/migrate/20251229071916_add_timing_to_pg_sql_triggers_registry.rb +0 -0
- data/db/migrate/{20260103000001_create_pg_sql_triggers_audit_log.rb → 20260103114508_create_pg_sql_triggers_audit_log.rb} +0 -0
- data/db/migrate/20260228162233_add_for_each_to_pg_sql_triggers_registry.rb +8 -0
- data/db/migrate/20260412185841_add_constraint_deferral_to_pg_sql_triggers_registry.rb +9 -0
- data/docs/README.md +3 -0
- data/docs/api-reference.md +176 -152
- data/docs/audit-trail.md +1 -1
- data/docs/configuration.md +196 -3
- data/docs/getting-started.md +31 -16
- data/docs/kill-switch.md +0 -0
- data/docs/permissions.md +6 -9
- data/docs/troubleshooting.md +0 -0
- data/docs/ui-guide.md +0 -0
- data/docs/usage-guide.md +112 -67
- data/docs/web-ui.md +3 -103
- data/lib/generators/pg_sql_triggers/install_generator.rb +0 -0
- data/lib/generators/pg_sql_triggers/templates/README +0 -0
- data/lib/generators/pg_sql_triggers/templates/create_pg_sql_triggers_tables.rb +0 -0
- data/lib/generators/pg_sql_triggers/templates/initializer.rb +14 -0
- data/lib/generators/pg_sql_triggers/templates/trigger_dsl.rb.tt +11 -0
- data/lib/generators/pg_sql_triggers/templates/trigger_migration.rb.erb +0 -0
- data/lib/generators/pg_sql_triggers/templates/trigger_migration_full.rb.tt +29 -0
- data/lib/generators/pg_sql_triggers/trigger_generator.rb +83 -0
- data/lib/generators/pg_sql_triggers/trigger_migration_generator.rb +0 -0
- data/lib/pg_sql_triggers/alerting.rb +77 -0
- data/lib/pg_sql_triggers/database_introspection.rb +0 -0
- data/lib/pg_sql_triggers/deferral_checksum.rb +54 -0
- data/lib/pg_sql_triggers/drift/db_queries.rb +26 -13
- data/lib/pg_sql_triggers/drift/detector.rb +59 -38
- data/lib/pg_sql_triggers/drift/reporter.rb +0 -0
- data/lib/pg_sql_triggers/drift.rb +5 -0
- data/lib/pg_sql_triggers/dsl/trigger_definition.rb +68 -20
- data/lib/pg_sql_triggers/dsl.rb +0 -0
- data/lib/pg_sql_triggers/engine.rb +49 -0
- data/lib/pg_sql_triggers/errors.rb +0 -0
- data/lib/pg_sql_triggers/events_checksum.rb +114 -0
- data/lib/pg_sql_triggers/migration.rb +5 -6
- data/lib/pg_sql_triggers/migrator/pre_apply_comparator.rb +85 -82
- data/lib/pg_sql_triggers/migrator/pre_apply_diff_reporter.rb +0 -0
- data/lib/pg_sql_triggers/migrator/safety_validator.rb +34 -12
- data/lib/pg_sql_triggers/migrator.rb +137 -94
- data/lib/pg_sql_triggers/permissions/checker.rb +12 -15
- data/lib/pg_sql_triggers/permissions.rb +1 -0
- data/lib/pg_sql_triggers/rake_development_boot.rb +65 -0
- data/lib/pg_sql_triggers/registry/manager.rb +60 -21
- data/lib/pg_sql_triggers/registry/validator.rb +287 -6
- data/lib/pg_sql_triggers/registry.rb +0 -0
- data/lib/pg_sql_triggers/schema_dumper_extension.rb +32 -0
- data/lib/pg_sql_triggers/sql/kill_switch.rb +154 -275
- data/lib/pg_sql_triggers/sql.rb +0 -6
- data/lib/pg_sql_triggers/testing/dry_run.rb +0 -0
- data/lib/pg_sql_triggers/testing/function_tester.rb +97 -107
- data/lib/pg_sql_triggers/testing/safe_executor.rb +0 -0
- data/lib/pg_sql_triggers/testing/syntax_validator.rb +0 -0
- data/lib/pg_sql_triggers/testing.rb +0 -0
- data/lib/pg_sql_triggers/trigger_structure_dumper.rb +111 -0
- data/lib/pg_sql_triggers/version.rb +1 -1
- data/lib/pg_sql_triggers.rb +21 -1
- data/lib/tasks/trigger_migrations.rake +235 -152
- data/rakelib/pg_sql_triggers_environment.rake +9 -0
- data/scripts/generate_coverage_report.rb +4 -1
- data/sig/pg_sql_triggers.rbs +0 -0
- metadata +68 -22
- data/Goal.md +0 -742
- data/app/controllers/pg_sql_triggers/generator_controller.rb +0 -213
- data/app/controllers/pg_sql_triggers/sql_capsules_controller.rb +0 -161
- data/app/views/pg_sql_triggers/generator/new.html.erb +0 -388
- data/app/views/pg_sql_triggers/generator/preview.html.erb +0 -305
- data/app/views/pg_sql_triggers/sql_capsules/new.html.erb +0 -81
- data/app/views/pg_sql_triggers/sql_capsules/show.html.erb +0 -85
- data/lib/generators/trigger/migration_generator.rb +0 -60
- data/lib/pg_sql_triggers/generator/form.rb +0 -80
- data/lib/pg_sql_triggers/generator/service.rb +0 -339
- data/lib/pg_sql_triggers/generator.rb +0 -8
- data/lib/pg_sql_triggers/sql/capsule.rb +0 -79
- data/lib/pg_sql_triggers/sql/executor.rb +0 -200
|
@@ -1,15 +1,296 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "json"
|
|
4
|
+
require "set"
|
|
5
|
+
require "tsort"
|
|
6
|
+
|
|
3
7
|
module PgSqlTriggers
|
|
4
8
|
module Registry
|
|
5
|
-
class Validator
|
|
6
|
-
|
|
9
|
+
class Validator # rubocop:disable Metrics/ClassLength -- validation rules grouped in one class
|
|
10
|
+
VALID_EVENTS = %w[insert update delete truncate].freeze
|
|
11
|
+
VALID_TIMINGS = %w[before after instead_of].freeze
|
|
12
|
+
VALID_FOR_EACH = %w[row statement].freeze
|
|
13
|
+
|
|
7
14
|
def self.validate!
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
15
|
+
errors = []
|
|
16
|
+
dsl_triggers = PgSqlTriggers::TriggerRegistry.where(source: "dsl").to_a
|
|
17
|
+
|
|
18
|
+
dsl_triggers.each do |trigger|
|
|
19
|
+
errors.concat(validate_dsl_trigger(trigger))
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
errors.concat(collect_dependency_and_order_errors(dsl_triggers))
|
|
23
|
+
|
|
24
|
+
return true if errors.empty?
|
|
25
|
+
|
|
26
|
+
raise PgSqlTriggers::ValidationError.new(
|
|
27
|
+
"Registry validation failed:\n#{errors.map { |e| " - #{e}" }.join("\n")}",
|
|
28
|
+
error_code: "VALIDATION_FAILED",
|
|
29
|
+
context: { errors: errors }
|
|
30
|
+
)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Returns dependency-related errors (missing refs, cycles, incompatible pairs, name order).
|
|
34
|
+
# Used by rake trigger:validate_order and included in {.validate!}.
|
|
35
|
+
def self.trigger_order_validation_errors
|
|
36
|
+
dsl_triggers = PgSqlTriggers::TriggerRegistry.where(source: "dsl").to_a
|
|
37
|
+
collect_dependency_and_order_errors(dsl_triggers)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Prerequisite and dependent DSL triggers for the trigger detail page.
|
|
41
|
+
def self.related_triggers_for_show(trigger_record)
|
|
42
|
+
empty = { prerequisites: [], dependents: [] }
|
|
43
|
+
return empty if trigger_record.blank? || trigger_record.source != "dsl"
|
|
44
|
+
|
|
45
|
+
defn = parse_definition(trigger_record.definition)
|
|
46
|
+
prerequisite_names = normalize_depends_on(defn)
|
|
47
|
+
prerequisites = prerequisite_names.filter_map do |dep_name|
|
|
48
|
+
PgSqlTriggers::TriggerRegistry.find_by(source: "dsl", trigger_name: dep_name)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
dependents = []
|
|
52
|
+
PgSqlTriggers::TriggerRegistry.where(source: "dsl").find_each do |row|
|
|
53
|
+
next if row.id == trigger_record.id
|
|
54
|
+
|
|
55
|
+
other = parse_definition(row.definition)
|
|
56
|
+
dependents << row if normalize_depends_on(other).include?(trigger_record.trigger_name)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
{
|
|
60
|
+
prerequisites: prerequisites.sort_by(&:trigger_name),
|
|
61
|
+
dependents: dependents.sort_by(&:trigger_name)
|
|
62
|
+
}
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
class << self
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
def normalize_depends_on(defn)
|
|
69
|
+
raw = defn["depends_on"]
|
|
70
|
+
list = case raw
|
|
71
|
+
when nil then []
|
|
72
|
+
when Array then raw
|
|
73
|
+
else [raw]
|
|
74
|
+
end
|
|
75
|
+
list.flatten.compact.map { |entry| entry.to_s.strip }.reject(&:empty?).uniq
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def effective_timing(defn)
|
|
79
|
+
return "after" if ActiveModel::Type::Boolean.new.cast(defn["constraint_trigger"])
|
|
80
|
+
|
|
81
|
+
(defn["timing"].presence || "before").to_s
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def for_each_level(defn)
|
|
85
|
+
(defn["for_each"].presence || "row").to_s
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def event_names_for_overlap(defn)
|
|
89
|
+
Array(defn["events"]).to_set(&:to_s)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def compatibility_errors(child_row, child_defn, parent_row, parent_defn)
|
|
93
|
+
child_name = child_row.trigger_name
|
|
94
|
+
parent_name = parent_row.trigger_name
|
|
95
|
+
errs = []
|
|
96
|
+
if child_row.table_name != parent_row.table_name
|
|
97
|
+
errs << "Trigger '#{child_name}': depends_on '#{parent_name}' must reference a trigger on the same " \
|
|
98
|
+
"table (#{child_row.table_name} vs #{parent_row.table_name})"
|
|
99
|
+
end
|
|
100
|
+
if effective_timing(child_defn) != effective_timing(parent_defn)
|
|
101
|
+
errs << "Trigger '#{child_name}': depends_on '#{parent_name}' requires the same timing " \
|
|
102
|
+
"(#{effective_timing(child_defn)} vs #{effective_timing(parent_defn)})"
|
|
103
|
+
end
|
|
104
|
+
if for_each_level(child_defn) != for_each_level(parent_defn)
|
|
105
|
+
errs << "Trigger '#{child_name}': depends_on '#{parent_name}' requires the same FOR EACH " \
|
|
106
|
+
"(#{for_each_level(child_defn)} vs #{for_each_level(parent_defn)})"
|
|
107
|
+
end
|
|
108
|
+
ch_ev = event_names_for_overlap(child_defn)
|
|
109
|
+
pa_ev = event_names_for_overlap(parent_defn)
|
|
110
|
+
if (ch_ev & pa_ev).empty?
|
|
111
|
+
errs << "Trigger '#{child_name}': depends_on '#{parent_name}' requires overlapping events"
|
|
112
|
+
end
|
|
113
|
+
errs
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def collect_dependency_and_order_errors(dsl_triggers)
|
|
117
|
+
errors = []
|
|
118
|
+
by_name = dsl_triggers.index_by(&:trigger_name)
|
|
119
|
+
valid_edges = []
|
|
120
|
+
|
|
121
|
+
dsl_triggers.each do |child|
|
|
122
|
+
child_name = child.trigger_name
|
|
123
|
+
child_defn = parse_definition(child.definition)
|
|
124
|
+
deps = normalize_depends_on(child_defn)
|
|
125
|
+
deps.each do |parent_name|
|
|
126
|
+
if parent_name == child_name
|
|
127
|
+
errors << "Trigger '#{child_name}': depends_on cannot reference itself"
|
|
128
|
+
next
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
parent = by_name[parent_name]
|
|
132
|
+
unless parent
|
|
133
|
+
errors << "Trigger '#{child_name}': depends_on references unknown trigger '#{parent_name}'"
|
|
134
|
+
next
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
parent_defn = parse_definition(parent.definition)
|
|
138
|
+
compat = compatibility_errors(child, child_defn, parent, parent_defn)
|
|
139
|
+
errors.concat(compat)
|
|
140
|
+
next if compat.any?
|
|
141
|
+
|
|
142
|
+
valid_edges << [parent_name, child_name]
|
|
143
|
+
unless parent_name < child_name
|
|
144
|
+
errors << "Trigger '#{child_name}': depends_on '#{parent_name}' must sort before " \
|
|
145
|
+
"'#{child_name}' alphabetically (PostgreSQL fires same-kind triggers in name order)"
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
errors.concat(cycle_dependency_errors(valid_edges))
|
|
151
|
+
errors
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def cycle_dependency_errors(edges)
|
|
155
|
+
return [] if edges.empty?
|
|
156
|
+
|
|
157
|
+
DependsOnSorter.new(edges).tsort
|
|
158
|
+
[]
|
|
159
|
+
rescue TSort::Cyclic
|
|
160
|
+
["depends_on: circular dependency chain detected among DSL triggers"]
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def validate_dsl_trigger(trigger)
|
|
164
|
+
errors = []
|
|
165
|
+
name = trigger.trigger_name
|
|
166
|
+
definition = parse_definition(trigger.definition)
|
|
167
|
+
|
|
168
|
+
errors << "Trigger '#{name}': missing table_name" if definition["table_name"].blank?
|
|
169
|
+
|
|
170
|
+
events = Array(definition["events"])
|
|
171
|
+
if events.empty?
|
|
172
|
+
errors << "Trigger '#{name}': events cannot be empty"
|
|
173
|
+
else
|
|
174
|
+
invalid = events - VALID_EVENTS
|
|
175
|
+
if invalid.any?
|
|
176
|
+
errors << "Trigger '#{name}': invalid events #{invalid.inspect} (valid: #{VALID_EVENTS.inspect})"
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
errors << "Trigger '#{name}': missing function_name" if definition["function_name"].blank?
|
|
181
|
+
|
|
182
|
+
errors.concat(validate_update_columns(name, events, definition))
|
|
183
|
+
|
|
184
|
+
timing = definition["timing"].to_s
|
|
185
|
+
if timing.present? && VALID_TIMINGS.exclude?(timing)
|
|
186
|
+
errors << "Trigger '#{name}': invalid timing '#{timing}' (valid: #{VALID_TIMINGS.inspect})"
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
for_each = definition["for_each"].to_s
|
|
190
|
+
if for_each.present? && VALID_FOR_EACH.exclude?(for_each)
|
|
191
|
+
errors << "Trigger '#{name}': invalid for_each '#{for_each}' (valid: #{VALID_FOR_EACH.inspect})"
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
errors.concat(validate_deferral(name, definition, timing))
|
|
195
|
+
|
|
196
|
+
errors
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def validate_deferral(name, definition, timing)
|
|
200
|
+
constraint = ActiveModel::Type::Boolean.new.cast(definition["constraint_trigger"])
|
|
201
|
+
deferrable_val = definition["deferrable"].presence&.to_s
|
|
202
|
+
initially_val = definition["initially"].presence&.to_s
|
|
203
|
+
|
|
204
|
+
errors = []
|
|
205
|
+
errors.concat(constraint_deferral_errors(name, constraint, timing, definition))
|
|
206
|
+
errors.concat(deferral_value_errors(name, deferrable_val, initially_val))
|
|
207
|
+
errors
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def constraint_deferral_errors(name, constraint, timing, definition)
|
|
211
|
+
deferrable_val = definition["deferrable"].presence&.to_s
|
|
212
|
+
initially_val = definition["initially"].presence&.to_s
|
|
213
|
+
errors = []
|
|
214
|
+
if (deferrable_val.present? || initially_val.present?) && !constraint
|
|
215
|
+
errors << "Trigger '#{name}': deferrable/initially require constraint_trigger (CONSTRAINT TRIGGER)"
|
|
216
|
+
end
|
|
217
|
+
if constraint && timing.to_s != "after"
|
|
218
|
+
errors << "Trigger '#{name}': constraint triggers must use after timing"
|
|
219
|
+
end
|
|
220
|
+
if constraint && events_include_truncate?(definition)
|
|
221
|
+
errors << "Trigger '#{name}': constraint triggers cannot use TRUNCATE events"
|
|
222
|
+
end
|
|
223
|
+
errors
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
VALID_DEFERRABLE = %w[deferrable not_deferrable].freeze
|
|
227
|
+
VALID_INITIALLY = %w[deferred immediate].freeze
|
|
228
|
+
private_constant :VALID_DEFERRABLE, :VALID_INITIALLY
|
|
229
|
+
|
|
230
|
+
def deferral_value_errors(name, deferrable_val, initially_val)
|
|
231
|
+
errors = []
|
|
232
|
+
if deferrable_val.present? && VALID_DEFERRABLE.exclude?(deferrable_val)
|
|
233
|
+
errors << "Trigger '#{name}': invalid deferrable '#{deferrable_val}' (valid: #{VALID_DEFERRABLE.inspect})"
|
|
234
|
+
end
|
|
235
|
+
if initially_val.present? && VALID_INITIALLY.exclude?(initially_val)
|
|
236
|
+
errors << "Trigger '#{name}': invalid initially '#{initially_val}' (valid: #{VALID_INITIALLY.inspect})"
|
|
237
|
+
end
|
|
238
|
+
if initially_val.present? && deferrable_val != "deferrable"
|
|
239
|
+
errors << "Trigger '#{name}': initially requires deferrable to be 'deferrable'"
|
|
240
|
+
end
|
|
241
|
+
errors
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def events_include_truncate?(definition)
|
|
245
|
+
Array(definition["events"]).map(&:to_s).include?("truncate")
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def validate_update_columns(name, events, definition)
|
|
249
|
+
errs = []
|
|
250
|
+
cols = Array(definition["columns"]).flatten.compact.map(&:to_s).map(&:strip).reject(&:empty?)
|
|
251
|
+
return errs if cols.empty?
|
|
252
|
+
|
|
253
|
+
unless events.map(&:to_s).include?("update")
|
|
254
|
+
errs << "Trigger '#{name}': columns require an update event " \
|
|
255
|
+
"(use on_update_of or include :update)"
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
cols.each do |col|
|
|
259
|
+
next if col.match?(/\A[a-zA-Z_][a-zA-Z0-9_]*\z/)
|
|
260
|
+
|
|
261
|
+
errs << "Trigger '#{name}': invalid column name #{col.inspect} (use simple SQL identifiers, no quoting)"
|
|
262
|
+
end
|
|
263
|
+
errs
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def parse_definition(definition_json)
|
|
267
|
+
return {} if definition_json.blank?
|
|
268
|
+
|
|
269
|
+
JSON.parse(definition_json)
|
|
270
|
+
rescue JSON::ParserError
|
|
271
|
+
{}
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
# Internal helper for cycle detection using Ruby's TSort.
|
|
276
|
+
class DependsOnSorter # :nodoc:
|
|
277
|
+
include TSort
|
|
278
|
+
|
|
279
|
+
def initialize(edges)
|
|
280
|
+
@adjacency = Hash.new { |h, k| h[k] = [] }
|
|
281
|
+
edges.each { |(from, to)| @adjacency[from] << to }
|
|
282
|
+
@nodes = (@adjacency.keys | @adjacency.values.flatten).uniq
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def tsort_each_node(&block)
|
|
286
|
+
@nodes.each(&block)
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def tsort_each_child(node, &block)
|
|
290
|
+
Array(@adjacency[node]).each(&block)
|
|
291
|
+
end
|
|
11
292
|
end
|
|
12
|
-
|
|
293
|
+
private_constant :DependsOnSorter
|
|
13
294
|
end
|
|
14
295
|
end
|
|
15
296
|
end
|
|
File without changes
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PgSqlTriggers
|
|
4
|
+
# Prepended onto ActiveRecord::SchemaDumper to document that triggers live outside schema.rb.
|
|
5
|
+
module SchemaDumperExtension
|
|
6
|
+
def trailer(stream)
|
|
7
|
+
stream.puts PgSqlTriggers::TriggerStructureDumper.schema_rb_annotation(connection: @connection) if append_notes?
|
|
8
|
+
super
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
def append_notes?
|
|
14
|
+
PgSqlTriggers.append_trigger_notes_to_schema_dump &&
|
|
15
|
+
defined?(Rails) &&
|
|
16
|
+
Rails.application &&
|
|
17
|
+
ruby_schema_format?
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def ruby_schema_format?
|
|
21
|
+
cfg = Rails.application.config
|
|
22
|
+
# Minimal Rails apps (e.g. tests without ActiveRecord::Railtie) have no config.active_record.
|
|
23
|
+
return true unless cfg.respond_to?(:active_record)
|
|
24
|
+
|
|
25
|
+
ar_cfg = cfg.active_record
|
|
26
|
+
return true unless ar_cfg.respond_to?(:schema_format)
|
|
27
|
+
|
|
28
|
+
format = ar_cfg.schema_format
|
|
29
|
+
format.nil? || format.to_sym == :ruby
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|