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