mini_sql 0.2.4 → 1.1.0

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.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +66 -0
  3. data/.rubocop-https---raw-githubusercontent-com-discourse-discourse-master--rubocop-yml +355 -0
  4. data/.rubocop.yml +8 -0
  5. data/CHANGELOG +22 -0
  6. data/Gemfile +3 -1
  7. data/Guardfile +2 -0
  8. data/README.md +125 -1
  9. data/Rakefile +3 -1
  10. data/bench/builder_perf.rb +138 -0
  11. data/bench/decorator_perf.rb +143 -0
  12. data/bench/mini_sql_methods_perf.rb +80 -0
  13. data/bench/prepared_perf.rb +59 -0
  14. data/bench/shared/generate_data.rb +133 -0
  15. data/bench/timestamp_perf.rb +22 -21
  16. data/bench/topic_mysql_perf.rb +1 -7
  17. data/bench/topic_perf.rb +27 -169
  18. data/bench/topic_wide_perf.rb +92 -0
  19. data/bin/console +1 -0
  20. data/lib/mini_sql.rb +20 -8
  21. data/lib/mini_sql/abstract/prepared_binds.rb +74 -0
  22. data/lib/mini_sql/abstract/prepared_cache.rb +45 -0
  23. data/lib/mini_sql/builder.rb +64 -24
  24. data/lib/mini_sql/connection.rb +15 -3
  25. data/lib/mini_sql/decoratable.rb +22 -0
  26. data/lib/mini_sql/deserializer_cache.rb +2 -0
  27. data/lib/mini_sql/inline_param_encoder.rb +12 -13
  28. data/lib/mini_sql/mysql/connection.rb +18 -3
  29. data/lib/mini_sql/mysql/deserializer_cache.rb +14 -16
  30. data/lib/mini_sql/mysql/prepared_binds.rb +15 -0
  31. data/lib/mini_sql/mysql/prepared_cache.rb +21 -0
  32. data/lib/mini_sql/mysql/prepared_connection.rb +44 -0
  33. data/lib/mini_sql/postgres/coders.rb +2 -0
  34. data/lib/mini_sql/postgres/connection.rb +89 -0
  35. data/lib/mini_sql/postgres/deserializer_cache.rb +36 -16
  36. data/lib/mini_sql/postgres/prepared_binds.rb +15 -0
  37. data/lib/mini_sql/postgres/prepared_cache.rb +25 -0
  38. data/lib/mini_sql/postgres/prepared_connection.rb +36 -0
  39. data/lib/mini_sql/postgres_jdbc/connection.rb +8 -1
  40. data/lib/mini_sql/postgres_jdbc/deserializer_cache.rb +43 -43
  41. data/lib/mini_sql/result.rb +30 -0
  42. data/lib/mini_sql/serializer.rb +84 -0
  43. data/lib/mini_sql/sqlite/connection.rb +20 -2
  44. data/lib/mini_sql/sqlite/deserializer_cache.rb +14 -16
  45. data/lib/mini_sql/sqlite/prepared_binds.rb +15 -0
  46. data/lib/mini_sql/sqlite/prepared_cache.rb +21 -0
  47. data/lib/mini_sql/sqlite/prepared_connection.rb +40 -0
  48. data/lib/mini_sql/version.rb +1 -1
  49. data/mini_sql.gemspec +7 -2
  50. metadata +75 -11
  51. data/.travis.yml +0 -26
@@ -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
@@ -0,0 +1,133 @@
1
+ # frozen_string_literal: true
2
+
3
+ class GenerateData
4
+ class ::Topic < ActiveRecord::Base;
5
+ belongs_to :user
6
+ belongs_to :category
7
+ end
8
+ class ::User < ActiveRecord::Base; end
9
+ class ::Category < ActiveRecord::Base; end
10
+
11
+ def initialize(count_records:)
12
+ @count_records = count_records
13
+ end
14
+
15
+ def call
16
+ conn_settings = {
17
+ password: 'postgres',
18
+ user: 'postgres',
19
+ host: 'localhost'
20
+ }
21
+
22
+ db_conn = conn_settings.merge(database: "test_db", adapter: "postgresql")
23
+
24
+ pg = PG::Connection.new(conn_settings)
25
+ pg.exec "DROP DATABASE IF EXISTS test_db"
26
+ pg.exec "CREATE DATABASE test_db"
27
+ pg.close
28
+
29
+ ActiveRecord::Base.establish_connection(db_conn)
30
+ pg = ActiveRecord::Base.connection.raw_connection
31
+
32
+ pg.exec <<~SQL
33
+ drop table if exists topics;
34
+ drop table if exists users;
35
+ drop table if exists categories;
36
+ CREATE TABLE topics (
37
+ id integer NOT NULL PRIMARY KEY,
38
+ title character varying NOT NULL,
39
+ last_posted_at timestamp without time zone,
40
+ created_at timestamp without time zone NOT NULL,
41
+ updated_at timestamp without time zone NOT NULL,
42
+ views integer DEFAULT 0 NOT NULL,
43
+ posts_count integer DEFAULT 0 NOT NULL,
44
+ user_id integer,
45
+ last_post_user_id integer NOT NULL,
46
+ reply_count integer DEFAULT 0 NOT NULL,
47
+ featured_user1_id integer,
48
+ featured_user2_id integer,
49
+ featured_user3_id integer,
50
+ avg_time integer,
51
+ deleted_at timestamp without time zone,
52
+ highest_post_number integer DEFAULT 0 NOT NULL,
53
+ image_url character varying,
54
+ like_count integer DEFAULT 0 NOT NULL,
55
+ incoming_link_count integer DEFAULT 0 NOT NULL,
56
+ category_id integer,
57
+ visible boolean DEFAULT true NOT NULL,
58
+ moderator_posts_count integer DEFAULT 0 NOT NULL,
59
+ closed boolean DEFAULT false NOT NULL,
60
+ archived boolean DEFAULT false NOT NULL,
61
+ bumped_at timestamp without time zone NOT NULL,
62
+ has_summary boolean DEFAULT false NOT NULL,
63
+ vote_count integer DEFAULT 0 NOT NULL,
64
+ archetype character varying DEFAULT 'regular'::character varying NOT NULL,
65
+ featured_user4_id integer,
66
+ notify_moderators_count integer DEFAULT 0 NOT NULL,
67
+ spam_count integer DEFAULT 0 NOT NULL,
68
+ pinned_at timestamp without time zone,
69
+ score double precision,
70
+ percent_rank double precision DEFAULT 1.0 NOT NULL,
71
+ subtype character varying,
72
+ slug character varying,
73
+ deleted_by_id integer,
74
+ participant_count integer DEFAULT 1,
75
+ word_count integer,
76
+ excerpt character varying(1000),
77
+ pinned_globally boolean DEFAULT false NOT NULL,
78
+ pinned_until timestamp without time zone,
79
+ fancy_title character varying(400),
80
+ highest_staff_post_number integer DEFAULT 0 NOT NULL,
81
+ featured_link character varying
82
+ );
83
+
84
+ CREATE TABLE users (
85
+ id integer NOT NULL PRIMARY KEY,
86
+ first_name character varying NOT NULL,
87
+ last_name character varying NOT NULL
88
+ );
89
+ CREATE TABLE categories (
90
+ id integer NOT NULL PRIMARY KEY,
91
+ name character varying NOT NULL,
92
+ title character varying NOT NULL,
93
+ description character varying NOT NULL
94
+ );
95
+ SQL
96
+
97
+ generate_table(Topic)
98
+ generate_table(User)
99
+ generate_table(Category)
100
+
101
+ pg.exec <<~SQL
102
+ CREATE INDEX user_id ON topics USING btree (user_id);
103
+ CREATE INDEX category_id ON topics USING btree (category_id);
104
+ SQL
105
+
106
+ pg.exec "vacuum full analyze topics"
107
+ pg.exec "vacuum full analyze users"
108
+ pg.exec "vacuum full analyze categories"
109
+
110
+ [ActiveRecord::Base.connection, db_conn]
111
+ end
112
+
113
+ def generate_table(klass)
114
+ data =
115
+ @count_records.times.map do |id|
116
+ topic = { id: id }
117
+ klass.columns.each do |c|
118
+ topic[c.name.to_sym] = value_from_type(c.type)
119
+ end
120
+ topic
121
+ end
122
+ klass.insert_all(data)
123
+ end
124
+
125
+ def value_from_type(type)
126
+ case type
127
+ when :integer then rand(1000)
128
+ when :datetime then Time.now
129
+ when :boolean then false
130
+ else "HELLO WORLD" * 2
131
+ end
132
+ end
133
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'bundler/inline'
2
4
 
3
5
  gemfile do
@@ -21,8 +23,8 @@ require 'benchmark/ips'
21
23
  require 'mini_sql'
22
24
 
23
25
  ActiveRecord::Base.establish_connection(
24
- :adapter => "postgresql",
25
- :database => "test_db"
26
+ adapter: "postgresql",
27
+ database: "test_db"
26
28
  )
27
29
 
28
30
  Sequel.default_timezone = :utc
@@ -47,20 +49,20 @@ SQL
47
49
  class Timestamp < ActiveRecord::Base
48
50
  end
49
51
 
50
- class TimestampSequel< Sequel::Model(:timestamps)
52
+ class TimestampSequel < Sequel::Model(:timestamps)
51
53
  end
52
54
 
53
-
54
55
  Timestamp.transaction do
55
56
  stamps = {
56
57
  }
57
58
  Timestamp.columns.each do |c|
58
- stamps[c.name.to_sym] = case c.type
59
- when :integer then 1
60
- when :datetime then Time.now
61
- when :boolean then false
62
- else "HELLO WORLD" * 2
63
- end
59
+ stamps[c.name.to_sym] =
60
+ case c.type
61
+ when :integer then 1
62
+ when :datetime then Time.now
63
+ when :boolean then false
64
+ else "HELLO WORLD" * 2
65
+ end
64
66
  end
65
67
 
66
68
  1000.times do |id|
@@ -71,7 +73,7 @@ end
71
73
 
72
74
  $conn = ActiveRecord::Base.connection.raw_connection
73
75
 
74
- def ar_pluck_times(l=1000)
76
+ def ar_pluck_times(l = 1000)
75
77
  s = +""
76
78
  Timestamp.limit(l).order(:id).pluck(:time1, :time2).each do |time1, time2|
77
79
  s << time1.to_f.to_s
@@ -80,7 +82,7 @@ def ar_pluck_times(l=1000)
80
82
  s
81
83
  end
82
84
 
83
- def ar_select_times(l=1000)
85
+ def ar_select_times(l = 1000)
84
86
  s = +""
85
87
  Timestamp.limit(l).order(:id).select(:time1, :time2).each do |t|
86
88
  s << t.time1.to_f.to_s
@@ -91,7 +93,7 @@ end
91
93
 
92
94
  $mini_sql = MiniSql::Connection.new($conn)
93
95
 
94
- def pg_times_params(l=1000)
96
+ def pg_times_params(l = 1000)
95
97
  s = +""
96
98
  # use the safe pattern here
97
99
  r = $conn.async_exec_params(-"select time1, time2 from timestamps order by id limit $1", [l])
@@ -110,7 +112,7 @@ def pg_times_params(l=1000)
110
112
  s
111
113
  end
112
114
 
113
- def pg_times(l=1000)
115
+ def pg_times(l = 1000)
114
116
  s = +""
115
117
  # use the safe pattern here
116
118
  r = $conn.async_exec("select time1, time2 from timestamps order by id limit #{l}")
@@ -129,7 +131,7 @@ def pg_times(l=1000)
129
131
  s
130
132
  end
131
133
 
132
- def mini_sql_times(l=1000)
134
+ def mini_sql_times(l = 1000)
133
135
  s = +""
134
136
  $mini_sql.query(-"select time1, time2 from timestamps order by id limit ?", l).each do |t|
135
137
  s << t.time1.to_f.to_s
@@ -138,7 +140,7 @@ def mini_sql_times(l=1000)
138
140
  s
139
141
  end
140
142
 
141
- def sequel_times(l=1000)
143
+ def sequel_times(l = 1000)
142
144
  s = +""
143
145
  TimestampSequel.limit(l).order(:id).select(:time1, :time2).each do |t|
144
146
  s << t.time1.to_f.to_s
@@ -147,7 +149,7 @@ def sequel_times(l=1000)
147
149
  s
148
150
  end
149
151
 
150
- def sequel_pluck_times(l=1000)
152
+ def sequel_pluck_times(l = 1000)
151
153
  s = +""
152
154
  TimestampSequel.limit(l).order(:id).select_map([:time1, :time2]).each do |t|
153
155
  s << t[0].to_f.to_s
@@ -156,7 +158,7 @@ def sequel_pluck_times(l=1000)
156
158
  s
157
159
  end
158
160
 
159
- def sequel_raw_times(l=1000)
161
+ def sequel_raw_times(l = 1000)
160
162
  s = +""
161
163
  DB[-"select time1, time2 from timestamps order by id limit ?", l].map([:time1, :time2]).each do |time1, time2|
162
164
  s << time1.to_f.to_s
@@ -166,13 +168,13 @@ def sequel_raw_times(l=1000)
166
168
  end
167
169
 
168
170
  # usage is not really recommended but just to compare to pluck lets have it
169
- def mini_sql_times_single(l=1000)
171
+ def mini_sql_times_single(l = 1000)
170
172
  s = +""
171
173
  i = 0
172
174
  r = $mini_sql.query_single(-"select time1, time2 from timestamps order by id limit ?", l)
173
175
  while i < r.length
174
176
  s << r[i].to_f.to_s
175
- s << r[i+1].to_f.to_s
177
+ s << r[i + 1].to_f.to_s
176
178
  i += 2
177
179
  end
178
180
  s
@@ -190,7 +192,6 @@ end
190
192
  # s
191
193
  # end
192
194
 
193
-
194
195
  results = [
195
196
  ar_select_times,
196
197
  ar_pluck_times,
@@ -91,7 +91,6 @@ end
91
91
  class TopicSequel < Sequel::Model(:topics)
92
92
  end
93
93
 
94
-
95
94
  Topic.transaction do
96
95
  topic = {
97
96
  }
@@ -179,7 +178,7 @@ def mini_sql_title_id_query_single
179
178
  r = $mini_sql.query_single(-"select id, title from topics order by id limit 1000")
180
179
  while i < r.length
181
180
  s << r[i].to_s
182
- s << r[i+1]
181
+ s << r[i + 1]
183
182
  i += 2
184
183
  end
185
184
  s
@@ -197,7 +196,6 @@ results = [
197
196
 
198
197
  exit(-1) unless results.uniq.length == 1
199
198
 
200
-
201
199
  Benchmark.ips do |r|
202
200
  r.report("ar select title id") do |n|
203
201
  while n > 0
@@ -244,8 +242,6 @@ Benchmark.ips do |r|
244
242
  r.compare!
245
243
  end
246
244
 
247
-
248
-
249
245
  def wide_topic_ar
250
246
  Topic.first
251
247
  end
@@ -301,10 +297,8 @@ end
301
297
  # ar select title id pluck: 317.1 i/s - 1.53x slower
302
298
  # ar select title id: 102.3 i/s - 4.74x slower
303
299
 
304
-
305
300
  # Comparison:
306
301
  # wide topic mini sql: 6768.7 i/s
307
302
  # wide topic mysql: 6063.9 i/s - same-ish: difference falls within error
308
303
  # wide topic sequel: 4908.6 i/s - same-ish: difference falls within error
309
304
  # wide topic ar: 2630.2 i/s - 2.57x slower
310
-