mini_sql 1.0.1 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +1 -1
  3. data/CHANGELOG +4 -0
  4. data/README.md +36 -0
  5. data/bench/builder_perf.rb +138 -0
  6. data/bench/decorator_perf.rb +143 -0
  7. data/bench/mini_sql_methods_perf.rb +80 -0
  8. data/bench/prepared_perf.rb +59 -0
  9. data/bench/shared/generate_data.rb +133 -0
  10. data/bench/topic_perf.rb +21 -327
  11. data/bench/topic_wide_perf.rb +92 -0
  12. data/lib/mini_sql.rb +17 -8
  13. data/lib/mini_sql/abstract/prepared_binds.rb +74 -0
  14. data/lib/mini_sql/abstract/prepared_cache.rb +45 -0
  15. data/lib/mini_sql/builder.rb +61 -30
  16. data/lib/mini_sql/inline_param_encoder.rb +4 -3
  17. data/lib/mini_sql/mysql/connection.rb +6 -0
  18. data/lib/mini_sql/mysql/deserializer_cache.rb +3 -3
  19. data/lib/mini_sql/mysql/prepared_binds.rb +15 -0
  20. data/lib/mini_sql/mysql/prepared_cache.rb +21 -0
  21. data/lib/mini_sql/mysql/prepared_connection.rb +44 -0
  22. data/lib/mini_sql/postgres/connection.rb +6 -0
  23. data/lib/mini_sql/postgres/deserializer_cache.rb +5 -5
  24. data/lib/mini_sql/postgres/prepared_binds.rb +15 -0
  25. data/lib/mini_sql/postgres/prepared_cache.rb +25 -0
  26. data/lib/mini_sql/postgres/prepared_connection.rb +36 -0
  27. data/lib/mini_sql/postgres_jdbc/deserializer_cache.rb +3 -3
  28. data/lib/mini_sql/result.rb +10 -0
  29. data/lib/mini_sql/serializer.rb +29 -15
  30. data/lib/mini_sql/sqlite/connection.rb +9 -1
  31. data/lib/mini_sql/sqlite/deserializer_cache.rb +3 -3
  32. data/lib/mini_sql/sqlite/prepared_binds.rb +15 -0
  33. data/lib/mini_sql/sqlite/prepared_cache.rb +21 -0
  34. data/lib/mini_sql/sqlite/prepared_connection.rb +40 -0
  35. data/lib/mini_sql/version.rb +1 -1
  36. metadata +19 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 78a25d1d6af83ae3c2f74f85c4a920a2a1019324ce8904bf3feda15b4ee486df
4
- data.tar.gz: 5d686631612a51a4134cf964c40bb41a883e19fa9847498668eca9f03e2f9816
3
+ metadata.gz: 7ce3e8b5fee8c39506de8439826252434ddce6249287bb3a74c411a41a5e5726
4
+ data.tar.gz: cbab24b31bbf55e39cb824224e694e117b42c30e20189f64ffd2163cab3e12b6
5
5
  SHA512:
6
- metadata.gz: 2c862314017d5be1cfe429b1632ae350d3f123ef7312506437eec04f9023e309dbdb7c366eb6afe88d65d4137accd43627856ae823bc11707a51c358219a19ed
7
- data.tar.gz: 6e3ac51e042bdbaff790a272c8e9207512d436493b005a4fe268128204125e44c3a955eef9ae38a3db2e07f207f86ee4dedbaa59145f0257055347581434b79f
6
+ metadata.gz: f01c57922c3ead43474e02585c365b914880ef6924f7acce041e2ee0e984217eadf4aa25f81a0c0c79ee2b9de7235311a2d7155be1a512f2f545d6fbe81ab6e5
7
+ data.tar.gz: 47de031da6a0efaeded3981fa2eadefd4a5261cbef73f2fcfb998dd1e671c6c19015a2b97ef70d7311d847558934f8ec8fe6678f2d665cc8ad6dffe6d10a879d
@@ -20,7 +20,7 @@ jobs:
20
20
  name: Ruby ${{ matrix.ruby }}
21
21
  services:
22
22
  postgres:
23
- image: postgres:9.6
23
+ image: postgres:10
24
24
  env:
25
25
  POSTGRES_PASSWORD: postgres
26
26
  ports:
data/CHANGELOG CHANGED
@@ -1,3 +1,7 @@
1
+ 2021-03-22 - 1.1.0
2
+
3
+ - FEATURE: added new APIs to support prepared statements
4
+
1
5
  2020-12-31 - 1.0.1
2
6
 
3
7
  - FIX: revert perf fix broke param_encoder interface, we were expecting never to be called if no encoding was to happen
data/README.md CHANGED
@@ -187,6 +187,21 @@ Note, in Postgres streaming is going to be slower than non-streaming options due
187
187
 
188
188
  Streaming support is only implemented in the postgres backend at the moment, PRs welcome to add to other backends.
189
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
+
190
205
  ## I want more features!
191
206
 
192
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.
@@ -194,6 +209,27 @@ MiniSql is designed to be very minimal. Even though the query builder and type m
194
209
  ## Development
195
210
 
196
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.
197
233
 
198
234
  ## Contributing
199
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
@@ -0,0 +1,80 @@
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 'draper'
13
+ gem 'pry'
14
+ end
15
+
16
+ require 'active_record'
17
+ require 'benchmark/ips'
18
+ require 'mini_sql'
19
+
20
+ require '../mini_sql/bench/shared/generate_data'
21
+
22
+ ar_connection, conn_config = GenerateData.new(count_records: 1_000).call
23
+ MINI_SQL = MiniSql::Connection.get(ar_connection.raw_connection)
24
+
25
+
26
+ Benchmark.ips do |r|
27
+ r.report('query_hash') do |n|
28
+ while n > 0
29
+ MINI_SQL.query_hash('select id, title from topics order by id limit 1000').each do |hash|
30
+ [hash['id'], hash['title']]
31
+ end
32
+ n -= 1
33
+ end
34
+ end
35
+ r.report('query_array') do |n|
36
+ while n > 0
37
+ MINI_SQL.query_array('select id, title from topics order by id limit 1000').each do |id, title|
38
+ [id, title]
39
+ end
40
+ n -= 1
41
+ end
42
+ end
43
+ r.report('query') do |n|
44
+ while n > 0
45
+ MINI_SQL.query('select id, title from topics order by id limit 1000').each do |obj|
46
+ [obj.id, obj.title]
47
+ end
48
+ n -= 1
49
+ end
50
+ end
51
+
52
+ r.compare!
53
+ end
54
+
55
+ # Comparison:
56
+ # query_array: 1663.3 i/s
57
+ # query: 1254.5 i/s - 1.33x (± 0.00) slower
58
+ # query_hash: 1095.4 i/s - 1.52x (± 0.00) slower
59
+
60
+
61
+ Benchmark.ips do |r|
62
+ r.report('query_single') do |n|
63
+ while n > 0
64
+ MINI_SQL.query_single('select id from topics order by id limit 1000')
65
+ n -= 1
66
+ end
67
+ end
68
+ r.report('query_array') do |n|
69
+ while n > 0
70
+ MINI_SQL.query_array('select id from topics order by id limit 1000').flatten
71
+ n -= 1
72
+ end
73
+ end
74
+
75
+ r.compare!
76
+ end
77
+
78
+ # Comparison:
79
+ # query_single: 2445.1 i/s
80
+ # query_array: 1681.1 i/s - 1.45x (± 0.00) slower
@@ -0,0 +1,59 @@
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
+
24
+ sql = <<~SQL
25
+ select users.first_name, count(distinct topics.id) topics_count
26
+ from topics
27
+ inner join users on user_id = users.id
28
+ inner join categories on category_id = categories.id
29
+ where users.id = ?
30
+ group by users.id
31
+ SQL
32
+
33
+ Benchmark.ips do |x|
34
+ x.report("ps") do |n|
35
+ while n > 0
36
+ MINI_SQL.prepared.query(sql, rand(100))
37
+ n -= 1
38
+ end
39
+ end
40
+ x.report("without ps") do |n|
41
+ while n > 0
42
+ MINI_SQL.query(sql, rand(100))
43
+ n -= 1
44
+ end
45
+ end
46
+
47
+ x.compare!
48
+ end
49
+
50
+ # Warming up --------------------------------------
51
+ # ps 1.008k i/100ms
52
+ # without ps 284.000 i/100ms
53
+ # Calculating -------------------------------------
54
+ # ps 10.287k (± 4.2%) i/s - 51.408k in 5.006807s
55
+ # without ps 2.970k (± 5.3%) i/s - 15.052k in 5.083272s
56
+ #
57
+ # Comparison:
58
+ # ps: 10287.2 i/s
59
+ # without ps: 2970.0 i/s - 3.46x (± 0.00) slower