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 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