hairtrigger 0.1.11 → 0.1.12

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc CHANGED
@@ -152,6 +152,29 @@ For MySQL, this will just create a single trigger with conditional logic
152
152
  distinct triggers. This same notation is also used within trigger migrations.
153
153
  MySQL does not currently support nested trigger groups.
154
154
 
155
+ === Database-specific trigger bodies
156
+
157
+ Although HairTrigger aims to be totally db-agnostic, at times you do need a
158
+ little more control over the body of the trigger. You can tailor it for
159
+ specific databases by returning a hash rather than a string. Make sure to set
160
+ a :default value if you aren't explicitly specifying all of them.
161
+
162
+ For example, MySQL generally performs poorly with subselects in UPDATE
163
+ statements, and it has its own proprietary syntax for multi-table UPDATEs. So
164
+ you might do something like the following:
165
+
166
+ trigger.after(:insert) do
167
+ {:default => <<-DEFAULT_SQL, :mysql => <<-MYSQL}
168
+
169
+ UPDATE users SET item_count = item_count + 1
170
+ WHERE id IN (SELECT user_id FROM buckets WHERE id = NEW.bucket_id)
171
+ DEFAULT_SQL
172
+
173
+ UPDATE users, buckets SET item_count = item_count + 1
174
+ WHERE users.id = user_id AND buckets.id = NEW.bucket_id
175
+ MYSQL
176
+ end
177
+
155
178
  == rake db:schema:dump
156
179
 
157
180
  HairTrigger hooks into rake db:schema:dump (and rake tasks that call it) to
@@ -228,14 +251,17 @@ you want to support.
228
251
 
229
252
  == Compatibility
230
253
 
231
- * Rails 2.3+
254
+ * Rails 2.3 - Rails 3.0.x
232
255
  * Postgres 8.0+
233
256
  * MySQL 5.0.10+
234
257
  * SQLite 3.3.8+
235
258
 
236
259
  == Version History
237
260
 
238
- * 0.1.9 mysql fixes for inferred root@localhost
261
+ * 0.1.12 DB-specific trigger body support, bugfixes
262
+ * 0.1.11 Safer migration loading, some speedups
263
+ * 0.1.10 Sped up migration evaluation
264
+ * 0.1.9 MySQL fixes for inferred root@localhost
239
265
  * 0.1.7 Rails 3 support, fixed a couple manual create_trigger bugs
240
266
  * 0.1.6 rake db:schema:dump support, respect non-timestamped migrations
241
267
  * 0.1.4 Compatibility tracking, fixed Postgres return bug, ensure last action
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.11
1
+ 0.1.12
@@ -5,11 +5,11 @@ module HairTrigger
5
5
  options = name
6
6
  name = nil
7
7
  end
8
- ::HairTrigger::Builder.new(name, options.merge(:execute => true))
8
+ ::HairTrigger::Builder.new(name, options.merge(:execute => true, :adapter => self))
9
9
  end
10
10
 
11
11
  def drop_trigger(name, table, options = {})
12
- ::HairTrigger::Builder.new(name, options.merge(:execute => true, :drop => true, :table => table)){}
12
+ ::HairTrigger::Builder.new(name, options.merge(:execute => true, :drop => true, :table => table, :adapter => self)).all{}
13
13
  end
14
14
 
15
15
  def triggers(options = {})
@@ -98,6 +98,10 @@ module HairTrigger
98
98
  options[:events] = events.map{ |e| e.to_s.upcase }
99
99
  end
100
100
 
101
+ def raw_actions
102
+ @raw_actions ||= prepared_actions.is_a?(Hash) ? prepared_actions[adapter_name] || prepared_actions[:default] : prepared_actions
103
+ end
104
+
101
105
  def prepared_name
102
106
  @prepared_name ||= options[:name] ||= infer_name
103
107
  end
@@ -141,7 +145,11 @@ module HairTrigger
141
145
  def prepare!
142
146
  @triggers.each(&:prepare!) if @triggers
143
147
  @prepared_where = options[:where] = interpolate(options[:where]) if options[:where]
144
- @prepared_actions = interpolate(@actions).rstrip if @actions
148
+ if @actions
149
+ @prepared_actions = @actions.is_a?(Hash) ?
150
+ @actions.inject({}){ |hash, (key, value)| hash[key] = interpolate(value).rstrip; hash } :
151
+ interpolate(@actions).rstrip
152
+ end
145
153
  end
146
154
 
147
155
  def validate!(direction = :down)
@@ -166,7 +174,7 @@ module HairTrigger
166
174
  if options[:drop]
167
175
  generate_drop_trigger
168
176
  else
169
- raise GenerationError, "no actions specified" if @triggers && create_grouped_trigger? ? @triggers.any?{ |t| t.prepared_actions.nil? } : prepared_actions.nil?
177
+ raise GenerationError, "no actions specified" if @triggers && create_grouped_trigger? ? @triggers.any?{ |t| t.raw_actions.nil? } : raw_actions.nil?
170
178
  raise GenerationError, "need to specify the event(s) (:insert, :update, :delete)" if !options[:events] || options[:events].empty?
171
179
  raise GenerationError, "need to specify the timing (:before/:after)" unless options[:timing]
172
180
 
@@ -255,7 +263,7 @@ module HairTrigger
255
263
  end
256
264
 
257
265
  def actions_to_ruby(indent = '')
258
- if prepared_actions =~ /\n/
266
+ if prepared_actions.is_a?(String) && prepared_actions =~ /\n/
259
267
  "#{indent}<<-SQL_ACTIONS\n#{prepared_actions}\n#{indent}SQL_ACTIONS"
260
268
  else
261
269
  indent + prepared_actions.inspect
@@ -271,7 +279,9 @@ module HairTrigger
271
279
  raise DeclarationError, "trigger group did not define any triggers" if @triggers.empty?
272
280
  else
273
281
  @actions = block.call
274
- @actions.sub!(/(\s*)\z/, ';\1') if @actions && !(@actions =~ /;\s*\z/)
282
+ (@actions.is_a?(Hash) ? @actions.values : [@actions]).each do |actions|
283
+ actions.sub!(/(\s*)\z/, ';\1') if actions && actions !~ /;\s*\z/
284
+ end
275
285
  end
276
286
  # only the top-most block actually executes
277
287
  Array(generate).each{ |action| adapter.execute(action)} if options[:execute] && !@trigger_group
@@ -312,7 +322,7 @@ module HairTrigger
312
322
  CREATE TRIGGER #{prepared_name} #{options[:timing]} #{options[:events]} ON #{options[:table]}
313
323
  FOR EACH #{options[:for_each]}#{prepared_where ? " WHEN " + prepared_where : ''}
314
324
  BEGIN
315
- #{normalize(prepared_actions, 1).rstrip}
325
+ #{normalize(raw_actions, 1).rstrip}
316
326
  END;
317
327
  SQL
318
328
  end
@@ -328,13 +338,13 @@ BEGIN
328
338
  SQL
329
339
  if prepared_where && db_version < 90000
330
340
  sql << normalize("IF #{prepared_where} THEN", 1)
331
- sql << normalize(prepared_actions, 2)
341
+ sql << normalize(raw_actions, 2)
332
342
  sql << normalize("END IF;", 1)
333
343
  else
334
- sql << normalize(prepared_actions, 1)
344
+ sql << normalize(raw_actions, 1)
335
345
  end
336
346
  # if no return is specified at the end, be sure we set a sane one
337
- unless prepared_actions =~ /return [^;]+;\s*\z/i
347
+ unless raw_actions =~ /return [^;]+;\s*\z/i
338
348
  if options[:timing] == "AFTER" || options[:for_each] == 'STATEMENT'
339
349
  sql << normalize("RETURN NULL;", 1)
340
350
  elsif options[:events].include?('DELETE')
@@ -363,10 +373,10 @@ BEGIN
363
373
  (@triggers ? @triggers : [self]).each do |trigger|
364
374
  if trigger.prepared_where
365
375
  sql << normalize("IF #{trigger.prepared_where} THEN", 1)
366
- sql << normalize(trigger.prepared_actions, 2)
376
+ sql << normalize(trigger.raw_actions, 2)
367
377
  sql << normalize("END IF;", 1)
368
378
  else
369
- sql << normalize(trigger.prepared_actions, 1)
379
+ sql << normalize(trigger.raw_actions, 1)
370
380
  end
371
381
  end
372
382
  sql << "END\n";
data/spec/builder_spec.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  require 'rspec'
2
- require 'hair_trigger/builder'
2
+ require 'active_record'
3
+ require 'hair_trigger'
3
4
 
4
5
  HairTrigger::Builder.show_warnings = false
5
6
 
@@ -29,7 +30,7 @@ describe "builder" do
29
30
  it "should tack on a semicolon if none is provided" do
30
31
  @adapter = MockAdapter.new("mysql")
31
32
  builder.on(:foos).after(:update){ "FOO " }.generate.
32
- grep(/FOO;/).size.should eql(1)
33
+ grep(/FOO;/).size.should eql(1)
33
34
  end
34
35
  end
35
36
 
@@ -37,16 +38,44 @@ describe "builder" do
37
38
  it "should view identical triggers as identical" do
38
39
  @adapter = MockAdapter.new("mysql")
39
40
  builder.on(:foos).after(:update){ "FOO" }.
40
- should eql(builder.on(:foos).after(:update){ "FOO" })
41
+ should eql(builder.on(:foos).after(:update){ "FOO" })
41
42
  end
42
43
 
43
44
  it "should view incompatible triggers as different" do
44
45
  @adapter = MockAdapter.new("mysql")
45
46
  HairTrigger::Builder.new(nil, :adapter => @adapter, :compatibility => 0).on(:foos).after(:update){ "FOO" }.
46
- should_not eql(builder.on(:foos).after(:update){ "FOO" })
47
+ should_not eql(builder.on(:foos).after(:update){ "FOO" })
47
48
  end
48
49
  end
49
50
 
51
+ context "adapter-specific actions" do
52
+ before(:each) do
53
+ @adapter = MockAdapter.new("mysql")
54
+ end
55
+
56
+ it "should generate the appropriate trigger for the adapter" do
57
+ sql = builder.on(:foos).after(:update).where('BAR'){
58
+ {:default => "DEFAULT", :mysql => "MYSQL"}
59
+ }.generate
60
+
61
+ sql.grep(/DEFAULT/).size.should eql(0)
62
+ sql.grep(/MYSQL/).size.should eql(1)
63
+
64
+ sql = builder.on(:foos).after(:update).where('BAR'){
65
+ {:default => "DEFAULT", :postgres => "POSTGRES"}
66
+ }.generate
67
+
68
+ sql.grep(/POSTGRES/).size.should eql(0)
69
+ sql.grep(/DEFAULT/).size.should eql(1)
70
+ end
71
+
72
+ it "should complain if no actions are provided for this adapter" do
73
+ lambda {
74
+ builder.on(:foos).after(:update).where('BAR'){ {:postgres => "POSTGRES"} }.generate
75
+ }.should raise_error
76
+ end
77
+ end
78
+
50
79
  context "mysql" do
51
80
  before(:each) do
52
81
  @adapter = MockAdapter.new("mysql")
@@ -156,7 +185,7 @@ describe "builder" do
156
185
 
157
186
  it "should allow truncate with for_each statement" do
158
187
  builder.on(:foos).after(:truncate).for_each(:statement){ "FOO" }.generate.
159
- grep(/TRUNCATE.*FOR EACH STATEMENT/m).size.should eql(1)
188
+ grep(/TRUNCATE.*FOR EACH STATEMENT/m).size.should eql(1)
160
189
  end
161
190
 
162
191
  it "should reject truncate with for_each row" do
@@ -167,7 +196,7 @@ describe "builder" do
167
196
 
168
197
  it "should add a return statement if none is provided" do
169
198
  builder.on(:foos).after(:update){ "FOO" }.generate.
170
- grep(/RETURN NULL;/).size.should eql(1)
199
+ grep(/RETURN NULL;/).size.should eql(1)
171
200
  end
172
201
 
173
202
  context "legacy" do
@@ -73,7 +73,7 @@ describe "schema" do
73
73
  # edit our model trigger, generate and apply a new migration
74
74
  user_model = File.read(HairTrigger.model_path + '/user.rb')
75
75
  File.open(HairTrigger.model_path + '/user.rb', 'w') { |f|
76
- f.write user_model.sub('UPDATE groups SET bob_count = bob_count + 1', 'UPDATE groups SET bob_count = bob_count + 2')
76
+ f.write user_model.sub('"UPDATE groups SET bob_count = bob_count + 1"', '{:default => "UPDATE groups SET bob_count = bob_count + 2"}')
77
77
  }
78
78
  migration = HairTrigger.generate_migration
79
79
  ActiveRecord::Migrator.migrate(HairTrigger.migration_path)
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: 13
4
+ hash: 3
5
5
  prerelease:
6
6
  segments:
7
7
  - 0
8
8
  - 1
9
- - 11
10
- version: 0.1.11
9
+ - 12
10
+ version: 0.1.12
11
11
  platform: ruby
12
12
  authors:
13
13
  - Jon Jensen
@@ -15,13 +15,11 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2011-05-31 00:00:00 -06:00
18
+ date: 2011-11-08 00:00:00 -07:00
19
19
  default_executable:
20
20
  dependencies:
21
21
  - !ruby/object:Gem::Dependency
22
- prerelease: false
23
- type: :runtime
24
- requirement: &id001 !ruby/object:Gem::Requirement
22
+ version_requirements: &id001 !ruby/object:Gem::Requirement
25
23
  none: false
26
24
  requirements:
27
25
  - - ">="
@@ -32,12 +30,12 @@ dependencies:
32
30
  - 3
33
31
  - 0
34
32
  version: 2.3.0
35
- name: activerecord
36
- version_requirements: *id001
37
- - !ruby/object:Gem::Dependency
38
33
  prerelease: false
39
34
  type: :runtime
40
- requirement: &id002 !ruby/object:Gem::Requirement
35
+ requirement: *id001
36
+ name: activerecord
37
+ - !ruby/object:Gem::Dependency
38
+ version_requirements: &id002 !ruby/object:Gem::Requirement
41
39
  none: false
42
40
  requirements:
43
41
  - - "="
@@ -48,12 +46,12 @@ dependencies:
48
46
  - 0
49
47
  - 6
50
48
  version: 2.0.6
51
- name: ruby_parser
52
- version_requirements: *id002
53
- - !ruby/object:Gem::Dependency
54
49
  prerelease: false
55
50
  type: :runtime
56
- requirement: &id003 !ruby/object:Gem::Requirement
51
+ requirement: *id002
52
+ name: ruby_parser
53
+ - !ruby/object:Gem::Dependency
54
+ version_requirements: &id003 !ruby/object:Gem::Requirement
57
55
  none: false
58
56
  requirements:
59
57
  - - "="
@@ -64,12 +62,12 @@ dependencies:
64
62
  - 2
65
63
  - 5
66
64
  version: 1.2.5
65
+ prerelease: false
66
+ type: :runtime
67
+ requirement: *id003
67
68
  name: ruby2ruby
68
- version_requirements: *id003
69
69
  - !ruby/object:Gem::Dependency
70
- prerelease: false
71
- type: :development
72
- requirement: &id004 !ruby/object:Gem::Requirement
70
+ version_requirements: &id004 !ruby/object:Gem::Requirement
73
71
  none: false
74
72
  requirements:
75
73
  - - ~>
@@ -80,12 +78,12 @@ dependencies:
80
78
  - 3
81
79
  - 0
82
80
  version: 2.3.0
83
- name: rspec
84
- version_requirements: *id004
85
- - !ruby/object:Gem::Dependency
86
81
  prerelease: false
87
82
  type: :development
88
- requirement: &id005 !ruby/object:Gem::Requirement
83
+ requirement: *id004
84
+ name: rspec
85
+ - !ruby/object:Gem::Dependency
86
+ version_requirements: &id005 !ruby/object:Gem::Requirement
89
87
  none: false
90
88
  requirements:
91
89
  - - ~>
@@ -96,12 +94,12 @@ dependencies:
96
94
  - 0
97
95
  - 0
98
96
  version: 1.0.0
99
- name: bundler
100
- version_requirements: *id005
101
- - !ruby/object:Gem::Dependency
102
97
  prerelease: false
103
98
  type: :development
104
- requirement: &id006 !ruby/object:Gem::Requirement
99
+ requirement: *id005
100
+ name: bundler
101
+ - !ruby/object:Gem::Dependency
102
+ version_requirements: &id006 !ruby/object:Gem::Requirement
105
103
  none: false
106
104
  requirements:
107
105
  - - ~>
@@ -112,12 +110,12 @@ dependencies:
112
110
  - 6
113
111
  - 1
114
112
  version: 1.6.1
115
- name: jeweler
116
- version_requirements: *id006
117
- - !ruby/object:Gem::Dependency
118
113
  prerelease: false
119
114
  type: :development
120
- requirement: &id007 !ruby/object:Gem::Requirement
115
+ requirement: *id006
116
+ name: jeweler
117
+ - !ruby/object:Gem::Dependency
118
+ version_requirements: &id007 !ruby/object:Gem::Requirement
121
119
  none: false
122
120
  requirements:
123
121
  - - ">="
@@ -126,12 +124,12 @@ dependencies:
126
124
  segments:
127
125
  - 0
128
126
  version: "0"
129
- name: rcov
130
- version_requirements: *id007
131
- - !ruby/object:Gem::Dependency
132
127
  prerelease: false
133
128
  type: :development
134
- requirement: &id008 !ruby/object:Gem::Requirement
129
+ requirement: *id007
130
+ name: rcov
131
+ - !ruby/object:Gem::Dependency
132
+ version_requirements: &id008 !ruby/object:Gem::Requirement
135
133
  none: false
136
134
  requirements:
137
135
  - - ">="
@@ -142,12 +140,12 @@ dependencies:
142
140
  - 8
143
141
  - 1
144
142
  version: 2.8.1
145
- name: mysql
146
- version_requirements: *id008
147
- - !ruby/object:Gem::Dependency
148
143
  prerelease: false
149
144
  type: :development
150
- requirement: &id009 !ruby/object:Gem::Requirement
145
+ requirement: *id008
146
+ name: mysql
147
+ - !ruby/object:Gem::Dependency
148
+ version_requirements: &id009 !ruby/object:Gem::Requirement
151
149
  none: false
152
150
  requirements:
153
151
  - - <
@@ -165,12 +163,12 @@ dependencies:
165
163
  - 2
166
164
  - 7
167
165
  version: 0.2.7
168
- name: mysql2
169
- version_requirements: *id009
170
- - !ruby/object:Gem::Dependency
171
166
  prerelease: false
172
167
  type: :development
173
- requirement: &id010 !ruby/object:Gem::Requirement
168
+ requirement: *id009
169
+ name: mysql2
170
+ - !ruby/object:Gem::Dependency
171
+ version_requirements: &id010 !ruby/object:Gem::Requirement
174
172
  none: false
175
173
  requirements:
176
174
  - - ">="
@@ -181,12 +179,12 @@ dependencies:
181
179
  - 10
182
180
  - 1
183
181
  version: 0.10.1
184
- name: pg
185
- version_requirements: *id010
186
- - !ruby/object:Gem::Dependency
187
182
  prerelease: false
188
183
  type: :development
189
- requirement: &id011 !ruby/object:Gem::Requirement
184
+ requirement: *id010
185
+ name: pg
186
+ - !ruby/object:Gem::Dependency
187
+ version_requirements: &id011 !ruby/object:Gem::Requirement
190
188
  none: false
191
189
  requirements:
192
190
  - - ">="
@@ -197,12 +195,12 @@ dependencies:
197
195
  - 3
198
196
  - 2
199
197
  version: 1.3.2
200
- name: sqlite3-ruby
201
- version_requirements: *id011
202
- - !ruby/object:Gem::Dependency
203
198
  prerelease: false
204
199
  type: :development
205
- requirement: &id012 !ruby/object:Gem::Requirement
200
+ requirement: *id011
201
+ name: sqlite3-ruby
202
+ - !ruby/object:Gem::Dependency
203
+ version_requirements: &id012 !ruby/object:Gem::Requirement
206
204
  none: false
207
205
  requirements:
208
206
  - - "="
@@ -213,12 +211,12 @@ dependencies:
213
211
  - 10
214
212
  - 4
215
213
  version: 0.10.4
214
+ prerelease: false
215
+ type: :development
216
+ requirement: *id012
216
217
  name: ruby-debug
217
- version_requirements: *id012
218
218
  - !ruby/object:Gem::Dependency
219
- prerelease: false
220
- type: :runtime
221
- requirement: &id013 !ruby/object:Gem::Requirement
219
+ version_requirements: &id013 !ruby/object:Gem::Requirement
222
220
  none: false
223
221
  requirements:
224
222
  - - ">="
@@ -229,12 +227,12 @@ dependencies:
229
227
  - 3
230
228
  - 0
231
229
  version: 2.3.0
230
+ prerelease: false
231
+ type: :runtime
232
+ requirement: *id013
232
233
  name: activerecord
233
- version_requirements: *id013
234
234
  - !ruby/object:Gem::Dependency
235
- prerelease: false
236
- type: :development
237
- requirement: &id014 !ruby/object:Gem::Requirement
235
+ version_requirements: &id014 !ruby/object:Gem::Requirement
238
236
  none: false
239
237
  requirements:
240
238
  - - ~>
@@ -245,8 +243,10 @@ dependencies:
245
243
  - 3
246
244
  - 0
247
245
  version: 2.3.0
246
+ prerelease: false
247
+ type: :development
248
+ requirement: *id014
248
249
  name: rspec
249
- version_requirements: *id014
250
250
  description: allows you to declare database triggers in ruby in your models, and then generate appropriate migrations as they change
251
251
  email: jenseng@gmail.com
252
252
  executables: []