sql_runner 0.1.0 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.github/FUNDING.yml +3 -0
- data/.gitignore +2 -1
- data/.rubocop.yml +16 -0
- data/.travis.yml +20 -4
- data/Gemfile +2 -0
- data/README.md +48 -10
- data/Rakefile +7 -2
- data/bin/console +1 -0
- data/examples/base.rb +96 -0
- data/examples/bench.rb +21 -5
- data/examples/profiling.rb +2 -0
- data/examples/test.rb +3 -84
- data/examples/test_active_record.rb +17 -0
- data/lib/sql_runner/adapters/active_record.rb +79 -0
- data/lib/sql_runner/adapters/mysql.rb +107 -0
- data/lib/sql_runner/adapters/postgresql.rb +24 -18
- data/lib/sql_runner/adapters/sqlite.rb +91 -0
- data/lib/sql_runner/adapters.rb +16 -7
- data/lib/sql_runner/configuration.rb +3 -3
- data/lib/sql_runner/connection.rb +8 -4
- data/lib/sql_runner/query/many.rb +2 -0
- data/lib/sql_runner/query/model.rb +3 -1
- data/lib/sql_runner/query/one.rb +10 -1
- data/lib/sql_runner/query.rb +26 -8
- data/lib/sql_runner/runner.rb +4 -2
- data/lib/sql_runner/version.rb +3 -1
- data/lib/sql_runner.rb +9 -2
- data/sql_runner.gemspec +23 -8
- metadata +94 -17
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: d8ae856dccc72baadf453eab3f66fc6b375757e81d042e309de58b80a3794c22
|
4
|
+
data.tar.gz: bc6cdefb9735e36c972e0ae23fc062e746c6ca44f981444fb604fd9e3db388d5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1651fa64b28884dc534d878114bfab992f7d592b30c4b86cb518c24bfb63685795b61bb792db072ce07f7ea9495310b73196d5763b4250f3ea6d2fe7346efb3f
|
7
|
+
data.tar.gz: b660ace92f4e0446c8f583505a4d2da82f4854b53cdce460202d83086dd968e5a2a89af8953c484a12dfa023e12dfaa4ac68c38bb9ddb4e78f3144e27bf703bf
|
data/.github/FUNDING.yml
ADDED
data/.gitignore
CHANGED
data/.rubocop.yml
ADDED
data/.travis.yml
CHANGED
@@ -1,13 +1,29 @@
|
|
1
|
+
---
|
1
2
|
language: ruby
|
2
3
|
cache: bundler
|
3
4
|
sudo: false
|
4
5
|
rvm:
|
5
|
-
- 2.
|
6
|
+
- 2.7.0
|
7
|
+
services:
|
8
|
+
- mysql
|
9
|
+
addons:
|
10
|
+
postgresql: "10"
|
11
|
+
apt:
|
12
|
+
packages:
|
13
|
+
- postgresql-10
|
14
|
+
- postgresql-client-10
|
15
|
+
before_script:
|
16
|
+
- curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
|
17
|
+
- chmod +x ./cc-test-reporter
|
18
|
+
- "./cc-test-reporter before-build"
|
19
|
+
after_script:
|
20
|
+
- "./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT"
|
6
21
|
before_install:
|
7
|
-
- gem install bundler
|
8
|
-
- createdb test
|
22
|
+
- gem install bundler
|
23
|
+
- createdb test
|
24
|
+
- mysql -e 'CREATE DATABASE IF NOT EXISTS test;'
|
9
25
|
notifications:
|
10
26
|
email: false
|
11
27
|
env:
|
12
28
|
global:
|
13
|
-
secure:
|
29
|
+
secure: 4dDrl8nCItKPRKpoA2KJVWsDm3o7U0pIsdY8t19pJXbziteT3zw5pEmFh+/qWGB1VszzFFZzXcZCIoKv3NX/x24g+LU+qvL7vLYLyhUR8ZjR4rGzGkwBCgEYBWgVtqg9ncYTbYsdbAqryt6f+HvpFj8EFvGSjIWVqJyh59qHdwAVT+BUKjllEH+t5Dbjd2knIQw83af+0A5ljP2uMziNcuHD7MUBIKY1DfFnBYQ5YA50o/mZe8sG5h6bZU/sf0pdoa+z2xDIJOPO7qaCwANACetDtsfs1DN39DsubvlFLg0s2z0XCuYyWSsJQ4zhRipHgnqYn+Rmpql17t0WmhVPA1xvLyk0/4X8rjRcYyYHMM3ryga5bnrpvSiPsqG+JFNhDczpN394KGa7o+/jCuNA0MVFcCzsvW2AMkYpUbPtADY7/Tl4iUJWmFbl0nO1n1wh5dDJ7Wo7mdkPAmdV7hNmMUmHbPeh8mSjMnuS2gcMc11wDgmR56TEMu/B7LUM31og7L+JouEJAOWtzThtpdYLpqDQ3Xe1G5uMl1oZ0lNOqag5Bg+AQCAIGr1N4G2m8fI74M9lUQUfhu87L+zycYMHInOH+Czc+RamlkH3vLl9Fu6aOKoXA0+zh85sVDj3Er+X2NEHaoRe4PSkOHOBNYSkAUd36TYMt8J5MgAD0w7HqmY=
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -1,12 +1,13 @@
|
|
1
1
|
# SQLRunner
|
2
2
|
|
3
|
-
[![Travis-CI](https://travis-ci.org/fnando/sql_runner.
|
3
|
+
[![Travis-CI](https://travis-ci.org/fnando/sql_runner.svg)](https://travis-ci.org/fnando/sql_runner)
|
4
4
|
[![Code Climate](https://codeclimate.com/github/fnando/sql_runner/badges/gpa.svg)](https://codeclimate.com/github/fnando/sql_runner)
|
5
5
|
[![Test Coverage](https://codeclimate.com/github/fnando/sql_runner/badges/coverage.svg)](https://codeclimate.com/github/fnando/sql_runner/coverage)
|
6
6
|
[![Gem](https://img.shields.io/gem/v/sql_runner.svg)](https://rubygems.org/gems/sql_runner)
|
7
7
|
[![Gem](https://img.shields.io/gem/dt/sql_runner.svg)](https://rubygems.org/gems/sql_runner)
|
8
8
|
|
9
|
-
SQLRunner allows you to load your queries out of SQL files, without using ORMs.
|
9
|
+
SQLRunner allows you to load your queries out of SQL files, without using ORMs.
|
10
|
+
Available for PostgreSQL and MySQL.
|
10
11
|
|
11
12
|
## Installation
|
12
13
|
|
@@ -76,6 +77,22 @@ class GetMembers < SQLRunner::Query
|
|
76
77
|
end
|
77
78
|
```
|
78
79
|
|
80
|
+
You can use this with ActiveRecord as well. To make it work, all you need to do
|
81
|
+
is establishing the connection using `activerecord:///`:
|
82
|
+
|
83
|
+
```ruby
|
84
|
+
require "active_record"
|
85
|
+
|
86
|
+
# You probably won't need this if you're using Rails.
|
87
|
+
ActiveRecord::Base.establish_connection("postgresql:///database")
|
88
|
+
|
89
|
+
# Set the adapter to be based on ActiveRecord.
|
90
|
+
SQLRunner.connect "activerecord:///"
|
91
|
+
|
92
|
+
SQLRunner.execute "SELECT 1"
|
93
|
+
#=> <PG:Result:0x008adf4d5495b0>
|
94
|
+
```
|
95
|
+
|
79
96
|
### Plugins
|
80
97
|
|
81
98
|
#### Load just one record
|
@@ -133,7 +150,9 @@ FindUsers.call
|
|
133
150
|
|
134
151
|
### Adding new plugins
|
135
152
|
|
136
|
-
First you have to create a class/module that implements the
|
153
|
+
First you have to create a class/module that implements the
|
154
|
+
`.activate(target, options)` class method. The following example overrides the
|
155
|
+
`call(**bind_vars)` method by using `Module.prepend`.
|
137
156
|
|
138
157
|
```ruby
|
139
158
|
module ReverseRecords
|
@@ -145,8 +164,11 @@ module ReverseRecords
|
|
145
164
|
super(**bind_vars).to_a.reverse
|
146
165
|
end
|
147
166
|
end
|
167
|
+
```
|
148
168
|
|
149
|
-
|
169
|
+
#### Register the plugin.
|
170
|
+
|
171
|
+
```ruby
|
150
172
|
SQLRunner::Query.register_plugin :reverse, ReverseRecords
|
151
173
|
|
152
174
|
class Users < SQLRunner::Query
|
@@ -157,11 +179,17 @@ end
|
|
157
179
|
Users.call
|
158
180
|
```
|
159
181
|
|
160
|
-
If
|
182
|
+
If your plugin can receive options, you can call it as
|
183
|
+
`plugin reverse: options`, where `options` can be anything (e.g. `Hash`,
|
184
|
+
`Array`, `Object`, etc).
|
161
185
|
|
162
186
|
## Benchmarks
|
163
187
|
|
164
|
-
You won't gain too much performance by using this gem.
|
188
|
+
You won't gain too much performance by using this gem. The idea is making SQL
|
189
|
+
easier to read by extracting complex stuff to their own files. These are the
|
190
|
+
results against ActiveRecord using different wrapping libraries like
|
191
|
+
[virtus](https://rubygems.org/gems/virtus) and
|
192
|
+
[dry-types](https://rubygems.org/gems/dry-types).
|
165
193
|
|
166
194
|
Loading just one record:
|
167
195
|
|
@@ -183,14 +211,24 @@ activerecord - find many : 2731.5 i/s - 2.49x slower
|
|
183
211
|
|
184
212
|
## Development
|
185
213
|
|
186
|
-
After checking out the repo, run `bin/setup` to install dependencies. Then, run
|
214
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run
|
215
|
+
`rake test` to run the tests. You can also run `bin/console` for an interactive
|
216
|
+
prompt that will allow you to experiment.
|
187
217
|
|
188
|
-
To install this gem onto your local machine, run `bundle exec rake install`. To
|
218
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To
|
219
|
+
release a new version, update the version number in `version.rb`, and then run
|
220
|
+
`bundle exec rake release`, which will create a git tag for the version, push
|
221
|
+
git commits and tags, and push the `.gem` file to
|
222
|
+
[rubygems.org](https://rubygems.org).
|
189
223
|
|
190
224
|
## Contributing
|
191
225
|
|
192
|
-
Bug reports and pull requests are welcome on GitHub at
|
226
|
+
Bug reports and pull requests are welcome on GitHub at
|
227
|
+
https://github.com/fnando/sql_runner. This project is intended to be a safe,
|
228
|
+
welcoming space for collaboration, and contributors are expected to adhere to
|
229
|
+
the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
|
193
230
|
|
194
231
|
## License
|
195
232
|
|
196
|
-
The gem is available as open source under the terms of the
|
233
|
+
The gem is available as open source under the terms of the
|
234
|
+
[MIT License](http://opensource.org/licenses/MIT).
|
data/Rakefile
CHANGED
@@ -1,11 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require "bundler/gem_tasks"
|
2
4
|
require "rake/testtask"
|
5
|
+
require "rubocop/rake_task"
|
3
6
|
|
4
7
|
Rake::TestTask.new(:test) do |t|
|
5
8
|
t.libs << "test"
|
6
9
|
t.libs << "lib"
|
7
|
-
t.test_files = FileList[
|
10
|
+
t.test_files = FileList["test/**/*_test.rb"]
|
8
11
|
t.warning = false
|
9
12
|
end
|
10
13
|
|
11
|
-
|
14
|
+
RuboCop::RakeTask.new
|
15
|
+
|
16
|
+
task default: %i[test rubocop]
|
data/bin/console
CHANGED
data/examples/base.rb
ADDED
@@ -0,0 +1,96 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
SQLRunner.execute <<~SQL
|
4
|
+
create table if not exists users (
|
5
|
+
id serial primary key not null,
|
6
|
+
name text not null,
|
7
|
+
email text not null
|
8
|
+
)
|
9
|
+
SQL
|
10
|
+
|
11
|
+
class Users < SQLRunner::Query
|
12
|
+
end
|
13
|
+
|
14
|
+
module NumericModel
|
15
|
+
def self.new(attrs)
|
16
|
+
attrs.values.first.to_i
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
class Numbers < SQLRunner::Query
|
21
|
+
plugin model: NumericModel
|
22
|
+
plugin :many
|
23
|
+
|
24
|
+
query <<-SQL
|
25
|
+
SELECT n FROM generate_series(1, 10) n
|
26
|
+
SQL
|
27
|
+
end
|
28
|
+
|
29
|
+
class User
|
30
|
+
include Virtus.model
|
31
|
+
|
32
|
+
attribute :id, String
|
33
|
+
attribute :name, String
|
34
|
+
attribute :email, Integer
|
35
|
+
end
|
36
|
+
|
37
|
+
class Customer < User
|
38
|
+
end
|
39
|
+
|
40
|
+
class FindUser < SQLRunner::Query
|
41
|
+
plugins :one
|
42
|
+
plugin model: User
|
43
|
+
end
|
44
|
+
|
45
|
+
class FindAllUsers < SQLRunner::Query
|
46
|
+
plugins :many
|
47
|
+
plugin model: User
|
48
|
+
end
|
49
|
+
|
50
|
+
class CreateUser < SQLRunner::Query
|
51
|
+
plugin :one
|
52
|
+
plugin model: User
|
53
|
+
end
|
54
|
+
|
55
|
+
class DeleteAllUsers < SQLRunner::Query
|
56
|
+
plugin :many
|
57
|
+
plugin model: User
|
58
|
+
end
|
59
|
+
|
60
|
+
class FindCustomer < SQLRunner::Query
|
61
|
+
query_name "find_user"
|
62
|
+
plugin :one
|
63
|
+
plugin model: Customer
|
64
|
+
end
|
65
|
+
|
66
|
+
result = SQLRunner.execute(
|
67
|
+
"select application_name from pg_stat_activity where pid = pg_backend_pid();"
|
68
|
+
)
|
69
|
+
p [:application_name, result.to_a]
|
70
|
+
|
71
|
+
result = SQLRunner.execute <<~SQL, name: "john", age: 18
|
72
|
+
select
|
73
|
+
'hello'::text as message,
|
74
|
+
:name::text as name,
|
75
|
+
:age::integer as age,
|
76
|
+
:name::text as name2
|
77
|
+
SQL
|
78
|
+
p [:select, result.to_a]
|
79
|
+
|
80
|
+
p [:delete_all_users, DeleteAllUsers.call]
|
81
|
+
p [:create_user, CreateUser.call(name: "Nando Vieira", email: "me@fnando.com")]
|
82
|
+
p [:create_user, CreateUser.call(name: "John Doe", email: "john@example.com")]
|
83
|
+
p [:numbers, Numbers.call]
|
84
|
+
p [:users, Users.call.to_a]
|
85
|
+
p [:find_user, FindUser.call(email: "me@fnando.com")]
|
86
|
+
p [:find_user, FindUser.call(email: "' OR 1=1 --me@fnando.com")]
|
87
|
+
p [:find_user, FindUser.call!(email: "me@fnando.com")]
|
88
|
+
p [:find_customer, FindCustomer.call!(email: "me@fnando.com")]
|
89
|
+
|
90
|
+
begin
|
91
|
+
FindUser.call!(email: "invalid@email")
|
92
|
+
rescue SQLRunner::RecordNotFound => error
|
93
|
+
p [:find_user, error]
|
94
|
+
end
|
95
|
+
|
96
|
+
SQLRunner.disconnect
|
data/examples/bench.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
$LOAD_PATH.push File.expand_path("#{__dir__}/../lib")
|
2
4
|
require "sql_runner"
|
3
5
|
require "virtus"
|
@@ -13,7 +15,9 @@ SQLRunner.pool = 25
|
|
13
15
|
SQLRunner.timeout = 10
|
14
16
|
SQLRunner.root_dir = "#{__dir__}/sql"
|
15
17
|
|
16
|
-
ActiveRecord::Base.establish_connection(
|
18
|
+
ActiveRecord::Base.establish_connection(
|
19
|
+
"#{connection_string}&prepared_statements=false&pool=25"
|
20
|
+
)
|
17
21
|
|
18
22
|
module Types
|
19
23
|
include Dry::Types.module
|
@@ -77,10 +81,22 @@ class Users < SQLRunner::Query
|
|
77
81
|
end
|
78
82
|
|
79
83
|
Benchmark.ips do |x|
|
80
|
-
x.report("activerecord - find one
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
+
x.report("activerecord - find one") do
|
85
|
+
User.find_by_email("me@fnando.com")
|
86
|
+
end
|
87
|
+
|
88
|
+
x.report(" sql_runner - find one (dry-types)") do
|
89
|
+
FindUserDry.call(email: "me@fnando.com")
|
90
|
+
end
|
91
|
+
|
92
|
+
x.report(" sql_runner - find one (virtus)") do
|
93
|
+
FindUserVirtus.call(email: "me@fnando.com")
|
94
|
+
end
|
95
|
+
|
96
|
+
x.report(" sql_runner - find one (raw)") do
|
97
|
+
FindUser.call(email: "me@fnando.com")
|
98
|
+
end
|
99
|
+
|
84
100
|
x.compare!
|
85
101
|
end
|
86
102
|
|
data/examples/profiling.rb
CHANGED
data/examples/test.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
$LOAD_PATH.push File.expand_path("#{__dir__}/../lib")
|
2
4
|
require "sql_runner"
|
3
5
|
require "virtus"
|
@@ -7,87 +9,4 @@ SQLRunner.pool = 25
|
|
7
9
|
SQLRunner.timeout = 10
|
8
10
|
SQLRunner.root_dir = "#{__dir__}/sql"
|
9
11
|
|
10
|
-
|
11
|
-
p result.to_a
|
12
|
-
|
13
|
-
result = SQLRunner.execute <<-SQL, name: "john", age: 18
|
14
|
-
select
|
15
|
-
'hello'::text as message,
|
16
|
-
:name::text as name,
|
17
|
-
:age::integer as age,
|
18
|
-
:name::text as name2
|
19
|
-
SQL
|
20
|
-
p result.to_a
|
21
|
-
|
22
|
-
class Users < SQLRunner::Query
|
23
|
-
end
|
24
|
-
|
25
|
-
module NumericModel
|
26
|
-
def self.new(attrs)
|
27
|
-
attrs.values.first.to_i
|
28
|
-
end
|
29
|
-
end
|
30
|
-
|
31
|
-
class Numbers < SQLRunner::Query
|
32
|
-
plugin model: NumericModel
|
33
|
-
plugin :many
|
34
|
-
|
35
|
-
query <<-SQL
|
36
|
-
SELECT n FROM generate_series(1, 10) n
|
37
|
-
SQL
|
38
|
-
end
|
39
|
-
|
40
|
-
class User
|
41
|
-
include Virtus.model
|
42
|
-
|
43
|
-
attribute :id, String
|
44
|
-
attribute :name, String
|
45
|
-
attribute :email, Integer
|
46
|
-
end
|
47
|
-
|
48
|
-
class Customer < User
|
49
|
-
end
|
50
|
-
|
51
|
-
class FindUser < SQLRunner::Query
|
52
|
-
plugins :one
|
53
|
-
plugin model: User
|
54
|
-
end
|
55
|
-
|
56
|
-
class FindAllUsers < SQLRunner::Query
|
57
|
-
plugins :many
|
58
|
-
plugin model: User
|
59
|
-
end
|
60
|
-
|
61
|
-
class CreateUser < SQLRunner::Query
|
62
|
-
plugin :one
|
63
|
-
plugin model: User
|
64
|
-
end
|
65
|
-
|
66
|
-
class DeleteAllUsers < SQLRunner::Query
|
67
|
-
plugin :many
|
68
|
-
plugin model: User
|
69
|
-
end
|
70
|
-
|
71
|
-
class FindCustomer < SQLRunner::Query
|
72
|
-
query_name "find_user"
|
73
|
-
plugin model: Customer
|
74
|
-
plugins :one
|
75
|
-
end
|
76
|
-
|
77
|
-
p DeleteAllUsers.call
|
78
|
-
p CreateUser.call(name: "Nando Vieira", email: "me@fnando.com")
|
79
|
-
p CreateUser.call(name: "John Doe", email: "john@example.com")
|
80
|
-
p Numbers.call
|
81
|
-
p Users.call.to_a
|
82
|
-
p FindUser.call(email: "me@fnando.com")
|
83
|
-
p FindUser.call(email: "' OR 1=1 --me@fnando.com")
|
84
|
-
p FindUser.call!(email: "me@fnando.com")
|
85
|
-
p FindCustomer.call!(email: "me@fnando.com")
|
86
|
-
|
87
|
-
begin
|
88
|
-
FindUser.call!(email: "me@fnando.coms")
|
89
|
-
rescue SQLRunner::RecordNotFound => error
|
90
|
-
p error
|
91
|
-
end
|
92
|
-
|
93
|
-
SQLRunner.disconnect
|
12
|
+
require_relative "base"
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
$LOAD_PATH.push File.expand_path("#{__dir__}/../lib")
|
4
|
+
require "sql_runner"
|
5
|
+
require "virtus"
|
6
|
+
require "active_record"
|
7
|
+
|
8
|
+
ActiveRecord::Base.establish_connection(
|
9
|
+
"postgres:///test?connect_timeout=2&application_name=myapp"
|
10
|
+
)
|
11
|
+
|
12
|
+
SQLRunner.connect "activerecord:///"
|
13
|
+
SQLRunner.pool = 25
|
14
|
+
SQLRunner.timeout = 10
|
15
|
+
SQLRunner.root_dir = "#{__dir__}/sql"
|
16
|
+
|
17
|
+
require_relative "base"
|
@@ -0,0 +1,79 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SQLRunner
|
4
|
+
module Adapters
|
5
|
+
class ActiveRecord
|
6
|
+
class PostgreSQL < SQLRunner::Adapters::PostgreSQL
|
7
|
+
def initialize(connection) # rubocop:disable Lint/MissingSuper
|
8
|
+
@connection = connection
|
9
|
+
end
|
10
|
+
|
11
|
+
def connect(*)
|
12
|
+
end
|
13
|
+
|
14
|
+
def disconnect(*)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
class MySQL < SQLRunner::Adapters::MySQL
|
19
|
+
def initialize(connection) # rubocop:disable Lint/MissingSuper
|
20
|
+
@connection = connection
|
21
|
+
end
|
22
|
+
|
23
|
+
def connect(*)
|
24
|
+
end
|
25
|
+
|
26
|
+
def disconnect(*)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
class SQLite < SQLRunner::Adapters::SQLite
|
31
|
+
def initialize(connection) # rubocop:disable Lint/MissingSuper
|
32
|
+
@connection = connection
|
33
|
+
end
|
34
|
+
|
35
|
+
def connect(*)
|
36
|
+
end
|
37
|
+
|
38
|
+
def disconnect(*)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
class ConnectionPool
|
43
|
+
def with
|
44
|
+
::ActiveRecord::Base.connection_pool.with_connection do |connection|
|
45
|
+
connection = connection.instance_variable_get(:@connection)
|
46
|
+
|
47
|
+
adapter = case connection.class.name
|
48
|
+
when "PG::Connection"
|
49
|
+
PostgreSQL.new(connection)
|
50
|
+
when "Mysql2::Client"
|
51
|
+
MySQL.new(connection)
|
52
|
+
when "SQLite3::Database"
|
53
|
+
SQLite.new(connection)
|
54
|
+
else
|
55
|
+
raise UnsupportedDatabase,
|
56
|
+
"#{connection.class.name} is not yet supported " \
|
57
|
+
"by the SQLRunner's ActiveRecord adapter"
|
58
|
+
end
|
59
|
+
|
60
|
+
yield(adapter)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def shutdown
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def self.load
|
69
|
+
require "active_record"
|
70
|
+
rescue LoadError
|
71
|
+
raise MissingDependency, "make sure the `activerecord` gem is available"
|
72
|
+
end
|
73
|
+
|
74
|
+
def self.create_connection_pool(*)
|
75
|
+
ConnectionPool.new
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SQLRunner
|
4
|
+
module Adapters
|
5
|
+
class MySQL
|
6
|
+
InvalidPreparedStatement = Class.new(StandardError)
|
7
|
+
|
8
|
+
def self.load
|
9
|
+
require "mysql2"
|
10
|
+
rescue LoadError
|
11
|
+
raise MissingDependency, "make sure the `mysql2` gem is available"
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.create_connection_pool(timeout:, size:, connection_string:)
|
15
|
+
ConnectionPool.new(timeout: timeout, size: size) do
|
16
|
+
new(connection_string)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def initialize(connection_string)
|
21
|
+
@connection_string = connection_string
|
22
|
+
@uri = URI.parse(@connection_string)
|
23
|
+
connect
|
24
|
+
end
|
25
|
+
|
26
|
+
def connect(started = Process.clock_gettime(Process::CLOCK_MONOTONIC))
|
27
|
+
@connection = Mysql2::Client.new(
|
28
|
+
host: @uri.host,
|
29
|
+
port: @uri.port,
|
30
|
+
username: @uri.user,
|
31
|
+
password: @uri.password,
|
32
|
+
database: @uri.path[1..-1]
|
33
|
+
)
|
34
|
+
rescue Mysql2::Error
|
35
|
+
ended = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
36
|
+
|
37
|
+
raise unless ended - started < SQLRunner.timeout
|
38
|
+
|
39
|
+
sleep 0.1
|
40
|
+
connect(started)
|
41
|
+
end
|
42
|
+
|
43
|
+
def disconnect
|
44
|
+
@connection&.close && (@connection = nil)
|
45
|
+
end
|
46
|
+
|
47
|
+
def reconnect
|
48
|
+
disconnect
|
49
|
+
connect
|
50
|
+
end
|
51
|
+
|
52
|
+
def execute(query, **bind_vars)
|
53
|
+
bound_query, bindings, names = parse(query, bind_vars)
|
54
|
+
validate_bindings(query, bind_vars, names)
|
55
|
+
|
56
|
+
statement = @connection.prepare(bound_query)
|
57
|
+
statement.execute(
|
58
|
+
*bindings,
|
59
|
+
cast: true,
|
60
|
+
as: :hash,
|
61
|
+
cast_booleans: true
|
62
|
+
)
|
63
|
+
end
|
64
|
+
|
65
|
+
def active?
|
66
|
+
!@connection&.closed?
|
67
|
+
rescue Mysql2::Error
|
68
|
+
false
|
69
|
+
end
|
70
|
+
|
71
|
+
def to_s
|
72
|
+
%[#<#{self.class.name} #{format('0x00%x', (object_id << 1))}>]
|
73
|
+
end
|
74
|
+
|
75
|
+
def inspect
|
76
|
+
to_s
|
77
|
+
end
|
78
|
+
|
79
|
+
def parse(query, bind_vars)
|
80
|
+
bindings = []
|
81
|
+
names = []
|
82
|
+
|
83
|
+
parsed_query = query.gsub(/(:?):([a-zA-Z]\w*)/) do |match|
|
84
|
+
next match if Regexp.last_match(1) == ":" # skip type casting
|
85
|
+
|
86
|
+
name = match[1..-1]
|
87
|
+
sym_name = name.to_sym
|
88
|
+
names << sym_name
|
89
|
+
bindings << bind_vars[sym_name]
|
90
|
+
|
91
|
+
"?"
|
92
|
+
end
|
93
|
+
|
94
|
+
[parsed_query, bindings, names]
|
95
|
+
end
|
96
|
+
|
97
|
+
private def validate_bindings(query, bind_vars, names)
|
98
|
+
names.each do |name|
|
99
|
+
next if bind_vars.key?(name)
|
100
|
+
|
101
|
+
raise InvalidPreparedStatement,
|
102
|
+
"missing value for :#{name} in #{query}"
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|