ensql 0.6.1 → 0.6.2
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/CHANGELOG.md +8 -0
- data/Gemfile.lock +3 -1
- data/README.md +33 -14
- data/ensql.gemspec +2 -0
- data/gemfiles/maintained.gemfile.lock +4 -2
- data/gemfiles/minimum.gemfile +2 -1
- data/gemfiles/minimum.gemfile.lock +6 -3
- data/lib/ensql.rb +20 -10
- data/lib/ensql/active_record_adapter.rb +59 -29
- data/lib/ensql/pool_wrapper.rb +22 -0
- data/lib/ensql/postgres_adapter.rb +159 -0
- data/lib/ensql/sequel_adapter.rb +54 -21
- data/lib/ensql/sql.rb +1 -0
- data/lib/ensql/version.rb +3 -1
- data/perf/adapter_benchmark.rb +99 -0
- metadata +25 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 926a86bf4e2b069dfee856bc77567048cca9f5e621d48e255fe3d5f1c041a42d
|
4
|
+
data.tar.gz: 294b707b6a8d8394ba12eafe20298bb800ab86584f354bacfe8cbdef66581531
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: fd0ed7157a3c0dd791a3635af47e7fb58ec54ab770e38eb1765e224e4674c4065278af952d74f142db42242b4ab7bb2cfb23f89eefbdb39ec3a5af44a3d7867a
|
7
|
+
data.tar.gz: 34a692c5053ab53c916faf15522d218365e926fd56aa4bf814eac547110b554b996c5ef6bfed09b76136b43705b00e438ad67508e30814b9734bd6bb6b53d07f
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,13 @@
|
|
1
1
|
# Change Log
|
2
2
|
|
3
|
+
## [0.6.2] - 2021-03-09
|
4
|
+
|
5
|
+
- Adds a specialised adapter for PostgreSQL.
|
6
|
+
- Uses instances instead of modules for SequelAdapter and ActiveRecordAdapter. The use of the (now) classes as adapters is deprecated.
|
7
|
+
- Adds connection pool wrappers for ActiveRecord and Sequel.
|
8
|
+
- Ensures SQL#each_row returns nil.
|
9
|
+
- Makes adapter attribute thread-safe.
|
10
|
+
|
3
11
|
## [0.6.1] - 2021-02-25
|
4
12
|
|
5
13
|
- Enables the use of streaming with the SequelAdapter
|
data/Gemfile.lock
CHANGED
@@ -1,7 +1,8 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
ensql (0.6.
|
4
|
+
ensql (0.6.2)
|
5
|
+
connection_pool (>= 0.9.3, < 3)
|
5
6
|
|
6
7
|
GEM
|
7
8
|
remote: https://rubygems.org/
|
@@ -18,6 +19,7 @@ GEM
|
|
18
19
|
tzinfo (~> 2.0)
|
19
20
|
zeitwerk (~> 2.3)
|
20
21
|
concurrent-ruby (1.1.8)
|
22
|
+
connection_pool (2.2.3)
|
21
23
|
diff-lcs (1.4.4)
|
22
24
|
docile (1.3.5)
|
23
25
|
i18n (1.8.9)
|
data/README.md
CHANGED
@@ -68,19 +68,33 @@ Or install it manually with:
|
|
68
68
|
|
69
69
|
Ensql requires:
|
70
70
|
|
71
|
-
*
|
72
|
-
*
|
73
|
-
*
|
71
|
+
* ruby >= 2.4.0
|
72
|
+
* sequel >= 5.9 if using SequelAdapter
|
73
|
+
* activerecord >= 5.0 if using ActiveRecordAdapter
|
74
|
+
* pg >= 0.19 if using PostgresAdapter
|
74
75
|
|
75
76
|
## Usage
|
76
77
|
|
77
78
|
Typically, you don't need to configure anything. Ensql will look for Sequel or ActiveRecord (in that order) and load the
|
78
|
-
appropriate adapter. You can override this if
|
79
|
-
|
79
|
+
appropriate adapter. You can override this if the wrong adapter is autoconfigured, or if you're using PostgreSQL and
|
80
|
+
want to use the much faster and more convenient PostgresAdapter.
|
80
81
|
|
81
82
|
```ruby
|
82
|
-
|
83
|
+
# Use ActiveRecord instead of Sequel if both are available
|
84
|
+
Ensql.adapter = Ensql::ActiveRecordAdapter.new
|
85
|
+
|
86
|
+
# Use the PostgreSQL specific adapter, with ActiveRecord's connection pool
|
87
|
+
Ensql.adapter = Ensql::PostgresAdapter.new Ensql::ActiveRecordAdapter.pool
|
88
|
+
|
89
|
+
# Use the PostgreSQL specific adapter, with Sequel's connection pool
|
90
|
+
DB = Sequel.connect(ENV['DATABASE_URL'])
|
91
|
+
Ensql.adapter = Ensql::PostgresAdapter.new Ensql::SequelAdapter.pool(DB)
|
92
|
+
|
93
|
+
# Use the PostgreSQL specific adapter, with our own thread-safe connection pool
|
94
|
+
Ensql.adapter = Ensql::PostgresAdapter.pool { PG.connect ENV['DATABASE_URL'] }
|
83
95
|
```
|
96
|
+
You can also supply your own adapter (see [the API docs](https://rubydoc.info/gems/ensql/Ensql/Adapter) for details of the interface).
|
97
|
+
|
84
98
|
|
85
99
|
SQL can be supplied directly or read from a file. You're encouraged to organise all but the most trivial statements in
|
86
100
|
their own *.sql files, for the reasons outlined above. You can organise them in whatever way makes most sense for your
|
@@ -178,21 +192,26 @@ Ensql.run('TRUNCATE logs') # same thing
|
|
178
192
|
- Maybe we could use type hinting like `%{param:pgarray}` to indicated how to serialise the object as a literal.
|
179
193
|
|
180
194
|
- Detecting the database and switching to a db specific adapters. This allows us to be more efficient and optimise some
|
181
|
-
literals in a database specific format, e.g.
|
195
|
+
literals in a database specific format, e.g. PostgreSQL array literals.
|
182
196
|
|
183
|
-
-
|
184
|
-
|
185
|
-
- Establishing connections directly.
|
197
|
+
- Proper single-row mode support for the pg adapter
|
186
198
|
|
187
199
|
## Development
|
188
200
|
|
189
201
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You'll
|
190
|
-
need a running
|
202
|
+
need a running PostgreSQL database. You can also run `bin/console` for an interactive prompt that will allow you to
|
191
203
|
experiment.
|
192
204
|
|
193
|
-
To install this gem onto your local machine, run `bundle exec rake install`.
|
194
|
-
|
195
|
-
|
205
|
+
To install this gem onto your local machine, run `bundle exec rake install`.
|
206
|
+
|
207
|
+
### Release Checklist
|
208
|
+
|
209
|
+
- [ ] Review changes in master since last release, especially the public API.
|
210
|
+
- [ ] Ensure documentation is up to date.
|
211
|
+
- [ ] Bump appropriate part of version in `version.rb`.
|
212
|
+
- [ ] Update the spec version in each `.lock`.
|
213
|
+
- [ ] Update `Changelog.md` with summary of new version.
|
214
|
+
- [ ] Run `rake release` to create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
196
215
|
|
197
216
|
## Contributing
|
198
217
|
|
data/ensql.gemspec
CHANGED
@@ -29,6 +29,8 @@ Gem::Specification.new do |spec|
|
|
29
29
|
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
30
30
|
spec.require_paths = ["lib"]
|
31
31
|
|
32
|
+
spec.add_dependency "connection_pool", ">= 0.9.3", "<3"
|
33
|
+
|
32
34
|
spec.add_development_dependency "rake", "~> 13.0"
|
33
35
|
spec.add_development_dependency "rspec", "~> 3.0"
|
34
36
|
spec.add_development_dependency "simplecov", "~> 0.21.2"
|
@@ -1,7 +1,8 @@
|
|
1
1
|
PATH
|
2
2
|
remote: ..
|
3
3
|
specs:
|
4
|
-
ensql (0.6.
|
4
|
+
ensql (0.6.2)
|
5
|
+
connection_pool (>= 0.9.3, < 3)
|
5
6
|
|
6
7
|
GEM
|
7
8
|
remote: https://rubygems.org/
|
@@ -19,6 +20,7 @@ GEM
|
|
19
20
|
tzinfo (~> 1.1)
|
20
21
|
arel (9.0.0)
|
21
22
|
concurrent-ruby (1.1.8)
|
23
|
+
connection_pool (2.2.3)
|
22
24
|
diff-lcs (1.4.4)
|
23
25
|
docile (1.3.5)
|
24
26
|
i18n (1.8.9)
|
@@ -39,7 +41,7 @@ GEM
|
|
39
41
|
diff-lcs (>= 1.2.0, < 2.0)
|
40
42
|
rspec-support (~> 3.10.0)
|
41
43
|
rspec-support (3.10.2)
|
42
|
-
sequel (5.
|
44
|
+
sequel (5.42.0)
|
43
45
|
sequel_pg (1.14.0)
|
44
46
|
pg (>= 0.18.0, != 1.2.0)
|
45
47
|
sequel (>= 4.38.0)
|
data/gemfiles/minimum.gemfile
CHANGED
@@ -13,6 +13,7 @@ gemspec path: '../'
|
|
13
13
|
|
14
14
|
# Downgrade simplecov for ruby 2.4 compat
|
15
15
|
gem 'simplecov', '~> 0.18.5'
|
16
|
+
gem 'connection_pool', '0.9.3'
|
16
17
|
|
17
18
|
# Optional runtime dependencies
|
18
19
|
group :adapters do
|
@@ -20,6 +21,6 @@ group :adapters do
|
|
20
21
|
gem "activerecord", Ensql::SUPPORTED_ACTIVERECORD_VERSIONS.to_s.scan(/\d+.\d+/).first
|
21
22
|
gem "sequel", Ensql::SUPPORTED_SEQUEL_VERSIONS.to_s.scan(/\d+.\d+/).first
|
22
23
|
gem "sqlite3", "~> 1.3.6" # AR version constraint
|
23
|
-
gem "pg",
|
24
|
+
gem "pg", Ensql::SUPPORTED_PG_VERSIONS.to_s.scan(/\d+.\d+/).first
|
24
25
|
gem "sequel_pg"
|
25
26
|
end
|
@@ -1,7 +1,8 @@
|
|
1
1
|
PATH
|
2
2
|
remote: ..
|
3
3
|
specs:
|
4
|
-
ensql (0.6.
|
4
|
+
ensql (0.6.2)
|
5
|
+
connection_pool (>= 0.9.3, < 3)
|
5
6
|
|
6
7
|
GEM
|
7
8
|
remote: https://rubygems.org/
|
@@ -19,12 +20,13 @@ GEM
|
|
19
20
|
tzinfo (~> 1.1)
|
20
21
|
arel (7.1.4)
|
21
22
|
concurrent-ruby (1.1.8)
|
23
|
+
connection_pool (0.9.3)
|
22
24
|
diff-lcs (1.4.4)
|
23
25
|
docile (1.3.5)
|
24
26
|
i18n (0.9.5)
|
25
27
|
concurrent-ruby (~> 1.0)
|
26
28
|
minitest (5.14.4)
|
27
|
-
pg (0.
|
29
|
+
pg (0.19.0)
|
28
30
|
rake (13.0.3)
|
29
31
|
rspec (3.10.0)
|
30
32
|
rspec-core (~> 3.10.0)
|
@@ -59,8 +61,9 @@ PLATFORMS
|
|
59
61
|
|
60
62
|
DEPENDENCIES
|
61
63
|
activerecord (= 5.0)
|
64
|
+
connection_pool (= 0.9.3)
|
62
65
|
ensql!
|
63
|
-
pg (
|
66
|
+
pg (= 0.19)
|
64
67
|
rake (~> 13.0)
|
65
68
|
rspec (~> 3.0)
|
66
69
|
sequel (= 5.9)
|
data/lib/ensql.rb
CHANGED
@@ -76,30 +76,40 @@ module Ensql
|
|
76
76
|
SQL.new(sql, params).run
|
77
77
|
end
|
78
78
|
|
79
|
-
#
|
80
|
-
#
|
81
|
-
#
|
79
|
+
# Get the current connection adapter. If not specified, it will try to
|
80
|
+
# autoload an adapter based on the availability of Sequel or ActiveRecord,
|
81
|
+
# in that order.
|
82
82
|
#
|
83
83
|
# @example
|
84
84
|
# require 'sequel'
|
85
|
-
# Ensql.adapter # => Ensql::SequelAdapter
|
86
|
-
# Ensql.adapter = Ensql::ActiveRecordAdapter # override adapter
|
87
|
-
# Ensql.adapter =
|
85
|
+
# Ensql.adapter # => Ensql::SequelAdapter.new
|
86
|
+
# Ensql.adapter = Ensql::ActiveRecordAdapter.new # override adapter
|
87
|
+
# Ensql.adapter = my_tsql_adapter # supply your own adapter
|
88
88
|
#
|
89
89
|
def adapter
|
90
|
-
|
90
|
+
Thread.current[:ensql_adapter] || Thread.main[:ensql_adapter] ||= autoload_adapter
|
91
|
+
end
|
92
|
+
|
93
|
+
# Set the connection adapter to use. Must implement the interface defined in
|
94
|
+
# {Ensql::Adapter}. This uses a thread-local variable so adapters can be
|
95
|
+
# switched safely in a multi-threaded web server.
|
96
|
+
def adapter=(adapter)
|
97
|
+
if adapter.is_a?(Module) && (adapter.name == 'Ensql::SequelAdapter' || adapter.name == 'Ensql::ActiveRecordAdapter')
|
98
|
+
warn "Using `#{adapter}` as an adapter is deprecated, use `#{adapter}.new`.", uplevel: 1
|
99
|
+
end
|
100
|
+
|
101
|
+
Thread.current[:ensql_adapter] = adapter
|
91
102
|
end
|
92
|
-
attr_writer :adapter
|
93
103
|
|
94
104
|
private
|
95
105
|
|
96
106
|
def autoload_adapter
|
97
107
|
if defined? Sequel
|
98
108
|
require_relative 'ensql/sequel_adapter'
|
99
|
-
SequelAdapter
|
109
|
+
SequelAdapter.new
|
100
110
|
elsif defined? ActiveRecord
|
101
111
|
require_relative 'ensql/active_record_adapter'
|
102
|
-
ActiveRecordAdapter
|
112
|
+
ActiveRecordAdapter.new
|
103
113
|
else
|
104
114
|
raise Error, "Couldn't autodetect an adapter, please specify manually."
|
105
115
|
end
|
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
require_relative 'version'
|
4
4
|
require_relative 'adapter'
|
5
|
+
require_relative 'pool_wrapper'
|
5
6
|
|
6
7
|
# Ensure our optional dependency has a compatible version
|
7
8
|
gem 'activerecord', Ensql::SUPPORTED_ACTIVERECORD_VERSIONS
|
@@ -9,30 +10,60 @@ require 'active_record'
|
|
9
10
|
|
10
11
|
module Ensql
|
11
12
|
#
|
12
|
-
#
|
13
|
-
# ActiveRecord connection to be configured and
|
14
|
-
#
|
13
|
+
# Wraps an ActiveRecord connection pool to implement the {Adapter} interface
|
14
|
+
# for ActiveRecord. Requires an ActiveRecord connection to be configured and
|
15
|
+
# established. By default, uses the connection pool on ActiveRecord::Base.
|
16
|
+
# Other pools can be passed to the constructor.
|
15
17
|
#
|
16
18
|
# @example
|
17
19
|
# require 'active_record'
|
18
20
|
# ActiveRecord::Base.establish_connection(adapter: 'postgresql', database: 'mydb')
|
19
|
-
# Ensql.adapter = Ensql::ActiveRecordAdapter
|
21
|
+
# Ensql.adapter = Ensql::ActiveRecordAdapter.new
|
22
|
+
# # Use database configuration for the Widget model instead
|
23
|
+
# Ensql.adapter = Ensql::ActiveRecordAdapter.new(Widget)
|
20
24
|
#
|
21
25
|
# @see SUPPORTED_ACTIVERECORD_VERSIONS
|
22
26
|
#
|
23
|
-
|
24
|
-
|
27
|
+
class ActiveRecordAdapter
|
28
|
+
include Adapter
|
25
29
|
|
26
|
-
#
|
27
|
-
|
30
|
+
# Wrap the raw connections from an Active Record connection pool. This
|
31
|
+
# allows us to safely checkout the underlying database connection for use in
|
32
|
+
# a database specific adapter.
|
33
|
+
#
|
34
|
+
# Ensql.adapter = MySqliteAdapter.new(ActiveRecordAdapter.pool)
|
35
|
+
#
|
36
|
+
# @param base [Class] an ActiveRecord class to source connections from
|
37
|
+
# @return [PoolWrapper] a pool adapter for raw connections
|
38
|
+
def self.pool(base = ActiveRecord::Base)
|
39
|
+
PoolWrapper.new do |client_block|
|
40
|
+
base.connection_pool.with_connection { |connection| client_block.call connection.raw_connection }
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# Support deprecated class method interface
|
45
|
+
class << self
|
46
|
+
require 'forwardable'
|
47
|
+
extend Forwardable
|
48
|
+
|
49
|
+
delegate [:literalize, :run, :fetch_count, :fetch_each_row, :fetch_rows, :fetch_first_column, :fetch_first_field, :fetch_first_row] => :new
|
50
|
+
end
|
51
|
+
|
52
|
+
# @param base [Class] an ActiveRecord class to source connections from
|
53
|
+
def initialize(base = ActiveRecord::Base)
|
54
|
+
@base = base
|
55
|
+
end
|
56
|
+
|
57
|
+
# @visibility private
|
58
|
+
def fetch_rows(sql)
|
28
59
|
fetch_each_row(sql).to_a
|
29
60
|
end
|
30
61
|
|
31
|
-
#
|
32
|
-
def
|
62
|
+
# @visibility private
|
63
|
+
def fetch_each_row(sql, &block)
|
33
64
|
return to_enum(:fetch_each_row, sql) unless block_given?
|
34
65
|
|
35
|
-
result =
|
66
|
+
result = with_connection { |c| c.exec_query(sql) }
|
36
67
|
# AR populates `column_types` with the types of any columns that haven't
|
37
68
|
# already been type casted by pg decoders. If present, we need to
|
38
69
|
# deserialize them now.
|
@@ -43,32 +74,31 @@ module Ensql
|
|
43
74
|
end
|
44
75
|
end
|
45
76
|
|
46
|
-
#
|
47
|
-
def
|
48
|
-
|
77
|
+
# @visibility private
|
78
|
+
def run(sql)
|
79
|
+
with_connection { |c| c.execute(sql) }
|
80
|
+
nil
|
49
81
|
end
|
50
82
|
|
51
|
-
#
|
52
|
-
def
|
53
|
-
|
83
|
+
# @visibility private
|
84
|
+
def fetch_count(sql)
|
85
|
+
with_connection { |c| c.exec_update(sql) }
|
54
86
|
end
|
55
87
|
|
56
|
-
#
|
57
|
-
def
|
58
|
-
|
88
|
+
# @visibility private
|
89
|
+
def literalize(value)
|
90
|
+
with_connection { |c| c.quote(value) }
|
59
91
|
end
|
60
92
|
|
61
|
-
|
62
|
-
ActiveRecord::Base.connection
|
63
|
-
end
|
93
|
+
private
|
64
94
|
|
65
|
-
def
|
66
|
-
|
67
|
-
hash[column] = column_types[column]&.deserialize(value) || value
|
68
|
-
}
|
95
|
+
def with_connection(&block)
|
96
|
+
@base.connection_pool.with_connection(&block)
|
69
97
|
end
|
70
98
|
|
71
|
-
|
72
|
-
|
99
|
+
def deserialize_types(row, column_types)
|
100
|
+
column_types.each { |column, type| row[column] = type.deserialize(row[column]) }
|
101
|
+
row
|
102
|
+
end
|
73
103
|
end
|
74
104
|
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Ensql
|
4
|
+
# Wrap a 3rd-party connection pool with a standard interface. Connections can be checked out by {with}
|
5
|
+
class PoolWrapper
|
6
|
+
|
7
|
+
# Wraps a block for accessing a connection from a pool.
|
8
|
+
#
|
9
|
+
# PoolWrapper.new do |client_block|
|
10
|
+
# my_connection_pool.with_connection(&client_block)
|
11
|
+
# end
|
12
|
+
def initialize(&connection_block)
|
13
|
+
@connection_block = connection_block
|
14
|
+
end
|
15
|
+
|
16
|
+
# Get a connection from our source pool
|
17
|
+
# @yield [connection] the database-specific connection
|
18
|
+
def with(&client_block)
|
19
|
+
@connection_block.call(client_block)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,159 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'version'
|
4
|
+
require_relative 'adapter'
|
5
|
+
|
6
|
+
gem 'pg', Ensql::SUPPORTED_PG_VERSIONS
|
7
|
+
require 'pg'
|
8
|
+
require 'connection_pool'
|
9
|
+
|
10
|
+
module Ensql
|
11
|
+
# Wraps a pool of PG connections to implement the {Adapter} interface. The
|
12
|
+
# adapter can use a 3rd-party pool (e.g. from ActiveRecord of Sequel) or
|
13
|
+
# manage its own using the simple
|
14
|
+
# [connection_pool gem](https://github.com/mperham/connection_pool).
|
15
|
+
#
|
16
|
+
# This adapter is much faster and offers much better PostgreSQL specific
|
17
|
+
# parameter interpolation than the framework adapters.
|
18
|
+
#
|
19
|
+
# @example
|
20
|
+
# # Use with ActiveRecord's connection pool
|
21
|
+
# Ensql.adapter = Ensql::PostgresAdapter.new(Ensql::ActiveRecordAdapter.pool)
|
22
|
+
#
|
23
|
+
# # Use with Sequel's connection pool
|
24
|
+
# DB = Sequel.connect(ENV['DATABASE_URL'])
|
25
|
+
# Ensql.adapter = Ensql::PostgresAdapter.new(Ensql::SequelAdapter.pool(DB))
|
26
|
+
#
|
27
|
+
# # Use with our own thread-safe connection pool
|
28
|
+
# Ensql.adapter = Ensql::PostgresAdapter.pool { PG.connect ENV['DATABASE_URL'] }
|
29
|
+
# Ensql.adapter = Ensql::PostgresAdapter.pool(size: 5) { PG.connect ENV['DATABASE_URL'] }
|
30
|
+
#
|
31
|
+
# @see SUPPORTED_PG_VERSIONS
|
32
|
+
#
|
33
|
+
class PostgresAdapter
|
34
|
+
include Adapter
|
35
|
+
|
36
|
+
# Set up a connection pool using the supplied block to initialise connections.
|
37
|
+
#
|
38
|
+
# PostgresAdapter.pool(size: 20) { PG.connect ENV['DATABASE_URL'] }
|
39
|
+
#
|
40
|
+
# @param pool_opts are sent straight to the ConnectionPool initializer.
|
41
|
+
# @option pool_opts [Integer] timeout (5) number of seconds to wait for a connection if none currently available.
|
42
|
+
# @option pool_opts [Integer] size (5) number of connections to pool.
|
43
|
+
# @yieldreturn [PG::Connection] a new connection.
|
44
|
+
def self.pool(**pool_opts, &connection_block)
|
45
|
+
new ConnectionPool.new(**pool_opts, &connection_block)
|
46
|
+
end
|
47
|
+
|
48
|
+
# @param pool [PoolWrapper, ConnectionPool, #with] a object that yields a PG::Connection using `#with`
|
49
|
+
def initialize(pool)
|
50
|
+
@pool = pool
|
51
|
+
@quoter = PG::TextEncoder::QuotedLiteral.new
|
52
|
+
@result_type_map = @pool.with { |c| PG::BasicTypeMapForResults.new(c) }
|
53
|
+
@query_type_map = @pool.with { |c| build_query_type_map(c) }
|
54
|
+
end
|
55
|
+
|
56
|
+
# @visibility private
|
57
|
+
def run(sql)
|
58
|
+
execute(sql) { nil }
|
59
|
+
end
|
60
|
+
|
61
|
+
# @visibility private
|
62
|
+
def literalize(value)
|
63
|
+
case value
|
64
|
+
when NilClass then 'NULL'
|
65
|
+
when Numeric, TrueClass, FalseClass then value.to_s
|
66
|
+
when String then @quoter.encode(value)
|
67
|
+
else
|
68
|
+
@quoter.encode(serialize(value))
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# @visibility private
|
73
|
+
def fetch_count(sql)
|
74
|
+
execute(sql, &:cmd_tuples)
|
75
|
+
end
|
76
|
+
|
77
|
+
# @visibility private
|
78
|
+
def fetch_first_field(sql)
|
79
|
+
fetch_result(sql) { |res| res.getvalue(0, 0) if res.ntuples > 0 && res.nfields > 0 }
|
80
|
+
end
|
81
|
+
|
82
|
+
# @visibility private
|
83
|
+
def fetch_first_row(sql)
|
84
|
+
fetch_result(sql) { |res| res[0] if res.ntuples > 0 }
|
85
|
+
end
|
86
|
+
|
87
|
+
# @visibility private
|
88
|
+
def fetch_first_column(sql)
|
89
|
+
# Return an array of nils if we don't have a column
|
90
|
+
fetch_result(sql) { |res| res.nfields > 0 ? res.column_values(0) : Array.new(res.ntuples) }
|
91
|
+
end
|
92
|
+
|
93
|
+
# @visibility private
|
94
|
+
def fetch_each_row(sql, &block)
|
95
|
+
return to_enum(:fetch_each_row, sql) unless block_given?
|
96
|
+
|
97
|
+
fetch_result(sql) { |res| res.each(&block) }
|
98
|
+
end
|
99
|
+
|
100
|
+
# @visibility private
|
101
|
+
def fetch_rows(sql)
|
102
|
+
fetch_result(sql, &:to_a)
|
103
|
+
end
|
104
|
+
|
105
|
+
private
|
106
|
+
|
107
|
+
def fetch_result(sql)
|
108
|
+
execute(sql) do |res|
|
109
|
+
res.type_map = @result_type_map
|
110
|
+
yield res
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def execute(sql, &block)
|
115
|
+
@pool.with { |c| c.async_exec(sql, &block) }
|
116
|
+
end
|
117
|
+
|
118
|
+
# Use PG's built-in type mapping to serialize objects into SQL strings.
|
119
|
+
def serialize(value)
|
120
|
+
coder = encoder_for(value) or raise TypeError, "No SQL serializer for #{value.class}"
|
121
|
+
coder.encode(value)
|
122
|
+
end
|
123
|
+
|
124
|
+
def encoder_for(value)
|
125
|
+
coder = @query_type_map[value.class]
|
126
|
+
# Handle the weird case where coder can be a method name
|
127
|
+
coder.is_a?(Symbol) ? @query_type_map.send(coder, value) : coder
|
128
|
+
end
|
129
|
+
|
130
|
+
# Ensure encoders are set up for old versions of the pg gem
|
131
|
+
def build_query_type_map(connection)
|
132
|
+
map = PG::BasicTypeMapForQueries.new(connection)
|
133
|
+
map[Date] ||= PG::TextEncoder::Date.new
|
134
|
+
map[Time] ||= PG::TextEncoder::TimestampWithoutTimeZone.new
|
135
|
+
map[Hash] ||= PG::TextEncoder::JSON.new
|
136
|
+
map[BigDecimal] ||= NumericEncoder.new
|
137
|
+
map
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
# PG < 1.1.0 doesn't have a numeric decoder
|
142
|
+
# This is copied from https://github.com/ged/ruby-pg/commit/d4ae41bb8fd447c92ef9c8810ec932acd03e0293
|
143
|
+
# :nocov:
|
144
|
+
unless defined? PG::TextEncoder::Numeric
|
145
|
+
class NumericDecoder < PG::SimpleDecoder
|
146
|
+
def decode(string, tuple=nil, field=nil)
|
147
|
+
BigDecimal(string)
|
148
|
+
end
|
149
|
+
end
|
150
|
+
class NumericEncoder < PG::SimpleEncoder
|
151
|
+
def encode(decimal)
|
152
|
+
decimal.to_s('F')
|
153
|
+
end
|
154
|
+
end
|
155
|
+
private_constant :NumericDecoder, :NumericEncoder
|
156
|
+
PG::BasicTypeRegistry.register_type(0, 'numeric', NumericEncoder, NumericDecoder)
|
157
|
+
end
|
158
|
+
# :nocov:
|
159
|
+
end
|
data/lib/ensql/sequel_adapter.rb
CHANGED
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
require_relative 'version'
|
4
4
|
require_relative 'adapter'
|
5
|
+
require_relative 'pool_wrapper'
|
5
6
|
|
6
7
|
# Ensure our optional dependency has a compatible version
|
7
8
|
gem 'sequel', Ensql::SUPPORTED_SEQUEL_VERSIONS
|
@@ -9,15 +10,16 @@ require 'sequel'
|
|
9
10
|
|
10
11
|
module Ensql
|
11
12
|
#
|
12
|
-
#
|
13
|
-
#
|
14
|
-
#
|
15
|
-
#
|
13
|
+
# Wraps a Sequel::Database to implement the {Adapter} interface for Sequel.
|
14
|
+
# You may want to utilize the relevant Sequel extensions to make the most of
|
15
|
+
# database-specific deserialization and other features. By default, uses the
|
16
|
+
# first database in Sequel::Databases. Other databases can be passed to the
|
17
|
+
# constructor.
|
16
18
|
#
|
17
19
|
# require 'sequel'
|
18
20
|
# DB = Sequel.connect('postgres://localhost/mydb')
|
19
21
|
# DB.extend(:pg_json)
|
20
|
-
# Ensql.adapter = Ensql::SequelAdapter
|
22
|
+
# Ensql.adapter = Ensql::SequelAdapter.new(DB)
|
21
23
|
#
|
22
24
|
# To stream rows, configure streaming on the connection and use
|
23
25
|
# {SQL.each_row}
|
@@ -25,6 +27,7 @@ module Ensql
|
|
25
27
|
# DB = Sequel.connect('postgresql:/')
|
26
28
|
# DB.extension(:pg_streaming)
|
27
29
|
# DB.stream_all_queries = true
|
30
|
+
# Ensql.adapter = Ensql::SequelAdapter.new(DB)
|
28
31
|
# Ensql.sql("select * from large_table").each_row do |row|
|
29
32
|
# # This now yields each row in single-row mode.
|
30
33
|
# # The connection cannot be used for other queries while this is streaming.
|
@@ -32,41 +35,71 @@ module Ensql
|
|
32
35
|
#
|
33
36
|
# @see SUPPORTED_SEQUEL_VERSIONS
|
34
37
|
#
|
35
|
-
|
36
|
-
|
38
|
+
class SequelAdapter
|
39
|
+
include Adapter
|
37
40
|
|
38
|
-
#
|
39
|
-
|
41
|
+
# Support deprecated class method interface
|
42
|
+
class << self
|
43
|
+
require 'forwardable'
|
44
|
+
extend Forwardable
|
45
|
+
|
46
|
+
delegate [:literalize, :run, :fetch_count, :fetch_each_row, :fetch_rows, :fetch_first_column, :fetch_first_field, :fetch_first_row] => :new
|
47
|
+
end
|
48
|
+
|
49
|
+
# Wrap the raw connections from a Sequel::Database connection pool. This
|
50
|
+
# allows us to safely checkout the underlying database connection for use in
|
51
|
+
# a database specific adapter.
|
52
|
+
#
|
53
|
+
# Ensql.adapter = MySqliteAdapter.new(SequelAdapter.pool)
|
54
|
+
#
|
55
|
+
# @param db [Sequel::Database]
|
56
|
+
# @return [PoolWrapper] a pool adapter for raw connections
|
57
|
+
def self.pool(db)
|
58
|
+
PoolWrapper.new do |client_block|
|
59
|
+
db.pool.hold(&client_block)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# @param db [Sequel::Database]
|
64
|
+
def initialize(db = first_configured_database)
|
65
|
+
@db = db
|
66
|
+
end
|
67
|
+
|
68
|
+
# @visibility private
|
69
|
+
def fetch_rows(sql)
|
40
70
|
fetch_each_row(sql).to_a
|
41
71
|
end
|
42
72
|
|
43
|
-
#
|
44
|
-
def
|
73
|
+
# @visibility private
|
74
|
+
def fetch_each_row(sql)
|
45
75
|
return to_enum(:fetch_each_row, sql) unless block_given?
|
46
76
|
|
47
77
|
db.fetch(sql) { |r| yield r.transform_keys(&:to_s) }
|
48
78
|
end
|
49
79
|
|
50
|
-
#
|
51
|
-
def
|
80
|
+
# @visibility private
|
81
|
+
def fetch_count(sql)
|
52
82
|
db.execute_dui(sql)
|
53
83
|
end
|
54
84
|
|
55
|
-
#
|
56
|
-
def
|
85
|
+
# @visibility private
|
86
|
+
def run(sql)
|
57
87
|
db << sql
|
88
|
+
nil
|
58
89
|
end
|
59
90
|
|
60
|
-
#
|
61
|
-
def
|
91
|
+
# @visibility private
|
92
|
+
def literalize(value)
|
62
93
|
db.literal(value)
|
63
94
|
end
|
64
95
|
|
65
|
-
|
66
|
-
Sequel::DATABASES.first or raise Error, "no connection found in Sequel::DATABASES"
|
67
|
-
end
|
96
|
+
private
|
68
97
|
|
69
|
-
|
98
|
+
attr_reader :db
|
99
|
+
|
100
|
+
def first_configured_database
|
101
|
+
Sequel::DATABASES.first or raise Error, "no database found in Sequel::DATABASES"
|
102
|
+
end
|
70
103
|
|
71
104
|
end
|
72
105
|
end
|
data/lib/ensql/sql.rb
CHANGED
data/lib/ensql/version.rb
CHANGED
@@ -2,9 +2,11 @@
|
|
2
2
|
|
3
3
|
module Ensql
|
4
4
|
# Gem version
|
5
|
-
VERSION = "0.6.
|
5
|
+
VERSION = "0.6.2"
|
6
6
|
# Versions of activerecord compatible with the {ActiveRecordAdapter}
|
7
7
|
SUPPORTED_ACTIVERECORD_VERSIONS = ['>= 5.0', '< 6.2'].freeze
|
8
8
|
# Versions of sequel compatible with the {SequelAdapter}
|
9
9
|
SUPPORTED_SEQUEL_VERSIONS = '~> 5.9'
|
10
|
+
# Versions of pg compatibile with the {PostgresAdapter}
|
11
|
+
SUPPORTED_PG_VERSIONS = ['>= 0.19', '< 2'].freeze
|
10
12
|
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
# Compare operations performed using each adapter
|
4
|
+
#
|
5
|
+
|
6
|
+
ENV['TZ'] = 'UTC'
|
7
|
+
|
8
|
+
require 'benchmark/ips'
|
9
|
+
|
10
|
+
require_relative 'lib/ensql/active_record_adapter'
|
11
|
+
require_relative 'lib/ensql/sequel_adapter'
|
12
|
+
require_relative 'lib/ensql/postgres_adapter'
|
13
|
+
|
14
|
+
ActiveRecord::Base.establish_connection(adapter: "postgresql")
|
15
|
+
DB = Sequel.connect("postgresql:/")
|
16
|
+
DB.extension(:pg_json)
|
17
|
+
|
18
|
+
adapters = {
|
19
|
+
'pg ': Ensql::PostgresAdapter.new { PG::Connection.open },
|
20
|
+
'ar ': Ensql::ActiveRecordAdapter.new(ActiveRecord::Base.connection_pool),
|
21
|
+
'seq ': Ensql::SequelAdapter.new(DB),
|
22
|
+
'pg-ar ': Ensql::PostgresAdapter.new(Ensql::ActiveRecordAdapter.pool),
|
23
|
+
'pg-seq ': Ensql::PostgresAdapter.new(Ensql::SequelAdapter.pool(DB)),
|
24
|
+
}
|
25
|
+
|
26
|
+
ADAPTER = adapters.values.first
|
27
|
+
|
28
|
+
ADAPTER.run('drop table if exists number_benchmark')
|
29
|
+
ADAPTER.run('create table number_benchmark as select generate_series(1,100) as number')
|
30
|
+
|
31
|
+
adapter_tests = {
|
32
|
+
'literalize (String)': [:literalize, "It's quoted"],
|
33
|
+
'literalize (Long String)': [:literalize, "It's quoted" * 1000],
|
34
|
+
'literalize (Time)': [:literalize, Time.now],
|
35
|
+
'literalize (Int)': [:literalize, 1234],
|
36
|
+
'literalize (bool)': [:literalize, true],
|
37
|
+
'run INSERT': [:run, 'insert into number_benchmark values (999)'],
|
38
|
+
'run SET': [:run, "set time zone UTC"],
|
39
|
+
'run SELECT': [:run, 'select generate_series(1,100)'],
|
40
|
+
'count UPDATE': [:fetch_count, 'update number_benchmark set number = number + 1'],
|
41
|
+
'count SELECT': [:fetch_count, 'select generate_series(1,100)'],
|
42
|
+
'first column': [:fetch_first_column, 'select generate_series(1,100)'],
|
43
|
+
'first column (of many)': [:fetch_first_column, 'select *, now() from generate_series(1,100) as number'],
|
44
|
+
'first field': [:fetch_first_field, 'select 1'],
|
45
|
+
'first field with cast': [:fetch_first_field, "select cast('2021-01-01' as timestamp)"],
|
46
|
+
'first field (of many)': [:fetch_first_field, 'select generate_series(1,100)'],
|
47
|
+
'first row': [:fetch_first_row, "select 1, 2, 3"],
|
48
|
+
'first row (cast)': [:fetch_first_row, "select cast('2021-01-01' as timestamp), cast('[1,2,3]' as json)"],
|
49
|
+
'first row (of many)': [:fetch_first_row, "select generate_series(1, 100)"],
|
50
|
+
'rows (1)': [:fetch_rows, "select 1, 1"],
|
51
|
+
'rows (100)': [:fetch_rows, "select 1, 1, generate_series(1, 100)"],
|
52
|
+
'rows (100,cast)': [:fetch_rows, "select cast('2021-01-01' as timestamp), cast('[1,2,3]' as json), generate_series(1, 100)"],
|
53
|
+
'rows (100000)': [:fetch_rows, "select 1, 1, generate_series(1, 100000)"],
|
54
|
+
}
|
55
|
+
|
56
|
+
fetch_each_row_tests = {
|
57
|
+
'each_row (1)': [:fetch_each_row, "select 1, 1" ],
|
58
|
+
'each_row (100)': [:fetch_each_row, "select 1, 1, generate_series(1, 100)"],
|
59
|
+
'each_row (100,cast)': [:fetch_each_row, "select cast('2021-01-01' as timestamp), cast('[1,2,3]' as json), generate_series(1, 100)"],
|
60
|
+
'each_row (100000)': [:fetch_each_row, "select 1, 1, generate_series(1, 100000)"],
|
61
|
+
}
|
62
|
+
|
63
|
+
# Verify results are the same
|
64
|
+
adapter_tests.each do |name, args|
|
65
|
+
results = adapters.map { |n, a| [n, a.send(*args)] }.uniq { |n, result| result }
|
66
|
+
next if results.count == 1
|
67
|
+
|
68
|
+
warn "Differing results for #{name}: #{args}"
|
69
|
+
results.each { |n, result| warn " #{n} => #{result.inspect[0..500]}" }
|
70
|
+
end
|
71
|
+
|
72
|
+
# Compare times
|
73
|
+
adapter_tests.each do |test_name, args|
|
74
|
+
puts args.map { |a| a.inspect[0..100] }.join(' ')
|
75
|
+
|
76
|
+
Benchmark.ips(quiet: true) do |x|
|
77
|
+
x.config(stats: :bootstrap, confidence: 95, warmup: 0.2, time: 0.5)
|
78
|
+
|
79
|
+
adapters.each do |name, adapter|
|
80
|
+
x.report("#{test_name} - #{name}") { adapter.send(*args) }
|
81
|
+
end
|
82
|
+
|
83
|
+
x.compare!
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
fetch_each_row_tests.each do |test_name, args|
|
88
|
+
Benchmark.ips(quiet: true) do |x|
|
89
|
+
x.config(stats: :bootstrap, confidence: 95, warmup: 0.2, time: 0.5)
|
90
|
+
|
91
|
+
adapters.each do |name, adapter|
|
92
|
+
x.report("#{test_name} - #{name}") { adapter.send(*args) { |r| r } }
|
93
|
+
end
|
94
|
+
|
95
|
+
x.compare!
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
ADAPTER.run('drop table number_benchmark')
|
metadata
CHANGED
@@ -1,15 +1,35 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ensql
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.6.
|
4
|
+
version: 0.6.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Daniel Fone
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-
|
11
|
+
date: 2021-03-09 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: connection_pool
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 0.9.3
|
20
|
+
- - "<"
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: '3'
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
requirements:
|
27
|
+
- - ">="
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: 0.9.3
|
30
|
+
- - "<"
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '3'
|
13
33
|
- !ruby/object:Gem::Dependency
|
14
34
|
name: rake
|
15
35
|
requirement: !ruby/object:Gem::Requirement
|
@@ -96,9 +116,12 @@ files:
|
|
96
116
|
- lib/ensql.rb
|
97
117
|
- lib/ensql/active_record_adapter.rb
|
98
118
|
- lib/ensql/adapter.rb
|
119
|
+
- lib/ensql/pool_wrapper.rb
|
120
|
+
- lib/ensql/postgres_adapter.rb
|
99
121
|
- lib/ensql/sequel_adapter.rb
|
100
122
|
- lib/ensql/sql.rb
|
101
123
|
- lib/ensql/version.rb
|
124
|
+
- perf/adapter_benchmark.rb
|
102
125
|
homepage: https://github.com/danielfone/ensql
|
103
126
|
licenses:
|
104
127
|
- MIT
|