ensql 0.6.1 → 0.6.2

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
2
  SHA256:
3
- metadata.gz: 3bc1fdbc384c83a4d0a8ab1544f095320d63d66bc814cc30877f63bdc766be40
4
- data.tar.gz: 7af7a154c0b4480278377f3dae635daf890ed69fd259f6bcffa3a14c3b4a8269
3
+ metadata.gz: 926a86bf4e2b069dfee856bc77567048cca9f5e621d48e255fe3d5f1c041a42d
4
+ data.tar.gz: 294b707b6a8d8394ba12eafe20298bb800ab86584f354bacfe8cbdef66581531
5
5
  SHA512:
6
- metadata.gz: 1d085a19d85b24a3a4d038e137b54b439fafe8b30ec5f5b2e384792af87c00e3daf462ed08a8d16c252568d1f1d6bb1bf390e23c23423a8f48de4085f0f6e487
7
- data.tar.gz: 7ee53cbd22ce7c5c059659b50fccd6a3e0f3b0836f0114d916d1faab3c6cdb7a598a5588102631cab3de6b2c435236939e0b43cdb550c7f068fb76c4eea579ed
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.1)
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
- * Ruby >= 2.4.0
72
- * Sequel >= 5.9 if using Sequel
73
- * ActiveRecord >= 5.0 if using ActiveRecord
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 you need to, or configure your own adapter. See [the API docs](https://rubydoc.info/gems/ensql/Ensql/Adapter) for
79
- details of the interface.
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
- Ensql.adapter = Ensql::ActiveRecordAdapter # Will use ActiveRecord instead
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. postgres array literals.
195
+ literals in a database specific format, e.g. PostgreSQL array literals.
182
196
 
183
- - Handling specific connections rather than just grabbing the default.
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 postgres database. You can also run `bin/console` for an interactive prompt that will allow you to
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`. To release a new version, update the
194
- version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version,
195
- push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
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.0)
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.41.0)
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)
@@ -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", "~> 0.18" # AR version constraint
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.0)
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.21.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 (~> 0.18)
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
- # Connection adapter to use. Must implement the interface defined in
80
- # {Ensql::Adapter}. If not specified, it will try to autoload an adapter
81
- # based on the availability of Sequel or ActiveRecord, in that order.
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 = CustomMSSQLAdapater # supply your own 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
- @adapter ||= autoload_adapter
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
- # Implements the {Adapter} interface for ActiveRecord. Requires an
13
- # ActiveRecord connection to be configured and established. Uses
14
- # ActiveRecord::Base for the connection.
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
- module ActiveRecordAdapter
24
- extend Adapter
27
+ class ActiveRecordAdapter
28
+ include Adapter
25
29
 
26
- # @!visibility private
27
- def self.fetch_rows(sql)
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
- # @!visibility private
32
- def self.fetch_each_row(sql, &block)
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 = connection.exec_query(sql)
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
- # @!visibility private
47
- def self.run(sql)
48
- connection.execute(sql)
77
+ # @visibility private
78
+ def run(sql)
79
+ with_connection { |c| c.execute(sql) }
80
+ nil
49
81
  end
50
82
 
51
- # @!visibility private
52
- def self.fetch_count(sql)
53
- connection.exec_update(sql)
83
+ # @visibility private
84
+ def fetch_count(sql)
85
+ with_connection { |c| c.exec_update(sql) }
54
86
  end
55
87
 
56
- # @!visibility private
57
- def self.literalize(value)
58
- connection.quote(value)
88
+ # @visibility private
89
+ def literalize(value)
90
+ with_connection { |c| c.quote(value) }
59
91
  end
60
92
 
61
- def self.connection
62
- ActiveRecord::Base.connection
63
- end
93
+ private
64
94
 
65
- def self.deserialize_types(row, column_types)
66
- row.each_with_object({}) { |(column, value), hash|
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
- private_class_method :connection, :deserialize_types
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
@@ -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
- # Implements the {Adapter} interface for Sequel. Requires a Sequel connection
13
- # to be established. Uses the first connection found in Sequel::DATABASES. You
14
- # may want to utilize the relevant extensions to make the most of
15
- # deserialization and other database features.
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
- module SequelAdapter
36
- extend Adapter
38
+ class SequelAdapter
39
+ include Adapter
37
40
 
38
- # @!visibility private
39
- def self.fetch_rows(sql)
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
- # @!visibility private
44
- def self.fetch_each_row(sql)
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
- # @!visibility private
51
- def self.fetch_count(sql)
80
+ # @visibility private
81
+ def fetch_count(sql)
52
82
  db.execute_dui(sql)
53
83
  end
54
84
 
55
- # @!visibility private
56
- def self.run(sql)
85
+ # @visibility private
86
+ def run(sql)
57
87
  db << sql
88
+ nil
58
89
  end
59
90
 
60
- # @!visibility private
61
- def self.literalize(value)
91
+ # @visibility private
92
+ def literalize(value)
62
93
  db.literal(value)
63
94
  end
64
95
 
65
- def self.db
66
- Sequel::DATABASES.first or raise Error, "no connection found in Sequel::DATABASES"
67
- end
96
+ private
68
97
 
69
- private_class_method :db
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
@@ -95,6 +95,7 @@ module Ensql
95
95
  # (see Adapter.fetch_each_row)
96
96
  def each_row(&block)
97
97
  adapter.fetch_each_row(to_sql, &block)
98
+ nil
98
99
  end
99
100
 
100
101
  # Interpolate the params into the SQL statement.
data/lib/ensql/version.rb CHANGED
@@ -2,9 +2,11 @@
2
2
 
3
3
  module Ensql
4
4
  # Gem version
5
- VERSION = "0.6.1"
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.1
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-02-25 00:00:00.000000000 Z
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