hairtrigger 0.1.2 → 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile CHANGED
@@ -1,7 +1,7 @@
1
1
  source "http://rubygems.org"
2
2
  gem "activerecord", ">=2.3.0", "<3.0"
3
3
  group :development do
4
- gem "rspec", ">= 2.3.0"
4
+ gem "rspec", "~> 2.3.0"
5
5
  gem "bundler", "~> 1.0.0"
6
6
  gem "jeweler", "~> 1.5.2"
7
7
  gem "rcov", ">= 0"
data/README.rdoc CHANGED
@@ -1,23 +1,32 @@
1
1
  = HairTrigger
2
2
 
3
3
  HairTrigger lets you create and manage database triggers in a concise,
4
- db-agnostic, Rails-y way.
4
+ db-agnostic, Rails-y way. You declare triggers right in your models in Ruby,
5
+ and a simple rake task does all the dirty work for you.
5
6
 
6
7
  == Install
7
8
 
8
- Assuming you're using bundler:
9
+ === Step 1.
9
10
 
10
- 1. install the gem
11
- 2. let rails know about it (via Gemfile, environment.rb, whatever)
12
- 3. create lib/tasks/hair_trigger.rake with the following:
11
+ If you are using bundler, just put hairtrigger in your Gemfile.
12
+
13
+ If you're not using bundler, you can "gem install hairtrigger" and then put
14
+ hairtrigger in environment.rb
15
+
16
+ === Step 2.
17
+
18
+ If you plan to use model triggers (which you should ;) you'll need to create
19
+ lib/tasks/hair_trigger.rake with the following:
13
20
 
14
21
  $VERBOSE = nil
15
22
  Dir["#{Gem.searcher.find('hair_trigger').full_gem_path}/lib/tasks/*.rake"].each { |ext| load ext }
16
23
 
17
- An alternative to steps 2 and 3 is to unpack it in vendor/plugins and delete
18
- its Gemfile.
24
+ If you are unpacking the gem in vendor/plugins, this step is not needed
25
+ (though you'll then want to delete its Gemfile to avoid possible conflicts).
19
26
 
20
27
  == Usage
28
+
29
+ === Models
21
30
 
22
31
  Declare triggers in your models and use a rake task to auto-generate the
23
32
  appropriate migration. For example:
@@ -33,34 +42,69 @@ and then:
33
42
  rake db:generate_trigger_migration
34
43
 
35
44
  This will create a db-agnostic migration for the trigger that mirrors the
36
- model declaration. You can also manage triggers manually in your migrations
37
- (via create_trigger/drop_trigger), though that ends up being more work.
38
-
39
- Note that these auto-generated create_trigger statements contain the
40
- ":generated => true" option, indicating that they were created from the
41
- model definition. This is important, as the rake task will also generate
42
- appropriate drop/create statements for any model triggers that get removed
43
- or updated. It does this by diffing the current model trigger declarations
44
- and any auto-generated triggers from previous migrations.
45
+ model declaration. The end result in MySQL will be something like this:
46
+
47
+ CREATE TRIGGER account_users_after_insert_row_tr AFTER INSERT ON account_users
48
+ FOR EACH ROW
49
+ BEGIN
50
+ UPDATE accounts SET user_count = user_count + 1 WHERE id = NEW.account_id;
51
+ END
52
+
53
+ Note that these auto-generated create_trigger statements in the migration
54
+ contain the ":generated => true" option, indicating that they were created
55
+ from the model definition. This is important, as the rake task will also
56
+ generate appropriate drop/create statements for any model triggers that get
57
+ removed or updated. It does this by diffing the current model trigger
58
+ declarations and any auto-generated triggers from previous migrations.
59
+
60
+ === Manual Migrations
61
+
62
+ You can also manage triggers manually in your migrations via create_trigger and
63
+ drop_trigger. They are a little more verbose than model triggers, and they can
64
+ be more work since you need to figure out the up/down create/drop logic when
65
+ you change things. An sample trigger:
66
+
67
+ create_trigger.on(:users).after(:insert) do
68
+ "UPDATE accounts SET user_count = user_count + 1 WHERE id = NEW.account_id;"
69
+ end
45
70
 
46
- == Chainable Methods
71
+ === Chainable Methods
47
72
 
48
73
  Triggers are built by chaining several methods together, ending in a block
49
74
  that specifies the SQL to be run when the trigger fires. Supported methods
50
75
  include:
51
76
 
52
- * name(trigger_name): Optional, inferred from other calls.
53
- * on(table_name): Ignored in models, required in migrations.
54
- * for_each(item): Defaults to :row, PostgreSQL/MySQL allow :statement.
55
- * before(*events): Shorthand for timing(:before).events(*events).
56
- * after(*events): Shorthand for timing(:after).events(*events).
57
- * where(conditions): Optional, limits when the trigger will fire.
58
- * security(user): Permissions/role to check when calling trigger. PostgreSQL supports :invoker (default) and :definer, MySQL supports :definer (default) and arbitrary users (syntax: 'user'@'host').
59
- * timing(timing): Required (but may be satisified by before/after). Possible values are :before/:after.
60
- * events(*events): Required (but may be satisified by before/after). MySQL/SQLite only support one action.
61
- * all: Noop, useful for trigger groups (see below).
77
+ ==== name(trigger_name)
78
+ Optional, inferred from other calls.
79
+
80
+ ==== on(table_name)
81
+ Ignored in models, required in migrations.
82
+
83
+ ==== for_each(item)
84
+ Defaults to :row, PostgreSQL/MySQL allow :statement.
85
+
86
+ ==== before(*events)
87
+ Shorthand for timing(:before).events(*events).
88
+
89
+ ==== after(*events)
90
+ Shorthand for timing(:after).events(*events).
91
+
92
+ ==== where(conditions)
93
+ Optional, SQL snippet limiting when the trigger will fire. Supports delayed interpolation of variables.
62
94
 
63
- == Trigger Groups
95
+ ==== security(user)
96
+ Permissions/role to check when calling trigger. PostgreSQL supports :invoker (default) and :definer, MySQL supports :definer (default) and arbitrary users (syntax: 'user'@'host').
97
+
98
+ ==== timing(timing)
99
+ Required (but may be satisified by before/after). Possible values are :before/:after.
100
+
101
+ ==== events(*events)
102
+ Required (but may be satisified by before/after). Possible values are :insert/:update/:delete/:truncate. MySQL/SQLite only support one action per trigger, and don't support :truncate.
103
+
104
+ ==== all
105
+ Noop, useful for trigger groups (see below).
106
+
107
+ === Trigger Groups
64
108
 
65
109
  Trigger groups allow you to use a slightly more concise notation if you have
66
110
  several triggers that fire on a given model. This is also important for MySQL,
@@ -96,10 +140,27 @@ create.
96
140
 
97
141
  == Warnings and Errors
98
142
 
99
- If you try something your adapter doesn't support (e.g. multiple triggering
100
- events for MySQL), you will get an error. If your adapter does support it, you
101
- will just get a warning to let you know your trigger is not portable. You can
102
- silence warnings via `HairTrigger::Builder.show_warnings = false`
143
+ There are a couple classes of errors: declaration errors and generation
144
+ errors/warnings.
145
+
146
+ Declaration errors happen if your trigger declaration is obviously wrong, and
147
+ will cause a runtime error in your model or migration class. An example would
148
+ be "trigger.after(:never)", since :never is not a valid event.
149
+
150
+ Generation errors happen if you try something that your adapter doesn't
151
+ support. An example would be something like "trigger.security(:invoker)" for
152
+ MySQL. These errors only happen when the trigger is actually generated, e.g.
153
+ when you attempt to run the migration.
154
+
155
+ Generation warnings are similar but they don't stop the trigger from being
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"
159
+
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.
162
+
163
+ > User.triggers.each(&:validate!)
103
164
 
104
165
  HairTrigger does not validate your SQL, so be sure to test it in all database
105
166
  you want to support.
@@ -125,10 +186,17 @@ you want to support.
125
186
  == Compatibility
126
187
 
127
188
  * Rails 2.3.x
128
- * Postgres 9.0+
189
+ * Postgres 8.0+
129
190
  * MySQL 5.0+
130
191
  * SQLite 3.0+
131
192
 
193
+ == Version History
194
+
195
+ * 0.1.3 Better error handling, Postgres 8.x support, updated docs
196
+ * 0.1.2 Fixed Builder#security, updated docs
197
+ * 0.1.1 Fixed bug in HairTrigger.migrations_current?, fixed up Gemfile
198
+ * 0.1.0 Initial release
199
+
132
200
  == Copyright
133
201
 
134
202
  Copyright (c) 2011 Jon Jensen. See LICENSE.txt for further details.
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.2
1
+ 0.1.3
@@ -1,13 +1,17 @@
1
1
  module HairTrigger
2
2
  class Builder
3
+ class DeclarationError < StandardError; end
4
+ class GenerationError < StandardError; end
5
+
3
6
  attr_accessor :options
4
7
  attr_reader :triggers # nil unless this is a trigger group
5
8
  attr_reader :prepared_actions, :prepared_where # after delayed interpolation
6
9
 
7
10
  def initialize(name = nil, options = {})
8
- @adapter = options[:adapter] || ActiveRecord::Base.connection rescue nil
11
+ @adapter = options[:adapter]
9
12
  @options = {}
10
13
  @chained_calls = []
14
+ @errors = []
11
15
  set_name(name) if name
12
16
  {:timing => :after, :for_each => :row}.update(options).each do |key, value|
13
17
  if respond_to?("set_#{key}")
@@ -22,6 +26,7 @@ module HairTrigger
22
26
  @trigger_group = other
23
27
  @triggers = nil
24
28
  @chained_calls = []
29
+ @errors = []
25
30
  @options = @options.dup
26
31
  @options.delete(:name) # this will be inferred (or set further down the line)
27
32
  @options.each do |key, value|
@@ -34,18 +39,18 @@ module HairTrigger
34
39
  end
35
40
 
36
41
  def name(name)
37
- raise_or_warn "trigger name cannot exceed 63 for postgres", :postgresql if name.to_s.size > 63
42
+ @errors << ["trigger name cannot exceed 63 for postgres", :postgresql] if name.to_s.size > 63
38
43
  options[:name] = name.to_s
39
44
  end
40
45
 
41
46
  def on(table)
42
- raise "table has already been specified" if options[:table]
47
+ raise DeclarationError, "table has already been specified" if options[:table]
43
48
  options[:table] = table.to_s
44
49
  end
45
50
 
46
51
  def for_each(for_each)
47
- raise_or_warn "sqlite doesn't support FOR EACH STATEMENT triggers", :sqlite if for_each == :statement
48
- raise "invalid for_each" unless [:row, :statement].include?(for_each)
52
+ @errors << ["sqlite doesn't support FOR EACH STATEMENT triggers", :sqlite] if for_each == :statement
53
+ raise DeclarationError, "invalid for_each" unless [:row, :statement].include?(for_each)
49
54
  options[:for_each] = for_each.to_s.upcase
50
55
  end
51
56
 
@@ -68,27 +73,27 @@ module HairTrigger
68
73
  end
69
74
 
70
75
  def security(user)
71
- # sqlite default is n/a, mysql default is :definer, postgres default is :invoker
72
- raise_or_warn "sqlite doesn't support trigger security", :sqlite
73
- raise_or_warn "postgresql doesn't support arbitrary users for trigger security", :postgresql unless [:definer, :invoker].include?(user)
74
- if user == :invoker
75
- raise_or_warn "mysql doesn't support invoker trigger security", :mysql
76
- elsif user != :definer && !(user.to_s =~ /\A'[^']+'@'[^']+'\z/) && !(user.to_s.downcase =~ /\Acurrent_user(\(\))?\z/)
77
- raise_or_warn "mysql trigger security should be :definer, CURRENT_USER, or a valid mysql user (e.g. 'user'@'host')", :mysql
76
+ unless [:invoker, :definer].include?(user) || user.to_s =~ /\A'[^']+'@'[^']+'\z/ || user.to_s.downcase =~ /\Acurrent_user(\(\))?\z/
77
+ raise DeclarationError, "trigger security should be :invoker, :definer, CURRENT_USER, or a valid user (e.g. 'user'@'host')"
78
78
  end
79
+ # sqlite default is n/a, mysql default is :definer, postgres default is :invoker
80
+ @errors << ["sqlite doesn't support trigger security", :sqlite]
81
+ @errors << ["postgresql doesn't support arbitrary users for trigger security", :postgresql] unless [:definer, :invoker].include?(user)
82
+ @errors << ["mysql doesn't support invoker trigger security", :mysql] if user == :invoker
79
83
  options[:security] = user
80
84
  end
81
85
 
82
86
  def timing(timing)
83
- raise "invalid timing" unless [:before, :after].include?(timing)
87
+ raise DeclarationError, "invalid timing" unless [:before, :after].include?(timing)
84
88
  options[:timing] = timing.to_s.upcase
85
89
  end
86
90
 
87
91
  def events(*events)
88
92
  events << :insert if events.delete(:create)
89
93
  events << :delete if events.delete(:destroy)
90
- raise "invalid events" unless events & [:insert, :update, :delete] == events
91
- raise_or_warn "sqlite and mysql triggers may not be shared by multiple actions", :mysql, :sqlite if events.size > 1
94
+ raise DeclarationError, "invalid events" unless events & [:insert, :update, :delete, :truncate] == events
95
+ @errors << ["sqlite and mysql triggers may not be shared by multiple actions", :mysql, :sqlite] if events.size > 1
96
+ @errors << ["sqlite and mysql do not support truncate triggers", :mysql, :sqlite] if events.include?(:truncate)
92
97
  options[:events] = events.map{ |e| e.to_s.upcase }
93
98
  end
94
99
 
@@ -107,7 +112,7 @@ module HairTrigger
107
112
  def #{method}(*args)
108
113
  @chained_calls << :#{method}
109
114
  if @triggers || @trigger_group
110
- raise_or_warn "mysql doesn't support #{method} within a trigger group", :mysql unless [:name, :where, :all].include?(:#{method})
115
+ @errors << ["mysql doesn't support #{method} within a trigger group", :mysql] unless [:name, :where, :all].include?(:#{method})
111
116
  end
112
117
  set_#{method}(*args, &(block_given? ? Proc.new : nil))
113
118
  end
@@ -138,16 +143,31 @@ module HairTrigger
138
143
  @prepared_actions = interpolate(@actions).rstrip if @actions
139
144
  end
140
145
 
141
- def generate
142
- return @triggers.map(&:generate).flatten if @triggers && !create_grouped_trigger?
146
+ def validate!(direction = :down)
147
+ @errors.each do |(error, *adapters)|
148
+ raise GenerationError, error if adapters.include?(adapter_name)
149
+ $stderr.puts "WARNING: " + message if self.class.show_warnings
150
+ end
151
+ if direction != :up
152
+ @triggers.each{ |t| t.validate!(:down) } if @triggers
153
+ end
154
+ if direction != :down
155
+ @trigger_group.validate!(:up) if @trigger_group
156
+ end
157
+ end
158
+
159
+ def generate(validate = true)
160
+ validate!(@trigger_group ? :both : :down) if validate
161
+
162
+ return @triggers.map{ |t| t.generate(false) }.flatten if @triggers && !create_grouped_trigger?
143
163
  prepare!
144
- raise "need to specify the table" unless options[:table]
164
+ raise GenerationError, "need to specify the table" unless options[:table]
145
165
  if options[:drop]
146
166
  generate_drop_trigger
147
167
  else
148
- raise "no actions specified" if @triggers && create_grouped_trigger? ? @triggers.any?{ |t| t.prepared_actions.nil? } : prepared_actions.nil?
149
- raise "need to specify the event(s) (:insert, :update, :delete)" if !options[:events] || options[:events].empty?
150
- raise "need to specify the timing (:before/:after)" unless options[:timing]
168
+ raise GenerationError, "no actions specified" if @triggers && create_grouped_trigger? ? @triggers.any?{ |t| t.prepared_actions.nil? } : prepared_actions.nil?
169
+ raise GenerationError, "need to specify the event(s) (:insert, :update, :delete)" if !options[:events] || options[:events].empty?
170
+ raise GenerationError, "need to specify the timing (:before/:after)" unless options[:timing]
151
171
 
152
172
  ret = [generate_drop_trigger]
153
173
  ret << case adapter_name
@@ -158,7 +178,7 @@ module HairTrigger
158
178
  when :postgresql
159
179
  generate_trigger_postgresql
160
180
  else
161
- raise "don't know how to build #{adapter_name} triggers yet"
181
+ raise GenerationError, "don't know how to build #{adapter_name} triggers yet"
162
182
  end
163
183
  ret
164
184
  end
@@ -203,6 +223,10 @@ module HairTrigger
203
223
  [self.options.hash, self.prepared_actions.hash, self.prepared_where.hash, self.triggers.hash].hash
204
224
  end
205
225
 
226
+ def errors
227
+ (@triggers || []).inject(@errors){ |errors, t| errors + t.errors }
228
+ end
229
+
206
230
  private
207
231
 
208
232
  def chained_calls_to_ruby(join_str = '.')
@@ -230,21 +254,25 @@ module HairTrigger
230
254
 
231
255
  def maybe_execute(&block)
232
256
  if block.arity > 0 # we're creating a trigger group, so set up some stuff and pass the buck
233
- raise_or_warn "trigger group must specify timing and event(s)", :mysql unless options[:timing] && options[:events]
234
- raise_or_warn "nested trigger groups are not supported for mysql", :mysql if create_grouped_trigger? && @trigger_group
257
+ @errors << ["trigger group must specify timing and event(s) for mysql", :mysql] unless options[:timing] && options[:events]
258
+ @errors << ["nested trigger groups are not supported for mysql", :mysql] if @trigger_group
235
259
  @triggers = []
236
260
  block.call(self)
237
- raise "trigger group did not define any triggers" if @triggers.empty?
261
+ raise DeclarationError, "trigger group did not define any triggers" if @triggers.empty?
238
262
  else
239
263
  @actions = block.call
240
264
  end
241
265
  # only the top-most block actually executes
242
- Array(generate).each{ |action| @adapter.execute(action)} if options[:execute] && !@trigger_group
266
+ Array(generate).each{ |action| adapter.execute(action)} if options[:execute] && !@trigger_group
243
267
  self
244
268
  end
245
269
 
246
270
  def adapter_name
247
- @adapter_name ||= @adapter.adapter_name.downcase.to_sym
271
+ @adapter_name ||= adapter.adapter_name.downcase.to_sym
272
+ end
273
+
274
+ def adapter
275
+ @adapter ||= ActiveRecord::Base.connection
248
276
  end
249
277
 
250
278
  def infer_name
@@ -264,7 +292,7 @@ module HairTrigger
264
292
  when :postgresql
265
293
  "DROP TRIGGER IF EXISTS #{prepared_name} ON #{options[:table]};\nDROP FUNCTION IF EXISTS #{prepared_name}();\n"
266
294
  else
267
- raise "don't know how to drop #{adapter_name} triggers yet"
295
+ raise GenerationError, "don't know how to drop #{adapter_name} triggers yet"
268
296
  end
269
297
  end
270
298
 
@@ -279,16 +307,26 @@ END;
279
307
  end
280
308
 
281
309
  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')
311
+ raise GenerationError, "FOR EACH ROW triggers may not be triggered by truncate events" if options[:for_each] == 'ROW' && options[:events].include?('TRUNCATE')
282
312
  security = options[:security] if options[:security] && options[:security] != :invoker
283
- <<-SQL
313
+ sql = <<-SQL
284
314
  CREATE FUNCTION #{prepared_name}()
285
315
  RETURNS TRIGGER AS $$
286
316
  BEGIN
287
- #{normalize(prepared_actions, 1).rstrip}
317
+ SQL
318
+ if prepared_where && version < 90000
319
+ sql << normalize("IF #{prepared_where} THEN", 1)
320
+ sql << normalize(prepared_actions, 2)
321
+ sql << normalize("END IF;", 1)
322
+ else
323
+ sql << normalize(prepared_actions, 1)
324
+ end
325
+ sql << <<-SQL
288
326
  END;
289
327
  $$ LANGUAGE plpgsql#{security ? " SECURITY #{security.to_s.upcase}" : ""};
290
328
  CREATE TRIGGER #{prepared_name} #{options[:timing]} #{options[:events].join(" OR ")} ON #{options[:table]}
291
- FOR EACH #{options[:for_each]}#{prepared_where ? " WHEN (" + prepared_where + ')': ''} EXECUTE PROCEDURE #{prepared_name}();
329
+ FOR EACH #{options[:for_each]}#{prepared_where && version >= 90000 ? " WHEN (" + prepared_where + ')': ''} EXECUTE PROCEDURE #{prepared_name}();
292
330
  SQL
293
331
  end
294
332
 
@@ -311,6 +349,13 @@ BEGIN
311
349
  sql << "END\n";
312
350
  end
313
351
 
352
+ def version
353
+ @version ||= case adapter_name
354
+ when :postgresql
355
+ adapter.send(:postgresql_version)
356
+ end
357
+ end
358
+
314
359
  def interpolate(str)
315
360
  eval("%@#{str.gsub('@', '\@')}@")
316
361
  end
@@ -327,14 +372,6 @@ BEGIN
327
372
  text.rstrip + "\n"
328
373
  end
329
374
 
330
- def raise_or_warn(message, *adapters)
331
- if adapters.include?(adapter_name)
332
- raise message
333
- else
334
- $stderr.puts "WARNING: " + message if self.class.show_warnings
335
- end
336
- end
337
-
338
375
  class << self
339
376
  attr_writer :tab_spacing
340
377
  attr_writer :show_warnings
data/spec/builder_spec.rb CHANGED
@@ -5,8 +5,11 @@ HairTrigger::Builder.show_warnings = false
5
5
 
6
6
  class MockAdapter
7
7
  attr_reader :adapter_name
8
- def initialize(type)
8
+ def initialize(type, methods = {})
9
9
  @adapter_name = type
10
+ methods.each do |key, value|
11
+ instance_eval("def #{key}; #{value.inspect}; end")
12
+ end
10
13
  end
11
14
  end
12
15
 
@@ -42,7 +45,7 @@ describe "builder" do
42
45
  t.where('BAR'){ 'BAR' }
43
46
  t.where('BAZ'){ 'BAZ' }
44
47
  }
45
- }
48
+ }.generate
46
49
  }.should raise_error
47
50
  end
48
51
 
@@ -56,19 +59,27 @@ describe "builder" do
56
59
  end
57
60
 
58
61
  it "should reject :invoker security" do
59
- lambda { builder.on(:foos).after(:update).security(:invoker){ "FOO" } }.
60
- should raise_error
62
+ lambda {
63
+ builder.on(:foos).after(:update).security(:invoker){ "FOO" }.generate
64
+ }.should raise_error
61
65
  end
62
66
 
63
- it "should reject multiple timings" do
64
- lambda { builder.on(:foos).after(:update, :delete){ "FOO" } }.
65
- should raise_error
67
+ it "should reject multiple events" do
68
+ lambda {
69
+ builder.on(:foos).after(:update, :delete){ "FOO" }.generate
70
+ }.should raise_error
71
+ end
72
+
73
+ it "should reject truncate" do
74
+ lambda {
75
+ builder.on(:foos).after(:truncate){ "FOO" }.generate
76
+ }.should raise_error
66
77
  end
67
78
  end
68
79
 
69
80
  context "postgresql" do
70
81
  before(:each) do
71
- @adapter = MockAdapter.new("postgresql")
82
+ @adapter = MockAdapter.new("postgresql", :postgresql_version => 90000)
72
83
  end
73
84
 
74
85
  it "should create multiple triggers for a group" do
@@ -98,18 +109,47 @@ describe "builder" do
98
109
  end
99
110
 
100
111
  it "should reject arbitrary user security" do
101
- lambda { builder.on(:foos).after(:update).security("'user'@'host'"){ "FOO" } }.
102
- should raise_error
112
+ lambda {
113
+ builder.on(:foos).after(:update).security("'user'@'host'"){ "FOO" }.
114
+ generate
115
+ }.should raise_error
103
116
  end
104
117
 
105
- it "should accept multiple timings" do
118
+ it "should accept multiple events" do
106
119
  builder.on(:foos).after(:update, :delete){ "FOO" }.generate.
107
120
  grep(/UPDATE OR DELETE/).size.should eql(1)
108
121
  end
109
122
 
110
123
  it "should reject long names" do
111
- lambda { builder.name('A'*65).on(:foos).after(:update){ "FOO" }}.
112
- should raise_error
124
+ lambda {
125
+ builder.name('A'*65).on(:foos).after(:update){ "FOO" }.generate
126
+ }.should raise_error
127
+ end
128
+
129
+ it "should allow truncate with for_each statement" do
130
+ builder.on(:foos).after(:truncate).for_each(:statement){ "FOO" }.generate.
131
+ grep(/TRUNCATE.*FOR EACH STATEMENT/m).size.should eql(1)
132
+ end
133
+
134
+ it "should reject truncate with for_each row" do
135
+ lambda {
136
+ builder.on(:foos).after(:truncate){ "FOO" }.generate
137
+ }.should raise_error
138
+ end
139
+
140
+ context "legacy" do
141
+ it "should reject truncate pre-8.4" do
142
+ @adapter = MockAdapter.new("postgresql", :postgresql_version => 80300)
143
+ lambda {
144
+ builder.on(:foos).after(:truncate).for_each(:statement){ "FOO" }.generate
145
+ }.should raise_error
146
+ end
147
+
148
+ it "should use conditionals pre-9.0" do
149
+ @adapter = MockAdapter.new("postgresql", :postgresql_version => 80400)
150
+ builder.on(:foos).after(:insert).where("BAR"){ "FOO" }.generate.
151
+ grep(/IF BAR/).size.should eql(1)
152
+ end
113
153
  end
114
154
  end
115
155
 
@@ -138,18 +178,27 @@ describe "builder" do
138
178
  end
139
179
 
140
180
  it "should reject security" do
141
- lambda { builder.on(:foos).after(:update).security(:definer){ "FOO" } }.
142
- should raise_error
181
+ lambda {
182
+ builder.on(:foos).after(:update).security(:definer){ "FOO" }.generate
183
+ }.should raise_error
143
184
  end
144
185
 
145
186
  it "should reject for_each :statement" do
146
- lambda { builder.on(:foos).after(:update).for_each(:statement){ "FOO" } }.
147
- should raise_error
187
+ lambda {
188
+ builder.on(:foos).after(:update).for_each(:statement){ "FOO" }.generate
189
+ }.should raise_error
190
+ end
191
+
192
+ it "should reject multiple events" do
193
+ lambda {
194
+ builder.on(:foos).after(:update, :delete){ "FOO" }.generate
195
+ }.should raise_error
148
196
  end
149
197
 
150
- it "should reject multiple timings" do
151
- lambda { builder.on(:foos).after(:update, :delete){ "FOO" } }.
152
- should raise_error
198
+ it "should reject truncate" do
199
+ lambda {
200
+ builder.on(:foos).after(:truncate){ "FOO" }.generate
201
+ }.should raise_error
153
202
  end
154
203
  end
155
204
  end
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: 31
4
+ hash: 29
5
5
  prerelease:
6
6
  segments:
7
7
  - 0
8
8
  - 1
9
- - 2
10
- version: 0.1.2
9
+ - 3
10
+ version: 0.1.3
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-23 00:00:00 -06:00
18
+ date: 2011-03-24 00:00:00 -06:00
19
19
  default_executable:
20
20
  dependencies:
21
21
  - !ruby/object:Gem::Dependency
@@ -45,7 +45,7 @@ dependencies:
45
45
  requirement: &id002 !ruby/object:Gem::Requirement
46
46
  none: false
47
47
  requirements:
48
- - - ">="
48
+ - - ~>
49
49
  - !ruby/object:Gem::Version
50
50
  hash: 3
51
51
  segments: