hairtrigger 0.1.2 → 0.1.3

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