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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: adffb212907d4617f9ed63b3bbed32f0f4c8e7113e50284eb560a2e9ac0f24cb
4
- data.tar.gz: a437870c199e11d4264e496ac45ff6d6f61593e80b9082bc8bf320de33bd0b84
3
+ metadata.gz: 89b859bf3d396f6ba9b213d9be337e111b39cc1a87a5ee73918d553ab03a16b7
4
+ data.tar.gz: f6cab257e8b963d9f8b740a77b5a336a42403db622df72a57b51254643042117
5
5
  SHA512:
6
- metadata.gz: cf089ae92b9427a39243f6c096c44eab7ea4d41e7e6bec757781ba2d9b5ebef09683b2528e8ce39c2c28b15919149b82461bd2edcef4d662161a28a52c24671f
7
- data.tar.gz: 83c64e0d188cfc45a6ffb701e6f5ee56350d4b636eaaa106405d545731563cc3751e792eecd6eefc1f4d534fdfd31369099e8a9875cec63082ce91d6d753118a
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 2.3.0+
366
- * Rails 5.0+
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-2022 Jon Jensen. See LICENSE.txt for further details.
390
+ Copyright (c) 2011-2024 Jon Jensen. See LICENSE.txt for further details.
@@ -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 :sqlite
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 :mysql, :trilogy
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 :postgresql, :postgis
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 (
@@ -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", :postgresql] if name.to_s.size > 63
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", :sqlite, :mysql] if for_each == :statement
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", :sqlite]
99
- @errors << ["postgresql doesn't support arbitrary users for trigger security", :postgresql] unless [:definer, :invoker].include?(user)
100
- @errors << ["mysql doesn't support invoker trigger security", :mysql] if user == :invoker
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", :mysql, :sqlite] if events.size > 1
114
- @errors << ["sqlite and mysql do not support truncate triggers", :mysql, :sqlite] if events.include?(:truncate)
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", :mysql] unless [:name, :where, :all, :of].include?(:#{method})
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 == :mysql || adapter_name == :trilogy
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 :sqlite
237
+ when *HairTrigger::SQLITE_ADAPTERS
226
238
  generate_trigger_sqlite
227
- when :mysql, :trilogy
239
+ when *HairTrigger::MYSQL_ADAPTERS
228
240
  generate_trigger_mysql
229
- when :postgresql, :postgis
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", :mysql] unless options[:timing] && options[:events]
333
- @errors << ["nested trigger groups are not supported for mysql", :mysql] if @trigger_group
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", :mysql]
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", :postgresql, :sqlite]
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 :sqlite
415
+ when *HairTrigger::SQLITE_ADAPTERS
400
416
  true
401
- when :postgresql, :postgis
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 :sqlite, :mysql, :trilogy
443
+ when *HairTrigger::SQLITE_ADAPTERS, *HairTrigger::MYSQL_ADAPTERS
411
444
  "DROP TRIGGER IF EXISTS #{prepared_name};\n"
412
- when :postgresql, :postgis
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 :postgresql, :postgis
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 == :postgresql || @adapter_name == :postgis
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,
@@ -1,5 +1,5 @@
1
1
  module HairTrigger
2
- VERSION = "1.1.0"
2
+ VERSION = "1.2.0"
3
3
 
4
4
  def VERSION.<=>(other)
5
5
  split(/\./).map(&:to_i) <=> other.split(/\./).map(&:to_i)
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 ActiveRecord::VERSION::STRING >= "7.1."
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
@@ -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
- next unless ActiveRecord::Base.schema_format == :ruby
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.1.0
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-01-02 00:00:00.000000000 Z
11
+ date: 2024-09-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord