hairtrigger 0.2.21 → 0.2.22
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.
- 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
|