sbader-lhm 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +6 -0
- data/.travis.yml +10 -0
- data/CHANGELOG.md +99 -0
- data/LICENSE +27 -0
- data/README.md +146 -0
- data/Rakefile +20 -0
- data/bin/lhm-kill-queue +172 -0
- data/bin/lhm-spec-clobber.sh +36 -0
- data/bin/lhm-spec-grants.sh +25 -0
- data/bin/lhm-spec-setup-cluster.sh +67 -0
- data/bin/lhm-test-all.sh +10 -0
- data/gemfiles/ar-2.3_mysql.gemfile +5 -0
- data/gemfiles/ar-3.2_mysql.gemfile +5 -0
- data/gemfiles/ar-3.2_mysql2.gemfile +5 -0
- data/lhm.gemspec +27 -0
- data/lib/lhm.rb +45 -0
- data/lib/lhm/atomic_switcher.rb +49 -0
- data/lib/lhm/chunker.rb +114 -0
- data/lib/lhm/command.rb +46 -0
- data/lib/lhm/entangler.rb +98 -0
- data/lib/lhm/intersection.rb +63 -0
- data/lib/lhm/invoker.rb +49 -0
- data/lib/lhm/locked_switcher.rb +71 -0
- data/lib/lhm/migration.rb +30 -0
- data/lib/lhm/migrator.rb +219 -0
- data/lib/lhm/sql_helper.rb +85 -0
- data/lib/lhm/table.rb +97 -0
- data/lib/lhm/version.rb +6 -0
- data/spec/.lhm.example +4 -0
- data/spec/README.md +51 -0
- data/spec/bootstrap.rb +13 -0
- data/spec/fixtures/destination.ddl +6 -0
- data/spec/fixtures/origin.ddl +6 -0
- data/spec/fixtures/small_table.ddl +4 -0
- data/spec/fixtures/users.ddl +12 -0
- data/spec/integration/atomic_switcher_spec.rb +42 -0
- data/spec/integration/chunker_spec.rb +32 -0
- data/spec/integration/entangler_spec.rb +66 -0
- data/spec/integration/integration_helper.rb +140 -0
- data/spec/integration/lhm_spec.rb +204 -0
- data/spec/integration/locked_switcher_spec.rb +42 -0
- data/spec/integration/table_spec.rb +48 -0
- data/spec/unit/atomic_switcher_spec.rb +31 -0
- data/spec/unit/chunker_spec.rb +111 -0
- data/spec/unit/entangler_spec.rb +76 -0
- data/spec/unit/intersection_spec.rb +39 -0
- data/spec/unit/locked_switcher_spec.rb +51 -0
- data/spec/unit/migration_spec.rb +23 -0
- data/spec/unit/migrator_spec.rb +134 -0
- data/spec/unit/sql_helper_spec.rb +32 -0
- data/spec/unit/table_spec.rb +34 -0
- data/spec/unit/unit_helper.rb +14 -0
- metadata +173 -0
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/CHANGELOG.md
ADDED
@@ -0,0 +1,99 @@
|
|
1
|
+
# 1.1.0 (April 29, 2012)
|
2
|
+
|
3
|
+
* Add option to specify custom index name
|
4
|
+
* Add mysql2 compatibility
|
5
|
+
* Add AtomicSwitcher
|
6
|
+
|
7
|
+
# 1.0.3 (February 23, 2012)
|
8
|
+
|
9
|
+
* Improve change_column
|
10
|
+
|
11
|
+
# 1.0.2 (February 17, 2012)
|
12
|
+
|
13
|
+
* closes https://github.com/soundcloud/large-hadron-migrator/issues/11
|
14
|
+
this critical bug could cause data loss. table parser was replaced with
|
15
|
+
an implementation that reads directly from information_schema.
|
16
|
+
|
17
|
+
# 1.0.1 (February 09, 2012)
|
18
|
+
|
19
|
+
* released to rubygems
|
20
|
+
|
21
|
+
# 1.0.0 (February 09, 2012)
|
22
|
+
|
23
|
+
* added change_column
|
24
|
+
* final 1.0 release
|
25
|
+
|
26
|
+
# 1.0.0.rc8 (February 09, 2012)
|
27
|
+
|
28
|
+
* removed spec binaries from gem bins
|
29
|
+
|
30
|
+
# 1.0.0.rc7 (January 31, 2012)
|
31
|
+
|
32
|
+
* added SqlHelper.annotation into the middle of trigger statements. this
|
33
|
+
is for the benefit of the killer script which should not kill trigger
|
34
|
+
statements.
|
35
|
+
|
36
|
+
# 1.0.0.rc6 (January 30, 2012)
|
37
|
+
|
38
|
+
* added --confirm to kill script; fixes to kill script
|
39
|
+
|
40
|
+
# 1.0.0.rc5 (January 30, 2012)
|
41
|
+
|
42
|
+
* moved scripts into bin, renamed, added to gem binaries
|
43
|
+
|
44
|
+
# 1.0.0.rc4 (January 29, 2012)
|
45
|
+
|
46
|
+
* added '-- lhm' to the end of statements for more visibility
|
47
|
+
|
48
|
+
# 1.0.0.rc3 (January 19, 2012)
|
49
|
+
|
50
|
+
* Speedup migrations for tables with large minimum id
|
51
|
+
* Add a bit yard documentation
|
52
|
+
* Fix issues with index creation on reserved column names
|
53
|
+
* Improve error handling
|
54
|
+
* Add tests for replication
|
55
|
+
* Rename public API method from `hadron_change_table` to `change_table`
|
56
|
+
* Add tests for ActiveRecord 2.3 and 3.1 compatibility
|
57
|
+
|
58
|
+
# 1.0.0.rc2 (January 18, 2012)
|
59
|
+
|
60
|
+
* Speedup migrations for tables with large ids
|
61
|
+
* Fix conversion of milliseconds to seconds
|
62
|
+
* Fix handling of sql errors
|
63
|
+
* Add helper to create unique index
|
64
|
+
* Allow index creation on prefix of column
|
65
|
+
* Quote column names on index creation
|
66
|
+
* Remove ambiguous method signature
|
67
|
+
* Documentation fix
|
68
|
+
* 1.8.7 compatibility
|
69
|
+
|
70
|
+
# 1.0.0.rc1 (January 15, 2012)
|
71
|
+
|
72
|
+
* rewrite.
|
73
|
+
|
74
|
+
# 0.2.1 (November 26, 2011)
|
75
|
+
|
76
|
+
* Include changelog in gem
|
77
|
+
|
78
|
+
# 0.2.0 (November 26, 2011)
|
79
|
+
|
80
|
+
* Add Ruby 1.8 compatibility
|
81
|
+
* Setup travis continuous integration
|
82
|
+
* Fix record lose issue
|
83
|
+
* Fix and speed up specs
|
84
|
+
|
85
|
+
# 0.1.4
|
86
|
+
|
87
|
+
* Merged [Pullrequest #9](https://github.com/soundcloud/large-hadron-migrator/pull/9)
|
88
|
+
|
89
|
+
# 0.1.3
|
90
|
+
|
91
|
+
* code cleanup
|
92
|
+
* Merged [Pullrequest #8](https://github.com/soundcloud/large-hadron-migrator/pull/8)
|
93
|
+
* Merged [Pullrequest #7](https://github.com/soundcloud/large-hadron-migrator/pull/7)
|
94
|
+
* Merged [Pullrequest #4](https://github.com/soundcloud/large-hadron-migrator/pull/4)
|
95
|
+
* Merged [Pullrequest #1](https://github.com/soundcloud/large-hadron-migrator/pull/1)
|
96
|
+
|
97
|
+
# 0.1.2
|
98
|
+
|
99
|
+
* Initial Release
|
data/LICENSE
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
Copyright (c) 2011, SoundCloud, Rany Keddo, Tobias Bielohlawek, Tobias Schmidt
|
2
|
+
|
3
|
+
All rights reserved.
|
4
|
+
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
7
|
+
|
8
|
+
- Redistributions of source code must retain the above copyright notice, this
|
9
|
+
list of conditions and the following disclaimer.
|
10
|
+
- Redistributions in binary form must reproduce the above copyright notice,
|
11
|
+
this list of conditions and the following disclaimer in the documentation
|
12
|
+
and/or other materials provided with the distribution.
|
13
|
+
- Neither the name of the SoundCloud nor the names of its contributors may be
|
14
|
+
used to endorse or promote products derived from this software without
|
15
|
+
specific prior written permission.
|
16
|
+
|
17
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
18
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
19
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
20
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
21
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
22
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
23
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
24
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
25
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
26
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
27
|
+
|
data/README.md
ADDED
@@ -0,0 +1,146 @@
|
|
1
|
+
# Large Hadron Migrator [![Build Status](https://secure.travis-ci.org/soundcloud/large-hadron-migrator.png)][4]
|
2
|
+
|
3
|
+
Rails style database migrations are a useful way to evolve your data schema in
|
4
|
+
an agile manner. Most Rails projects start like this, and at first, making
|
5
|
+
changes is fast and easy.
|
6
|
+
|
7
|
+
That is until your tables grow to millions of records. At this point, the
|
8
|
+
locking nature of `ALTER TABLE` may take your site down for an hour or more
|
9
|
+
while critical tables are migrated. In order to avoid this, developers begin
|
10
|
+
to design around the problem by introducing join tables or moving the data
|
11
|
+
into another layer. Development gets less and less agile as tables grow and
|
12
|
+
grow. To make the problem worse, adding or changing indices to optimize data
|
13
|
+
access becomes just as difficult.
|
14
|
+
|
15
|
+
> Side effects may include black holes and universe implosion.
|
16
|
+
|
17
|
+
There are few things that can be done at the server or engine level. It is
|
18
|
+
possible to change default values in an `ALTER TABLE` without locking the
|
19
|
+
table. The InnoDB Plugin provides facilities for online index creation, which
|
20
|
+
is great if you are using this engine, but only solves half the problem.
|
21
|
+
|
22
|
+
At SoundCloud we started having migration pains quite a while ago, and after
|
23
|
+
looking around for third party solutions, we decided to create our
|
24
|
+
own. We called it Large Hadron Migrator, and it is a gem for online
|
25
|
+
ActiveRecord migrations.
|
26
|
+
|
27
|
+
![LHC](http://farm4.static.flickr.com/3093/2844971993_17f2ddf2a8_z.jpg)
|
28
|
+
|
29
|
+
[The Large Hadron collider at CERN](http://en.wikipedia.org/wiki/Large_Hadron_Collider)
|
30
|
+
|
31
|
+
## The idea
|
32
|
+
|
33
|
+
The basic idea is to perform the migration online while the system is live,
|
34
|
+
without locking the table. In contrast to [OAK][0] and the
|
35
|
+
[facebook tool][1], we only use a copy table and triggers.
|
36
|
+
|
37
|
+
The Large Hadron is a test driven Ruby solution which can easily be dropped
|
38
|
+
into an ActiveRecord migration. It presumes a single auto incremented
|
39
|
+
numerical primary key called id as per the Rails convention. Unlike the
|
40
|
+
[twitter solution][2], it does not require the presence of an indexed
|
41
|
+
`updated_at` column.
|
42
|
+
|
43
|
+
## Requirements
|
44
|
+
|
45
|
+
Lhm currently only works with MySQL databases and requires an established
|
46
|
+
ActiveRecord connection.
|
47
|
+
|
48
|
+
It is compatible and [continuously tested][4] with Ruby 1.8.7 and Ruby 1.9.x,
|
49
|
+
ActiveRecord 2.3.x and 3.x as well as mysql and mysql2 adapters.
|
50
|
+
|
51
|
+
## Installation
|
52
|
+
|
53
|
+
Install it via `gem install lhm` or add `gem "lhm"` to your Gemfile.
|
54
|
+
|
55
|
+
## Usage
|
56
|
+
|
57
|
+
You can invoke Lhm directly from a plain ruby file after connecting ActiveRecord
|
58
|
+
to your mysql instance:
|
59
|
+
|
60
|
+
```ruby
|
61
|
+
require 'lhm'
|
62
|
+
|
63
|
+
ActiveRecord::Base.establish_connection(
|
64
|
+
:adapter => 'mysql',
|
65
|
+
:host => '127.0.0.1',
|
66
|
+
:database => 'lhm'
|
67
|
+
)
|
68
|
+
|
69
|
+
Lhm.change_table :users do |m|
|
70
|
+
m.add_column :arbitrary, "INT(12)"
|
71
|
+
m.add_index [:arbitrary_id, :created_at]
|
72
|
+
m.ddl("alter table %s add column flag tinyint(1)" % m.name)
|
73
|
+
end
|
74
|
+
```
|
75
|
+
|
76
|
+
To use Lhm from an ActiveRecord::Migration in a Rails project, add it to your
|
77
|
+
Gemfile, then invoke as follows:
|
78
|
+
|
79
|
+
```ruby
|
80
|
+
require 'lhm'
|
81
|
+
|
82
|
+
class MigrateUsers < ActiveRecord::Migration
|
83
|
+
def self.up
|
84
|
+
Lhm.change_table :users do |m|
|
85
|
+
m.add_column :arbitrary, "INT(12)"
|
86
|
+
m.add_index [:arbitrary_id, :created_at]
|
87
|
+
m.ddl("alter table %s add column flag tinyint(1)" % m.name)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def self.down
|
92
|
+
Lhm.change_table :users do |m|
|
93
|
+
m.remove_index [:arbitrary_id, :created_at]
|
94
|
+
m.remove_column :arbitrary)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
```
|
99
|
+
|
100
|
+
## Table rename strategies
|
101
|
+
|
102
|
+
There are two different table rename strategies available: LockedSwitcher and
|
103
|
+
AtomicSwitcher.
|
104
|
+
|
105
|
+
For all setups which use replication and a MySQL version
|
106
|
+
affected by the the [binlog bug #39675](http://bugs.mysql.com/bug.php?id=39675),
|
107
|
+
we recommend the LockedSwitcher strategy to avoid replication issues. This
|
108
|
+
strategy locks the table being migrated and issues two ALTER TABLE statements.
|
109
|
+
The AtomicSwitcher uses a single atomic RENAME TABLE query and should be favored
|
110
|
+
in setups which do not suffer from the mentioned replication bug.
|
111
|
+
|
112
|
+
Lhm chooses the strategy automatically based on the used MySQL server version,
|
113
|
+
but you can override the behavior with an option:
|
114
|
+
|
115
|
+
```ruby
|
116
|
+
Lhm.change_table :users, :atomic_switch => true do |m|
|
117
|
+
# ...
|
118
|
+
end
|
119
|
+
```
|
120
|
+
|
121
|
+
## Contributing
|
122
|
+
|
123
|
+
We'll check out your contribution if you:
|
124
|
+
|
125
|
+
* Provide a comprehensive suite of tests for your fork.
|
126
|
+
* Have a clear and documented rationale for your changes.
|
127
|
+
* Package these up in a pull request.
|
128
|
+
|
129
|
+
We'll do our best to help you out with any contribution issues you may have.
|
130
|
+
|
131
|
+
## License
|
132
|
+
|
133
|
+
The license is included as LICENSE in this directory.
|
134
|
+
|
135
|
+
## Similar solutions
|
136
|
+
|
137
|
+
* [OAK: online alter table][0]
|
138
|
+
* [Facebook][1]
|
139
|
+
* [Twitter][2]
|
140
|
+
* [pt-online-schema-change][3]
|
141
|
+
|
142
|
+
[0]: http://openarkkit.googlecode.com
|
143
|
+
[1]: http://www.facebook.com/note.php?note\_id=430801045932
|
144
|
+
[2]: https://github.com/freels/table_migrator
|
145
|
+
[3]: http://www.percona.com/doc/percona-toolkit/2.1/pt-online-schema-change.html
|
146
|
+
[4]: http://travis-ci.org/soundcloud/large-hadron-migrator
|
data/Rakefile
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'rake/testtask'
|
2
|
+
require 'bundler'
|
3
|
+
|
4
|
+
Bundler::GemHelper.install_tasks
|
5
|
+
|
6
|
+
Rake::TestTask.new("unit") do |t|
|
7
|
+
t.libs.push "lib"
|
8
|
+
t.test_files = FileList['spec/unit/*_spec.rb']
|
9
|
+
t.verbose = true
|
10
|
+
end
|
11
|
+
|
12
|
+
Rake::TestTask.new("integration") do |t|
|
13
|
+
t.libs.push "lib"
|
14
|
+
t.test_files = FileList['spec/integration/*_spec.rb']
|
15
|
+
t.verbose = true
|
16
|
+
end
|
17
|
+
|
18
|
+
task :specs => [:unit, :integration]
|
19
|
+
task :default => :specs
|
20
|
+
|
data/bin/lhm-kill-queue
ADDED
@@ -0,0 +1,172 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'active_record'
|
4
|
+
require 'lhm/sql_helper'
|
5
|
+
require 'optparse'
|
6
|
+
|
7
|
+
module Lhm
|
8
|
+
class KillQueue
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
@port = 3306
|
12
|
+
@grace = 10
|
13
|
+
@tiny = 0.1
|
14
|
+
@marker = "%#{ SqlHelper.annotation }%"
|
15
|
+
|
16
|
+
OptionParser.new do |opts|
|
17
|
+
opts.on("-h", "--hostname HOSTNAME") { |v| @hostname = v }
|
18
|
+
opts.on("-u", "--username USERNAME") { |v| @username = v }
|
19
|
+
opts.on("-p", "--password PASSWORD") { |v| @password = v }
|
20
|
+
opts.on("-d", "--database DATABASE") { |v| @database = v }
|
21
|
+
opts.on("-m", "--mode MODE") { |v| @mode = v.to_sym }
|
22
|
+
opts.on("-y", "--confirm") { |v| @confirm = true }
|
23
|
+
end.parse!
|
24
|
+
|
25
|
+
unless(@hostname && @username && @password && @database)
|
26
|
+
abort usage
|
27
|
+
end
|
28
|
+
|
29
|
+
unless([:kill, :master, :slave].include?(@mode))
|
30
|
+
abort "specify -m kill OR -m master OR -m slave"
|
31
|
+
end
|
32
|
+
|
33
|
+
connect
|
34
|
+
end
|
35
|
+
|
36
|
+
def usage
|
37
|
+
<<-desc.gsub(/^ /, '')
|
38
|
+
kills queries on the given server after detecting 'lock table#{ @marker }'.
|
39
|
+
usage:
|
40
|
+
lhm-kill-queue -h hostname -u username -p password -d database \\
|
41
|
+
(-m kill | -m master | -m slave) [--confirm]
|
42
|
+
|
43
|
+
desc
|
44
|
+
end
|
45
|
+
|
46
|
+
def run
|
47
|
+
case @mode
|
48
|
+
when :kill then kill
|
49
|
+
when :master then master
|
50
|
+
when :slave then slave
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def kill
|
55
|
+
lock = trip
|
56
|
+
kill_process(lock)
|
57
|
+
end
|
58
|
+
|
59
|
+
def master
|
60
|
+
lock = trip
|
61
|
+
puts "starting to kill non lhm processes in #{ @grace } seconds"
|
62
|
+
sleep(@grace + @tiny)
|
63
|
+
|
64
|
+
[list_non_lhm].flatten.each do |process|
|
65
|
+
kill_process(process)
|
66
|
+
sleep(@tiny)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def slave
|
71
|
+
lock = trip
|
72
|
+
puts "starting to kill non lhm SELECT processes in #{ @grace } seconds"
|
73
|
+
sleep(@grace + @tiny)
|
74
|
+
|
75
|
+
[list_non_lhm].flatten.each do |process|
|
76
|
+
if(select?(process))
|
77
|
+
kill_process(process)
|
78
|
+
sleep(@tiny)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
private
|
84
|
+
|
85
|
+
def connect
|
86
|
+
ActiveRecord::Base.establish_connection({
|
87
|
+
:adapter => 'mysql',
|
88
|
+
:host => @hostname,
|
89
|
+
:port => @port,
|
90
|
+
:username => @username,
|
91
|
+
:password => @password,
|
92
|
+
:database => @database
|
93
|
+
})
|
94
|
+
end
|
95
|
+
|
96
|
+
def connection
|
97
|
+
ActiveRecord::Base.connection
|
98
|
+
end
|
99
|
+
|
100
|
+
def list_non_lhm
|
101
|
+
select_processes %Q(
|
102
|
+
info not like '#{ @marker }' and time > #{ @grace } and command = 'Query'
|
103
|
+
)
|
104
|
+
end
|
105
|
+
|
106
|
+
def trip
|
107
|
+
until res = select_processes("info like 'lock table#{ @marker }'").first
|
108
|
+
sleep @tiny
|
109
|
+
print '.'
|
110
|
+
end
|
111
|
+
|
112
|
+
res
|
113
|
+
end
|
114
|
+
|
115
|
+
def kill_process(process_id)
|
116
|
+
puts "killing #{ select_statement(process_id) }"
|
117
|
+
|
118
|
+
if(@confirm)
|
119
|
+
print "confirm ('y' to confirm): "
|
120
|
+
|
121
|
+
if(gets.strip != 'y')
|
122
|
+
puts "skipped."
|
123
|
+
return
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
connection.execute("kill #{ process_id }")
|
128
|
+
puts "killed #{ process_id }"
|
129
|
+
end
|
130
|
+
|
131
|
+
def select?(process)
|
132
|
+
if statement = select_statement(process)
|
133
|
+
case statement
|
134
|
+
when /delete/i then false
|
135
|
+
when /update/i then false
|
136
|
+
when /insert/i then false
|
137
|
+
else
|
138
|
+
!!statement.match(/select/i)
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
def select_statement(process)
|
144
|
+
if process
|
145
|
+
value %Q(
|
146
|
+
select info from information_schema.processlist where id = #{ process }
|
147
|
+
)
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
def select_processes(predicate)
|
152
|
+
values %Q(
|
153
|
+
select id from information_schema.processlist
|
154
|
+
where db = '#{ @database }'
|
155
|
+
and user = '#{ @username }'
|
156
|
+
and #{ predicate }
|
157
|
+
)
|
158
|
+
end
|
159
|
+
|
160
|
+
def value(statement)
|
161
|
+
connection.select_value(statement)
|
162
|
+
end
|
163
|
+
|
164
|
+
def values(statement)
|
165
|
+
connection.select_values(statement)
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
killer = Lhm::KillQueue.new
|
171
|
+
killer.run
|
172
|
+
|