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 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. An sample trigger:
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 can
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 approriate errors/warnings so that you know what to fix, e.g.
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 database
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.3
1
+ 0.1.4
@@ -7,6 +7,7 @@ module HairTrigger
7
7
  options = name
8
8
  name = nil
9
9
  end
10
+ options[:compatibility] ||= ::HairTrigger::Builder::compatibility
10
11
  @triggers ||= []
11
12
  trigger = ::HairTrigger::Builder.new(name, options)
12
13
  @triggers << trigger
@@ -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 version < 80400 && options[:events].include?('TRUNCATE')
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 && version < 90000
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 && version >= 90000 ? " WHEN (" + prepared_where + ')': ''} EXECUTE PROCEDURE #{prepared_name}();
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 version
353
- @version ||= case adapter_name
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
- trigger = ::HairTrigger::Builder.new(*arguments) if arguments[1].delete(:generated)
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}) if arguments[2] && arguments[2].delete(:generated)
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
@@ -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: 29
4
+ hash: 19
5
5
  prerelease:
6
6
  segments:
7
7
  - 0
8
8
  - 1
9
- - 3
10
- version: 0.1.3
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-24 00:00:00 -06:00
18
+ date: 2011-03-25 00:00:00 -06:00
19
19
  default_executable:
20
20
  dependencies:
21
21
  - !ruby/object:Gem::Dependency