mini_sql 0.3 → 1.1.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) 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 +36 -0
  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/decoratable.rb +22 -0
  18. data/lib/mini_sql/inline_param_encoder.rb +4 -5
  19. data/lib/mini_sql/mysql/connection.rb +8 -0
  20. data/lib/mini_sql/mysql/deserializer_cache.rb +9 -15
  21. data/lib/mini_sql/mysql/prepared_binds.rb +15 -0
  22. data/lib/mini_sql/mysql/prepared_cache.rb +21 -0
  23. data/lib/mini_sql/mysql/prepared_connection.rb +44 -0
  24. data/lib/mini_sql/postgres/connection.rb +10 -2
  25. data/lib/mini_sql/postgres/deserializer_cache.rb +11 -17
  26. data/lib/mini_sql/postgres/prepared_binds.rb +15 -0
  27. data/lib/mini_sql/postgres/prepared_cache.rb +25 -0
  28. data/lib/mini_sql/postgres/prepared_connection.rb +36 -0
  29. data/lib/mini_sql/postgres_jdbc/connection.rb +3 -1
  30. data/lib/mini_sql/postgres_jdbc/deserializer_cache.rb +10 -14
  31. data/lib/mini_sql/result.rb +30 -0
  32. data/lib/mini_sql/serializer.rb +84 -0
  33. data/lib/mini_sql/sqlite/connection.rb +11 -1
  34. data/lib/mini_sql/sqlite/deserializer_cache.rb +9 -15
  35. data/lib/mini_sql/sqlite/prepared_binds.rb +15 -0
  36. data/lib/mini_sql/sqlite/prepared_cache.rb +21 -0
  37. data/lib/mini_sql/sqlite/prepared_connection.rb +40 -0
  38. data/lib/mini_sql/version.rb +1 -1
  39. data/mini_sql.gemspec +5 -4
  40. metadata +42 -8
  41. data/.travis.yml +0 -28
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3ea4564f53848c314c76c45db0c2af652c1e128ebfc1ba52d6ae11b3799149f4
4
- data.tar.gz: ecdfdc07fdf11e60338bc2ceb0415cbcc027720c5bba645281217a51befc2402
3
+ metadata.gz: 335047554a8bf88fb9f9c3152d5f7056806ee541667ca47ffdfe678d85d16f2e
4
+ data.tar.gz: fa574144f2ca9e8427b5d72292af8fbb67e8e89b799bd2ab550639edd6f0eca0
5
5
  SHA512:
6
- metadata.gz: 76a040187f4932dc9fbc80ae3323633a38462753caec47233dbfad3e276e90d8eeb0fbddcbfe974ade3b0b99c18a487d5ef865ebdb1effc67048352ecd35d99d
7
- data.tar.gz: 73c3519bf51a7934af89827830e3e7c45366eec01b1d65dc0c61afb2b87c12b093e9f3db99b1c4db081e6a6a1e983fd24d88db3aa13f63b9e546af0dce36beae
6
+ metadata.gz: b5b92a31e176d19b2c006dfec04884c295e7e9d6eaf04d5222f777b2e4d012225efb8e46699b9be7ad1cd103e87cc1637096a0bbef6cae0dfec928545e5a2538
7
+ data.tar.gz: '0685f098c60980117f0278a0f0887b0bd95f6d06ace0aeeba520dd163a7422b686b7d7f4009e29de97f8b713900b206174d3765903f4b6e28c953cfd174f03c3'
@@ -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.2
2
+
3
+ - FEATURE: improve compatability with clients overriding raw_connection
4
+
5
+ 2021-03-22 - 1.1.1
6
+
7
+ - FIX: compatability with ActiveRecord param encoder
8
+
9
+ 2021-03-22 - 1.1.0
10
+
11
+ - FEATURE: added new APIs to support prepared statements
12
+
13
+ 2020-12-31 - 1.0.1
14
+
15
+ - FIX: revert perf fix broke param_encoder interface, we were expecting never to be called if no encoding was to happen
16
+
17
+ 2020-12-30 - 1.0
18
+
19
+ - Added serialization support using MiniSql::Serializer.to_json / .from_json
20
+ - Fixed minor issue with cache poisoning when using query_decorator
21
+ - Version 1.0 to reflect the stability of the interfaces and project, used in productions for almost 2 years now
22
+
1
23
  2020-06-25 - 0.3
2
24
 
3
25
  - Added support for query_each and query_each_hash, which lazily queries rows and enables selecting large result sets by streaming
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