mini_sql 0.2.2 → 1.0

Sign up to get free protection for your applications and to get access to all the features.
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