moneta 1.1.0 → 1.1.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 238904687ead475c2961fdd367d90c57fd54598e9a802d81df557058fe71019a
4
- data.tar.gz: 422fc9af8f65e1fe01cf9cde5fa69d5e4c57a9e3121b92b8796fd81f5f8290b7
3
+ metadata.gz: 867a31ea01c90748ed86ae701f221d4a628b7c4932c8e0aa6c8b0f13f02a516b
4
+ data.tar.gz: e3f89ab42ca23c9db8ddbcede6c6c447a8829ab78e01e1eb67edd1a6f2ac402e
5
5
  SHA512:
6
- metadata.gz: fc36f41ff84caa03b78c7ecd6c2dd69db87f27c96204c8a25429314b860838d79c4aa3087ce71b6074777448a26c9d21ba3bdfa8d747e9a9d841228ecd3231ed
7
- data.tar.gz: b1adfbeb3abcfe01867356752a968ded5e188652d55c6bbdc7ef5dca53eb906ff2ff0b82fc32b80a66a8caec75ab38c7dd4968639e600666f4194e239ce547f3
6
+ metadata.gz: 58e3592ad92768341454f0c7b637fc614f3cd3d820566840333b2c229601627ac10ae77509fec769acd9c71a08f602b2c7d059ee0e6c7e2042d814f91201acf2
7
+ data.tar.gz: 8d9eeb664e800fd1ebff92dea3701fb01aa25a453647f2ba76d54b1a3602e6897899f8cedff823046096ee675f6089355912de240ae5c8e96ed606c41fa5573f
data/CHANGES CHANGED
@@ -1,3 +1,8 @@
1
+ 1.1.1
2
+
3
+ * Adapters::Sequel - use prepared statements
4
+ * Adapters::Sqlite - use upsert for increment where supported
5
+
1
6
  1.1.0
2
7
 
3
8
  * Adapters::ActiveRecord - rewrite to use Arel directly; support for Rails 5
@@ -110,6 +110,8 @@ module Moneta
110
110
  when :Sequel
111
111
  # Sequel accept only base64 keys
112
112
  transformer[:key] << :base64
113
+ # If using HStore, binary data is not allowed
114
+ transformer[:value] << :base64 if options[:hstore]
113
115
  when :ActiveRecord, :DataMapper
114
116
  # DataMapper and AR accept only base64 keys and values
115
117
  transformer[:key] << :base64
@@ -30,6 +30,10 @@ module Moneta
30
30
  # row of the table in the value_column using the hstore format. The row to use is
31
31
  # the one where the value_column is equal to the value of this option, and will be created
32
32
  # if it doesn't exist.
33
+ # @option options [Symbol] :each_key_server Some adapters are unable to do
34
+ # multiple operations with a single connection. For these, it is
35
+ # possible to specify a separate connection to use for `#each_key`. Use
36
+ # in conjunction with Sequel's `:servers` option
33
37
  # @option options All other options passed to `Sequel#connect`
34
38
  def self.new(options = {})
35
39
  extensions = options.delete(:extensions)
@@ -79,6 +83,7 @@ module Moneta
79
83
  @table_name = (options.delete(:table) || :moneta).to_sym
80
84
  @key_column = options.delete(:key_column) || :k
81
85
  @value_column = options.delete(:value_column) || :v
86
+ @each_key_server = options.delete(:each_key_server)
82
87
 
83
88
  create_proc = options.delete(:create_table)
84
89
  if create_proc.nil?
@@ -88,23 +93,26 @@ module Moneta
88
93
  end
89
94
 
90
95
  @table = @backend[@table_name]
96
+ prepare_statements
91
97
  end
92
98
 
93
99
  # (see Proxy#key?)
94
100
  def key?(key, options = {})
95
- !@table.where(key_column => key).empty?
101
+ @key.call(key: key) != nil
96
102
  end
97
103
 
98
104
  # (see Proxy#load)
99
105
  def load(key, options = {})
100
- @table.where(key_column => key).get(value_column)
106
+ if row = @load.call(key: key)
107
+ row[value_column]
108
+ end
101
109
  end
102
110
 
103
111
  # (see Proxy#store)
104
112
  def store(key, value, options = {})
105
113
  blob_value = blob(value)
106
- unless @table.where(key_column => key).update(value_column => blob_value) == 1
107
- @table.insert(key_column => key, value_column => blob_value)
114
+ unless @store_update.call(key: key, value: blob_value) == 1
115
+ @create.call(key: key, value: blob_value)
108
116
  end
109
117
  value
110
118
  rescue ::Sequel::DatabaseError
@@ -112,9 +120,9 @@ module Moneta
112
120
  (tries += 1) < 10 ? retry : raise
113
121
  end
114
122
 
115
- # (see Proxy#store)
123
+ # (see Proxy#create)
116
124
  def create(key, value, options = {})
117
- @table.insert(key_column => key, value_column => blob(value))
125
+ @create.call(key: key, value: blob(value))
118
126
  true
119
127
  rescue UniqueConstraintViolation
120
128
  false
@@ -123,13 +131,16 @@ module Moneta
123
131
  # (see Proxy#increment)
124
132
  def increment(key, amount = 1, options = {})
125
133
  @backend.transaction do
126
- if existing = @table.where(key_column => key).for_update.get(value_column)
127
- amount += Integer(existing)
128
- raise IncrementError, "no update" unless @table.
129
- where(key_column => key, value_column => existing).
130
- update(value_column => blob(amount.to_s)) == 1
134
+ if existing = @load_for_update.call(key: key)
135
+ existing_value = existing[value_column]
136
+ amount += Integer(existing_value)
137
+ raise IncrementError, "no update" unless @increment_update.call(
138
+ key: key,
139
+ value: existing_value,
140
+ new_value: blob(amount.to_s)
141
+ ) == 1
131
142
  else
132
- @table.insert(key_column => key, value_column => blob(amount.to_s))
143
+ @create.call(key: key, value: blob(amount.to_s))
133
144
  end
134
145
  amount
135
146
  end
@@ -142,7 +153,7 @@ module Moneta
142
153
  # (see Proxy#delete)
143
154
  def delete(key, options = {})
144
155
  value = load(key, options)
145
- @table.filter(key_column => key).delete
156
+ @delete.call(key: key)
146
157
  value
147
158
  end
148
159
 
@@ -160,19 +171,19 @@ module Moneta
160
171
 
161
172
  # (see Proxy#slice)
162
173
  def slice(*keys, **options)
163
- @table.filter(key_column => keys).as_hash(key_column, value_column)
174
+ @slice.all(keys).map! { |row| [row[key_column], row[value_column]] }
164
175
  end
165
176
 
166
177
  # (see Proxy#values_at)
167
178
  def values_at(*keys, **options)
168
- pairs = slice(*keys, **options)
179
+ pairs = Hash[slice(*keys, **options)]
169
180
  keys.map { |key| pairs[key] }
170
181
  end
171
182
 
172
183
  # (see Proxy#fetch_values)
173
184
  def fetch_values(*keys, **options)
174
185
  return values_at(*keys, **options) unless block_given?
175
- existing = slice(*keys, **options)
186
+ existing = Hash[slice(*keys, **options)]
176
187
  keys.map do |key|
177
188
  if existing.key? key
178
189
  existing[key]
@@ -185,7 +196,7 @@ module Moneta
185
196
  # (see Proxy#merge!)
186
197
  def merge!(pairs, options = {})
187
198
  @backend.transaction do
188
- existing = existing_for_update(pairs)
199
+ existing = Hash[slice_for_update(pairs)]
189
200
  update_pairs, insert_pairs = pairs.partition { |k, _| existing.key?(k) }
190
201
  @table.import([key_column, value_column], blob_pairs(insert_pairs))
191
202
 
@@ -196,7 +207,7 @@ module Moneta
196
207
  end
197
208
 
198
209
  update_pairs.each do |key, value|
199
- @table.filter(key_column => key).update(value_column => blob(value))
210
+ @store_update.call(key: key, value: blob(value))
200
211
  end
201
212
  end
202
213
 
@@ -206,23 +217,22 @@ module Moneta
206
217
  # (see Proxy#each_key)
207
218
  def each_key
208
219
  return enum_for(:each_key) { @table.count } unless block_given?
209
- @table.select(key_column).each do |row|
210
- yield row[key_column]
220
+ if @each_key_server
221
+ @table.server(@each_key_server).order(key_column).select(key_column).paged_each do |row|
222
+ yield row[key_column]
223
+ end
224
+ else
225
+ @table.select(key_column).order(key_column).paged_each(stream: false) do |row|
226
+ yield row[key_column]
227
+ end
211
228
  end
212
229
  self
213
230
  end
214
231
 
215
232
  protected
216
233
 
217
- # See https://github.com/jeremyevans/sequel/issues/715
218
234
  def blob(s)
219
- if s == nil
220
- nil
221
- elsif s.empty?
222
- ''
223
- else
224
- ::Sequel.blob(s)
225
- end
235
+ ::Sequel.blob(s) unless s == nil
226
236
  end
227
237
 
228
238
  def blob_pairs(pairs)
@@ -240,30 +250,89 @@ module Moneta
240
250
  end
241
251
  end
242
252
 
243
- def existing_for_update(pairs)
244
- @table.
245
- filter(key_column => pairs.map { |k, _| k }.to_a).
246
- for_update.
247
- as_hash(key_column, value_column)
253
+ def slice_for_update(pairs)
254
+ @slice_for_update.all(pairs.map { |k, _| k }.to_a).map! do |row|
255
+ [row[key_column], row[value_column]]
256
+ end
248
257
  end
249
258
 
250
259
  def yield_merge_pairs(pairs)
251
- existing = existing_for_update(pairs)
260
+ existing = Hash[slice_for_update(pairs)]
252
261
  pairs.map do |key, new_value|
253
262
  new_value = yield(key, existing[key], new_value) if existing.key?(key)
254
263
  [key, new_value]
255
264
  end
256
265
  end
257
266
 
267
+ def statement_id(id)
268
+ "moneta_#{@table_name}_#{id}".to_sym
269
+ end
270
+
271
+ def prepare_statements
272
+ prepare_key
273
+ prepare_load
274
+ prepare_store
275
+ prepare_create
276
+ prepare_increment
277
+ prepare_delete
278
+ prepare_slice
279
+ end
280
+
281
+ def prepare_key
282
+ @key = @table.
283
+ where(key_column => :$key).select(1).
284
+ prepare(:first, statement_id(:key))
285
+ end
286
+
287
+ def prepare_load
288
+ @load = @table.
289
+ where(key_column => :$key).select(value_column).
290
+ prepare(:first, statement_id(:load))
291
+ end
292
+
293
+ def prepare_store
294
+ @store_update = @table.
295
+ where(key_column => :$key).
296
+ prepare(:update, statement_id(:store_update), value_column => :$value)
297
+ end
298
+
299
+ def prepare_create
300
+ @create = @table.
301
+ prepare(:insert, statement_id(:create), key_column => :$key, value_column => :$value)
302
+ end
303
+
304
+ def prepare_increment
305
+ @load_for_update = @table.
306
+ where(key_column => :$key).for_update.
307
+ select(value_column).
308
+ prepare(:first, statement_id(:load_for_update))
309
+ @increment_update ||= @table.
310
+ where(key_column => :$key, value_column => :$value).
311
+ prepare(:update, statement_id(:increment_update), value_column => :$new_value)
312
+ end
313
+
314
+ def prepare_delete
315
+ @delete = @table.where(key_column => :$key).
316
+ prepare(:delete, statement_id(:delete))
317
+ end
318
+
319
+ def prepare_slice
320
+ @slice_for_update = ::Sequel::Dataset::PlaceholderLiteralizer.loader(@table) do |pl, ds|
321
+ ds.filter(key_column => pl.arg).select(key_column, value_column).for_update
322
+ end
323
+
324
+ @slice = ::Sequel::Dataset::PlaceholderLiteralizer.loader(@table) do |pl, ds|
325
+ ds.filter(key_column => pl.arg).select(key_column, value_column)
326
+ end
327
+ end
328
+
258
329
  # @api private
259
330
  class IncrementError < ::Sequel::DatabaseError; end
260
331
 
261
332
  # @api private
262
333
  class MySQL < Sequel
263
334
  def store(key, value, options = {})
264
- @table.
265
- on_duplicate_key_update.
266
- insert(key_column => key, value_column => blob(value))
335
+ @store.call(key: key, value: blob(value))
267
336
  value
268
337
  end
269
338
 
@@ -271,12 +340,12 @@ module Moneta
271
340
  @backend.transaction do
272
341
  # this creates a row-level lock even if there is no existing row (a
273
342
  # "gap lock").
274
- if existing = @table.where(key_column => key).for_update.get(value_column)
343
+ if row = @load_for_update.call(key: key)
275
344
  # Integer() will raise an exception if the existing value cannot be parsed
276
- amount += Integer(existing)
277
- @table.where(key_column => key).update(value_column => amount)
345
+ amount += Integer(row[value_column])
346
+ @increment_update.call(key: key, value: amount)
278
347
  else
279
- @table.insert(key_column => key, value_column => amount)
348
+ @create.call(key: key, value: amount)
280
349
  end
281
350
  amount
282
351
  end
@@ -295,38 +364,49 @@ module Moneta
295
364
 
296
365
  self
297
366
  end
367
+
368
+ def each_key
369
+ return super unless block_given? && @each_key_server && @table.respond_to?(:stream)
370
+ # Order is not required when streaming
371
+ @table.server(@each_key_server).select(key_column).paged_each do |row|
372
+ yield row[key_column]
373
+ end
374
+ self
375
+ end
376
+
377
+ protected
378
+
379
+ def prepare_store
380
+ @store = @table.
381
+ on_duplicate_key_update.
382
+ prepare(:insert, statement_id(:store), key_column => :$key, value_column => :$value)
383
+ end
384
+
385
+ def prepare_increment
386
+ @increment_update = @table.
387
+ where(key_column => :$key).
388
+ prepare(:update, statement_id(:increment_update), value_column => :$value)
389
+ super
390
+ end
298
391
  end
299
392
 
300
393
  # @api private
301
394
  class Postgres < Sequel
302
395
  def store(key, value, options = {})
303
- @table.
304
- insert_conflict(
305
- target: key_column,
306
- update: {value_column => ::Sequel[:excluded][value_column]}).
307
- insert(key_column => key, value_column => blob(value))
396
+ @store.call(key: key, value: blob(value))
308
397
  value
309
398
  end
310
399
 
311
400
  def increment(key, amount = 1, options = {})
312
- update_expr = ::Sequel[:convert_to].function(
313
- (::Sequel[:convert_from].function(
314
- ::Sequel[@table_name][value_column],
315
- 'UTF8').cast(Integer) + amount).cast(String),
316
- 'UTF8')
317
-
318
- if row = @table.
319
- returning(value_column).
320
- insert_conflict(target: key_column, update: {value_column => update_expr}).
321
- insert(key_column => key, value_column => amount.to_s).
322
- first
323
- then
401
+ result = @increment.call(key: key, value: blob(amount.to_s), amount: amount)
402
+ if row = result.first
324
403
  row[value_column].to_i
325
404
  end
326
405
  end
327
406
 
328
407
  def delete(key, options = {})
329
- if row = @table.returning(value_column).where(key_column => key).delete.first
408
+ result = @delete.call(key: key)
409
+ if row = result.first
330
410
  row[value_column]
331
411
  end
332
412
  end
@@ -343,6 +423,45 @@ module Moneta
343
423
 
344
424
  self
345
425
  end
426
+
427
+ def each_key
428
+ return super unless block_given? && !@each_key_server && @table.respond_to?(:use_cursor)
429
+ # With a cursor, this will Just Work.
430
+ @table.select(key_column).paged_each do |row|
431
+ yield row[key_column]
432
+ end
433
+ self
434
+ end
435
+
436
+ protected
437
+
438
+ def prepare_store
439
+ @store = @table.
440
+ insert_conflict(
441
+ target: key_column,
442
+ update: {value_column => ::Sequel[:excluded][value_column]}).
443
+ prepare(:insert, statement_id(:store), key_column => :$key, value_column => :$value)
444
+ end
445
+
446
+ def prepare_increment
447
+ update_expr = ::Sequel[:convert_to].function(
448
+ (::Sequel[:convert_from].function(
449
+ ::Sequel[@table_name][value_column],
450
+ 'UTF8').cast(Integer) + :$amount).cast(String),
451
+ 'UTF8')
452
+
453
+ @increment = @table.
454
+ returning(value_column).
455
+ insert_conflict(target: key_column, update: {value_column => update_expr}).
456
+ prepare(:insert, statement_id(:increment), key_column => :$key, value_column => :$value)
457
+ end
458
+
459
+ def prepare_delete
460
+ @delete = @table.
461
+ returning(value_column).
462
+ where(key_column => :$key).
463
+ prepare(:delete, statement_id(:delete))
464
+ end
346
465
  end
347
466
 
348
467
  # @api private
@@ -356,66 +475,79 @@ module Moneta
356
475
  end
357
476
 
358
477
  def key?(key, options = {})
359
- !!@table.where(key_column => @row).get(::Sequel[value_column].hstore.key?(key))
478
+ if @key
479
+ row = @key.call(row: @row, key: key) || false
480
+ row && row[:present]
481
+ else
482
+ @key_pl.get(key)
483
+ end
360
484
  end
361
485
 
362
486
  def store(key, value, options = {})
363
- create_row
364
- @table.
365
- where(key_column => @row).
366
- update(value_column => ::Sequel[@table_name][value_column].hstore.merge(key => value))
487
+ @backend.transaction do
488
+ create_row
489
+ @store.call(row: @row, pair: ::Sequel.hstore(key => value))
490
+ end
367
491
  value
368
492
  end
369
493
 
370
494
  def load(key, options = {})
371
- @table.where(key_column => @row).get(::Sequel[value_column].hstore[key])
495
+ if row = @load.call(row: @row, key: key)
496
+ row[:value]
497
+ end
372
498
  end
373
499
 
374
500
  def delete(key, options = {})
375
- value = load(key, options)
376
- @table.where(key_column => @row).update(value_column => ::Sequel[value_column].hstore.delete(key))
377
- value
501
+ @backend.transaction do
502
+ value = load(key, options)
503
+ @delete.call(row: @row, key: key)
504
+ value
505
+ end
378
506
  end
379
507
 
380
508
  def increment(key, amount = 1, options = {})
381
- create_row
382
- pair = ::Sequel[:hstore].function(
383
- key,
384
- (::Sequel[:coalesce].function(
385
- ::Sequel[value_column].hstore[key].cast(Integer),
386
- 0) + amount).cast(String))
387
-
388
- if row = @table.
389
- returning(::Sequel[value_column].hstore[key].as(:value)).
390
- where(key_column => @row).
391
- update(value_column => ::Sequel.join([value_column, pair])).
392
- first
393
- then
394
- row[:value].to_i
509
+ @backend.transaction do
510
+ create_row
511
+ if row = @increment.call(row: @row, key: key, amount: amount).first
512
+ row[:value].to_i
513
+ end
395
514
  end
396
515
  end
397
516
 
398
517
  def create(key, value, options = {})
399
- create_row
400
- 1 == @table.
401
- where(key_column => @row).
402
- exclude(::Sequel[value_column].hstore.key?(key)).
403
- update(value_column => ::Sequel[value_column].hstore.merge(key => value))
518
+ @backend.transaction do
519
+ create_row
520
+ 1 ==
521
+ if @create
522
+ @create.call(row: @row, key: key, pair: ::Sequel.hstore(key => value))
523
+ else
524
+ @table.
525
+ where(key_column => @row).
526
+ exclude(::Sequel[value_column].hstore.key?(key)).
527
+ update(value_column => ::Sequel[value_column].hstore.merge(key => value))
528
+ end
529
+ end
404
530
  end
405
531
 
406
532
  def clear(options = {})
407
- @table.where(key_column => @row).update(value_column => '')
533
+ @clear.call(row: @row)
408
534
  self
409
535
  end
410
536
 
411
537
  def values_at(*keys, **options)
412
- @table.
413
- where(key_column => @row).
414
- get(::Sequel[value_column].hstore[::Sequel.pg_array(keys)]).to_a
538
+ if row = @values_at.call(row: @row, keys: ::Sequel.pg_array(keys))
539
+ row[:values].to_a
540
+ else
541
+ []
542
+ end
415
543
  end
416
544
 
417
545
  def slice(*keys, **options)
418
- @table.where(key_column => @row).get(::Sequel[value_column].hstore.slice(keys)).to_h
546
+ if row = @slice.call(row: @row, keys: ::Sequel.pg_array(keys))
547
+ row[:pairs].to_h
548
+ else
549
+ []
550
+ end
419
551
  end
420
552
 
421
553
  def merge!(pairs, options = {}, &block)
@@ -423,32 +555,25 @@ module Moneta
423
555
  create_row
424
556
  pairs = yield_merge_pairs(pairs, &block) if block_given?
425
557
  hash = Hash === pairs ? pairs : Hash[pairs.to_a]
426
- @table.
427
- where(key_column => @row).
428
- update(value_column => ::Sequel[@table_name][value_column].hstore.merge(hash))
558
+ @store.call(row: @row, pair: ::Sequel.hstore(hash))
429
559
  end
430
560
 
431
561
  self
432
562
  end
433
563
 
434
564
  def each_key
435
- unless block_given?
436
- return enum_for(:each_key) do
437
- @backend.from(
438
- @table.
439
- where(key_column => @row).
440
- select(::Sequel[@table_name][value_column].hstore.each)).count
565
+ return enum_for(:each_key) { @size.call(row: @row)[:size] } unless block_given?
566
+
567
+ ds =
568
+ if @each_key_server
569
+ @table.server(@each_key_server)
570
+ else
571
+ @table
441
572
  end
442
- end
443
- first = false
444
- @table.
445
- where(key_column => @row).
446
- select(::Sequel[@table_name][value_column].hstore.skeys).
447
- each do |row|
448
- if first
449
- first = false
450
- next
451
- end
573
+ ds = ds.order(:skeys) unless @table.respond_to?(:use_cursor)
574
+ ds.where(key_column => @row).
575
+ select(::Sequel[value_column].hstore.skeys).
576
+ paged_each do |row|
452
577
  yield row[:skeys]
453
578
  end
454
579
  self
@@ -457,9 +582,7 @@ module Moneta
457
582
  protected
458
583
 
459
584
  def create_row
460
- @table.
461
- insert_ignore.
462
- insert(key_column => @row, value_column => '')
585
+ @create_row.call(row: @row)
463
586
  end
464
587
 
465
588
  def create_table
@@ -473,9 +596,109 @@ module Moneta
473
596
  end
474
597
  end
475
598
 
476
- def existing_for_update(pairs)
477
- @table.where(key_column => @row).for_update.
478
- get(::Sequel[value_column].hstore.slice(pairs.map { |k, _| k }.to_a)).to_h
599
+ def slice_for_update(pairs)
600
+ keys = pairs.map { |k, _| k }.to_a
601
+ if row = @slice_for_update.call(row: @row, keys: ::Sequel.pg_array(keys))
602
+ row[:pairs].to_h
603
+ else
604
+ {}
605
+ end
606
+ end
607
+
608
+ def prepare_statements
609
+ super
610
+ prepare_create_row
611
+ prepare_clear
612
+ prepare_values_at
613
+ prepare_size
614
+ end
615
+
616
+ def prepare_create_row
617
+ @create_row = @table.
618
+ insert_ignore.
619
+ prepare(:insert, statement_id(:hstore_create_row), key_column => :$row, value_column => '')
620
+ end
621
+
622
+ def prepare_clear
623
+ @clear = @table.where(key_column => :$row).prepare(:update, statement_id(:hstore_clear), value_column => '')
624
+ end
625
+
626
+ def prepare_key
627
+ if defined?(JRUBY_VERSION)
628
+ @key_pl = ::Sequel::Dataset::PlaceholderLiteralizer.loader(@table) do |pl, ds|
629
+ ds.where(key_column => @row).select(::Sequel[value_column].hstore.key?(pl.arg))
630
+ end
631
+ else
632
+ @key = @table.where(key_column => :$row).
633
+ select(::Sequel[value_column].hstore.key?(:$key).as(:present)).
634
+ prepare(:first, statement_id(:hstore_key))
635
+ end
636
+ end
637
+
638
+ def prepare_store
639
+ @store = @table.
640
+ where(key_column => :$row).
641
+ prepare(:update, statement_id(:hstore_store), value_column => ::Sequel[value_column].hstore.merge(:$pair))
642
+ end
643
+
644
+ def prepare_increment
645
+ pair = ::Sequel[:hstore].function(
646
+ :$key,
647
+ (::Sequel[:coalesce].function(
648
+ ::Sequel[value_column].hstore[:$key].cast(Integer),
649
+ 0) + :$amount).cast(String))
650
+
651
+ @increment = @table.
652
+ returning(::Sequel[value_column].hstore[:$key].as(:value)).
653
+ where(key_column => :$row).
654
+ prepare(:update, statement_id(:hstore_increment), value_column => ::Sequel.join([value_column, pair]))
655
+ end
656
+
657
+ def prepare_load
658
+ @load = @table.where(key_column => :$row).
659
+ select(::Sequel[value_column].hstore[:$key].as(:value)).
660
+ prepare(:first, statement_id(:hstore_load))
661
+ end
662
+
663
+ def prepare_delete
664
+ @delete = @table.where(key_column => :$row).
665
+ prepare(:update, statement_id(:hstore_delete), value_column => ::Sequel[value_column].hstore.delete(:$key))
666
+ end
667
+
668
+ def prepare_create
669
+ # Under JRuby we can't use a prepared statement for queries involving
670
+ # the hstore `?` (key?) operator. See
671
+ # https://stackoverflow.com/questions/11940401/escaping-hstore-contains-operators-in-a-jdbc-prepared-statement
672
+ return if defined?(JRUBY_VERSION)
673
+ @create = @table.
674
+ where(key_column => :$row).
675
+ exclude(::Sequel[value_column].hstore.key?(:$key)).
676
+ prepare(:update, statement_id(:hstore_create), value_column => ::Sequel[value_column].hstore.merge(:$pair))
677
+ end
678
+
679
+ def prepare_values_at
680
+ @values_at = @table.
681
+ where(key_column => :$row).
682
+ select(::Sequel[value_column].hstore[::Sequel.cast(:$keys, :"text[]")].as(:values)).
683
+ prepare(:first, statement_id(:hstore_values_at))
684
+ end
685
+
686
+ def prepare_slice
687
+ slice = @table.
688
+ where(key_column => :$row).
689
+ select(::Sequel[value_column].hstore.slice(:$keys).as(:pairs))
690
+ @slice = slice.prepare(:first, statement_id(:hstore_slice))
691
+ @slice_for_update = slice.for_update.prepare(:first, statement_id(:hstore_slice_for_update))
692
+ end
693
+
694
+ def prepare_size
695
+ @size =
696
+ @backend.from(
697
+ @table.
698
+ where(key_column => :$row).
699
+ select(::Sequel[value_column].hstore.each)).
700
+ select { count.function.*.as(:size) }.
701
+ prepare(:first, statement_id(:hstore_size))
479
702
  end
480
703
  end
481
704
 
@@ -495,18 +718,8 @@ module Moneta
495
718
 
496
719
  def increment(key, amount = 1, options = {})
497
720
  return super unless @can_upsert
498
- update_expr = (::Sequel[@table_name][value_column].cast(Integer) + amount).cast(:blob)
499
-
500
721
  @backend.transaction do
501
- @table.
502
- insert_conflict(
503
- target: key_column,
504
- update: {value_column => update_expr},
505
- update_where:
506
- ::Sequel.|(
507
- {value_column => blob("0")},
508
- ::Sequel.~(::Sequel[@table_name][value_column].cast(Integer)) => 0)).
509
- insert(key_column => key, value_column => blob(amount.to_s))
722
+ @increment.call(key: key, value: amount.to_s, amount: amount)
510
723
  Integer(load(key))
511
724
  end
512
725
  end
@@ -519,6 +732,28 @@ module Moneta
519
732
 
520
733
  self
521
734
  end
735
+
736
+ protected
737
+
738
+ def prepare_store
739
+ @store = @table.
740
+ insert_conflict(:replace).
741
+ prepare(:insert, statement_id(:store), key_column => :$key, value_column => :$value)
742
+ end
743
+
744
+ def prepare_increment
745
+ return super unless @can_upsert
746
+ update_expr = (::Sequel[value_column].cast(Integer) + :$amount).cast(:blob)
747
+ @increment = @table.
748
+ insert_conflict(
749
+ target: key_column,
750
+ update: {value_column => update_expr},
751
+ update_where:
752
+ ::Sequel.|(
753
+ {value_column => blob("0")},
754
+ ::Sequel.~(::Sequel[value_column].cast(Integer)) => 0)).
755
+ prepare(:insert, statement_id(:increment), key_column => :$key, value_column => :$value)
756
+ end
522
757
  end
523
758
  end
524
759
  end
@@ -38,6 +38,16 @@ module Moneta
38
38
  @create = @backend.prepare("insert into #{@table} values (?, ?)"),
39
39
  @keys = @backend.prepare("select k from #{@table}"),
40
40
  @count = @backend.prepare("select count(*) from #{@table}")]
41
+
42
+ version = @backend.execute("select sqlite_version()").first.first
43
+ if @can_upsert = ::Gem::Version.new(version) >= ::Gem::Version.new('3.24.0')
44
+ @stmts << (@increment = @backend.prepare <<-SQL)
45
+ insert into #{@table} values (?, ?)
46
+ on conflict (k)
47
+ do update set v = cast(cast(v as integer) + ? as blob)
48
+ where v = '0' or v = X'30' or cast(v as integer) != 0
49
+ SQL
50
+ end
41
51
  end
42
52
 
43
53
  # (see Proxy#key?)
@@ -66,7 +76,11 @@ module Moneta
66
76
 
67
77
  # (see Proxy#increment)
68
78
  def increment(key, amount = 1, options = {})
69
- @backend.transaction(:exclusive) { return super }
79
+ @backend.transaction(:exclusive) { return super } unless @can_upsert
80
+ @backend.transaction do
81
+ @increment.execute!(key, amount.to_s, amount)
82
+ return Integer(load(key))
83
+ end
70
84
  end
71
85
 
72
86
  # (see Proxy#clear)
@@ -1,5 +1,5 @@
1
1
  module Moneta
2
2
  # Moneta version number
3
3
  # @api public
4
- VERSION = '1.1.0'
4
+ VERSION = '1.1.1'
5
5
  end
@@ -5,6 +5,7 @@ require 'benchmark'
5
5
  require 'moneta'
6
6
  require 'fileutils'
7
7
  require 'active_support'
8
+ require 'active_support/cache/moneta_store'
8
9
 
9
10
  class String
10
11
  def random(n)
@@ -62,6 +63,17 @@ class MonetaBenchmarks
62
63
  }
63
64
  }
64
65
  },
66
+ {
67
+ name: "ActiveRecord (Sqlite)",
68
+ adapter: :ActiveRecord,
69
+ options: {
70
+ table: 'activerecord',
71
+ connection: {
72
+ adapter: (defined?(JRUBY_VERSION) ? 'jdbcsqlite3' : 'sqlite3'),
73
+ database: "#{DIR}/activerecord_sqlite.db"
74
+ }
75
+ }
76
+ },
65
77
  {
66
78
  name: "ActiveSupportCache (Memory)",
67
79
  adapter: :ActiveSupportCache,
@@ -76,6 +88,20 @@ class MonetaBenchmarks
76
88
  backend: ::ActiveSupport::Cache::RedisCacheStore.new
77
89
  }
78
90
  },
91
+ {
92
+ name: "ActiveSupportCache (Moneta Memory)",
93
+ adapter: :ActiveSupportCache,
94
+ options: {
95
+ backend: ::ActiveSupport::Cache::MonetaStore.new(store: Moneta.new(:Memory))
96
+ }
97
+ },
98
+ {
99
+ name: "ActiveSupportCache (Moneta Redis)",
100
+ adapter: :ActiveSupportCache,
101
+ options: {
102
+ backend: ::ActiveSupport::Cache::MonetaStore.new(store: Moneta.new(:Redis))
103
+ }
104
+ },
79
105
  {name: "Cassandra"},
80
106
  {name: "Client (Memory)", adapter: :Client},
81
107
  {name: "Couch"},
@@ -151,6 +177,27 @@ class MonetaBenchmarks
151
177
  }
152
178
  end.merge(table: 'sequel')
153
179
  },
180
+ {
181
+ name: "Sequel (HStore)",
182
+ adapter: :Sequel,
183
+ options:
184
+ if defined?(JRUBY_VERSION)
185
+ {db: "jdbc:postgresql://localhost/#{postgres_database1}?user=#{postgres_username}"}
186
+ else
187
+ {
188
+ db: "postgres://localhost/#{postgres_database1}",
189
+ user: postgres_username
190
+ }
191
+ end.merge(table: 'sequel_hstore', hstore: 'row')
192
+ },
193
+ {
194
+ name: "Sequel (Sqlite)",
195
+ adapter: :Sequel,
196
+ options: {
197
+ table: 'sequel',
198
+ db: "#{defined?(JRUBY_VERSION) && 'jdbc:'}sqlite://#{DIR}/sequel"
199
+ }
200
+ },
154
201
  {
155
202
  name: "Sqlite (Memory)",
156
203
  adapter: :Sqlite,
@@ -158,12 +205,29 @@ class MonetaBenchmarks
158
205
  file: ':memory:'
159
206
  }
160
207
  },
208
+ {
209
+ name: "Sqlite (File)",
210
+ adapter: :Sqlite,
211
+ options: {
212
+ file: "#{DIR}/sqlite"
213
+ }
214
+ },
161
215
  {name: "TDB", options: { file: "#{DIR}/tdb" }},
162
216
  {name: "TokyoCabinet", options: { file: "#{DIR}/tokyocabinet" }},
163
217
  {name: "TokyoTyrant"},
164
218
  ].compact
165
219
 
166
220
  CONFIGS = {
221
+ test: {
222
+ runs: 2,
223
+ keys: 10,
224
+ min_key_len: 1,
225
+ max_key_len: 32,
226
+ key_dist: :uniform,
227
+ min_val_len: 0,
228
+ max_val_len: 256,
229
+ val_dist: :uniform
230
+ },
167
231
  uniform_small: {
168
232
  runs: 3,
169
233
  keys: 1000,
@@ -227,8 +291,6 @@ class MonetaBenchmarks
227
291
  }
228
292
 
229
293
  DICT = 'ABCDEFGHIJKLNOPQRSTUVWXYZabcdefghijklnopqrstuvwxyz123456789'.freeze
230
- HEADER = "\n Minimum Maximum Total Mean Stddev Ops/s"
231
- SEPARATOR = '=' * 77
232
294
 
233
295
  module Rand
234
296
  extend self
@@ -256,6 +318,14 @@ class MonetaBenchmarks
256
318
  end
257
319
  end
258
320
 
321
+ def header
322
+ (" " * @name_len) + " Minimum Maximum Total Mean Stddev Ops/s"
323
+ end
324
+
325
+ def separator
326
+ "=" * header.length
327
+ end
328
+
259
329
  def parallel(&block)
260
330
  if defined?(JRUBY_VERSION)
261
331
  Thread.new(&block)
@@ -357,26 +427,26 @@ class MonetaBenchmarks
357
427
  write_histogram("#{DIR}/key.histogram", key_lens)
358
428
  write_histogram("#{DIR}/value.histogram", val_lens)
359
429
 
360
- puts "\n\e[1m\e[34m#{SEPARATOR}\n\e[34mComputing keys and values...\n\e[34m#{SEPARATOR}\e[0m"
361
- puts %{ Minimum Maximum Total Mean Stddev}
362
- puts 'Key Length % 8d % 8d % 8d % 8d % 8d' % [key_lens.min, key_lens.max, key_lens.sum, mean(key_lens), stddev(key_lens)]
363
- puts 'Value Length % 8d % 8d % 8d % 8d % 8d' % [val_lens.min, val_lens.max, val_lens.sum, mean(val_lens), stddev(val_lens)]
430
+ puts "\n\e[1m\e[34m#{separator}\n\e[34mComputing keys and values...\n\e[34m#{separator}\e[0m"
431
+ puts " " * @name_len + %{ Minimum Maximum Total Mean Stddev}
432
+ puts 'Key Length'.ljust(@name_len) + ' % 8d % 8d % 8d % 8d % 8d' % [key_lens.min, key_lens.max, key_lens.sum, mean(key_lens), stddev(key_lens)]
433
+ puts 'Value Length'.ljust(@name_len) + ' % 8d % 8d % 8d % 8d % 8d' % [val_lens.min, val_lens.max, val_lens.sum, mean(val_lens), stddev(val_lens)]
364
434
  end
365
435
 
366
436
  def print_config
367
- puts "\e[1m\e[36m#{SEPARATOR}\n\e[36mConfig #{@config_name}\n\e[36m#{SEPARATOR}\e[0m"
437
+ puts "\e[1m\e[36m#{separator}\n\e[36mConfig #{@config_name}\n\e[36m#{separator}\e[0m"
368
438
  @config.each do |k,v|
369
439
  puts '%-16s = %-10s' % [k,v]
370
440
  end
371
441
  end
372
442
 
373
443
  def print_store_stats(name)
374
- puts HEADER
444
+ puts "\n" + header
375
445
  [:write, :read, :sum].each do |i|
376
446
  ops = (1000 * @config[:runs] * @data.size) / @stats[name][i].sum
377
- line = '%-17.17s %-5s % 8d % 8d % 8d % 8d % 8d % 8d' %
447
+ line = "%-#{@name_len-1}.#{@name_len-1}s %-5s % 8d % 8d % 8d % 8d % 8d % 8d" %
378
448
  [name, i, @stats[name][i].min, @stats[name][i].max, @stats[name][i].sum,
379
- mean(@stats[name][i]), mean(@stats[name][i]), ops]
449
+ mean(@stats[name][i]), stddev(@stats[name][i]), ops]
380
450
  @summary << [-ops, line << "\n"] if i == :sum
381
451
  puts line
382
452
  end
@@ -394,7 +464,7 @@ class MonetaBenchmarks
394
464
  name = spec[:name]
395
465
  adapter = spec[:adapter] || spec[:name].to_sym
396
466
  options = spec[:options] || {}
397
- puts "\n\e[1m\e[34m#{SEPARATOR}\n\e[34m#{name}\n\e[34m#{SEPARATOR}\e[0m"
467
+ puts "\n\e[1m\e[34m#{separator}\n\e[34m#{name}\n\e[34m#{separator}\e[0m"
398
468
 
399
469
  store = Moneta.new(adapter, options.dup)
400
470
 
@@ -453,7 +523,7 @@ class MonetaBenchmarks
453
523
  end
454
524
 
455
525
  def print_summary
456
- puts "\n\e[1m\e[36m#{SEPARATOR}\n\e[36mSummary #{@config_name}: #{@config[:runs]} runs, #{@data.size} keys\n\e[36m#{SEPARATOR}\e[0m#{HEADER}\n"
526
+ puts "\n\e[1m\e[36m#{separator}\n\e[36mSummary #{@config_name}: #{@config[:runs]} runs, #{@data.size} keys\n\e[36m#{separator}\e[0m\n#{header}\n"
457
527
  @summary.sort_by(&:first).each do |entry|
458
528
  puts entry.last
459
529
  end
@@ -478,6 +548,8 @@ class MonetaBenchmarks
478
548
  STORES
479
549
  end.select { |spec| !spec.key?(:sizes) || spec[:sizes].include?(@size) }
480
550
 
551
+ @name_len = (@stores.map { |spec| spec[:name] }.map(&:length) + ["Value Length".length]).max + 2
552
+
481
553
  # Disable jruby stdout pollution by memcached
482
554
  if defined?(JRUBY_VERSION)
483
555
  require 'java'
@@ -1,80 +1,58 @@
1
+ require_relative './helper.rb'
1
2
 
2
- describe 'adapter_sequel', adapter: :Sequel do
3
- before :all do
4
- require 'sequel'
5
- end
6
-
3
+ describe ':Sequel adapter', adapter: :Sequel do
7
4
  specs = ADAPTER_SPECS.with_each_key.with_values(:nil)
8
5
 
9
- shared_examples :adapter_sequel do
10
- context 'with MySQL' do
11
- moneta_build do
12
- Moneta::Adapters::Sequel.new(opts.merge(
13
- db: if defined?(JRUBY_VERSION)
14
- "jdbc:mysql://localhost/#{mysql_database1}?user=#{mysql_username}"
15
- else
16
- "mysql2://#{mysql_username}:@localhost/#{mysql_database1}"
17
- end
18
- ))
19
- end
20
-
21
- moneta_specs specs
22
- end
23
-
24
- context "with SQLite" do
25
- moneta_build do
26
- Moneta::Adapters::Sequel.new(opts.merge(
27
- db: "#{defined?(JRUBY_VERSION) && 'jdbc:'}sqlite://" + File.join(tempdir, 'adapter_sequel.db')))
28
- end
29
-
30
- moneta_specs specs.without_concurrent
31
- end
32
-
33
- context "with Postgres" do
34
- moneta_build do
35
- Moneta::Adapters::Sequel.new(opts.merge(
36
- if defined?(JRUBY_VERSION)
37
- {db: "jdbc:postgresql://localhost/#{postgres_database1}?user=#{postgres_username}"}
38
- else
39
- {
40
- db: "postgres://localhost/#{postgres_database1}",
41
- user: postgres_username
42
- }
43
- end
6
+ context 'with MySQL backend' do
7
+ moneta_build do
8
+ Moneta::Adapters::Sequel.new(opts.merge(
9
+ db: if defined?(JRUBY_VERSION)
10
+ "jdbc:mysql://localhost/#{mysql_database1}?user=#{mysql_username}"
11
+ else
12
+ "mysql2://#{mysql_username}:@localhost/#{mysql_database1}"
13
+ end
44
14
  ))
45
- end
46
-
47
- moneta_specs specs
48
15
  end
49
16
 
50
- context "with H2", unsupported: !defined?(JRUBY_VERSION) do
51
- moneta_build do
52
- Moneta::Adapters::Sequel.new(opts.merge(
53
- db: "jdbc:h2:" + tempdir))
54
- end
17
+ include_examples :adapter_sequel, specs
18
+ end
55
19
 
56
- moneta_specs specs
20
+ context "with SQLite backend" do
21
+ moneta_build do
22
+ Moneta::Adapters::Sequel.new(opts.merge(
23
+ db: "#{defined?(JRUBY_VERSION) && 'jdbc:'}sqlite://" + File.join(tempdir, 'adapter_sequel.db')))
57
24
  end
25
+
26
+ include_examples :adapter_sequel, specs.without_concurrent
58
27
  end
59
28
 
60
- context 'with backend optimisations' do
61
- let(:opts) { {table: "adapter_sequel"} }
29
+ context "with Postgres backend" do
30
+ moneta_build do
31
+ Moneta::Adapters::Sequel.new(opts.merge(
32
+ if defined?(JRUBY_VERSION)
33
+ {db: "jdbc:postgresql://localhost/#{postgres_database1}?user=#{postgres_username}"}
34
+ else
35
+ {
36
+ db: "postgres://localhost/#{postgres_database1}",
37
+ user: postgres_username
38
+ }
39
+ end
40
+ ))
41
+ end
62
42
 
63
- include_examples :adapter_sequel
43
+ include_examples :adapter_sequel, specs
64
44
  end
65
45
 
66
- context 'without backend optimisations' do
67
- let(:opts) do
68
- {
69
- table: "adapter_sequel",
70
- optimize: false
71
- }
46
+ context "with H2 backend", unsupported: !defined?(JRUBY_VERSION) do
47
+ moneta_build do
48
+ Moneta::Adapters::Sequel.new(opts.merge(
49
+ db: "jdbc:h2:" + tempdir))
72
50
  end
73
51
 
74
- include_examples :adapter_sequel
52
+ include_examples :adapter_sequel, specs, optimize: false
75
53
  end
76
54
 
77
- context "with Postgres HStore" do
55
+ context "with Postgres HStore backend" do
78
56
  moneta_build do
79
57
  Moneta::Adapters::Sequel.new(
80
58
  if defined?(JRUBY_VERSION)
@@ -86,12 +64,13 @@ describe 'adapter_sequel', adapter: :Sequel do
86
64
  }
87
65
  end.merge(
88
66
  table: 'hstore_table1',
89
- hstore: 'row')
67
+ hstore: 'row'
68
+ )
90
69
  )
91
70
  end
92
71
 
93
72
  # Concurrency is too slow, and binary values cannot be stored in an hstore
94
- moneta_specs specs.without_values(:binary).without_concurrent
73
+ include_examples :adapter_sequel, specs.without_values(:binary).without_concurrent, optimize: false
95
74
  end
96
75
 
97
76
  describe 'table creation' do
@@ -107,15 +86,15 @@ describe 'adapter_sequel', adapter: :Sequel do
107
86
 
108
87
  before { backend.drop_table?(table_name) }
109
88
 
110
- shared_examples :create_table do
111
- it "creates the table" do
112
- store = new_store
113
- expect(backend.table_exists?(table_name)).to be true
114
- expect(backend[table_name].columns).to include(store.key_column, store.value_column)
89
+ shared_examples :table_creation do
90
+ shared_examples :create_table do
91
+ it "creates the table" do
92
+ store = new_store
93
+ expect(backend.table_exists?(table_name)).to be true
94
+ expect(backend[table_name].columns).to include(store.key_column, store.value_column)
95
+ end
115
96
  end
116
- end
117
97
 
118
- shared_examples :table_creation do
119
98
  context "with :db parameter" do
120
99
  moneta_build do
121
100
  Moneta::Adapters::Sequel.new(opts.merge(db: conn_str, table: table_name))
@@ -0,0 +1,38 @@
1
+ RSpec.shared_examples :adapter_sequel do |specs, optimize: true|
2
+ shared_examples :each_key_server do
3
+ context "with each_key server" do
4
+ let(:opts) do
5
+ base_opts.merge(
6
+ servers: {each_key: {}},
7
+ each_key_server: :each_key
8
+ )
9
+ end
10
+
11
+ moneta_specs specs
12
+ end
13
+
14
+ context "without each_key server" do
15
+ let(:opts) { base_opts }
16
+ moneta_specs specs
17
+ end
18
+ end
19
+
20
+ if optimize
21
+ context 'with backend optimizations' do
22
+ let(:base_opts) { {table: "adapter_sequel"} }
23
+
24
+ include_examples :each_key_server
25
+ end
26
+ end
27
+
28
+ context 'without backend optimizations' do
29
+ let(:base_opts) do
30
+ {
31
+ table: "adapter_sequel",
32
+ optimize: false
33
+ }
34
+ end
35
+
36
+ include_examples :each_key_server
37
+ end
38
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: moneta
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 1.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Daniel Mendler
@@ -11,7 +11,7 @@ authors:
11
11
  autorequire:
12
12
  bindir: bin
13
13
  cert_chain: []
14
- date: 2019-03-16 00:00:00.000000000 Z
14
+ date: 2019-04-10 00:00:00.000000000 Z
15
15
  dependencies: []
16
16
  description: A unified interface to key/value stores
17
17
  email:
@@ -242,6 +242,7 @@ files:
242
242
  - spec/moneta/adapters/sdbm/standard_sdbm_spec.rb
243
243
  - spec/moneta/adapters/sdbm/standard_sdbm_with_expires_spec.rb
244
244
  - spec/moneta/adapters/sequel/adapter_sequel_spec.rb
245
+ - spec/moneta/adapters/sequel/helper.rb
245
246
  - spec/moneta/adapters/sequel/standard_sequel_spec.rb
246
247
  - spec/moneta/adapters/sequel/standard_sequel_with_expires_spec.rb
247
248
  - spec/moneta/adapters/sqlite/adapter_sqlite_spec.rb
@@ -348,7 +349,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
348
349
  - !ruby/object:Gem::Version
349
350
  version: '0'
350
351
  requirements: []
351
- rubygems_version: 3.0.2
352
+ rubygems_version: 3.0.3
352
353
  signing_key:
353
354
  specification_version: 4
354
355
  summary: A unified interface to key/value stores, including Redis, Memcached, TokyoCabinet,
@@ -481,6 +482,7 @@ test_files:
481
482
  - spec/moneta/adapters/sdbm/standard_sdbm_spec.rb
482
483
  - spec/moneta/adapters/sdbm/standard_sdbm_with_expires_spec.rb
483
484
  - spec/moneta/adapters/sequel/adapter_sequel_spec.rb
485
+ - spec/moneta/adapters/sequel/helper.rb
484
486
  - spec/moneta/adapters/sequel/standard_sequel_spec.rb
485
487
  - spec/moneta/adapters/sequel/standard_sequel_with_expires_spec.rb
486
488
  - spec/moneta/adapters/sqlite/adapter_sqlite_spec.rb