mini_sql 0.2.5 → 1.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +66 -0
  3. data/.rubocop.yml +5 -2
  4. data/CHANGELOG +22 -0
  5. data/README.md +66 -1
  6. data/bench/builder_perf.rb +138 -0
  7. data/bench/decorator_perf.rb +143 -0
  8. data/bench/mini_sql_methods_perf.rb +80 -0
  9. data/bench/prepared_perf.rb +59 -0
  10. data/bench/shared/generate_data.rb +133 -0
  11. data/bench/topic_perf.rb +21 -327
  12. data/bench/topic_wide_perf.rb +92 -0
  13. data/lib/mini_sql.rb +20 -8
  14. data/lib/mini_sql/abstract/prepared_binds.rb +74 -0
  15. data/lib/mini_sql/abstract/prepared_cache.rb +45 -0
  16. data/lib/mini_sql/builder.rb +66 -23
  17. data/lib/mini_sql/connection.rb +14 -2
  18. data/lib/mini_sql/decoratable.rb +22 -0
  19. data/lib/mini_sql/inline_param_encoder.rb +4 -5
  20. data/lib/mini_sql/mysql/connection.rb +6 -0
  21. data/lib/mini_sql/mysql/deserializer_cache.rb +9 -15
  22. data/lib/mini_sql/mysql/prepared_binds.rb +15 -0
  23. data/lib/mini_sql/mysql/prepared_cache.rb +21 -0
  24. data/lib/mini_sql/mysql/prepared_connection.rb +44 -0
  25. data/lib/mini_sql/postgres/connection.rb +75 -3
  26. data/lib/mini_sql/postgres/deserializer_cache.rb +32 -15
  27. data/lib/mini_sql/postgres/prepared_binds.rb +15 -0
  28. data/lib/mini_sql/postgres/prepared_cache.rb +25 -0
  29. data/lib/mini_sql/postgres/prepared_connection.rb +36 -0
  30. data/lib/mini_sql/postgres_jdbc/connection.rb +3 -1
  31. data/lib/mini_sql/postgres_jdbc/deserializer_cache.rb +10 -14
  32. data/lib/mini_sql/result.rb +30 -0
  33. data/lib/mini_sql/serializer.rb +84 -0
  34. data/lib/mini_sql/sqlite/connection.rb +9 -1
  35. data/lib/mini_sql/sqlite/deserializer_cache.rb +9 -15
  36. data/lib/mini_sql/sqlite/prepared_binds.rb +15 -0
  37. data/lib/mini_sql/sqlite/prepared_cache.rb +21 -0
  38. data/lib/mini_sql/sqlite/prepared_connection.rb +40 -0
  39. data/lib/mini_sql/version.rb +1 -1
  40. data/mini_sql.gemspec +6 -5
  41. metadata +49 -15
  42. data/.travis.yml +0 -28
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c8f5bcd09064cc2353fb1c2f172845cb738c72f9da2ff44786fdf1f119f7c36c
4
- data.tar.gz: 1d7abcda16fa7d972aa0ae6b64bc1f23532feda03daf0098dcbff00f1e2e0d49
3
+ metadata.gz: df275ffff82e8f6fc97b466178d50bcb8e3d5af55167f811abce10356e248770
4
+ data.tar.gz: 7b943e8a2dca0d546c0140d0957d4712d4f11610f169caefcd01aa7ea8e75e9a
5
5
  SHA512:
6
- metadata.gz: a84dca1674849011cd77e796046d0ece93e7952603470a51b959332180a1533fb1d15f1d3e3b69cb6f5bcc2f43e4f80c6902d85c0709443bc9004a15bc721921
7
- data.tar.gz: 4433947c712bcf6c9346fa2b0df792bc95c7fdaa453e5ae19b649241c24ba7ab3b57397cb66114b68ba4e622dfcc6242e6631beed129f5a8cd08bc618e13381d
6
+ metadata.gz: 5fda186dcae83e032b1f120782edd86c8d1d7e276afeb040479fca4e5cc6a52e40499f45df5c9a3303c7823e0e9fed128c17b422bdd12903fa50c4198732c25b
7
+ data.tar.gz: affcf46e3c8cfffd001fbef3c5d49f938c694a7b85412df7855938fb1326330a386d0557021bd2b1156f7e0bd10a6c1c560cc171190e0ff21e5a06e3375ee984
@@ -0,0 +1,66 @@
1
+ name: Mini SQL Tests
2
+
3
+ on:
4
+ pull_request:
5
+ push:
6
+ branches:
7
+ - master
8
+
9
+ env:
10
+ PGHOST: localhost
11
+ PGPORT: 5432
12
+ PGPASSWORD: postgres
13
+ PGUSER: postgres
14
+ MINI_SQL_MYSQL_HOST: 127.0.0.1
15
+ MINI_SQL_MYSQL_PORT: 3306
16
+ MINI_SQL_MYSQL_PASSWORD: mysql
17
+ jobs:
18
+ build:
19
+ runs-on: ubuntu-latest
20
+ name: Ruby ${{ matrix.ruby }}
21
+ services:
22
+ postgres:
23
+ image: postgres:10
24
+ env:
25
+ POSTGRES_PASSWORD: postgres
26
+ ports:
27
+ - 5432:5432
28
+ options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
29
+ mysql:
30
+ image: mysql:5.7
31
+ env:
32
+ MYSQL_ROOT_PASSWORD: mysql
33
+ ports:
34
+ - 3306:3306
35
+ options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
36
+ strategy:
37
+ matrix:
38
+ ruby: ["2.7", "2.6", "2.5"]
39
+ experimental: [false]
40
+ include:
41
+ - ruby: ruby-head
42
+ experimental: true
43
+ steps:
44
+ - uses: actions/checkout@v2
45
+ - uses: ruby/setup-ruby@v1
46
+ with:
47
+ ruby-version: ${{ matrix.ruby }}
48
+ - name: Bundler cache
49
+ uses: actions/cache@v2
50
+ with:
51
+ path: vendor/bundle
52
+ key: ${{ runner.os }}-${{ matrix.ruby }}-gems-${{ hashFiles('**/Gemfile.lock') }}
53
+ restore-keys: |
54
+ ${{ runner.os }}-${{ matrix.ruby }}-gems-
55
+ - name: Create Databases
56
+ run: |
57
+ createdb test_mini_sql
58
+ mysql --host=127.0.0.1 --port=3306 --user=root --password=mysql -e 'CREATE DATABASE test_mini_sql'
59
+ - name: Setup gems
60
+ run: |
61
+ bundle config path vendor/bundle
62
+ bundle install --jobs 4
63
+ - name: Tests
64
+ run: bundle exec rake test
65
+ - name: Rubocop
66
+ run: bundle exec rubocop
data/.rubocop.yml CHANGED
@@ -1,5 +1,8 @@
1
- inherit_from: https://raw.githubusercontent.com/discourse/discourse/master/.rubocop.yml
2
-
1
+ inherit_gem:
2
+ rubocop-discourse: default.yml
3
+ inherit_mode:
4
+ merge:
5
+ - Exclude
3
6
  AllCops:
4
7
  Exclude:
5
8
  - 'bench/**/*'
data/CHANGELOG CHANGED
@@ -1,3 +1,25 @@
1
+ 2021-03-22 - 1.1.1
2
+
3
+ - FIX: compatability with ActiveRecord param encoder
4
+
5
+ 2021-03-22 - 1.1.0
6
+
7
+ - FEATURE: added new APIs to support prepared statements
8
+
9
+ 2020-12-31 - 1.0.1
10
+
11
+ - FIX: revert perf fix broke param_encoder interface, we were expecting never to be called if no encoding was to happen
12
+
13
+ 2020-12-30 - 1.0
14
+
15
+ - Added serialization support using MiniSql::Serializer.to_json / .from_json
16
+ - Fixed minor issue with cache poisoning when using query_decorator
17
+ - Version 1.0 to reflect the stability of the interfaces and project, used in productions for almost 2 years now
18
+
19
+ 2020-06-25 - 0.3
20
+
21
+ - Added support for query_each and query_each_hash, which lazily queries rows and enables selecting large result sets by streaming
22
+
1
23
  2020-04-07 - 0.2.5
2
24
 
3
25
  - Added support for custom type maps with Postgres connections
data/README.md CHANGED
@@ -130,10 +130,12 @@ When using Postgres, native type mapping implementation is used. This is roughly
130
130
  implemented as:
131
131
 
132
132
  ```ruby
133
- type_map = PG::BasicTypeMapForResults.new(conn)
133
+ type_map ||= PG::BasicTypeMapForResults.new(conn)
134
134
  # additional specific decoders
135
135
  ```
136
136
 
137
+ The type mapper instansitated once on-demand at boot and reused by all mini_sql connections.
138
+
137
139
  Initializing the basic type map for Postgres can be a costly operation. You may
138
140
  wish to amend the type mapper so for example you only return strings:
139
141
 
@@ -158,6 +160,48 @@ mini_sql_cnn = MiniSql::Connection.get(pg_cnn, type_map: pg_cnn.type_map_for_res
158
160
 
159
161
  Note the type mapper for Rails may miss some of the mapping MiniSql ships with such as `IPAddr`, MiniSql is also careful to use the very efficient TimestampUtc decoders where available.
160
162
 
163
+ ## Streaming support
164
+
165
+ In some exceptional cases you may want to stream results directly from the database. This enables selection of 100s of thousands of rows with limited memory impact.
166
+
167
+ Two interfaces exists for this:
168
+
169
+ `query_each` : which can be used to get materialized objects
170
+ `query_each_hash` : which can be used to iterate through Hash objects
171
+
172
+ Usage:
173
+
174
+ ```ruby
175
+ mini_sql_cnn.query_each("SELECT * FROM tons_of_cows limit :limit", limit: 1_000_000) do |row|
176
+ puts row.cow_name
177
+ puts row.cow_age
178
+ end
179
+
180
+ mini_sql_cnn.query_each_hash("SELECT * FROM one_million_cows") do |row|
181
+ puts row["cow_name"]
182
+ puts row["cow_age"]
183
+ end
184
+ ```
185
+
186
+ Note, in Postgres streaming is going to be slower than non-streaming options due to internal implementation in the pq gem, each row gets a full result object and additional bookkeeping is needed. Only use it if you need to optimize memory usage.
187
+
188
+ Streaming support is only implemented in the postgres backend at the moment, PRs welcome to add to other backends.
189
+
190
+ ## Prepared Statements
191
+ See [benchmark mini_sql](https://github.com/discourse/mini_sql/tree/master/bench/prepared_perf.rb)
192
+ [benchmark mini_sql vs rails](https://github.com/discourse/mini_sql/tree/master/bench/bilder_perf.rb).
193
+
194
+ By default prepared cache size is 500 queries. Use prepared queries only for frequent queries.
195
+
196
+ ```ruby
197
+ conn.prepared.query("select * from table where id = ?", id: 10)
198
+
199
+ ids = rand(100) < 90 ? [1] : [1, 2]
200
+ builder = conn.build("select * from table /*where*/")
201
+ builder.where("id IN (?)", ids)
202
+ builder.prepared(ids.size == 1).query # most frequent query
203
+ ```
204
+
161
205
  ## I want more features!
162
206
 
163
207
  MiniSql is designed to be very minimal. Even though the query builder and type materializer give you a lot of mileage, it is not intended to be a fully fledged ORM. If you are looking for an ORM I recommend investigating ActiveRecord or Sequel which provide significantly more features.
@@ -165,6 +209,27 @@ MiniSql is designed to be very minimal. Even though the query builder and type m
165
209
  ## Development
166
210
 
167
211
  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).
212
+ ### Local testing
213
+ ```bash
214
+ docker run --name mini-sql-mysql --rm -it -p 33306:3306 -e MYSQL_DATABASE=test_mini_sql -e MYSQL_ALLOW_EMPTY_PASSWORD=yes -d mysql:5.7
215
+ export MINI_SQL_MYSQL_HOST=127.0.0.1
216
+ export MINI_SQL_MYSQL_PORT=33306
217
+
218
+ docker run --name mini-sql-postgres --rm -it -p 55432:5432 -e POSTGRES_DB=test_mini_sql -e POSTGRES_HOST_AUTH_METHOD=trust -d postgres
219
+ export MINI_SQL_PG_USER=postgres
220
+ export MINI_SQL_PG_HOST=127.0.0.1
221
+ export MINI_SQL_PG_PORT=55432
222
+
223
+ sleep 10 # waiting for up databases
224
+
225
+ bundle exec rake
226
+
227
+ # end working on mini-sql
228
+ docker stop mini-sql-postgres mini-sql-mysql
229
+ ```
230
+
231
+ Sqlite tests rely on the SQLITE_STMT view existing. This is enabled by default on most systems, however some may
232
+ opt for a leaner install. See: https://bugs.archlinux.org/task/70072. You may have to recompile sqlite on such systems.
168
233
 
169
234
  ## Contributing
170
235
 
@@ -0,0 +1,138 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/inline'
4
+
5
+ gemfile do
6
+ source 'https://rubygems.org'
7
+ gem 'mini_sql', path: '../'
8
+ gem 'pg'
9
+ gem 'activerecord'
10
+ gem 'activemodel'
11
+ gem 'benchmark-ips'
12
+ end
13
+
14
+ require 'active_record'
15
+ require 'benchmark/ips'
16
+ require 'mini_sql'
17
+
18
+ require '../mini_sql/bench/shared/generate_data'
19
+
20
+ ar_connection, _ = GenerateData.new(count_records: 10_000).call
21
+ MINI_SQL = MiniSql::Connection.get(ar_connection.raw_connection)
22
+
23
+ def mini_sql(is_prepared, user_id)
24
+ MINI_SQL
25
+ .build(<<~SQL)
26
+ /*select*/ from topics /*join*/ /*where*/ /*group_by*/
27
+ SQL
28
+ .select('users.first_name, count(distinct topics.id) topics_count')
29
+ .join('users on user_id = users.id')
30
+ .join('categories on category_id = categories.id')
31
+ .where('users.id = ?', user_id)
32
+ .group_by('users.id')
33
+ .prepared(is_prepared)
34
+ .query
35
+ end
36
+
37
+ def mini_sql_optim(is_prepared, user_id)
38
+ @builder ||=
39
+ MINI_SQL
40
+ .build(<<~SQL)
41
+ /*select*/ from topics /*join*/ /*where*/ /*group_by*/
42
+ SQL
43
+ .select('users.first_name, count(distinct topics.id) topics_count')
44
+ .join('users on user_id = users.id')
45
+ .join('categories on category_id = categories.id')
46
+ .where('users.id = :user_id')
47
+ .group_by('users.id')
48
+
49
+ @builder
50
+ .prepared(is_prepared)
51
+ .query(user_id: user_id)
52
+ end
53
+
54
+ def ar_prepared(user_id)
55
+ Topic
56
+ .select(User.arel_table[:first_name] , Topic.arel_table[:id].count)
57
+ .joins(:user, :category)
58
+ .where(user_id: user_id)
59
+ .group(User.arel_table[:id])
60
+ .load
61
+ end
62
+
63
+ def ar_prepared_optim(user_id)
64
+ @rel ||= Topic
65
+ .select(User.arel_table[:first_name] , Topic.arel_table[:id].count)
66
+ .joins(:user, :category)
67
+ .group(User.arel_table[:id])
68
+
69
+ @rel
70
+ .where(user_id: user_id)
71
+ .load
72
+ end
73
+
74
+ def ar_unprepared(user_id)
75
+ Topic
76
+ .select('users.first_name, count(distinct topics.id) topics_count')
77
+ .joins(:user, :category)
78
+ .where(user_id: user_id)
79
+ .group('users.id')
80
+ .load
81
+ end
82
+
83
+ Benchmark.ips do |x|
84
+ x.report("mini_sql_prepared") do |n|
85
+ while n > 0
86
+ mini_sql(true, rand(100))
87
+ n -= 1
88
+ end
89
+ end
90
+ x.report("mini_sql") do |n|
91
+ while n > 0
92
+ mini_sql(false, rand(100))
93
+ n -= 1
94
+ end
95
+ end
96
+
97
+ x.report("mini_sql_prepared_optim") do |n|
98
+ while n > 0
99
+ mini_sql_optim(true, rand(100))
100
+ n -= 1
101
+ end
102
+ end
103
+ x.report("mini_sql_optim") do |n|
104
+ while n > 0
105
+ mini_sql_optim(false, rand(100))
106
+ n -= 1
107
+ end
108
+ end
109
+
110
+ x.report("ar_prepared") do |n|
111
+ while n > 0
112
+ ar_prepared(rand(100))
113
+ n -= 1
114
+ end
115
+ end
116
+
117
+ x.report("ar_prepared_optim") do |n|
118
+ while n > 0
119
+ ar_prepared_optim(rand(100))
120
+ n -= 1
121
+ end
122
+ end
123
+
124
+ x.report("ar_unprepared") do |n|
125
+ while n > 0
126
+ ar_unprepared(rand(100))
127
+ n -= 1
128
+ end
129
+ end
130
+
131
+ x.compare!
132
+ end
133
+
134
+ # Comparison:
135
+ # mini_sql_prepared: 8386.2 i/s
136
+ # mini_sql: 2742.3 i/s - 3.06x (± 0.00) slower
137
+ # ar_prepared: 1599.3 i/s - 5.24x (± 0.00) slower
138
+ # ar_unprepared: 868.9 i/s - 9.65x (± 0.00) slower
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/inline'
4
+
5
+ gemfile do
6
+ source 'https://rubygems.org'
7
+ gem 'pg', github: 'ged/ruby-pg'
8
+ gem 'mini_sql', path: '../'
9
+ gem 'activerecord'
10
+ gem 'activemodel'
11
+ gem 'benchmark-ips'
12
+ gem 'sequel', github: 'jeremyevans/sequel'
13
+ gem 'sequel_pg', github: 'jeremyevans/sequel_pg', require: 'sequel'
14
+ gem 'draper'
15
+ gem 'pry'
16
+ end
17
+
18
+ require 'active_record'
19
+ require 'benchmark/ips'
20
+ require 'mini_sql'
21
+
22
+ require '../mini_sql/bench/shared/generate_data'
23
+
24
+ ar_connection, conn_config = GenerateData.new(count_records: 1_000).call
25
+ MINI_SQL = MiniSql::Connection.get(ar_connection.raw_connection)
26
+ DB = Sequel.connect(ar_connection.instance_variable_get(:@config).slice(:database, :user, :password, :host, :adapter))
27
+
28
+
29
+ # https://github.com/drapergem/draper
30
+ class TopicDraper < Draper::Decorator
31
+ delegate :id
32
+
33
+ def title_bang
34
+ object.title + '!!!'
35
+ end
36
+ end
37
+
38
+ # https://ruby-doc.org/stdlib-2.5.1/libdoc/delegate/rdoc/SimpleDelegator.html
39
+ class TopicSimpleDelegator < SimpleDelegator
40
+ def title_bang
41
+ title + '!!!'
42
+ end
43
+ end
44
+
45
+ class TopicSequel < Sequel::Model(DB[:topics]); end
46
+ class TopicDecoratorSequel < TopicSequel
47
+ def title_bang
48
+ title + '!!!'
49
+ end
50
+ end
51
+
52
+ class Topic < ActiveRecord::Base;end
53
+ class TopicArModel < Topic
54
+ def title_bang
55
+ title + '!!!'
56
+ end
57
+ end
58
+
59
+ module TopicDecorator
60
+ def title_bang
61
+ title + '!!!'
62
+ end
63
+ end
64
+
65
+ Benchmark.ips do |r|
66
+ r.report('query_decorator') do |n|
67
+ while n > 0
68
+ MINI_SQL.query_decorator(TopicDecorator, 'select id, title from topics order by id limit 1000').each do |obj|
69
+ obj.title_bang
70
+ obj.id
71
+ end
72
+ n -= 1
73
+ end
74
+ end
75
+ r.report('extend') do |n|
76
+ while n > 0
77
+ MINI_SQL.query('select id, title from topics order by id limit 1000').each do |obj|
78
+ d_obj = obj.extend(TopicDecorator)
79
+ d_obj.title_bang
80
+ d_obj.id
81
+ end
82
+ n -= 1
83
+ end
84
+ end
85
+ r.report('draper') do |n|
86
+ while n > 0
87
+ MINI_SQL.query('select id, title from topics order by id limit 1000').each do |obj|
88
+ d_obj = TopicDraper.new(obj)
89
+ d_obj.title_bang
90
+ d_obj.id
91
+ end
92
+ n -= 1
93
+ end
94
+ end
95
+ r.report('simple_delegator') do |n|
96
+ while n > 0
97
+ MINI_SQL.query('select id, title from topics order by id limit 1000').each do |obj|
98
+ d_obj = TopicSimpleDelegator.new(obj)
99
+ d_obj.title_bang
100
+ d_obj.id
101
+ end
102
+ n -= 1
103
+ end
104
+ end
105
+ r.report('query') do |n|
106
+ while n > 0
107
+ MINI_SQL.query('select id, title from topics order by id limit 1000').each do |obj|
108
+ obj.title + '!!!'
109
+ obj.id
110
+ end
111
+ n -= 1
112
+ end
113
+ end
114
+ r.report('ar model') do |n|
115
+ while n > 0
116
+ TopicArModel.limit(1000).order(:id).select(:id, :title).each do |obj|
117
+ obj.title_bang
118
+ obj.id
119
+ end
120
+ n -= 1
121
+ end
122
+ end
123
+ r.report('sequel model') do |n|
124
+ while n > 0
125
+ TopicDecoratorSequel.limit(1000).order(:id).select(:id, :title).each do |obj|
126
+ obj.title_bang
127
+ obj.id
128
+ end
129
+ n -= 1
130
+ end
131
+ end
132
+
133
+ r.compare!
134
+ end
135
+
136
+ # Comparison:
137
+ # query: 1102.9 i/s
138
+ # query_decorator: 1089.0 i/s - same-ish: difference falls within error
139
+ # sequel model: 860.2 i/s - 1.28x (± 0.00) slower
140
+ # simple_delegator: 679.8 i/s - 1.62x (± 0.00) slower
141
+ # extend: 678.1 i/s - 1.63x (± 0.00) slower
142
+ # draper: 587.2 i/s - 1.88x (± 0.00) slower
143
+ # ar model: 172.5 i/s - 6.39x (± 0.00) slower