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