sql_runner 0.1.0 → 0.4.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 0fe73046f6047d45f926dcc2a2dfe481698d6c27
4
- data.tar.gz: fdabc17b0da986f3e0f1676dbc35d1b74ca0510c
2
+ SHA256:
3
+ metadata.gz: d8ae856dccc72baadf453eab3f66fc6b375757e81d042e309de58b80a3794c22
4
+ data.tar.gz: bc6cdefb9735e36c972e0ae23fc062e746c6ca44f981444fb604fd9e3db388d5
5
5
  SHA512:
6
- metadata.gz: 539aa4ac64c12629feb865ffde679507bc00a0951353261c42fbd4fcf547228593d2bc0675cf792c94f065ff908c9b8eb1f8239f673f883ff07d2c4789fa73aa
7
- data.tar.gz: 014fa997673cd75e241ca74e667d7f3fb31b7b40acb1a044c0f983272cd7543e8b61e31bbdebb092b362ae0771c73109776d491608981a47019bec73c28059ce
6
+ metadata.gz: 1651fa64b28884dc534d878114bfab992f7d592b30c4b86cb518c24bfb63685795b61bb792db072ce07f7ea9495310b73196d5763b4250f3ea6d2fe7346efb3f
7
+ data.tar.gz: b660ace92f4e0446c8f583505a4d2da82f4854b53cdce460202d83086dd968e5a2a89af8953c484a12dfa023e12dfaa4ac68c38bb9ddb4e78f3144e27bf703bf
@@ -0,0 +1,3 @@
1
+ ---
2
+ github: [fnando]
3
+ custom: ["https://www.paypal.me/nandovieira/🍕"]
data/.gitignore CHANGED
@@ -1,6 +1,6 @@
1
1
  /.bundle/
2
2
  /.yardoc
3
- /Gemfile.lock
3
+ Gemfile.lock
4
4
  /_yardoc/
5
5
  /coverage/
6
6
  /doc/
@@ -8,3 +8,4 @@
8
8
  /spec/reports/
9
9
  /tmp/
10
10
  /examples/**/*.html
11
+ *.db
data/.rubocop.yml ADDED
@@ -0,0 +1,16 @@
1
+ ---
2
+ inherit_gem:
3
+ rubocop-fnando: .rubocop.yml
4
+
5
+ AllCops:
6
+ TargetRubyVersion: 2.6
7
+
8
+ Metrics/MethodLength:
9
+ Enabled: false
10
+
11
+ Metrics/AbcSize:
12
+ Enabled: false
13
+
14
+ Style/OpenStructUse:
15
+ Exclude:
16
+ - test/**/*.rb
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.3.1
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: S5CYGmnmHV6FE/P0W2H7sEJwR3jGcuW97wIzNLBSDkl1Nvmc2NAw6WynoqOGSEVafmauVjX+w+gOkF2UgJImBYwDjwGmSVcKSPj1SWuuT648pq5mJjW4kyS/lKnA8PaiqS1nMVAbGHtZjH3cEG0w0oFPilDN7nsCCzm4kztkRWtZgHhGPc/UU0EYjsGkUVq3gsYFNm09dzMxzmg9U8jdOLUM354mxP2FKqBQ795/HkEXtb5mGSKnSBWB4jdk9enmBe/M6/Vvo9X98drz4GRVdHpvy5LvWPwYSvwKNprErPcIasLxONOUXTEzA45/RSJZBLUuGlrNu7f9tA9JNbrUaVRp+vnhRpue5nNYIowPUVOnjxfE/qgx3X4k5K26pbO6wKfhdDYRK6Qh85rYNuHpd0caiEoTJJlXQp6sTo/wKQI5p9BF4b3FRG1RtCoMbjLFrQin4l0JwILoAyGyTbk50XdQvwDDlFs1lGiGxe0AK0lxX6s5yqdykaXkx5qwTSuWZqFc+9+bAEy6TNzEqBuT+r1mxIfVep8bNxynDLRg26NibGAFHzMHuqgEM4mLeOUkKnBDv1LghRJ0fzhroINJHAj5KBp145r6iJSVR7xRjKJpQptpIZ3asi5Xl37sQb98+p+1Pg4Wwzn9Y8lEyx49lml7oppDvabcbOCTy0LFuuk=
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
@@ -1,2 +1,4 @@
1
+ # frozen_string_literal: true
2
+
1
3
  source "https://rubygems.org"
2
4
  gemspec
data/README.md CHANGED
@@ -1,12 +1,13 @@
1
1
  # SQLRunner
2
2
 
3
- [![Travis-CI](https://travis-ci.org/fnando/sql_runner.png)](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. Available only for PostgreSQL.
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 `.activate(target, options)` class method. The following example overrides the `call(**bind_vars)` method by using `Module.prepend`.
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
- # Register the plugin.
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 you plugin can receive options, you can call it as `plugin reverse: options`, where `options` can be anything (e.g. `Hash`, `Array`, `Object`, etc).
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. These are the results against ActiveRecord using different wrapping libraries like [virtus](https://rubygems.org/gems/virtus) and [dry-types](https://rubygems.org/gems/dry-types).
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 `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
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 release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
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 https://github.com/fnando/sql_runner. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
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 [MIT License](http://opensource.org/licenses/MIT).
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['test/**/*_test.rb']
10
+ t.test_files = FileList["test/**/*_test.rb"]
8
11
  t.warning = false
9
12
  end
10
13
 
11
- task :default => :test
14
+ RuboCop::RakeTask.new
15
+
16
+ task default: %i[test rubocop]
data/bin/console CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
2
3
 
3
4
  require "bundler/setup"
4
5
  require "sql_runner"
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("#{connection_string}&prepared_statements=false&pool=25")
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 ") { User.find_by_email("me@fnando.com") }
81
- x.report(" sql_runner - find one (dry-types)") { FindUserDry.call(email: "me@fnando.com") }
82
- x.report(" sql_runner - find one (virtus) ") { FindUserVirtus.call(email: "me@fnando.com") }
83
- x.report(" sql_runner - find one (raw) ") { FindUser.call(email: "me@fnando.com") }
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
 
@@ -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 "ruby-prof"
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
- result = SQLRunner.execute "select application_name from pg_stat_activity where pid = pg_backend_pid();"
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