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 +4 -4
- data/.travis.yml +2 -0
- data/github-ds.gemspec +2 -0
- data/lib/github/ds/version.rb +1 -1
- data/lib/github/kv.rb +132 -0
- metadata +30 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 19e5298765d4c64c5b96a860463b3ffbc804b71388cc328a08c39acfbdce5b4b
|
4
|
+
data.tar.gz: 42e94eb85be200d4f9183f406f9c0de52f4d5fd00fa007f82f50f159816b4152
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0d0ace44595e6add1f4570b7d22dc9fe01e56172f8f3b63584d558353a1f25afec0ad744f788271b43b0d0e3b8c609d8d720cef5ee12072ab8843dcb70f52b85
|
7
|
+
data.tar.gz: a9c3918434873b9bae164ecf5042cd87154965380d95f476a2ad6b793b8d8200fd529b1d9df0ea8552734379a12ed948844675e0c9e24ea0d2bb814c871948d3
|
data/.travis.yml
CHANGED
data/github-ds.gemspec
CHANGED
@@ -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
|
data/lib/github/ds/version.rb
CHANGED
data/lib/github/kv.rb
CHANGED
@@ -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.
|
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-
|
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:
|