hairtrigger 0.1.3 → 0.1.4
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.
- data/README.rdoc +6 -5
- data/VERSION +1 -1
- data/lib/hair_trigger/base.rb +1 -0
- data/lib/hair_trigger/builder.rb +44 -7
- data/lib/hair_trigger/migration.rb +5 -4
- data/lib/tasks/hair_trigger.rake +2 -2
- data/spec/builder_spec.rb +19 -0
- metadata +4 -4
data/README.rdoc
CHANGED
@@ -62,7 +62,7 @@ declarations and any auto-generated triggers from previous migrations.
|
|
62
62
|
You can also manage triggers manually in your migrations via create_trigger and
|
63
63
|
drop_trigger. They are a little more verbose than model triggers, and they can
|
64
64
|
be more work since you need to figure out the up/down create/drop logic when
|
65
|
-
you change things.
|
65
|
+
you change things. A sample trigger:
|
66
66
|
|
67
67
|
create_trigger.on(:users).after(:insert) do
|
68
68
|
"UPDATE accounts SET user_count = user_count + 1 WHERE id = NEW.account_id;"
|
@@ -154,15 +154,16 @@ when you attempt to run the migration.
|
|
154
154
|
|
155
155
|
Generation warnings are similar but they don't stop the trigger from being
|
156
156
|
generated. If you do something adapter-specific supported by your database,
|
157
|
-
you will still get a warning that your trigger is not portable. You
|
158
|
-
silence warnings via "HairTrigger::Builder.show_warnings = false"
|
157
|
+
you will still get a warning ($stderr) that your trigger is not portable. You
|
158
|
+
can silence warnings via "HairTrigger::Builder.show_warnings = false"
|
159
159
|
|
160
160
|
You can validate your triggers beforehand using the Builder#validate! method.
|
161
|
-
It will throw the
|
161
|
+
It will throw the appropriate errors/warnings so that you know what to fix,
|
162
|
+
e.g.
|
162
163
|
|
163
164
|
> User.triggers.each(&:validate!)
|
164
165
|
|
165
|
-
HairTrigger does not validate your SQL, so be sure to test it in all
|
166
|
+
HairTrigger does not validate your SQL, so be sure to test it in all databases
|
166
167
|
you want to support.
|
167
168
|
|
168
169
|
== Gotchas, Known Issues
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.1.
|
1
|
+
0.1.4
|
data/lib/hair_trigger/base.rb
CHANGED
data/lib/hair_trigger/builder.rb
CHANGED
@@ -9,6 +9,7 @@ module HairTrigger
|
|
9
9
|
|
10
10
|
def initialize(name = nil, options = {})
|
11
11
|
@adapter = options[:adapter]
|
12
|
+
@compatibility = options.delete(:compatibility) || self.class.compatibility
|
12
13
|
@options = {}
|
13
14
|
@chained_calls = []
|
14
15
|
@errors = []
|
@@ -194,7 +195,7 @@ module HairTrigger
|
|
194
195
|
str << actions_to_ruby("#{indent} ") + "\n"
|
195
196
|
str << "#{indent}end"
|
196
197
|
else
|
197
|
-
str = "#{indent}create_trigger(#{prepared_name.inspect}, :generated => true).\n" +
|
198
|
+
str = "#{indent}create_trigger(#{prepared_name.inspect}, :generated => true, :compatibility => #{@compatibility}).\n" +
|
198
199
|
"#{indent} " + chained_calls_to_ruby(".\n#{indent} ")
|
199
200
|
if @triggers
|
200
201
|
str << " do |t|\n"
|
@@ -220,7 +221,7 @@ module HairTrigger
|
|
220
221
|
|
221
222
|
def hash
|
222
223
|
prepare!
|
223
|
-
[self.options.hash, self.prepared_actions.hash, self.prepared_where.hash, self.triggers.hash].hash
|
224
|
+
[self.options.hash, self.prepared_actions.hash, self.prepared_where.hash, self.triggers.hash, @compatibility].hash
|
224
225
|
end
|
225
226
|
|
226
227
|
def errors
|
@@ -261,6 +262,7 @@ module HairTrigger
|
|
261
262
|
raise DeclarationError, "trigger group did not define any triggers" if @triggers.empty?
|
262
263
|
else
|
263
264
|
@actions = block.call
|
265
|
+
@actions.sub!(/(\s*)\z/, ';\1') if @actions && !(@actions =~ /;\s*\z/)
|
264
266
|
end
|
265
267
|
# only the top-most block actually executes
|
266
268
|
Array(generate).each{ |action| adapter.execute(action)} if options[:execute] && !@trigger_group
|
@@ -307,7 +309,7 @@ END;
|
|
307
309
|
end
|
308
310
|
|
309
311
|
def generate_trigger_postgresql
|
310
|
-
raise GenerationError, "truncate triggers are only supported on postgres 8.4 and greater" if
|
312
|
+
raise GenerationError, "truncate triggers are only supported on postgres 8.4 and greater" if db_version < 80400 && options[:events].include?('TRUNCATE')
|
311
313
|
raise GenerationError, "FOR EACH ROW triggers may not be triggered by truncate events" if options[:for_each] == 'ROW' && options[:events].include?('TRUNCATE')
|
312
314
|
security = options[:security] if options[:security] && options[:security] != :invoker
|
313
315
|
sql = <<-SQL
|
@@ -315,18 +317,28 @@ CREATE FUNCTION #{prepared_name}()
|
|
315
317
|
RETURNS TRIGGER AS $$
|
316
318
|
BEGIN
|
317
319
|
SQL
|
318
|
-
if prepared_where &&
|
320
|
+
if prepared_where && db_version < 90000
|
319
321
|
sql << normalize("IF #{prepared_where} THEN", 1)
|
320
322
|
sql << normalize(prepared_actions, 2)
|
321
323
|
sql << normalize("END IF;", 1)
|
322
324
|
else
|
323
325
|
sql << normalize(prepared_actions, 1)
|
324
326
|
end
|
327
|
+
# if no return is specified at the end, be sure we set a sane one
|
328
|
+
unless prepared_actions =~ /return [^;]+;\s*\z/i
|
329
|
+
if options[:timing] == "AFTER" || options[:for_each] == 'STATEMENT'
|
330
|
+
sql << normalize("RETURN NULL;", 1)
|
331
|
+
elsif options[:events].include?('DELETE')
|
332
|
+
sql << normalize("RETURN OLD;", 1)
|
333
|
+
else
|
334
|
+
sql << normalize("RETURN NEW;", 1)
|
335
|
+
end
|
336
|
+
end
|
325
337
|
sql << <<-SQL
|
326
338
|
END;
|
327
339
|
$$ LANGUAGE plpgsql#{security ? " SECURITY #{security.to_s.upcase}" : ""};
|
328
340
|
CREATE TRIGGER #{prepared_name} #{options[:timing]} #{options[:events].join(" OR ")} ON #{options[:table]}
|
329
|
-
FOR EACH #{options[:for_each]}#{prepared_where &&
|
341
|
+
FOR EACH #{options[:for_each]}#{prepared_where && db_version >= 90000 ? " WHEN (" + prepared_where + ')': ''} EXECUTE PROCEDURE #{prepared_name}();
|
330
342
|
SQL
|
331
343
|
end
|
332
344
|
|
@@ -349,8 +361,8 @@ BEGIN
|
|
349
361
|
sql << "END\n";
|
350
362
|
end
|
351
363
|
|
352
|
-
def
|
353
|
-
@
|
364
|
+
def db_version
|
365
|
+
@db_version ||= case adapter_name
|
354
366
|
when :postgresql
|
355
367
|
adapter.send(:postgresql_version)
|
356
368
|
end
|
@@ -375,13 +387,38 @@ BEGIN
|
|
375
387
|
class << self
|
376
388
|
attr_writer :tab_spacing
|
377
389
|
attr_writer :show_warnings
|
390
|
+
|
378
391
|
def tab_spacing
|
379
392
|
@tab_spacing ||= 4
|
380
393
|
end
|
394
|
+
|
381
395
|
def show_warnings
|
382
396
|
@show_warnings = true if @show_warnings.nil?
|
383
397
|
@show_warnings
|
384
398
|
end
|
399
|
+
|
400
|
+
def compatibility
|
401
|
+
@compatibility ||= begin
|
402
|
+
gem_version = (File.read(File.dirname(__FILE__) + '/../../VERSION').chomp rescue '0.1.3').split(/\./).map(&:to_i)
|
403
|
+
gem_version.instance_eval(<<-METHODS)
|
404
|
+
def <=>(other)
|
405
|
+
[size, other.size].max.times do |i|
|
406
|
+
c = self[i].to_i <=> other[i].to_i
|
407
|
+
return c unless c == 0
|
408
|
+
end
|
409
|
+
0
|
410
|
+
end
|
411
|
+
extend Comparable
|
412
|
+
METHODS
|
413
|
+
if gem_version <= [0, 1, 3]
|
414
|
+
0 # initial releases
|
415
|
+
else
|
416
|
+
1 # postgres RETURN bugfix
|
417
|
+
# TODO: add more as we implement things that change the generated
|
418
|
+
# triggers (e.g. chained call merging)
|
419
|
+
end
|
420
|
+
end
|
421
|
+
end
|
385
422
|
end
|
386
423
|
end
|
387
424
|
end
|
@@ -4,13 +4,14 @@ module HairTrigger
|
|
4
4
|
|
5
5
|
def method_missing_with_trigger_building(method, *arguments, &block)
|
6
6
|
if extract_trigger_builders
|
7
|
-
if method.to_sym == :create_trigger
|
7
|
+
if method.to_sym == :create_trigger && arguments[1].delete(:generated)
|
8
8
|
arguments.unshift(nil) if arguments.first.is_a?(Hash)
|
9
|
-
|
9
|
+
arguments[1][:compatibility] ||= 0
|
10
|
+
trigger = ::HairTrigger::Builder.new(*arguments)
|
10
11
|
(@trigger_builders ||= []) << trigger
|
11
12
|
trigger
|
12
|
-
elsif method.to_sym == :drop_trigger
|
13
|
-
trigger = ::HairTrigger::Builder.new(arguments[0], {:table => arguments[1], :drop => true})
|
13
|
+
elsif method.to_sym == :drop_trigger && arguments[2] && arguments[2].delete(:generated)
|
14
|
+
trigger = ::HairTrigger::Builder.new(arguments[0], {:table => arguments[1], :drop => true})
|
14
15
|
(@trigger_builders ||= []) << trigger
|
15
16
|
trigger
|
16
17
|
end
|
data/lib/tasks/hair_trigger.rake
CHANGED
@@ -61,11 +61,11 @@ namespace :db do
|
|
61
61
|
|
62
62
|
class #{migration_name} < ActiveRecord::Migration
|
63
63
|
def self.up
|
64
|
-
#{(up_drop_triggers + up_create_triggers).map{ |t| t.to_ruby(' ') }.join("\n").lstrip}
|
64
|
+
#{(up_drop_triggers + up_create_triggers).map{ |t| t.to_ruby(' ') }.join("\n\n").lstrip}
|
65
65
|
end
|
66
66
|
|
67
67
|
def self.down
|
68
|
-
#{(down_drop_triggers + down_create_triggers).map{ |t| t.to_ruby(' ') }.join("\n").lstrip}
|
68
|
+
#{(down_drop_triggers + down_create_triggers).map{ |t| t.to_ruby(' ') }.join("\n\n").lstrip}
|
69
69
|
end
|
70
70
|
end
|
71
71
|
MIGRATION
|
data/spec/builder_spec.rb
CHANGED
@@ -25,6 +25,20 @@ describe "builder" do
|
|
25
25
|
end
|
26
26
|
end
|
27
27
|
|
28
|
+
context "comparison" do
|
29
|
+
it "should view identical triggers as identical" do
|
30
|
+
@adapter = MockAdapter.new("mysql")
|
31
|
+
builder.on(:foos).after(:update){ "FOO" }.
|
32
|
+
should eql(builder.on(:foos).after(:update){ "FOO" })
|
33
|
+
end
|
34
|
+
|
35
|
+
it "should view incompatible triggers as different" do
|
36
|
+
@adapter = MockAdapter.new("mysql")
|
37
|
+
HairTrigger::Builder.new(nil, :adapter => @adapter, :compatibility => 0).on(:foos).after(:update){ "FOO" }.
|
38
|
+
should_not eql(builder.on(:foos).after(:update){ "FOO" })
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
28
42
|
context "mysql" do
|
29
43
|
before(:each) do
|
30
44
|
@adapter = MockAdapter.new("mysql")
|
@@ -137,6 +151,11 @@ describe "builder" do
|
|
137
151
|
}.should raise_error
|
138
152
|
end
|
139
153
|
|
154
|
+
it "should add a return statement if none is provided" do
|
155
|
+
builder.on(:foos).after(:update){ "FOO" }.generate.
|
156
|
+
grep(/RETURN NULL;/).size.should eql(1)
|
157
|
+
end
|
158
|
+
|
140
159
|
context "legacy" do
|
141
160
|
it "should reject truncate pre-8.4" do
|
142
161
|
@adapter = MockAdapter.new("postgresql", :postgresql_version => 80300)
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: hairtrigger
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 19
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 0
|
8
8
|
- 1
|
9
|
-
-
|
10
|
-
version: 0.1.
|
9
|
+
- 4
|
10
|
+
version: 0.1.4
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Jon Jensen
|
@@ -15,7 +15,7 @@ autorequire:
|
|
15
15
|
bindir: bin
|
16
16
|
cert_chain: []
|
17
17
|
|
18
|
-
date: 2011-03-
|
18
|
+
date: 2011-03-25 00:00:00 -06:00
|
19
19
|
default_executable:
|
20
20
|
dependencies:
|
21
21
|
- !ruby/object:Gem::Dependency
|