mini_sql 0.2.2 → 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.
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
 
@@ -109,7 +197,7 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
109
197
 
110
198
  ## Contributing
111
199
 
112
- Bug reports and pull requests are welcome on GitHub at https://github.com/SamSaffron/mini_sql. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
200
+ Bug reports and pull requests are welcome on GitHub at https://github.com/discourse/mini_sql. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
113
201
 
114
202
  ## License
115
203
 
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,
@@ -0,0 +1,304 @@
1
+ require 'bundler/inline'
2
+
3
+ gemfile do
4
+ source 'https://rubygems.org'
5
+ gem 'mysql2'
6
+ gem 'mini_sql', path: '../'
7
+ gem 'activesupport'
8
+ gem 'activerecord'
9
+ gem 'activemodel'
10
+ gem 'memory_profiler'
11
+ gem 'benchmark-ips'
12
+ gem 'sequel', github: 'jeremyevans/sequel'
13
+ end
14
+
15
+ require 'mysql2'
16
+ require 'sequel'
17
+ require 'active_record'
18
+ require 'memory_profiler'
19
+ require 'benchmark/ips'
20
+ require 'mini_sql'
21
+
22
+ ActiveRecord::Base.establish_connection(
23
+ :adapter => "mysql2",
24
+ :database => "test_db",
25
+ :username => "root",
26
+ :password => ''
27
+ )
28
+
29
+ DB = Sequel.connect("mysql2://root:@localhost/test_db")
30
+
31
+ mysql = ActiveRecord::Base.connection.raw_connection
32
+
33
+ mysql.query <<SQL
34
+ drop table if exists topics
35
+ SQL
36
+
37
+ mysql.query <<~SQL
38
+ CREATE TABLE `topics` (
39
+ `id` bigint(20) NOT NULL AUTO_INCREMENT,
40
+ `title` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
41
+ `last_posted_at` datetime DEFAULT NULL,
42
+ `created_at` datetime NOT NULL,
43
+ `updated_at` datetime NOT NULL,
44
+ `views` int(11) NOT NULL DEFAULT '0',
45
+ `posts_count` int(11) NOT NULL DEFAULT '0',
46
+ `user_id` int(11) DEFAULT NULL,
47
+ `last_post_user_id` int(11) NOT NULL,
48
+ `reply_count` int(11) NOT NULL DEFAULT '0',
49
+ `featured_user1_id` int(11) DEFAULT NULL,
50
+ `featured_user2_id` int(11) DEFAULT NULL,
51
+ `featured_user3_id` int(11) DEFAULT NULL,
52
+ `avg_time` int(11) DEFAULT NULL,
53
+ `deleted_at` datetime DEFAULT NULL,
54
+ `highest_post_number` int(11) NOT NULL DEFAULT '0',
55
+ `image_url` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
56
+ `like_count` int(11) NOT NULL DEFAULT '0',
57
+ `incoming_link_count` int(11) NOT NULL DEFAULT '0',
58
+ `category_id` int(11) DEFAULT NULL,
59
+ `visible` tinyint(1) NOT NULL DEFAULT '1',
60
+ `moderator_posts_count` int(11) NOT NULL DEFAULT '0',
61
+ `closed` tinyint(1) NOT NULL DEFAULT '0',
62
+ `archived` tinyint(1) NOT NULL DEFAULT '0',
63
+ `bumped_at` datetime NOT NULL,
64
+ `has_summary` tinyint(1) NOT NULL DEFAULT '0',
65
+ `archetype` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'regular',
66
+ `featured_user4_id` int(11) DEFAULT NULL,
67
+ `notify_moderators_count` int(11) NOT NULL DEFAULT '0',
68
+ `spam_count` int(11) NOT NULL DEFAULT '0',
69
+ `pinned_at` datetime DEFAULT NULL,
70
+ `score` float DEFAULT NULL,
71
+ `percent_rank` float NOT NULL DEFAULT '1',
72
+ `subtype` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
73
+ `slug` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
74
+ `deleted_by_id` int(11) DEFAULT NULL,
75
+ `participant_count` int(11) DEFAULT '1',
76
+ `word_count` int(11) DEFAULT NULL,
77
+ `excerpt` varchar(1000) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
78
+ `pinned_globally` tinyint(1) NOT NULL DEFAULT '0',
79
+ `pinned_until` datetime DEFAULT NULL,
80
+ `fancy_title` varchar(400) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
81
+ `highest_staff_post_number` int(11) NOT NULL DEFAULT '0',
82
+ `featured_link` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
83
+ `reviewable_score` float NOT NULL DEFAULT '0',
84
+ PRIMARY KEY (`id`)
85
+ )
86
+ SQL
87
+
88
+ class Topic < ActiveRecord::Base
89
+ end
90
+
91
+ class TopicSequel < Sequel::Model(:topics)
92
+ end
93
+
94
+ Topic.transaction do
95
+ topic = {
96
+ }
97
+ Topic.columns.each do |c|
98
+ topic[c.name.to_sym] = case c.type
99
+ when :integer then 1
100
+ when :datetime then Time.now
101
+ when :boolean then false
102
+ when :float then 1.0
103
+ else "HELLO WORLD" * 2
104
+ end
105
+ end
106
+
107
+ 1000.times do |id|
108
+ topic[:id] = id
109
+ Topic.create!(topic)
110
+ end
111
+ end
112
+
113
+ $conn = ActiveRecord::Base.connection.raw_connection
114
+
115
+ def ar_title_id_pluck
116
+ s = +""
117
+ Topic.limit(1000).order(:id).pluck(:id, :title).each do |id, title|
118
+ s << id.to_s
119
+ s << title
120
+ end
121
+ s
122
+ end
123
+
124
+ def ar_title_id
125
+ s = +""
126
+ Topic.limit(1000).order(:id).select(:id, :title).each do |t|
127
+ s << t.id.to_s
128
+ s << t.title
129
+ end
130
+ s
131
+ end
132
+
133
+ def mysql_title_id
134
+ s = +""
135
+ # use the safe pattern here
136
+ r = $conn.query(-"select id, title from topics order by id limit 1000", as: :array)
137
+
138
+ r.each do |row|
139
+ s << row[0].to_s
140
+ s << row[1]
141
+ end
142
+ s
143
+ end
144
+
145
+ $mini_sql = MiniSql::Connection.get($conn)
146
+
147
+ def mini_sql_title_id
148
+ s = +""
149
+ $mini_sql.query(-"select id, title from topics order by id limit 1000").each do |t|
150
+ s << t.id.to_s
151
+ s << t.title
152
+ end
153
+ s
154
+ end
155
+
156
+ def sequel_select_title_id
157
+ s = +""
158
+ TopicSequel.limit(1000).order(:id).select(:id, :title).each do |t|
159
+ s << t.id.to_s
160
+ s << t.title
161
+ end
162
+ s
163
+ end
164
+
165
+ def sequel_pluck_title_id
166
+ s = +""
167
+ TopicSequel.limit(1000).order(:id).select_map([:id, :title]).each do |t|
168
+ s << t[0].to_s
169
+ s << t[1]
170
+ end
171
+ s
172
+ end
173
+
174
+ # usage is not really recommended but just to compare to pluck lets have it
175
+ def mini_sql_title_id_query_single
176
+ s = +""
177
+ i = 0
178
+ r = $mini_sql.query_single(-"select id, title from topics order by id limit 1000")
179
+ while i < r.length
180
+ s << r[i].to_s
181
+ s << r[i + 1]
182
+ i += 2
183
+ end
184
+ s
185
+ end
186
+
187
+ results = [
188
+ ar_title_id,
189
+ ar_title_id_pluck,
190
+ mysql_title_id,
191
+ mini_sql_title_id,
192
+ sequel_pluck_title_id,
193
+ sequel_select_title_id,
194
+ mini_sql_title_id_query_single
195
+ ]
196
+
197
+ exit(-1) unless results.uniq.length == 1
198
+
199
+ Benchmark.ips do |r|
200
+ r.report("ar select title id") do |n|
201
+ while n > 0
202
+ ar_title_id
203
+ n -= 1
204
+ end
205
+ end
206
+ r.report("ar select title id pluck") do |n|
207
+ while n > 0
208
+ ar_title_id_pluck
209
+ n -= 1
210
+ end
211
+ end
212
+ r.report("sequel title id select") do |n|
213
+ while n > 0
214
+ sequel_select_title_id
215
+ n -= 1
216
+ end
217
+ end
218
+ r.report("mysql select title id") do |n|
219
+ while n > 0
220
+ mysql_title_id
221
+ n -= 1
222
+ end
223
+ end
224
+ r.report("mini_sql select title id") do |n|
225
+ while n > 0
226
+ mini_sql_title_id
227
+ n -= 1
228
+ end
229
+ end
230
+ r.report("sequel title id pluck") do |n|
231
+ while n > 0
232
+ sequel_pluck_title_id
233
+ n -= 1
234
+ end
235
+ end
236
+ r.report("mini_sql query_single title id") do |n|
237
+ while n > 0
238
+ mini_sql_title_id_query_single
239
+ n -= 1
240
+ end
241
+ end
242
+ r.compare!
243
+ end
244
+
245
+ def wide_topic_ar
246
+ Topic.first
247
+ end
248
+
249
+ def wide_topic_mysql
250
+ r = $conn.query("select * from topics limit 1", as: :hash)
251
+ row = r.first
252
+ row
253
+ end
254
+
255
+ def wide_topic_sequel
256
+ TopicSequel.first
257
+ end
258
+
259
+ def wide_topic_mini_sql
260
+ $conn.query("select * from topics limit 1").first
261
+ end
262
+
263
+ Benchmark.ips do |r|
264
+ r.report("wide topic ar") do |n|
265
+ while n > 0
266
+ wide_topic_ar
267
+ n -= 1
268
+ end
269
+ end
270
+ r.report("wide topic sequel") do |n|
271
+ while n > 0
272
+ wide_topic_sequel
273
+ n -= 1
274
+ end
275
+ end
276
+ r.report("wide topic mysql") do |n|
277
+ while n > 0
278
+ wide_topic_mysql
279
+ n -= 1
280
+ end
281
+ end
282
+ r.report("wide topic mini sql") do |n|
283
+ while n > 0
284
+ wide_topic_mini_sql
285
+ n -= 1
286
+ end
287
+ end
288
+ r.compare!
289
+ end
290
+
291
+ # Comparison:
292
+ # mysql select title id: 485.0 i/s
293
+ # mini_sql query_single title id: 447.2 i/s - same-ish: difference falls within error
294
+ # mini_sql select title id: 417.4 i/s - 1.16x slower
295
+ # sequel title id pluck: 370.2 i/s - 1.31x slower
296
+ # sequel title id select: 351.0 i/s - 1.38x slower
297
+ # ar select title id pluck: 317.1 i/s - 1.53x slower
298
+ # ar select title id: 102.3 i/s - 4.74x slower
299
+
300
+ # Comparison:
301
+ # wide topic mini sql: 6768.7 i/s
302
+ # wide topic mysql: 6063.9 i/s - same-ish: difference falls within error
303
+ # wide topic sequel: 4908.6 i/s - same-ish: difference falls within error
304
+ # wide topic ar: 2630.2 i/s - 2.57x slower