github-ds 0.2.11 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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: