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 +1 -1
- data/README.rdoc +101 -33
- data/VERSION +1 -1
- data/lib/hair_trigger/builder.rb +77 -40
- data/spec/builder_spec.rb +69 -20
- metadata +5 -5
data/Gemfile
CHANGED
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
|
-
|
9
|
+
=== Step 1.
|
9
10
|
|
10
|
-
|
11
|
-
|
12
|
-
|
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
|
-
|
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.
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
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
|
-
|
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
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
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
|
-
|
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
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
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
|
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.
|
1
|
+
0.1.3
|
data/lib/hair_trigger/builder.rb
CHANGED
@@ -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]
|
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
|
-
|
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
|
-
|
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
|
-
|
72
|
-
|
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
|
-
|
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
|
-
|
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
|
142
|
-
|
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
|
-
|
234
|
-
|
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|
|
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 ||=
|
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
|
-
|
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 {
|
60
|
-
|
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
|
64
|
-
lambda {
|
65
|
-
|
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 {
|
102
|
-
|
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
|
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 {
|
112
|
-
|
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 {
|
142
|
-
|
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 {
|
147
|
-
|
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
|
151
|
-
lambda {
|
152
|
-
|
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:
|
4
|
+
hash: 29
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 0
|
8
8
|
- 1
|
9
|
-
-
|
10
|
-
version: 0.1.
|
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-
|
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:
|