mini_sql 0.2.3 → 1.0.1

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.
data/Gemfile CHANGED
@@ -1,6 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  source "https://rubygems.org"
2
4
 
3
- git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
5
+ git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
4
6
 
5
7
  # Specify your gem's dependencies in mini_sql.gemspec
6
8
  gemspec
data/Guardfile CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  guard :minitest do
2
4
  watch(%r{^test/(.*)_test\.rb$})
3
5
  watch(%r{^lib/(.*/)?([^/]+)\.rb$}) { |m| "test/#{m[1]}#{m[2]}_test.rb" }
data/README.md CHANGED
@@ -32,11 +32,28 @@ conn.query("select 1 id, 'bob' name").each do |user|
32
32
  puts user.id # 1
33
33
  end
34
34
 
35
+ # extend result objects with additional method
36
+ module ProductDecorator
37
+ def amount_price
38
+ price * quantity
39
+ end
40
+ end
41
+
42
+ conn.query_decorator(ProductDecorator, "select 20 price, 3 quantity").each do |user|
43
+ puts user.amount_price # 60
44
+ end
45
+
35
46
  p conn.query_single('select 1 union select 2')
36
47
  # [1,2]
37
48
 
38
49
  p conn.query_hash('select 1 as a, 2 as b union select 3, 4')
39
50
  # [{"a" => 1, "b"=> 1},{"a" => 3, "b" => 4}
51
+
52
+ p conn.query_array("select 1 as a, '2' as b union select 3, 'e'")
53
+ # [[1, '2'], [3, 'e']]
54
+
55
+ p conn.query_array("select 1 as a, '2' as b union select 3, 'e'").to_h
56
+ # {1 => '2', 3 => 'e'}
40
57
  ```
41
58
 
42
59
  ## The query builder
@@ -63,9 +80,18 @@ end
63
80
  The builder allows for `order_by`, `where`, `select`, `set`, `limit`, `join`, `left_join` and `offset`.
64
81
 
65
82
  ## Is it fast?
66
-
67
83
  Yes, it is very fast. See benchmarks in [the bench directory](https://github.com/discourse/mini_sql/tree/master/bench).
68
84
 
85
+ **Comparison mini_sql methods**
86
+ ```
87
+ query_array 1351.6 i/s
88
+ query 963.8 i/s - 1.40x slower
89
+ query_hash 787.4 i/s - 1.72x slower
90
+
91
+ query_single('select id from topics limit 1000') 2368.9 i/s
92
+ query_array('select id from topics limit 1000').flatten 1350.1 i/s - 1.75x slower
93
+ ```
94
+
69
95
  As a rule it will outperform similar naive PG code while remaining safe.
70
96
 
71
97
  ```ruby
@@ -98,6 +124,68 @@ MiniSql is careful to always clear results as soon as possible.
98
124
 
99
125
  MiniSql's default type mapper prefers treating `timestamp without time zone` columns as utc. This is done to ensure widest amount of compatability and is a departure from the default in the PG 1.0 gem. If you wish to amend behavior feel free to pass in a custom type_map.
100
126
 
127
+ ## Custom type maps
128
+
129
+ When using Postgres, native type mapping implementation is used. This is roughly
130
+ implemented as:
131
+
132
+ ```ruby
133
+ type_map ||= PG::BasicTypeMapForResults.new(conn)
134
+ # additional specific decoders
135
+ ```
136
+
137
+ The type mapper instansitated once on-demand at boot and reused by all mini_sql connections.
138
+
139
+ Initializing the basic type map for Postgres can be a costly operation. You may
140
+ wish to amend the type mapper so for example you only return strings:
141
+
142
+ ```
143
+ # maybe you do not want Integer
144
+ p cnn.query("select a 1").first.a
145
+ "1"
146
+ ```
147
+
148
+ To specify a different type mapper for your results use:
149
+
150
+ ```
151
+ MiniSql::Connections.get(pg_connection, type_map: custom_type_map)
152
+ ```
153
+
154
+ In the case of Rails you can opt to use the type mapper Rails uses with:
155
+
156
+ ```
157
+ pg_cnn = ActiveRecord::Base.connection.raw_connection
158
+ mini_sql_cnn = MiniSql::Connection.get(pg_cnn, type_map: pg_cnn.type_map_for_results)
159
+ ```
160
+
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.
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.
101
189
 
102
190
  ## I want more features!
103
191
 
data/Rakefile CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "bundler/gem_tasks"
2
4
  require "rake/testtask"
3
5
 
@@ -13,4 +15,4 @@ Rake::TestTask.new(:test) do |t|
13
15
  t.test_files = FileList[test_glob]
14
16
  end
15
17
 
16
- task :default => :test
18
+ task default: :test
@@ -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
-
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'bundler/inline'
2
4
 
3
5
  gemfile do
@@ -12,6 +14,7 @@ gemfile do
12
14
  gem 'sequel', github: 'jeremyevans/sequel'
13
15
  gem 'sequel_pg', github: 'jeremyevans/sequel_pg', require: 'sequel'
14
16
  gem 'swift-db-postgres', github: 'deepfryed/swift-db-postgres'
17
+ gem 'draper'
15
18
  end
16
19
 
17
20
  require 'sequel'
@@ -89,7 +92,6 @@ end
89
92
  class TopicSequel < Sequel::Model(:topics)
90
93
  end
91
94
 
92
-
93
95
  Topic.transaction do
94
96
  topic = {
95
97
  }
@@ -184,7 +186,7 @@ def mini_sql_title_id_query_single
184
186
  r = $mini_sql.query_single(-"select id, title from topics order by id limit 1000")
185
187
  while i < r.length
186
188
  s << r[i].to_s
187
- s << r[i+1]
189
+ s << r[i + 1]
188
190
  i += 2
189
191
  end
190
192
  s
@@ -193,8 +195,8 @@ end
193
195
  # connects over unix socket
194
196
  $swift = Swift::DB::Postgres.new(db: "test_db")
195
197
 
196
- def swift_select_title_id(l=1000)
197
- s = ""
198
+ def swift_select_title_id(l = 1000)
199
+ s = +''
198
200
  i = 0
199
201
  r = $swift.execute("select id, title from topics order by id limit 1000")
200
202
  while i < r.selected_rows
@@ -218,6 +220,174 @@ results = [
218
220
 
219
221
  exit(-1) unless results.uniq.length == 1
220
222
 
223
+ # https://github.com/drapergem/draper
224
+ class TopicDraper < Draper::Decorator
225
+ delegate :id
226
+
227
+ def title_bang
228
+ object.title + '!!!'
229
+ end
230
+ end
231
+
232
+ # https://ruby-doc.org/stdlib-2.5.1/libdoc/delegate/rdoc/SimpleDelegator.html
233
+ class TopicSimpleDelegator < SimpleDelegator
234
+ def title_bang
235
+ title + '!!!'
236
+ end
237
+ end
238
+
239
+ class TopicDecoratorSequel < TopicSequel
240
+ def title_bang
241
+ title + '!!!'
242
+ end
243
+ end
244
+
245
+ class TopicArModel < Topic
246
+ def title_bang
247
+ title + '!!!'
248
+ end
249
+ end
250
+
251
+ module TopicDecorator
252
+ def title_bang
253
+ title + '!!!'
254
+ end
255
+ end
256
+
257
+ Benchmark.ips do |r|
258
+ r.report('query_decorator') do |n|
259
+ while n > 0
260
+ $mini_sql.query_decorator(TopicDecorator, 'select id, title from topics order by id limit 1000').each do |obj|
261
+ obj.title_bang
262
+ obj.id
263
+ end
264
+ n -= 1
265
+ end
266
+ end
267
+ r.report('extend') do |n|
268
+ while n > 0
269
+ $mini_sql.query('select id, title from topics order by id limit 1000').each do |obj|
270
+ d_obj = obj.extend(TopicDecorator)
271
+ d_obj.title_bang
272
+ d_obj.id
273
+ end
274
+ n -= 1
275
+ end
276
+ end
277
+ r.report('draper') do |n|
278
+ while n > 0
279
+ $mini_sql.query('select id, title from topics order by id limit 1000').each do |obj|
280
+ d_obj = TopicDraper.new(obj)
281
+ d_obj.title_bang
282
+ d_obj.id
283
+ end
284
+ n -= 1
285
+ end
286
+ end
287
+ r.report('simple_delegator') do |n|
288
+ while n > 0
289
+ $mini_sql.query('select id, title from topics order by id limit 1000').each do |obj|
290
+ d_obj = TopicSimpleDelegator.new(obj)
291
+ d_obj.title_bang
292
+ d_obj.id
293
+ end
294
+ n -= 1
295
+ end
296
+ end
297
+ r.report('query') do |n|
298
+ while n > 0
299
+ $mini_sql.query('select id, title from topics order by id limit 1000').each do |obj|
300
+ obj.title + '!!!'
301
+ obj.id
302
+ end
303
+ n -= 1
304
+ end
305
+ end
306
+ r.report('ar model') do |n|
307
+ while n > 0
308
+ TopicArModel.limit(1000).order(:id).select(:id, :title).each do |obj|
309
+ obj.title_bang
310
+ obj.id
311
+ end
312
+ n -= 1
313
+ end
314
+ end
315
+ r.report('sequel model') do |n|
316
+ while n > 0
317
+ TopicDecoratorSequel.limit(1000).order(:id).select(:id, :title).each do |obj|
318
+ obj.title_bang
319
+ obj.id
320
+ end
321
+ n -= 1
322
+ end
323
+ end
324
+
325
+ r.compare!
326
+ end
327
+
328
+ # Comparison:
329
+ # query: 828.4 i/s
330
+ # query_decorator: 819.3 i/s - same-ish: difference falls within error
331
+ # sequel model: 672.4 i/s - 1.23x slower
332
+ # extend: 519.4 i/s - 1.59x slower
333
+ # simple_delegator: 496.8 i/s - 1.67x slower
334
+ # draper: 416.2 i/s - 1.99x slower
335
+ # ar model: 113.4 i/s - 7.30x slower
336
+
337
+ Benchmark.ips do |r|
338
+ r.report('query_hash') do |n|
339
+ while n > 0
340
+ $mini_sql.query_hash('select id, title from topics order by id limit 1000').each do |hash|
341
+ [hash['id'], hash['title']]
342
+ end
343
+ n -= 1
344
+ end
345
+ end
346
+ r.report('query_array') do |n|
347
+ while n > 0
348
+ $mini_sql.query_array('select id, title from topics order by id limit 1000').each do |id, title|
349
+ [id, title]
350
+ end
351
+ n -= 1
352
+ end
353
+ end
354
+ r.report('query') do |n|
355
+ while n > 0
356
+ $mini_sql.query('select id, title from topics order by id limit 1000').each do |obj|
357
+ [obj.id, obj.title]
358
+ end
359
+ n -= 1
360
+ end
361
+ end
362
+
363
+ r.compare!
364
+ end
365
+
366
+ # Comparison:
367
+ # query_array: 1351.6 i/s
368
+ # query: 963.8 i/s - 1.40x slower
369
+ # query_hash: 787.4 i/s - 1.72x slower
370
+
371
+ Benchmark.ips do |r|
372
+ r.report('query_single') do |n|
373
+ while n > 0
374
+ $mini_sql.query_single('select id from topics order by id limit 1000')
375
+ n -= 1
376
+ end
377
+ end
378
+ r.report('query_array') do |n|
379
+ while n > 0
380
+ $mini_sql.query_array('select id from topics order by id limit 1000').flatten
381
+ n -= 1
382
+ end
383
+ end
384
+
385
+ r.compare!
386
+ end
387
+
388
+ # Comparison:
389
+ # query_single: 2368.9 i/s
390
+ # query_array: 1350.1 i/s - 1.75x slower
221
391
 
222
392
  Benchmark.ips do |r|
223
393
  r.report("ar select title id") do |n|
@@ -271,8 +441,6 @@ Benchmark.ips do |r|
271
441
  r.compare!
272
442
  end
273
443
 
274
-
275
-
276
444
  def wide_topic_ar
277
445
  Topic.first
278
446
  end
@@ -320,7 +488,6 @@ Benchmark.ips do |r|
320
488
  r.compare!
321
489
  end
322
490
 
323
-
324
491
  # Comparison:
325
492
  # pg select title id: 1519.7 i/s
326
493
  # mini_sql query_single title id: 1335.0 i/s - 1.14x slower
@@ -338,10 +505,7 @@ end
338
505
  # wide topic sequel: 5696.8 i/s - 1.31x slower
339
506
  # wide topic ar: 2515.0 i/s - 2.97x slower
340
507
 
341
-
342
-
343
508
  # to run deep analysis run
344
509
  # MemoryProfiler.report do
345
510
  # ar
346
511
  # end.pretty_print
347
-