hairtrigger 0.2.19 → 0.2.24

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.
@@ -1,95 +0,0 @@
1
- require 'spec_helper'
2
-
3
- # for this spec to work, you need to have postgres and mysql installed (in
4
- # addition to the gems), and you should make sure that you have set up
5
- # appropriate users and permissions. see database.yml for more info
6
-
7
- describe "adapter" do
8
- include_context "hairtrigger utils"
9
-
10
- describe ".triggers" do
11
- before do
12
- reset_tmp(:migration_glob => "*initial_tables*")
13
- initialize_db
14
- migrate_db
15
- end
16
-
17
- shared_examples_for "mysql" do
18
- # have to stub SHOW TRIGGERS to get back a '%' host, since GRANTs
19
- # and such get a little dicey for testing (local vs travis, etc.)
20
- it "matches the generated trigger with a '%' grant" do
21
- conn.instance_variable_get(:@config)[:host] = "somehost" # wheeeee!
22
- implicit_definer = "'root'@'somehost'"
23
- show_triggers_definer = "root@%"
24
-
25
- builder = trigger.on(:users).before(:insert){ "UPDATE foos SET bar = 1" }
26
- triggers = builder.generate.select{|t|t !~ /\ADROP/}
27
- expect(conn).to receive(:implicit_mysql_definer).and_return(implicit_definer)
28
- expect(conn).to receive(:select_rows).with("SHOW TRIGGERS").and_return([
29
- ['users_before_insert_row_tr', 'INSERT', 'users', "BEGIN\n UPDATE foos SET bar = 1;\nEND", 'BEFORE', 'NULL', 'STRICT_ALL_TABLES', show_triggers_definer]
30
- ])
31
-
32
- expect(db_triggers).to eq(triggers)
33
- end
34
-
35
- it "quotes table names" do
36
- conn.execute <<-SQL
37
- CREATE TRIGGER foos_tr AFTER DELETE ON users
38
- FOR EACH ROW
39
- BEGIN
40
- UPDATE groups SET bob_count = bob_count - 1;
41
- END
42
- SQL
43
-
44
- expect(conn.triggers["foos_tr"]).to match(/CREATE TRIGGER foos_tr AFTER DELETE ON `users`/)
45
- end
46
- end
47
-
48
- context "mysql" do
49
- let(:adapter) { :mysql }
50
- it_behaves_like "mysql"
51
- end if ADAPTERS.include? :mysql
52
-
53
- context "mysql2" do
54
- let(:adapter) { :mysql2 }
55
- it_behaves_like "mysql"
56
- end if ADAPTERS.include? :mysql2
57
-
58
- context "postgresql" do
59
- let(:adapter) { :postgresql }
60
-
61
- it "quotes table names" do
62
- conn.execute <<-SQL
63
- CREATE FUNCTION foos_tr()
64
- RETURNS TRIGGER AS $$
65
- BEGIN
66
- UPDATE groups SET bob_count = bob_count - 1;
67
- END;
68
- $$ LANGUAGE plpgsql;
69
-
70
- CREATE TRIGGER foos_tr AFTER DELETE ON users
71
- FOR EACH ROW EXECUTE PROCEDURE foos_tr();
72
- SQL
73
-
74
- expect(conn.triggers["foos_tr"]).to match(/CREATE TRIGGER foos_tr AFTER DELETE ON "users"/)
75
- end
76
- end
77
-
78
- context "sqlite3" do
79
- let(:adapter) { :sqlite3 }
80
-
81
- it "quotes table names" do
82
- conn.execute <<-SQL
83
- CREATE TRIGGER foos_tr AFTER DELETE ON users
84
- FOR EACH ROW
85
- BEGIN
86
- UPDATE groups SET bob_count = bob_count - 1;
87
- END;
88
- SQL
89
-
90
- expect(conn.triggers["foos_tr"]).to match(/CREATE TRIGGER foos_tr AFTER DELETE ON "users"/)
91
- end
92
- end
93
- end
94
- end
95
-
@@ -1,433 +0,0 @@
1
- require 'spec_helper'
2
-
3
- HairTrigger::Builder.show_warnings = false
4
-
5
- class MockAdapter
6
- attr_reader :adapter_name
7
- def initialize(type, methods = {})
8
- @adapter_name = type
9
- methods.each do |key, value|
10
- instance_eval("def #{key}; #{value.inspect}; end")
11
- end
12
- end
13
-
14
- def quote_table_name(table)
15
- table
16
- end
17
- end
18
-
19
- def builder(name = nil)
20
- HairTrigger::Builder.new(name, :adapter => @adapter)
21
- end
22
-
23
- describe "builder" do
24
- context "chaining" do
25
- it "should use the last redundant chained call" do
26
- @adapter = MockAdapter.new("mysql")
27
- builder.where(:foo).where(:bar).options[:where].should be(:bar)
28
- end
29
- end
30
-
31
- context "generation" do
32
- it "should tack on a semicolon if none is provided" do
33
- @adapter = MockAdapter.new("mysql")
34
- builder.on(:foos).after(:update){ "FOO " }.generate.
35
- grep(/FOO;/).size.should eql(1)
36
- end
37
- end
38
-
39
- context "comparison" do
40
- it "should view identical triggers as identical" do
41
- @adapter = MockAdapter.new("mysql")
42
- builder.on(:foos).after(:update){ "FOO" }.
43
- should eql(builder.on(:foos).after(:update){ "FOO" })
44
- end
45
-
46
- it "should view incompatible triggers as different" do
47
- @adapter = MockAdapter.new("mysql")
48
- HairTrigger::Builder.new(nil, :adapter => @adapter, :compatibility => 0).on(:foos).after(:update){ "FOO" }.
49
- should_not eql(builder.on(:foos).after(:update){ "FOO" })
50
- end
51
- end
52
-
53
- describe "name" do
54
- it "should be inferred if none is provided" do
55
- builder.on(:foos).after(:update){ "foo" }.prepared_name.
56
- should == "foos_after_update_row_tr"
57
- end
58
-
59
- it "should respect the last chained name" do
60
- builder("lolwut").on(:foos).after(:update){ "foo" }.prepared_name.
61
- should == "lolwut"
62
- builder("lolwut").on(:foos).name("zomg").after(:update).name("yolo"){ "foo" }.prepared_name.
63
- should == "yolo"
64
- end
65
- end
66
-
67
- describe "`of' columns" do
68
- it "should be disallowed for non-update triggers" do
69
- lambda {
70
- builder.on(:foos).after(:insert).of(:bar, :baz){ "BAR" }
71
- }.should raise_error /of may only be specified on update triggers/
72
- end
73
- end
74
-
75
- describe "groups" do
76
- it "should allow chained methods" do
77
- triggers = builder.on(:foos){ |t|
78
- t.where('bar=1').name('bar'){ 'BAR;' }
79
- t.where('baz=1').name('baz'){ 'BAZ;' }
80
- }.triggers
81
- triggers.map(&:prepare!)
82
- triggers.map(&:prepared_name).should == ['bar', 'baz']
83
- triggers.map(&:prepared_where).should == ['bar=1', 'baz=1']
84
- triggers.map(&:prepared_actions).should == ['BAR;', 'BAZ;']
85
- end
86
- end
87
-
88
- context "adapter-specific actions" do
89
- before(:each) do
90
- @adapter = MockAdapter.new("mysql")
91
- end
92
-
93
- it "should generate the appropriate trigger for the adapter" do
94
- sql = builder.on(:foos).after(:update).where('BAR'){
95
- {:default => "DEFAULT", :mysql => "MYSQL"}
96
- }.generate
97
-
98
- sql.grep(/DEFAULT/).size.should eql(0)
99
- sql.grep(/MYSQL/).size.should eql(1)
100
-
101
- sql = builder.on(:foos).after(:update).where('BAR'){
102
- {:default => "DEFAULT", :postgres => "POSTGRES"}
103
- }.generate
104
-
105
- sql.grep(/POSTGRES/).size.should eql(0)
106
- sql.grep(/DEFAULT/).size.should eql(1)
107
- end
108
-
109
- it "should complain if no actions are provided for this adapter" do
110
- lambda {
111
- builder.on(:foos).after(:update).where('BAR'){ {:postgres => "POSTGRES"} }.generate
112
- }.should raise_error
113
- end
114
- end
115
-
116
- context "mysql" do
117
- before(:each) do
118
- @adapter = MockAdapter.new("mysql")
119
- end
120
-
121
- it "should create a single trigger for a group" do
122
- trigger = builder.on(:foos).after(:update){ |t|
123
- t.where('BAR'){ 'BAR' }
124
- t.where('BAZ'){ 'BAZ' }
125
- }
126
- trigger.generate.grep(/CREATE.*TRIGGER/).size.should eql(1)
127
- end
128
-
129
- it "should disallow nested groups" do
130
- lambda {
131
- builder.on(:foos){ |t|
132
- t.after(:update){ |t|
133
- t.where('BAR'){ 'BAR' }
134
- t.where('BAZ'){ 'BAZ' }
135
- }
136
- }.generate
137
- }.should raise_error
138
- end
139
-
140
- it "should warn on explicit subtrigger names and no group name" do
141
- trigger = builder.on(:foos){ |t|
142
- t.where('bar=1').name('bar'){ 'BAR;' }
143
- t.where('baz=1').name('baz'){ 'BAZ;' }
144
- }
145
- trigger.warnings.size.should == 1
146
- trigger.warnings.first.first.should =~ /nested triggers have explicit names/
147
- end
148
-
149
- it "should accept security" do
150
- builder.on(:foos).after(:update).security(:definer){ "FOO" }.generate.
151
- grep(/DEFINER/).size.should eql(0) # default, so we don't include it
152
- builder.on(:foos).after(:update).security("CURRENT_USER"){ "FOO" }.generate.
153
- grep(/DEFINER = CURRENT_USER/).size.should eql(1)
154
- builder.on(:foos).after(:update).security("'user'@'host'"){ "FOO" }.generate.
155
- grep(/DEFINER = 'user'@'host'/).size.should eql(1)
156
- end
157
-
158
- it "should infer `if' conditionals from `of' columns" do
159
- builder.on(:foos).after(:update).of(:bar){ "BAZ" }.generate.join("\n").
160
- should include("IF NEW.bar <> OLD.bar OR (NEW.bar IS NULL) <> (OLD.bar IS NULL) THEN")
161
- end
162
-
163
- it "should merge `where` and `of` into an `if` conditional" do
164
- builder.on(:foos).after(:update).of(:bar).where("lol"){ "BAZ" }.generate.join("\n").
165
- should include("IF (lol) AND (NEW.bar <> OLD.bar OR (NEW.bar IS NULL) <> (OLD.bar IS NULL)) THEN")
166
- end
167
-
168
- it "should reject :invoker security" do
169
- lambda {
170
- builder.on(:foos).after(:update).security(:invoker){ "FOO" }.generate
171
- }.should raise_error
172
- end
173
-
174
- it "should reject for_each :statement" do
175
- lambda {
176
- builder.on(:foos).after(:update).for_each(:statement){ "FOO" }.generate
177
- }.should raise_error
178
- end
179
-
180
- it "should reject multiple events" do
181
- lambda {
182
- builder.on(:foos).after(:update, :delete){ "FOO" }.generate
183
- }.should raise_error
184
- end
185
-
186
- it "should reject truncate" do
187
- lambda {
188
- builder.on(:foos).after(:truncate){ "FOO" }.generate
189
- }.should raise_error
190
- end
191
-
192
- describe "#to_ruby" do
193
- it "should fully represent the builder" do
194
- code = <<-CODE.strip.gsub(/^ +/, '')
195
- on("foos").
196
- security(:definer).
197
- for_each(:row).
198
- before(:update) do |t|
199
- t.where("NEW.foo") do
200
- "FOO;"
201
- end
202
- end
203
- CODE
204
- b = builder
205
- b.instance_eval(code)
206
- b.to_ruby.strip.gsub(/^ +/, '').should be_include(code)
207
- end
208
- end
209
- end
210
-
211
- context "postgresql" do
212
- before(:each) do
213
- @adapter = MockAdapter.new("postgresql", :postgresql_version => 94000)
214
- end
215
-
216
- it "should create multiple triggers for a group" do
217
- trigger = builder.on(:foos).after(:update){ |t|
218
- t.where('BAR'){ 'BAR' }
219
- t.where('BAZ'){ 'BAZ' }
220
- }
221
- trigger.generate.grep(/CREATE.*TRIGGER/).size.should eql(2)
222
- end
223
-
224
- it "should allow nested groups" do
225
- trigger = builder.on(:foos){ |t|
226
- t.after(:update){ |t|
227
- t.where('BAR'){ 'BAR' }
228
- t.where('BAZ'){ 'BAZ' }
229
- }
230
- t.after(:insert){ 'BAZ' }
231
- }
232
- trigger.generate.grep(/CREATE.*TRIGGER/).size.should eql(3)
233
- end
234
-
235
- it "should warn on an explicit group names and no subtrigger names" do
236
- trigger = builder.on(:foos).name('foos'){ |t|
237
- t.where('bar=1'){ 'BAR;' }
238
- t.where('baz=1'){ 'BAZ;' }
239
- }
240
- trigger.warnings.size.should == 1
241
- trigger.warnings.first.first.should =~ /trigger group has an explicit name/
242
- end
243
-
244
- it "should accept `of' columns" do
245
- trigger = builder.on(:foos).after(:update).of(:bar, :baz){ "BAR" }
246
- trigger.generate.grep(/AFTER UPDATE OF bar, baz/).size.should eql(1)
247
- end
248
-
249
- it "should accept security" do
250
- builder.on(:foos).after(:update).security(:invoker){ "FOO" }.generate.
251
- grep(/SECURITY/).size.should eql(0) # default, so we don't include it
252
- builder.on(:foos).after(:update).security(:definer){ "FOO" }.generate.
253
- grep(/SECURITY DEFINER/).size.should eql(1)
254
- end
255
-
256
- it "should reject arbitrary user security" do
257
- lambda {
258
- builder.on(:foos).after(:update).security("'user'@'host'"){ "FOO" }.
259
- generate
260
- }.should raise_error
261
- end
262
-
263
- it "should accept multiple events" do
264
- builder.on(:foos).after(:update, :delete){ "FOO" }.generate.
265
- grep(/UPDATE OR DELETE/).size.should eql(1)
266
- end
267
-
268
- it "should reject long names" do
269
- lambda {
270
- builder.name('A'*65).on(:foos).after(:update){ "FOO" }.generate
271
- }.should raise_error
272
- end
273
-
274
- it "should allow truncate with for_each statement" do
275
- builder.on(:foos).after(:truncate).for_each(:statement){ "FOO" }.generate.
276
- grep(/TRUNCATE.*FOR EACH STATEMENT/m).size.should eql(1)
277
- end
278
-
279
- it "should reject truncate with for_each row" do
280
- lambda {
281
- builder.on(:foos).after(:truncate){ "FOO" }.generate
282
- }.should raise_error
283
- end
284
-
285
- it "should add a return statement if none is provided" do
286
- builder.on(:foos).after(:update){ "FOO" }.generate.
287
- grep(/RETURN NULL;/).size.should eql(1)
288
- end
289
-
290
- it "should not wrap the action in a function" do
291
- builder.on(:foos).after(:update).nowrap{ 'existing_procedure()' }.generate.
292
- grep(/CREATE FUNCTION/).size.should eql(0)
293
- end
294
-
295
- it "should reject combined use of security and nowrap" do
296
- lambda {
297
- builder.on(:foos).after(:update).security("'user'@'host'").nowrap{ "FOO" }.generate
298
- }.should raise_error
299
- end
300
-
301
- it "should allow variable declarations" do
302
- builder.on(:foos).after(:insert).declare("foo INT"){ "FOO" }.generate.join("\n").
303
- should match(/DECLARE\s*foo INT;\s*BEGIN\s*FOO/)
304
- end
305
-
306
- context "legacy" do
307
- it "should reject truncate pre-8.4" do
308
- @adapter = MockAdapter.new("postgresql", :postgresql_version => 80300)
309
- lambda {
310
- builder.on(:foos).after(:truncate).for_each(:statement){ "FOO" }.generate
311
- }.should raise_error
312
- end
313
-
314
- it "should use conditionals pre-9.0" do
315
- @adapter = MockAdapter.new("postgresql", :postgresql_version => 80400)
316
- builder.on(:foos).after(:insert).where("BAR"){ "FOO" }.generate.
317
- grep(/IF BAR/).size.should eql(1)
318
- end
319
-
320
- it "should reject combined use of where and nowrap pre-9.0" do
321
- @adapter = MockAdapter.new("postgresql", :postgresql_version => 80400)
322
- lambda {
323
- builder.on(:foos).after(:insert).where("BAR").nowrap{ "FOO" }.generate
324
- }.should raise_error
325
- end
326
-
327
- it "should infer `if' conditionals from `of' columns on pre-9.0" do
328
- @adapter = MockAdapter.new("postgresql", :postgresql_version => 80400)
329
- builder.on(:foos).after(:update).of(:bar){ "BAZ" }.generate.join("\n").
330
- should include("IF NEW.bar <> OLD.bar OR (NEW.bar IS NULL) <> (OLD.bar IS NULL) THEN")
331
- end
332
- end
333
-
334
- describe "#to_ruby" do
335
- it "should fully represent the builder" do
336
- code = <<-CODE.strip.gsub(/^ +/, '')
337
- on("foos").
338
- of("bar").
339
- security(:invoker).
340
- for_each(:row).
341
- before(:update) do |t|
342
- t.where("NEW.foo").declare("row RECORD") do
343
- "FOO;"
344
- end
345
- end
346
- CODE
347
- b = builder
348
- b.instance_eval(code)
349
- b.to_ruby.strip.gsub(/^ +/, '').should be_include(code)
350
- end
351
- end
352
- end
353
-
354
- context "sqlite" do
355
- before(:each) do
356
- @adapter = MockAdapter.new("sqlite")
357
- end
358
-
359
- it "should create multiple triggers for a group" do
360
- trigger = builder.on(:foos).after(:update){ |t|
361
- t.where('BAR'){ 'BAR' }
362
- t.where('BAZ'){ 'BAZ' }
363
- }
364
- trigger.generate.grep(/CREATE.*TRIGGER/).size.should eql(2)
365
- end
366
-
367
- it "should allow nested groups" do
368
- trigger = builder.on(:foos){ |t|
369
- t.after(:update){ |t|
370
- t.where('BAR'){ 'BAR' }
371
- t.where('BAZ'){ 'BAZ' }
372
- }
373
- t.after(:insert){ 'BAZ' }
374
- }
375
- trigger.generate.grep(/CREATE.*TRIGGER/).size.should eql(3)
376
- end
377
-
378
- it "should warn on an explicit group names and no subtrigger names" do
379
- trigger = builder.on(:foos).name('foos'){ |t|
380
- t.where('bar=1'){ 'BAR;' }
381
- t.where('baz=1'){ 'BAZ;' }
382
- }
383
- trigger.warnings.size.should == 1
384
- trigger.warnings.first.first.should =~ /trigger group has an explicit name/
385
- end
386
-
387
- it "should accept `of' columns" do
388
- trigger = builder.on(:foos).after(:update).of(:bar, :baz){ "BAR" }
389
- trigger.generate.grep(/AFTER UPDATE OF bar, baz/).size.should eql(1)
390
- end
391
-
392
- it "should reject security" do
393
- lambda {
394
- builder.on(:foos).after(:update).security(:definer){ "FOO" }.generate
395
- }.should raise_error
396
- end
397
-
398
- it "should reject for_each :statement" do
399
- lambda {
400
- builder.on(:foos).after(:update).for_each(:statement){ "FOO" }.generate
401
- }.should raise_error
402
- end
403
-
404
- it "should reject multiple events" do
405
- lambda {
406
- builder.on(:foos).after(:update, :delete){ "FOO" }.generate
407
- }.should raise_error
408
- end
409
-
410
- it "should reject truncate" do
411
- lambda {
412
- builder.on(:foos).after(:truncate){ "FOO" }.generate
413
- }.should raise_error
414
- end
415
-
416
- describe "#to_ruby" do
417
- it "should fully represent the builder" do
418
- code = <<-CODE.strip.gsub(/^ +/, '')
419
- on("foos").
420
- of("bar").
421
- before(:update) do |t|
422
- t.where("NEW.foo") do
423
- "FOO;"
424
- end
425
- end
426
- CODE
427
- b = builder
428
- b.instance_eval(code)
429
- b.to_ruby.strip.gsub(/^ +/, '').should be_include(code)
430
- end
431
- end
432
- end
433
- end