moneta 1.1.0 → 1.1.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.
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