hairtrigger 1.1.0 → 1.2.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/README.md +18 -3
- data/lib/hair_trigger/adapter.rb +3 -3
- data/lib/hair_trigger/builder.rb +57 -22
- data/lib/hair_trigger/schema_dumper.rb +1 -1
- data/lib/hair_trigger/version.rb +1 -1
- data/lib/hair_trigger.rb +9 -1
- data/lib/tasks/hair_trigger.rake +3 -2
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 89b859bf3d396f6ba9b213d9be337e111b39cc1a87a5ee73918d553ab03a16b7
|
4
|
+
data.tar.gz: f6cab257e8b963d9f8b740a77b5a336a42403db622df72a57b51254643042117
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 616a0dfd17908b605b99ba5bedad6c986c98affde8128c3beff540fda85040c61b86fa2ab527d762a3f820ebd3136fab97860a9dd7a9ae0679f1cf73ac4a615c
|
7
|
+
data.tar.gz: 63a07f2d193d6f7e752c61f3b72a906c50059813a0204ffa480781248d00c8309c2c0a3620471b4a194db117534544e6a3b51ab6c7cc833baee199f5013df7d4
|
data/README.md
CHANGED
@@ -125,6 +125,21 @@ trigger.after(:insert).declare("user_type text; status text") do
|
|
125
125
|
end
|
126
126
|
```
|
127
127
|
|
128
|
+
#### new_as(name) or old_as(name)
|
129
|
+
PostgreSQL-specific option for "after" triggers to allow accessing the row as it was before the operation (`old`) or as it is after the operation (`new`). This is useful in statement trigger when you want to compare the old and new values of all rows changed during an update trigger. For example:
|
130
|
+
|
131
|
+
```ruby
|
132
|
+
trigger.after(:update).for_each(:statement).new_as(:new_users).old_as(:old_users) do
|
133
|
+
<<-SQL
|
134
|
+
INSERT INTO user_changes(id, old_name, new_name) FROM (
|
135
|
+
SELECT new_users.id, old_users.name AS old_name, new_users.name AS new_name
|
136
|
+
FROM new_users
|
137
|
+
INNER JOIN old_users ON new_users.id = old_users.id
|
138
|
+
) agg
|
139
|
+
SQL
|
140
|
+
end
|
141
|
+
```
|
142
|
+
|
128
143
|
#### all
|
129
144
|
Noop, useful for trigger groups (see below).
|
130
145
|
|
@@ -362,8 +377,8 @@ to manage all that w/ automagical gemfiles. So the tl;dr when testing locally is
|
|
362
377
|
|
363
378
|
## Compatibility
|
364
379
|
|
365
|
-
* Ruby
|
366
|
-
* Rails
|
380
|
+
* Ruby 3.0+
|
381
|
+
* Rails 6.1+
|
367
382
|
* PostgreSQL 8.0+
|
368
383
|
* MySQL 5.0.10+
|
369
384
|
* SQLite 3.3.8+
|
@@ -372,4 +387,4 @@ to manage all that w/ automagical gemfiles. So the tl;dr when testing locally is
|
|
372
387
|
|
373
388
|
## Copyright
|
374
389
|
|
375
|
-
Copyright (c) 2011-
|
390
|
+
Copyright (c) 2011-2024 Jon Jensen. See LICENSE.txt for further details.
|
data/lib/hair_trigger/adapter.rb
CHANGED
@@ -27,11 +27,11 @@ module HairTrigger
|
|
27
27
|
name_clause = options[:only] ? "IN ('" + options[:only].join("', '") + "')" : nil
|
28
28
|
adapter_name = HairTrigger.adapter_name_for(self)
|
29
29
|
case adapter_name
|
30
|
-
when
|
30
|
+
when *HairTrigger::SQLITE_ADAPTERS
|
31
31
|
select_rows("SELECT name, sql FROM sqlite_master WHERE type = 'trigger' #{name_clause ? " AND name " + name_clause : ""}").each do |(name, definition)|
|
32
32
|
triggers[name] = quote_table_name_in_trigger(definition) + ";\n"
|
33
33
|
end
|
34
|
-
when
|
34
|
+
when *HairTrigger::MYSQL_ADAPTERS
|
35
35
|
select_rows("SHOW TRIGGERS").each do |(name, event, table, actions, timing, created, sql_mode, definer)|
|
36
36
|
definer = normalize_mysql_definer(definer)
|
37
37
|
next if options[:only] && !options[:only].include?(name)
|
@@ -41,7 +41,7 @@ FOR EACH ROW
|
|
41
41
|
#{actions}
|
42
42
|
SQL
|
43
43
|
end
|
44
|
-
when
|
44
|
+
when *POSTGRESQL_ADAPTERS
|
45
45
|
function_conditions = "(SELECT typname FROM pg_type WHERE oid = prorettype) = 'trigger'"
|
46
46
|
function_conditions << <<-SQL unless options[:simple_check]
|
47
47
|
AND oid IN (
|
data/lib/hair_trigger/builder.rb
CHANGED
@@ -44,7 +44,7 @@ module HairTrigger
|
|
44
44
|
end
|
45
45
|
|
46
46
|
def name(name)
|
47
|
-
@errors << ["trigger name cannot exceed 63 for postgres",
|
47
|
+
@errors << ["trigger name cannot exceed 63 for postgres", *HairTrigger::POSTGRESQL_ADAPTERS] if name.to_s.size > 63
|
48
48
|
options[:name] = name.to_s
|
49
49
|
end
|
50
50
|
|
@@ -54,7 +54,7 @@ module HairTrigger
|
|
54
54
|
end
|
55
55
|
|
56
56
|
def for_each(for_each)
|
57
|
-
@errors << ["sqlite and mysql don't support FOR EACH STATEMENT triggers",
|
57
|
+
@errors << ["sqlite and mysql don't support FOR EACH STATEMENT triggers", *HairTrigger::SQLITE_ADAPTERS, *HairTrigger::MYSQL_ADAPTERS] if for_each == :statement
|
58
58
|
raise DeclarationError, "invalid for_each" unless [:row, :statement].include?(for_each)
|
59
59
|
options[:for_each] = for_each.to_s.upcase
|
60
60
|
end
|
@@ -82,6 +82,18 @@ module HairTrigger
|
|
82
82
|
options[:of] = columns
|
83
83
|
end
|
84
84
|
|
85
|
+
def old_as(table)
|
86
|
+
raise DeclarationError, "`old_as' requested, but no table_name specified" unless table.present?
|
87
|
+
options[:referencing] ||= {}
|
88
|
+
options[:referencing][:old] = table
|
89
|
+
end
|
90
|
+
|
91
|
+
def new_as(table)
|
92
|
+
raise DeclarationError, "`new_as' requested, but no table_name specified" unless table.present?
|
93
|
+
options[:referencing] ||= {}
|
94
|
+
options[:referencing][:new] = table
|
95
|
+
end
|
96
|
+
|
85
97
|
def declare(declarations)
|
86
98
|
options[:declarations] = declarations
|
87
99
|
end
|
@@ -95,9 +107,9 @@ module HairTrigger
|
|
95
107
|
raise DeclarationError, "trigger security should be :invoker, :definer, CURRENT_USER, or a valid user (e.g. 'user'@'host')"
|
96
108
|
end
|
97
109
|
# sqlite default is n/a, mysql default is :definer, postgres default is :invoker
|
98
|
-
@errors << ["sqlite doesn't support trigger security",
|
99
|
-
@errors << ["postgresql doesn't support arbitrary users for trigger security",
|
100
|
-
@errors << ["mysql doesn't support invoker trigger security",
|
110
|
+
@errors << ["sqlite doesn't support trigger security", *HairTrigger::SQLITE_ADAPTERS]
|
111
|
+
@errors << ["postgresql doesn't support arbitrary users for trigger security", *HairTrigger::POSTGRESQL_ADAPTERS] unless [:definer, :invoker].include?(user)
|
112
|
+
@errors << ["mysql doesn't support invoker trigger security", *HairTrigger::MYSQL_ADAPTERS] if user == :invoker
|
101
113
|
options[:security] = user
|
102
114
|
end
|
103
115
|
|
@@ -110,8 +122,8 @@ module HairTrigger
|
|
110
122
|
events << :insert if events.delete(:create)
|
111
123
|
events << :delete if events.delete(:destroy)
|
112
124
|
raise DeclarationError, "invalid events" unless events & [:insert, :update, :delete, :truncate] == events
|
113
|
-
@errors << ["sqlite and mysql triggers may not be shared by multiple actions",
|
114
|
-
@errors << ["sqlite and mysql do not support truncate triggers",
|
125
|
+
@errors << ["sqlite and mysql triggers may not be shared by multiple actions", *HairTrigger::MYSQL_ADAPTERS, *HairTrigger::SQLITE_ADAPTERS] if events.size > 1
|
126
|
+
@errors << ["sqlite and mysql do not support truncate triggers", *HairTrigger::MYSQL_ADAPTERS, *HairTrigger::SQLITE_ADAPTERS] if events.include?(:truncate)
|
115
127
|
options[:events] = events.map{ |e| e.to_s.upcase }
|
116
128
|
end
|
117
129
|
|
@@ -140,7 +152,7 @@ module HairTrigger
|
|
140
152
|
def #{method}(*args, &block)
|
141
153
|
@chained_calls << :#{method}
|
142
154
|
if @triggers || @trigger_group
|
143
|
-
@errors << ["mysql doesn't support #{method} within a trigger group",
|
155
|
+
@errors << ["mysql doesn't support #{method} within a trigger group", *HairTrigger::MYSQL_ADAPTERS] unless [:name, :where, :all, :of].include?(:#{method})
|
144
156
|
end
|
145
157
|
set_#{method}(*args, &(block_given? ? block : nil))
|
146
158
|
end
|
@@ -159,10 +171,10 @@ module HairTrigger
|
|
159
171
|
METHOD
|
160
172
|
end
|
161
173
|
end
|
162
|
-
chainable_methods :name, :on, :for_each, :before, :after, :where, :security, :timing, :events, :all, :nowrap, :of, :declare
|
174
|
+
chainable_methods :name, :on, :for_each, :before, :after, :where, :security, :timing, :events, :all, :nowrap, :of, :declare, :old_as, :new_as
|
163
175
|
|
164
176
|
def create_grouped_trigger?
|
165
|
-
adapter_name
|
177
|
+
HairTrigger::MYSQL_ADAPTERS.include?(adapter_name)
|
166
178
|
end
|
167
179
|
|
168
180
|
def prepare!
|
@@ -222,11 +234,11 @@ module HairTrigger
|
|
222
234
|
|
223
235
|
[generate_drop_trigger] +
|
224
236
|
[case adapter_name
|
225
|
-
when
|
237
|
+
when *HairTrigger::SQLITE_ADAPTERS
|
226
238
|
generate_trigger_sqlite
|
227
|
-
when
|
239
|
+
when *HairTrigger::MYSQL_ADAPTERS
|
228
240
|
generate_trigger_mysql
|
229
|
-
when
|
241
|
+
when *HairTrigger::POSTGRESQL_ADAPTERS
|
230
242
|
generate_trigger_postgresql
|
231
243
|
else
|
232
244
|
raise GenerationError, "don't know how to build #{adapter_name} triggers yet"
|
@@ -306,6 +318,10 @@ module HairTrigger
|
|
306
318
|
"where(#{prepared_where.inspect})"
|
307
319
|
when :of
|
308
320
|
"of(#{options[:of].inspect[1..-2]})"
|
321
|
+
when :old_as
|
322
|
+
"old_as(#{options[:referencing][:old].inspect})"
|
323
|
+
when :new_as
|
324
|
+
"new_as(#{options[:referencing][:new].inspect})"
|
309
325
|
when :for_each
|
310
326
|
"for_each(#{options[:for_each].downcase.to_sym.inspect})"
|
311
327
|
when :declare
|
@@ -329,8 +345,8 @@ module HairTrigger
|
|
329
345
|
def maybe_execute(&block)
|
330
346
|
raise DeclarationError, "of may only be specified on update triggers" if options[:of] && options[:events] != ["UPDATE"]
|
331
347
|
if block.arity > 0 # we're creating a trigger group, so set up some stuff and pass the buck
|
332
|
-
@errors << ["trigger group must specify timing and event(s) for mysql",
|
333
|
-
@errors << ["nested trigger groups are not supported for mysql",
|
348
|
+
@errors << ["trigger group must specify timing and event(s) for mysql", *HairTrigger::MYSQL_ADAPTERS] unless options[:timing] && options[:events]
|
349
|
+
@errors << ["nested trigger groups are not supported for mysql", *HairTrigger::MYSQL_ADAPTERS] if @trigger_group
|
334
350
|
@triggers = []
|
335
351
|
block.call(self)
|
336
352
|
raise DeclarationError, "trigger group did not define any triggers" if @triggers.empty?
|
@@ -359,9 +375,9 @@ module HairTrigger
|
|
359
375
|
subtriggers = all_triggers(false)
|
360
376
|
named_subtriggers = subtriggers.select{ |t| t.options[:name] }
|
361
377
|
if named_subtriggers.present? && !options[:name]
|
362
|
-
@warnings << ["nested triggers have explicit names, but trigger group does not. trigger name will be inferred",
|
378
|
+
@warnings << ["nested triggers have explicit names, but trigger group does not. trigger name will be inferred", *HairTrigger::MYSQL_ADAPTERS]
|
363
379
|
elsif subtriggers.present? && !named_subtriggers.present? && options[:name]
|
364
|
-
@warnings << ["trigger group has an explicit name, but nested triggers do not. trigger names will be inferred",
|
380
|
+
@warnings << ["trigger group has an explicit name, but nested triggers do not. trigger names will be inferred", *HairTrigger::POSTGRESQL_ADAPTERS, *HairTrigger::SQLITE_ADAPTERS]
|
365
381
|
end
|
366
382
|
end
|
367
383
|
|
@@ -396,20 +412,37 @@ module HairTrigger
|
|
396
412
|
|
397
413
|
def supports_of?
|
398
414
|
case adapter_name
|
399
|
-
when
|
415
|
+
when *HairTrigger::SQLITE_ADAPTERS
|
400
416
|
true
|
401
|
-
when
|
417
|
+
when *HairTrigger::POSTGRESQL_ADAPTERS
|
402
418
|
db_version >= 90000
|
403
419
|
else
|
404
420
|
false
|
405
421
|
end
|
406
422
|
end
|
407
423
|
|
424
|
+
def referencing_clause(check_support = true)
|
425
|
+
if options[:referencing] && (!check_support || supports_referencing?)
|
426
|
+
"REFERENCING " + options[:referencing].map{ |k, v| "#{k.to_s.upcase} TABLE AS #{v}" }.join(" ")
|
427
|
+
end
|
428
|
+
end
|
429
|
+
|
430
|
+
def supports_referencing?
|
431
|
+
case adapter_name
|
432
|
+
when *HairTrigger::SQLITE_ADAPTERS, *HairTrigger::MYSQL_ADAPTERS
|
433
|
+
false
|
434
|
+
when *HairTrigger::POSTGRESQL_ADAPTERS
|
435
|
+
db_version >= 100000
|
436
|
+
else
|
437
|
+
false
|
438
|
+
end
|
439
|
+
end
|
440
|
+
|
408
441
|
def generate_drop_trigger
|
409
442
|
case adapter_name
|
410
|
-
when
|
443
|
+
when *HairTrigger::SQLITE_ADAPTERS, *HairTrigger::MYSQL_ADAPTERS
|
411
444
|
"DROP TRIGGER IF EXISTS #{prepared_name};\n"
|
412
|
-
when
|
445
|
+
when *HairTrigger::POSTGRESQL_ADAPTERS
|
413
446
|
"DROP TRIGGER IF EXISTS #{prepared_name} ON #{adapter.quote_table_name(options[:table])};\nDROP FUNCTION IF EXISTS #{adapter.quote_table_name(prepared_name)}();\n"
|
414
447
|
else
|
415
448
|
raise GenerationError, "don't know how to drop #{adapter_name} triggers yet"
|
@@ -433,6 +466,7 @@ END;
|
|
433
466
|
raise GenerationError, "security cannot be used in conjunction with nowrap" if options[:nowrap] && options[:security]
|
434
467
|
raise GenerationError, "where can only be used in conjunction with nowrap on postgres 9.0 and greater" if options[:nowrap] && prepared_where && db_version < 90000
|
435
468
|
raise GenerationError, "of can only be used in conjunction with nowrap on postgres 9.1 and greater" if options[:nowrap] && options[:of] && db_version < 90100
|
469
|
+
raise GenerationError, "referencing can only be used on postgres 10.0 and greater" if options[:referencing] && db_version < 100000
|
436
470
|
|
437
471
|
sql = ''
|
438
472
|
|
@@ -472,6 +506,7 @@ $$ LANGUAGE plpgsql#{security ? " SECURITY #{security.to_s.upcase}" : ""};
|
|
472
506
|
|
473
507
|
[sql, <<-SQL]
|
474
508
|
CREATE TRIGGER #{prepared_name} #{options[:timing]} #{options[:events].join(" OR ")} #{of_clause}ON #{adapter.quote_table_name(options[:table])}
|
509
|
+
#{referencing_clause}
|
475
510
|
FOR EACH #{options[:for_each]}#{prepared_where && db_version >= 90000 ? " WHEN (" + prepared_where + ')': ''} EXECUTE PROCEDURE #{trigger_action};
|
476
511
|
SQL
|
477
512
|
end
|
@@ -497,7 +532,7 @@ BEGIN
|
|
497
532
|
|
498
533
|
def db_version
|
499
534
|
@db_version ||= case adapter_name
|
500
|
-
when
|
535
|
+
when *HairTrigger::POSTGRESQL_ADAPTERS
|
501
536
|
adapter.send(:postgresql_version)
|
502
537
|
end
|
503
538
|
end
|
@@ -74,7 +74,7 @@ module HairTrigger
|
|
74
74
|
def normalize_trigger(name, definition, type)
|
75
75
|
@adapter_name = @connection.adapter_name.downcase.to_sym
|
76
76
|
|
77
|
-
return definition unless @adapter_name
|
77
|
+
return definition unless HairTrigger::POSTGRESQL_ADAPTERS.include?(@adapter_name)
|
78
78
|
# because postgres does not preserve the original CREATE TRIGGER/
|
79
79
|
# FUNCTION statements, its decompiled reconstruction will not match
|
80
80
|
# ours. we work around it by creating our generated trigger/function,
|
data/lib/hair_trigger/version.rb
CHANGED
data/lib/hair_trigger.rb
CHANGED
@@ -8,6 +8,9 @@ require 'hair_trigger/schema_dumper'
|
|
8
8
|
require 'hair_trigger/railtie' if defined?(Rails::Railtie)
|
9
9
|
|
10
10
|
module HairTrigger
|
11
|
+
POSTGRESQL_ADAPTERS = %i[postgresql postgis]
|
12
|
+
MYSQL_ADAPTERS = %i[mysql mysql2rgeo trilogy]
|
13
|
+
SQLITE_ADAPTERS = %i[sqlite litedb]
|
11
14
|
|
12
15
|
autoload :Builder, 'hair_trigger/builder'
|
13
16
|
autoload :MigrationReader, 'hair_trigger/migration_reader'
|
@@ -39,7 +42,12 @@ module HairTrigger
|
|
39
42
|
end
|
40
43
|
|
41
44
|
def migrator
|
42
|
-
if
|
45
|
+
if Gem::Version.new("7.2.0") <= ActiveRecord.gem_version
|
46
|
+
connection = ActiveRecord::Tasks::DatabaseTasks.migration_connection_pool
|
47
|
+
schema_migration = connection.schema_migration
|
48
|
+
migrations = ActiveRecord::MigrationContext.new(migration_path, schema_migration).migrations
|
49
|
+
ActiveRecord::Migrator.new(:up, migrations, schema_migration, ActiveRecord::InternalMetadata.new(connection))
|
50
|
+
elsif Gem::Version.new("7.1.0") <= ActiveRecord.gem_version
|
43
51
|
connection = ActiveRecord::Tasks::DatabaseTasks.migration_connection
|
44
52
|
schema_migration = connection.schema_migration
|
45
53
|
migrations = ActiveRecord::MigrationContext.new(migration_path, schema_migration).migrations
|
data/lib/tasks/hair_trigger.rake
CHANGED
@@ -11,7 +11,8 @@ namespace :db do
|
|
11
11
|
namespace :schema do
|
12
12
|
desc "Create a db/schema.rb file that can be portably used against any DB supported by AR"
|
13
13
|
task :dump => :environment do
|
14
|
-
|
14
|
+
format = ActiveRecord.respond_to?(:schema_format) ? ActiveRecord.schema_format : ActiveRecord::Base.schema_format
|
15
|
+
next unless format == :ruby
|
15
16
|
|
16
17
|
require 'active_record/schema_dumper'
|
17
18
|
|
@@ -43,7 +44,7 @@ namespace :db do
|
|
43
44
|
|
44
45
|
# code adopted from activerecord/lib/active_record/tasks/database_tasks.rb#L441
|
45
46
|
def dump_filename(db_config_name)
|
46
|
-
format = ActiveRecord::Base.schema_format
|
47
|
+
format = ActiveRecord.respond_to?(:schema_format) ? ActiveRecord.schema_format : ActiveRecord::Base.schema_format
|
47
48
|
filename = if ActiveRecord::Base.configurations.primary?(db_config_name)
|
48
49
|
schema_file_type(format)
|
49
50
|
else
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: hairtrigger
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jon Jensen
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-
|
11
|
+
date: 2024-09-04 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|