github-ds 0.2.11 → 0.3.0

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: e66116af0b535ccdf52ef101f485bd38d4bbbc61de8ebee968576006b14fa24d
4
- data.tar.gz: 037a17c54393e7a3b276393c521a40a39b891c859b214f440a9a54a0945a6881
3
+ metadata.gz: 19e5298765d4c64c5b96a860463b3ffbc804b71388cc328a08c39acfbdce5b4b
4
+ data.tar.gz: 42e94eb85be200d4f9183f406f9c0de52f4d5fd00fa007f82f50f159816b4152
5
5
  SHA512:
6
- metadata.gz: 4d1f7260b82336ccf53659df796a3b7f013ae39fbcb6f766590f6f853b1e0295679a29c00db35d7919925ccf3953b54fa25d0be5e4c784c4297260c5fec3bd31
7
- data.tar.gz: d297712f24a6169185f6a55fa29548e7b417842e4d9919fc23fcaeed0cf0cf72135d3ed225d122f70733c020e9f4a5dec925b995165a1f94737f4dda9642bc8f
6
+ metadata.gz: 0d0ace44595e6add1f4570b7d22dc9fe01e56172f8f3b63584d558353a1f25afec0ad744f788271b43b0d0e3b8c609d8d720cef5ee12072ab8843dcb70f52b85
7
+ data.tar.gz: a9c3918434873b9bae164ecf5042cd87154965380d95f476a2ad6b793b8d8200fd529b1d9df0ea8552734379a12ed948844675e0c9e24ea0d2bb814c871948d3
@@ -13,3 +13,5 @@ env:
13
13
  - RAILS_VERSION=5.1.5
14
14
  - RAILS_VERSION=5.0.6
15
15
  - RAILS_VERSION=4.2.10
16
+ services:
17
+ - mysql
@@ -39,4 +39,6 @@ Gem::Specification.new do |spec|
39
39
  spec.add_development_dependency "activesupport"
40
40
  spec.add_development_dependency "mysql2"
41
41
  spec.add_development_dependency "mocha", "~> 1.2.1"
42
+ spec.add_development_dependency "minitest-focus", "~> 1.1.2"
43
+ spec.add_development_dependency "pry", "~> 0.12.2"
42
44
  end
@@ -1,5 +1,5 @@
1
1
  module GitHub
2
2
  module DS
3
- VERSION = "0.2.11"
3
+ VERSION = "0.3.0"
4
4
  end
5
5
  end
@@ -48,6 +48,7 @@ module GitHub
48
48
  KeyLengthError = Class.new(StandardError)
49
49
  ValueLengthError = Class.new(StandardError)
50
50
  UnavailableError = Class.new(StandardError)
51
+ InvalidValueError = Class.new(StandardError)
51
52
 
52
53
  class MissingConnectionError < StandardError; end
53
54
 
@@ -252,6 +253,102 @@ module GitHub
252
253
  }
253
254
  end
254
255
 
256
+ # increment :: String, Integer, expires: Time? -> Integer
257
+ #
258
+ # Increment the key's value by an amount.
259
+ #
260
+ # key - The key to increment.
261
+ # amount - The amount to increment the key's value by.
262
+ # The user can increment by both positive and
263
+ # negative values
264
+ # expires - When the key should expire.
265
+ # touch_on_insert - Only when expires is specified. When true
266
+ # the expires value is only touched upon
267
+ # inserts. Otherwise the record is always
268
+ # touched.
269
+ #
270
+ # Returns the key's value after incrementing.
271
+ def increment(key, amount: 1, expires: nil, touch_on_insert: false)
272
+ validate_key(key)
273
+ validate_amount(amount) if amount
274
+ validate_expires(expires) if expires
275
+ validate_touch(touch_on_insert, expires)
276
+
277
+ expires ||= GitHub::SQL::NULL
278
+
279
+ # This query uses a few MySQL "hacks" to ensure that the incrementing
280
+ # is done atomically and the value is returned. The first trick is done
281
+ # using the `LAST_INSERT_ID` function. This allows us to manually set
282
+ # the LAST_INSERT_ID returned by the query. Here we are able to set it
283
+ # to the new value when an increment takes place, essentially allowing us
284
+ # to do: `UPDATE...;SELECT value from key_value where key=:key` in a
285
+ # single step.
286
+ #
287
+ # However the `LAST_INSERT_ID` trick is only used when the value is
288
+ # updated. Upon a fresh insert we know the amount is going to be set
289
+ # to the amount specified.
290
+ #
291
+ # Lastly we only do these tricks when the value at the key is an integer.
292
+ # If the value is not an integer the update ensures the values remain the
293
+ # same and we raise an error.
294
+ encapsulate_error {
295
+ sql = GitHub::SQL.run(<<-SQL, key: key, amount: amount, now: now, expires: expires, touch: !touch_on_insert, connection: connection)
296
+ INSERT INTO key_values (`key`, `value`, `created_at`, `updated_at`, `expires_at`)
297
+ VALUES(:key, :amount, :now, :now, :expires)
298
+ ON DUPLICATE KEY UPDATE
299
+ `value`=IF(
300
+ concat('',`value`*1) = `value`,
301
+ LAST_INSERT_ID(IF(
302
+ `expires_at` IS NULL OR `expires_at`>=:now,
303
+ `value`+:amount,
304
+ :amount
305
+ )),
306
+ `value`
307
+ ),
308
+ `updated_at`=IF(
309
+ concat('',`value`*1) = `value`,
310
+ :now,
311
+ `updated_at`
312
+ ),
313
+ `expires_at`=IF(
314
+ concat('',`value`*1) = `value`,
315
+ IF(
316
+ :touch,
317
+ :expires,
318
+ `expires_at`
319
+ ),
320
+ `expires_at`
321
+ )
322
+ SQL
323
+
324
+ # The ordering of these statements is extremely important if we are to
325
+ # support incrementing a negative amount. The checks occur in this order:
326
+ # 1. Check if an update with new values occurred? If so return the result
327
+ # This could potentially result in `sql.last_insert_id` with a value
328
+ # of 0, thus it must be before the second check.
329
+ # 2. Check if an update took place but nothing changed (I.E. no new value
330
+ # was set)
331
+ # 3. Check if an insert took place.
332
+ #
333
+ # See https://dev.mysql.com/doc/refman/8.0/en/insert-on-duplicate.html for
334
+ # more information (NOTE: CLIENT_FOUND_ROWS is set)
335
+ if sql.affected_rows == 2
336
+ # An update took place in which data changed. We use a hack to set
337
+ # the last insert ID to be the new value.
338
+ sql.last_insert_id
339
+ elsif sql.affected_rows == 0 || (sql.affected_rows == 1 && sql.last_insert_id == 0)
340
+ # No insert took place nor did any update occur. This means that
341
+ # the value was not an integer thus not incremented.
342
+ raise InvalidValueError
343
+ elsif sql.affected_rows == 1
344
+ # If the number of affected_rows is 1 then a new value was inserted
345
+ # thus we can just return the amount given to us since that is the
346
+ # value at the key
347
+ amount
348
+ end
349
+ }
350
+ end
351
+
255
352
  # del :: String -> nil
256
353
  #
257
354
  # Deletes the specified key. Returns nil. Raises on error.
@@ -311,6 +408,28 @@ module GitHub
311
408
  }
312
409
  end
313
410
 
411
+ # mttl :: [String] -> Result<[Time | nil]>
412
+ #
413
+ # Returns the expires_at time for the specified key or nil.
414
+ #
415
+ # Example:
416
+ #
417
+ # kv.mttl(["foo", "octocat"])
418
+ # # => #<Result value: [2018-04-23 11:34:54 +0200, nil]>
419
+ #
420
+ def mttl(keys)
421
+ validate_key_array(keys)
422
+
423
+ Result.new {
424
+ kvs = GitHub::SQL.results(<<-SQL, :keys => keys, :now => now, :connection => connection).to_h
425
+ SELECT `key`, expires_at FROM key_values
426
+ WHERE `key` in :keys AND (expires_at IS NULL OR expires_at > :now)
427
+ SQL
428
+
429
+ keys.map { |key| kvs[key] }
430
+ }
431
+ end
432
+
314
433
  private
315
434
  def now
316
435
  use_local_time ? Time.now : GitHub::SQL::NOW
@@ -369,6 +488,19 @@ module GitHub
369
488
  end
370
489
  end
371
490
 
491
+ def validate_amount(amount)
492
+ raise ArgumentError.new("The amount specified must be an integer") unless amount.is_a? Integer
493
+ raise ArgumentError.new("The amount specified cannot be 0") if amount == 0
494
+ end
495
+
496
+ def validate_touch(touch, expires)
497
+ raise ArgumentError.new("touch_on_insert must be a boolean value") unless [true, false].include?(touch)
498
+
499
+ if touch && expires.nil?
500
+ raise ArgumentError.new("Please specify an expires value if you wish to touch on insert")
501
+ end
502
+ end
503
+
372
504
  def validate_expires(expires)
373
505
  unless expires.respond_to?(:to_time)
374
506
  raise TypeError, "expires must be a time of some sort (Time, DateTime, ActiveSupport::TimeWithZone, etc.), but was #{expires.class}"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: github-ds
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.11
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - GitHub Open Source
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: exe
11
11
  cert_chain: []
12
- date: 2019-07-25 00:00:00.000000000 Z
12
+ date: 2019-08-26 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: activerecord
@@ -123,6 +123,34 @@ dependencies:
123
123
  - - "~>"
124
124
  - !ruby/object:Gem::Version
125
125
  version: 1.2.1
126
+ - !ruby/object:Gem::Dependency
127
+ name: minitest-focus
128
+ requirement: !ruby/object:Gem::Requirement
129
+ requirements:
130
+ - - "~>"
131
+ - !ruby/object:Gem::Version
132
+ version: 1.1.2
133
+ type: :development
134
+ prerelease: false
135
+ version_requirements: !ruby/object:Gem::Requirement
136
+ requirements:
137
+ - - "~>"
138
+ - !ruby/object:Gem::Version
139
+ version: 1.1.2
140
+ - !ruby/object:Gem::Dependency
141
+ name: pry
142
+ requirement: !ruby/object:Gem::Requirement
143
+ requirements:
144
+ - - "~>"
145
+ - !ruby/object:Gem::Version
146
+ version: 0.12.2
147
+ type: :development
148
+ prerelease: false
149
+ version_requirements: !ruby/object:Gem::Requirement
150
+ requirements:
151
+ - - "~>"
152
+ - !ruby/object:Gem::Version
153
+ version: 0.12.2
126
154
  description: A collection of libraries for working with SQL on top of ActiveRecord's
127
155
  connection.
128
156
  email: