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 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