hairtrigger 0.2.21 → 0.2.22
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +29 -6
- data/lib/hair_trigger.rb +19 -28
- data/lib/hair_trigger/builder.rb +9 -4
- data/lib/hair_trigger/schema_dumper.rb +3 -4
- data/lib/hair_trigger/version.rb +1 -1
- metadata +12 -22
- data/spec/adapter_spec.rb +0 -94
- data/spec/builder_spec.rb +0 -433
- data/spec/migrations-3.2/20110331212003_initial_tables.rb +0 -18
- data/spec/migrations-3.2/20110331212631_user_trigger.rb +0 -18
- data/spec/migrations-3.2/20110417185102_manual_user_trigger.rb +0 -10
- data/spec/migrations-pre-3.1/20110331212003_initial_tables.rb +0 -18
- data/spec/migrations-pre-3.1/20110331212631_user_trigger.rb +0 -18
- data/spec/migrations-pre-3.1/20110417185102_manual_user_trigger.rb +0 -10
- data/spec/migrations/20110331212003_initial_tables.rb +0 -18
- data/spec/migrations/20110331212631_user_trigger.rb +0 -18
- data/spec/migrations/20110417185102_manual_user_trigger.rb +0 -10
- data/spec/migrations_spec.rb +0 -60
- data/spec/models/user.rb +0 -6
- data/spec/models/user_group.rb +0 -3
- data/spec/schema_dumper_spec.rb +0 -124
- data/spec/spec_helper.rb +0 -104
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 935691cbc5895c59ed3a1302e29112a6bdfd3722
|
4
|
+
data.tar.gz: fcc0c590a2c112a895d303e6027a202c3b192c1b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 15b0da9b7da0f7c0534393a6ee88c034ec6b69e03589f6675fec1d8f3d221d69ec672d3bf9b7058c0ef5095897a071ad7247a904b3fd1d0a133c0548fdbf4c54
|
7
|
+
data.tar.gz: bed6e7c495e5dd1d6866f7ce86ddaa7521244ba7e3176e99c983494715a804d5fdeaeeab0ffaada4b78348bbd3b964f9c56d4df7b80fa601fc65c9a8ac64e965
|
data/README.md
CHANGED
@@ -7,8 +7,15 @@ and a simple rake task does all the dirty work for you.
|
|
7
7
|
|
8
8
|
## Installation
|
9
9
|
|
10
|
-
|
11
|
-
|
10
|
+
HairTrigger works with Rails 5.0 onwards. Add the following line to your Gemfile:
|
11
|
+
|
12
|
+
```ruby
|
13
|
+
gem 'hairtrigger'
|
14
|
+
```
|
15
|
+
|
16
|
+
Then run `bundle install`
|
17
|
+
|
18
|
+
For older versions of Rails check the last [0.2 release](https://github.com/jenseng/hair_trigger/tree/v0.2.21)
|
12
19
|
|
13
20
|
## Usage
|
14
21
|
|
@@ -248,7 +255,7 @@ create.
|
|
248
255
|
|
249
256
|
## Warnings and Errors
|
250
257
|
|
251
|
-
There are a couple classes of errors: declaration errors and generation
|
258
|
+
There are a couple classes of errors: declaration errors and generation
|
252
259
|
errors/warnings.
|
253
260
|
|
254
261
|
Declaration errors happen if your trigger declaration is obviously wrong, and
|
@@ -313,10 +320,26 @@ existing trigger if you wish to redefine it.
|
|
313
320
|
* Manual `create_trigger` statements have some gotchas. See the section
|
314
321
|
"Manual triggers and :compatibility"
|
315
322
|
|
323
|
+
## Contributing
|
324
|
+
|
325
|
+
Contributions welcome! I don't write much Ruby these days 😢 (and haven't used this
|
326
|
+
gem in years 😬) but am happy to take contributions. If I'm slow to respond, don't
|
327
|
+
hesitate to @ me repeatedly, sometimes those github notifications slip through
|
328
|
+
the cracks. 😆.
|
329
|
+
|
330
|
+
If you want to add a feature/bugfix, you can rely on Travis to run the tests, but
|
331
|
+
do also run them locally (especially if you are changing supported railses/etc).
|
332
|
+
HairTrigger uses [appraisal](https://github.com/thoughtbot/appraisal) to manage all
|
333
|
+
that w/ automagical gemfiles. So the tl;dr when testing locally is:
|
334
|
+
|
335
|
+
1. make sure you have mysql and postgres installed (homebrew or whatever)
|
336
|
+
2. `bundle exec appraisal install` -- get all the dependencies
|
337
|
+
3. `bundle exec appraisal rake` -- run the specs every which way
|
338
|
+
|
316
339
|
## Compatibility
|
317
340
|
|
318
|
-
* Ruby
|
319
|
-
* Rails
|
341
|
+
* Ruby 2.3.0+
|
342
|
+
* Rails 5.0+
|
320
343
|
* PostgreSQL 8.0+
|
321
344
|
* MySQL 5.0.10+
|
322
345
|
* SQLite 3.3.8+
|
@@ -325,4 +348,4 @@ existing trigger if you wish to redefine it.
|
|
325
348
|
|
326
349
|
## Copyright
|
327
350
|
|
328
|
-
Copyright (c) 2011-
|
351
|
+
Copyright (c) 2011-2019 Jon Jensen. See LICENSE.txt for further details.
|
data/lib/hair_trigger.rb
CHANGED
@@ -22,7 +22,7 @@ module HairTrigger
|
|
22
22
|
end
|
23
23
|
|
24
24
|
def models
|
25
|
-
if defined?(Rails)
|
25
|
+
if defined?(Rails)
|
26
26
|
Rails.application.eager_load!
|
27
27
|
else
|
28
28
|
Dir[model_path + '/*rb'].each do |model|
|
@@ -35,17 +35,13 @@ module HairTrigger
|
|
35
35
|
end
|
36
36
|
end
|
37
37
|
end
|
38
|
-
ActiveRecord::
|
39
|
-
ActiveRecord::Base.send(:subclasses) :
|
40
|
-
ActiveRecord::Base.descendants
|
38
|
+
ActiveRecord::Base.descendants
|
41
39
|
end
|
42
40
|
|
43
41
|
def migrator
|
44
42
|
version = ActiveRecord::VERSION::STRING
|
45
43
|
if version >= "5.2."
|
46
44
|
migrations = ActiveRecord::MigrationContext.new(migration_path).migrations
|
47
|
-
elsif version < "4.0."
|
48
|
-
migrations = migration_path
|
49
45
|
else # version >= "4.0."
|
50
46
|
migrations = ActiveRecord::Migrator.migrations(migration_path)
|
51
47
|
end
|
@@ -59,7 +55,7 @@ module HairTrigger
|
|
59
55
|
options[:schema_rb_first] = true
|
60
56
|
options[:skip_pending_migrations] = true
|
61
57
|
end
|
62
|
-
|
58
|
+
|
63
59
|
# if we're in a db:schema:dump task (explict or kicked off by db:migrate),
|
64
60
|
# we evaluate the previous schema.rb (if it exists), and then all applied
|
65
61
|
# migrations in order (even ones older than schema.rb). this ensures we
|
@@ -76,7 +72,7 @@ module HairTrigger
|
|
76
72
|
triggers = MigrationReader.get_triggers(migration, options)
|
77
73
|
migrations << [migration, triggers] unless triggers.empty?
|
78
74
|
end
|
79
|
-
|
75
|
+
|
80
76
|
if previous_schema = (options.has_key?(:previous_schema) ? options[:previous_schema] : File.exist?(schema_rb_path) && File.read(schema_rb_path))
|
81
77
|
base_triggers = MigrationReader.get_triggers(previous_schema, options)
|
82
78
|
unless base_triggers.empty?
|
@@ -84,9 +80,9 @@ module HairTrigger
|
|
84
80
|
migrations.unshift [OpenStruct.new({:version => version}), base_triggers]
|
85
81
|
end
|
86
82
|
end
|
87
|
-
|
83
|
+
|
88
84
|
migrations = migrations.sort_by{|(migration, triggers)| migration.version} unless options[:schema_rb_first]
|
89
|
-
|
85
|
+
|
90
86
|
all_builders = []
|
91
87
|
migrations.each do |(migration, triggers)|
|
92
88
|
triggers.each do |new_trigger|
|
@@ -97,7 +93,7 @@ module HairTrigger
|
|
97
93
|
all_builders << [migration.name, new_trigger] unless new_trigger.options[:drop]
|
98
94
|
end
|
99
95
|
end
|
100
|
-
|
96
|
+
|
101
97
|
all_builders
|
102
98
|
end
|
103
99
|
|
@@ -108,27 +104,27 @@ module HairTrigger
|
|
108
104
|
def generate_migration(silent = false)
|
109
105
|
begin
|
110
106
|
canonical_triggers = current_triggers
|
111
|
-
rescue
|
107
|
+
rescue
|
112
108
|
$stderr.puts $!
|
113
109
|
exit 1
|
114
110
|
end
|
115
|
-
|
111
|
+
|
116
112
|
migrations = current_migrations
|
117
113
|
migration_names = migrations.map(&:first)
|
118
114
|
existing_triggers = migrations.map(&:last)
|
119
|
-
|
115
|
+
|
120
116
|
up_drop_triggers = []
|
121
117
|
up_create_triggers = []
|
122
118
|
down_drop_triggers = []
|
123
119
|
down_create_triggers = []
|
124
|
-
|
120
|
+
|
125
121
|
# see which triggers need to be dropped
|
126
122
|
existing_triggers.each do |existing|
|
127
123
|
next if canonical_triggers.any?{ |t| t.prepared_name == existing.prepared_name }
|
128
124
|
up_drop_triggers.concat existing.drop_triggers
|
129
125
|
down_create_triggers << existing
|
130
126
|
end
|
131
|
-
|
127
|
+
|
132
128
|
# see which triggers need to be added/replaced
|
133
129
|
(canonical_triggers - existing_triggers).each do |new_trigger|
|
134
130
|
up_create_triggers << new_trigger
|
@@ -141,29 +137,28 @@ module HairTrigger
|
|
141
137
|
down_create_triggers << existing
|
142
138
|
end
|
143
139
|
end
|
144
|
-
|
140
|
+
|
145
141
|
return if up_drop_triggers.empty? && up_create_triggers.empty?
|
146
142
|
|
147
143
|
migration_name = infer_migration_name(migration_names, up_create_triggers, up_drop_triggers)
|
148
144
|
migration_version = infer_migration_version(migration_name)
|
149
145
|
file_name = migration_path + '/' + migration_version + "_" + migration_name.underscore + ".rb"
|
150
146
|
FileUtils.mkdir_p migration_path
|
151
|
-
|
152
|
-
File.open(file_name, "w"){ |f| f.write <<-MIGRATION }
|
147
|
+
File.open(file_name, "w") { |f| f.write <<-RUBY }
|
153
148
|
# This migration was auto-generated via `rake db:generate_trigger_migration'.
|
154
149
|
# While you can edit this file, any changes you make to the definitions here
|
155
150
|
# will be undone by the next auto-generated trigger migration.
|
156
151
|
|
157
|
-
class #{migration_name} < ActiveRecord::Migration
|
158
|
-
def
|
152
|
+
class #{migration_name} < ActiveRecord::Migration[#{ActiveRecord::Migration.current_version}]
|
153
|
+
def up
|
159
154
|
#{(up_drop_triggers + up_create_triggers).map{ |t| t.to_ruby(' ') }.join("\n\n").lstrip}
|
160
155
|
end
|
161
156
|
|
162
|
-
def
|
157
|
+
def down
|
163
158
|
#{(down_drop_triggers + down_create_triggers).map{ |t| t.to_ruby(' ') }.join("\n\n").lstrip}
|
164
159
|
end
|
165
160
|
end
|
166
|
-
|
161
|
+
RUBY
|
167
162
|
file_name
|
168
163
|
end
|
169
164
|
|
@@ -226,10 +221,6 @@ end
|
|
226
221
|
end
|
227
222
|
|
228
223
|
ActiveRecord::Base.send :extend, HairTrigger::Base
|
229
|
-
|
230
|
-
ActiveRecord::Migrator.send :extend, HairTrigger::Migrator
|
231
|
-
else
|
232
|
-
ActiveRecord::Migration.send :include, HairTrigger::Migrator
|
233
|
-
end
|
224
|
+
ActiveRecord::Migration.send :include, HairTrigger::Migrator
|
234
225
|
ActiveRecord::ConnectionAdapters::AbstractAdapter.class_eval { include HairTrigger::Adapter }
|
235
226
|
ActiveRecord::SchemaDumper.class_eval { include HairTrigger::SchemaDumper }
|
data/lib/hair_trigger/builder.rb
CHANGED
@@ -335,10 +335,11 @@ module HairTrigger
|
|
335
335
|
block.call(self)
|
336
336
|
raise DeclarationError, "trigger group did not define any triggers" if @triggers.empty?
|
337
337
|
else
|
338
|
-
@actions =
|
339
|
-
|
340
|
-
actions.
|
341
|
-
|
338
|
+
@actions =
|
339
|
+
case (actions = block.call)
|
340
|
+
when Hash then actions.map { |key, action| [key, ensure_semicolon(action)] }.to_h
|
341
|
+
else ensure_semicolon(actions)
|
342
|
+
end
|
342
343
|
end
|
343
344
|
# only the top-most block actually executes
|
344
345
|
if !@trigger_group
|
@@ -350,6 +351,10 @@ module HairTrigger
|
|
350
351
|
self
|
351
352
|
end
|
352
353
|
|
354
|
+
def ensure_semicolon(action)
|
355
|
+
action && action !~ /;\s*\z/ ? action.sub(/(\s*)\z/, ';\1') : action
|
356
|
+
end
|
357
|
+
|
353
358
|
def validate_names!
|
354
359
|
subtriggers = all_triggers(false)
|
355
360
|
named_subtriggers = subtriggers.select{ |t| t.options[:name] }
|
@@ -55,7 +55,7 @@ module HairTrigger
|
|
55
55
|
stream.puts " # no candidate create_trigger statement could be found, creating an adapter-specific one"
|
56
56
|
end
|
57
57
|
if definition =~ /\n/
|
58
|
-
stream.print " execute(<<-
|
58
|
+
stream.print " execute(<<-SQL)\n#{definition.rstrip}\n SQL\n\n"
|
59
59
|
else
|
60
60
|
stream.print " execute(#{definition.inspect})\n\n"
|
61
61
|
end
|
@@ -99,9 +99,8 @@ module HairTrigger
|
|
99
99
|
def self.included(base)
|
100
100
|
base.class_eval do
|
101
101
|
prepend TrailerWithTriggersSupport
|
102
|
-
|
103
|
-
|
104
|
-
end
|
102
|
+
|
103
|
+
class_attribute :previous_schema
|
105
104
|
end
|
106
105
|
end
|
107
106
|
end
|
data/lib/hair_trigger/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: hairtrigger
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.2.
|
4
|
+
version: 0.2.22
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jon Jensen
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2019-02-08 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -16,14 +16,20 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '
|
19
|
+
version: '5.0'
|
20
|
+
- - "<"
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: '6.0'
|
20
23
|
type: :runtime
|
21
24
|
prerelease: false
|
22
25
|
version_requirements: !ruby/object:Gem::Requirement
|
23
26
|
requirements:
|
24
27
|
- - ">="
|
25
28
|
- !ruby/object:Gem::Version
|
26
|
-
version: '
|
29
|
+
version: '5.0'
|
30
|
+
- - "<"
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '6.0'
|
27
33
|
- !ruby/object:Gem::Dependency
|
28
34
|
name: ruby_parser
|
29
35
|
requirement: !ruby/object:Gem::Requirement
|
@@ -73,22 +79,6 @@ files:
|
|
73
79
|
- lib/hair_trigger/version.rb
|
74
80
|
- lib/hairtrigger.rb
|
75
81
|
- lib/tasks/hair_trigger.rake
|
76
|
-
- spec/adapter_spec.rb
|
77
|
-
- spec/builder_spec.rb
|
78
|
-
- spec/migrations-3.2/20110331212003_initial_tables.rb
|
79
|
-
- spec/migrations-3.2/20110331212631_user_trigger.rb
|
80
|
-
- spec/migrations-3.2/20110417185102_manual_user_trigger.rb
|
81
|
-
- spec/migrations-pre-3.1/20110331212003_initial_tables.rb
|
82
|
-
- spec/migrations-pre-3.1/20110331212631_user_trigger.rb
|
83
|
-
- spec/migrations-pre-3.1/20110417185102_manual_user_trigger.rb
|
84
|
-
- spec/migrations/20110331212003_initial_tables.rb
|
85
|
-
- spec/migrations/20110331212631_user_trigger.rb
|
86
|
-
- spec/migrations/20110417185102_manual_user_trigger.rb
|
87
|
-
- spec/migrations_spec.rb
|
88
|
-
- spec/models/user.rb
|
89
|
-
- spec/models/user_group.rb
|
90
|
-
- spec/schema_dumper_spec.rb
|
91
|
-
- spec/spec_helper.rb
|
92
82
|
homepage: http://github.com/jenseng/hair_trigger
|
93
83
|
licenses:
|
94
84
|
- MIT
|
@@ -101,12 +91,12 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
101
91
|
requirements:
|
102
92
|
- - ">="
|
103
93
|
- !ruby/object:Gem::Version
|
104
|
-
version:
|
94
|
+
version: 2.3.0
|
105
95
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
106
96
|
requirements:
|
107
97
|
- - ">="
|
108
98
|
- !ruby/object:Gem::Version
|
109
|
-
version:
|
99
|
+
version: '0'
|
110
100
|
requirements: []
|
111
101
|
rubyforge_project:
|
112
102
|
rubygems_version: 2.6.13
|
data/spec/adapter_spec.rb
DELETED
@@ -1,94 +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 user_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 user_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 user_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
|
data/spec/builder_spec.rb
DELETED
@@ -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
|