hairtrigger 1.1.0 → 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|