hotkv 1.0.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.
Files changed (52) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +202 -0
  3. data/NOTICE +6 -0
  4. data/README.md +459 -0
  5. data/SECURITY.md +30 -0
  6. data/lib/hotkv/ai/call_result.rb +25 -0
  7. data/lib/hotkv/ai/prompt_cache.rb +135 -0
  8. data/lib/hotkv/ai/semantic_cache.rb +66 -0
  9. data/lib/hotkv/client.rb +201 -0
  10. data/lib/hotkv/cluster/crc16.rb +43 -0
  11. data/lib/hotkv/cluster.rb +184 -0
  12. data/lib/hotkv/commands/admin.rb +36 -0
  13. data/lib/hotkv/commands/all.rb +49 -0
  14. data/lib/hotkv/commands/bitmaps.rb +52 -0
  15. data/lib/hotkv/commands/geo.rb +146 -0
  16. data/lib/hotkv/commands/hashes.rb +169 -0
  17. data/lib/hotkv/commands/hyperloglog.rb +21 -0
  18. data/lib/hotkv/commands/keys.rb +170 -0
  19. data/lib/hotkv/commands/lists.rb +113 -0
  20. data/lib/hotkv/commands/probabilistic.rb +23 -0
  21. data/lib/hotkv/commands/publishing.rb +46 -0
  22. data/lib/hotkv/commands/scan.rb +82 -0
  23. data/lib/hotkv/commands/scripting.rb +128 -0
  24. data/lib/hotkv/commands/server.rb +175 -0
  25. data/lib/hotkv/commands/sets.rb +75 -0
  26. data/lib/hotkv/commands/sorted_sets.rb +258 -0
  27. data/lib/hotkv/commands/streams.rb +207 -0
  28. data/lib/hotkv/commands/strings.rb +178 -0
  29. data/lib/hotkv/commands/support.rb +69 -0
  30. data/lib/hotkv/connection.rb +221 -0
  31. data/lib/hotkv/errors.rb +145 -0
  32. data/lib/hotkv/extensions.rb +118 -0
  33. data/lib/hotkv/future.rb +55 -0
  34. data/lib/hotkv/llm_price.rb +53 -0
  35. data/lib/hotkv/namespaces/admin.rb +261 -0
  36. data/lib/hotkv/namespaces/amem.rb +172 -0
  37. data/lib/hotkv/namespaces/caching.rb +397 -0
  38. data/lib/hotkv/namespaces/core.rb +190 -0
  39. data/lib/hotkv/namespaces/ops.rb +166 -0
  40. data/lib/hotkv/namespaces/probabilistic.rb +180 -0
  41. data/lib/hotkv/namespaces/rag.rb +63 -0
  42. data/lib/hotkv/pipeline.rb +57 -0
  43. data/lib/hotkv/pool.rb +123 -0
  44. data/lib/hotkv/pubsub.rb +60 -0
  45. data/lib/hotkv/resp.rb +176 -0
  46. data/lib/hotkv/transaction.rb +115 -0
  47. data/lib/hotkv/url.rb +100 -0
  48. data/lib/hotkv/usage.rb +42 -0
  49. data/lib/hotkv/version.rb +7 -0
  50. data/lib/hotkv/wire.rb +145 -0
  51. data/lib/hotkv.rb +28 -0
  52. metadata +137 -0
@@ -0,0 +1,36 @@
1
+ # Copyright (c) 2026 HotKV Ltd
2
+ # Licensed under the Apache License, Version 2.0. See LICENSE in the project root.
3
+
4
+ require_relative "../namespaces/admin"
5
+
6
+ module HotKV
7
+ module Commands
8
+ # Accessors for the administrative command families: ACL (core) and the
9
+ # licensed TENANT, IAM, AUDIT, ENCRYPTION and REPLICATION families.
10
+ module Admin
11
+ def acl
12
+ @hotkv_ns_acl ||= Namespaces::ACL.new(self)
13
+ end
14
+
15
+ def tenant
16
+ @hotkv_ns_tenant ||= Namespaces::Tenant.new(self)
17
+ end
18
+
19
+ def iam
20
+ @hotkv_ns_iam ||= Namespaces::IAM.new(self)
21
+ end
22
+
23
+ def audit
24
+ @hotkv_ns_audit ||= Namespaces::Audit.new(self)
25
+ end
26
+
27
+ def encryption
28
+ @hotkv_ns_encryption ||= Namespaces::Encryption.new(self)
29
+ end
30
+
31
+ def replication
32
+ @hotkv_ns_replication ||= Namespaces::Replication.new(self)
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,49 @@
1
+ # Copyright (c) 2026 HotKV Ltd
2
+ # Licensed under the Apache License, Version 2.0. See LICENSE in the project root.
3
+
4
+ require_relative "support"
5
+ require_relative "strings"
6
+ require_relative "keys"
7
+ require_relative "lists"
8
+ require_relative "sets"
9
+ require_relative "hashes"
10
+ require_relative "sorted_sets"
11
+ require_relative "streams"
12
+ require_relative "geo"
13
+ require_relative "bitmaps"
14
+ require_relative "hyperloglog"
15
+ require_relative "server"
16
+ require_relative "scripting"
17
+ require_relative "scan"
18
+ require_relative "publishing"
19
+ require_relative "probabilistic"
20
+ require_relative "admin"
21
+ require_relative "../extensions"
22
+
23
+ module HotKV
24
+ module Commands
25
+ # Every standard command family, mixed
26
+ # into HotKV::Client, HotKV::Cluster, HotKV::Pipeline,
27
+ # HotKV::Transaction and HotKV::WatchSession alike. Each of these
28
+ # classes only has to implement #invoke(*args, &transform) itself.
29
+ module All
30
+ include Strings
31
+ include Keys
32
+ include Lists
33
+ include Sets
34
+ include Hashes
35
+ include SortedSets
36
+ include Streams
37
+ include Geo
38
+ include Bitmaps
39
+ include HyperLogLog
40
+ include Server
41
+ include Scripting
42
+ include Scan
43
+ include Publishing
44
+ include Probabilistic
45
+ include Admin
46
+ include Extensions
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,52 @@
1
+ # Copyright (c) 2026 HotKV Ltd
2
+ # Licensed under the Apache License, Version 2.0. See LICENSE in the project root.
3
+
4
+ module HotKV
5
+ module Commands
6
+ # Bitmap commands.
7
+ module Bitmaps
8
+ def setbit(key, offset, value)
9
+ invoke("SETBIT", key, offset, value)
10
+ end
11
+
12
+ def getbit(key, offset)
13
+ invoke("GETBIT", key, offset)
14
+ end
15
+
16
+ # :bit_mode selects BIT range indexing (byte indexing is the default).
17
+ def bitcount(key, start = nil, stop = nil, bit_mode: false)
18
+ args = ["BITCOUNT", key]
19
+ if start
20
+ args.push(start, stop)
21
+ args.push("BIT") if bit_mode
22
+ end
23
+ invoke(*args)
24
+ end
25
+
26
+ def bitpos(key, bit, start = nil, stop = nil, bit_mode: false)
27
+ args = ["BITPOS", key, bit]
28
+ if start
29
+ args.push(start)
30
+ args.push(stop) if stop
31
+ args.push("BIT") if bit_mode
32
+ end
33
+ invoke(*args)
34
+ end
35
+
36
+ def bitop(operation, destination, *source_keys)
37
+ invoke("BITOP", operation, destination, *source_keys.flatten)
38
+ end
39
+
40
+ # Raw escape hatch for BITFIELD: pass the GET/SET/INCRBY/OVERFLOW
41
+ # sub-operations exactly as the command syntax lists them, e.g.
42
+ # bitfield("k", "INCRBY", "u8", 0, 10, "GET", "u8", 0).
43
+ def bitfield(key, *operations)
44
+ invoke("BITFIELD", key, *operations.flatten)
45
+ end
46
+
47
+ def bitfield_ro(key, *operations)
48
+ invoke("BITFIELD_RO", key, *operations.flatten)
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,146 @@
1
+ # Copyright (c) 2026 HotKV Ltd
2
+ # Licensed under the Apache License, Version 2.0. See LICENSE in the project root.
3
+
4
+ module HotKV
5
+ module Commands
6
+ # Geospatial commands.
7
+ module Geo
8
+ # +entries+ is a flat [lon, lat, member, ...] list, or an Array of
9
+ # [lon, lat, member] triples.
10
+ def geoadd(key, entries, nx: false, xx: false, ch: false)
11
+ args = ["GEOADD", key]
12
+ args.push("NX") if nx
13
+ args.push("XX") if xx
14
+ args.push("CH") if ch
15
+ args.concat(finite_geo_entries(entries.flatten))
16
+ invoke(*args)
17
+ end
18
+
19
+ # -> Array of [lon, lat] Float pairs (nil for a member not found).
20
+ def geopos(key, *members)
21
+ invoke("GEOPOS", key, *members.flatten) { |r| r.map { |p| p.nil? ? nil : p.map { |v| Wire.parse_float(v) } } }
22
+ end
23
+
24
+ def geodist(key, member1, member2, unit: "m")
25
+ invoke("GEODIST", key, member1, member2, unit) { |r| r.nil? ? nil : Wire.parse_float(r) }
26
+ end
27
+
28
+ def geohash(key, *members)
29
+ invoke("GEOHASH", key, *members.flatten)
30
+ end
31
+
32
+ # Exactly one of :member or :lonlat (a [lon, lat] pair) selects the
33
+ # search center; exactly one of :radius (a [radius, unit] pair) or
34
+ # :box (a [width, height, unit] triple) selects the search area.
35
+ def geosearch(key, member: nil, lonlat: nil, radius: nil, box: nil, asc: nil, desc: nil, count: nil,
36
+ any: false, withcoord: false, withdist: false, withhash: false)
37
+ args = ["GEOSEARCH", key]
38
+ args.concat(geosearch_origin(member, lonlat))
39
+ args.concat(geosearch_area(radius, box))
40
+ args.push("ASC") if asc
41
+ args.push("DESC") if desc
42
+ if count
43
+ args.push("COUNT", count)
44
+ args.push("ANY") if any
45
+ end
46
+ args.push("WITHCOORD") if withcoord
47
+ args.push("WITHDIST") if withdist
48
+ args.push("WITHHASH") if withhash
49
+ invoke(*args) { |r| geosearch_result(r, withcoord, withdist, withhash) }
50
+ end
51
+
52
+ def geosearchstore(destination, source, member: nil, lonlat: nil, radius: nil, box: nil, count: nil,
53
+ any: false, storedist: false)
54
+ args = ["GEOSEARCHSTORE", destination, source]
55
+ args.concat(geosearch_origin(member, lonlat))
56
+ args.concat(geosearch_area(radius, box))
57
+ if count
58
+ args.push("COUNT", count)
59
+ args.push("ANY") if any
60
+ end
61
+ args.push("STOREDIST") if storedist
62
+ invoke(*args)
63
+ end
64
+
65
+ # Deprecated GEORADIUS: members within +radius+ (+unit+ m/km/ft/mi) of
66
+ # a longitude/latitude. Options as #geosearch plus :store/:storedist
67
+ # (destination key; then the reply is the stored count).
68
+ def georadius(key, lon, lat, radius, unit, store: nil, storedist: nil, **opts)
69
+ args = ["GEORADIUS", key, Wire.finite(lon), Wire.finite(lat), Wire.finite(radius), unit]
70
+ georadius_run(args, opts, store, storedist)
71
+ end
72
+
73
+ # Read-only GEORADIUS.
74
+ def georadius_ro(key, lon, lat, radius, unit, **opts)
75
+ args = ["GEORADIUS_RO", key, Wire.finite(lon), Wire.finite(lat), Wire.finite(radius), unit]
76
+ georadius_run(args, opts, nil, nil)
77
+ end
78
+
79
+ # Deprecated GEORADIUSBYMEMBER: like #georadius around an existing member.
80
+ def georadiusbymember(key, member, radius, unit, store: nil, storedist: nil, **opts)
81
+ args = ["GEORADIUSBYMEMBER", key, member, Wire.finite(radius), unit]
82
+ georadius_run(args, opts, store, storedist)
83
+ end
84
+
85
+ # Read-only GEORADIUSBYMEMBER.
86
+ def georadiusbymember_ro(key, member, radius, unit, **opts)
87
+ args = ["GEORADIUSBYMEMBER_RO", key, member, Wire.finite(radius), unit]
88
+ georadius_run(args, opts, nil, nil)
89
+ end
90
+
91
+ private
92
+
93
+ def georadius_run(args, opts, store, storedist)
94
+ unknown = opts.keys - %i[withcoord withdist withhash count any asc desc]
95
+ raise Error::ArgumentError, "unknown georadius option(s): #{unknown.join(', ')}" unless unknown.empty?
96
+
97
+ args.push("WITHCOORD") if opts[:withcoord]
98
+ args.push("WITHDIST") if opts[:withdist]
99
+ args.push("WITHHASH") if opts[:withhash]
100
+ if opts[:count]
101
+ args.push("COUNT", opts[:count])
102
+ args.push("ANY") if opts[:any]
103
+ end
104
+ args.push("ASC") if opts[:asc]
105
+ args.push("DESC") if opts[:desc]
106
+ args.push("STORE", store) if store
107
+ args.push("STOREDIST", storedist) if storedist
108
+ withcoord, withdist, withhash = opts.values_at(:withcoord, :withdist, :withhash)
109
+ invoke(*args) { |r| (store || storedist) ? r : geosearch_result(r, withcoord, withdist, withhash) }
110
+ end
111
+
112
+ # Longitude and latitude must be finite numbers; the member name that
113
+ # follows each pair is left alone.
114
+ def finite_geo_entries(flat)
115
+ flat.each_with_index.map { |v, i| (i % 3) == 2 ? v : Wire.finite(v) }
116
+ end
117
+
118
+ def geosearch_origin(member, lonlat)
119
+ return ["FROMMEMBER", member] if member
120
+ return ["FROMLONLAT", Wire.finite(lonlat[0]), Wire.finite(lonlat[1])] if lonlat
121
+
122
+ raise Error::ArgumentError, "geosearch needs :member or :lonlat"
123
+ end
124
+
125
+ def geosearch_area(radius, box)
126
+ return ["BYRADIUS", Wire.finite(radius[0]), radius[1]] if radius
127
+ return ["BYBOX", Wire.finite(box[0]), Wire.finite(box[1]), box[2]] if box
128
+
129
+ raise Error::ArgumentError, "geosearch needs :radius or :box"
130
+ end
131
+
132
+ def geosearch_result(reply, withcoord, withdist, withhash)
133
+ return reply unless withcoord || withdist || withhash
134
+
135
+ reply.map do |row|
136
+ row = Array(row)
137
+ entry = { member: row.shift }
138
+ entry[:dist] = Wire.parse_float(row.shift) if withdist
139
+ entry[:hash] = Integer(row.shift) if withhash
140
+ entry[:coord] = row.shift.map { |v| Wire.parse_float(v) } if withcoord
141
+ entry
142
+ end
143
+ end
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,169 @@
1
+ # Copyright (c) 2026 HotKV Ltd
2
+ # Licensed under the Apache License, Version 2.0. See LICENSE in the project root.
3
+
4
+ module HotKV
5
+ module Commands
6
+ # Hash commands, including the per field expiry (HEXPIRE/HTTL) family.
7
+ module Hashes
8
+ # Accepts a Hash, or a flat [field, value, field, value, ...] list.
9
+ def hset(key, hash_or_pairs)
10
+ invoke("HSET", key, *flatten_pairs(hash_or_pairs))
11
+ end
12
+
13
+ def hsetnx(key, field, value)
14
+ invoke("HSETNX", key, field, value) { |r| r == 1 }
15
+ end
16
+
17
+ def hget(key, field)
18
+ invoke("HGET", key, field)
19
+ end
20
+
21
+ def hmget(key, *fields)
22
+ invoke("HMGET", key, *fields.flatten)
23
+ end
24
+
25
+ # Normalizes to a Hash regardless of whether the server replied with
26
+ # a RESP3 map or a RESP2 flat array.
27
+ def hgetall(key)
28
+ invoke("HGETALL", key) { |r| r.is_a?(Hash) ? r : r.each_slice(2).to_h }
29
+ end
30
+
31
+ def hdel(key, *fields)
32
+ invoke("HDEL", key, *fields.flatten)
33
+ end
34
+
35
+ def hexists(key, field)
36
+ invoke("HEXISTS", key, field) { |r| r == 1 }
37
+ end
38
+
39
+ def hlen(key)
40
+ invoke("HLEN", key)
41
+ end
42
+
43
+ def hkeys(key)
44
+ invoke("HKEYS", key)
45
+ end
46
+
47
+ def hvals(key)
48
+ invoke("HVALS", key)
49
+ end
50
+
51
+ def hincrby(key, field, amount)
52
+ invoke("HINCRBY", key, field, amount)
53
+ end
54
+
55
+ def hincrbyfloat(key, field, amount)
56
+ invoke("HINCRBYFLOAT", key, field, Wire.finite(amount)) { |r| Wire.parse_float(r) }
57
+ end
58
+
59
+ def hstrlen(key, field)
60
+ invoke("HSTRLEN", key, field)
61
+ end
62
+
63
+ def hrandfield(key, count = nil, withvalues: false)
64
+ return invoke("HRANDFIELD", key) if count.nil?
65
+
66
+ args = ["HRANDFIELD", key, count]
67
+ args.push("WITHVALUES") if withvalues
68
+ invoke(*args) { |r| withvalues ? Support.pairify(r) { |v| v } : r }
69
+ end
70
+
71
+ # Field level TTL (seconds). Returns one status Integer per field: 1
72
+ # set, 0 condition not met, -2 no such field/key.
73
+ def hexpire(key, seconds, *fields, nx: false, xx: false, gt: false, lt: false)
74
+ invoke(*hfield_ttl_args("HEXPIRE", key, seconds, fields.flatten, nx, xx, gt, lt))
75
+ end
76
+
77
+ def hpexpire(key, milliseconds, *fields, nx: false, xx: false, gt: false, lt: false)
78
+ invoke(*hfield_ttl_args("HPEXPIRE", key, milliseconds, fields.flatten, nx, xx, gt, lt))
79
+ end
80
+
81
+ def hexpireat(key, unix_seconds, *fields, nx: false, xx: false, gt: false, lt: false)
82
+ invoke(*hfield_ttl_args("HEXPIREAT", key, unix_seconds, fields.flatten, nx, xx, gt, lt))
83
+ end
84
+
85
+ def hpexpireat(key, unix_milliseconds, *fields, nx: false, xx: false, gt: false, lt: false)
86
+ invoke(*hfield_ttl_args("HPEXPIREAT", key, unix_milliseconds, fields.flatten, nx, xx, gt, lt))
87
+ end
88
+
89
+ def httl(key, *fields)
90
+ invoke("HTTL", key, "FIELDS", fields.flatten.length, *fields.flatten)
91
+ end
92
+
93
+ def hpttl(key, *fields)
94
+ invoke("HPTTL", key, "FIELDS", fields.flatten.length, *fields.flatten)
95
+ end
96
+
97
+ def hexpiretime(key, *fields)
98
+ invoke("HEXPIRETIME", key, "FIELDS", fields.flatten.length, *fields.flatten)
99
+ end
100
+
101
+ def hpexpiretime(key, *fields)
102
+ invoke("HPEXPIRETIME", key, "FIELDS", fields.flatten.length, *fields.flatten)
103
+ end
104
+
105
+ def hpersist(key, *fields)
106
+ invoke("HPERSIST", key, "FIELDS", fields.flatten.length, *fields.flatten)
107
+ end
108
+
109
+ # Deprecated form of #hset that replies OK. Returns true.
110
+ def hmset(key, hash_or_pairs)
111
+ invoke("HMSET", key, *flatten_pairs(hash_or_pairs)) { |r| r == "OK" }
112
+ end
113
+
114
+ # Gets the values of +fields+ and deletes them atomically (HGETDEL). Returns the values in order, nil for a missing field.
115
+ def hgetdel(key, *fields)
116
+ flat = fields.flatten
117
+ invoke("HGETDEL", key, "FIELDS", flat.length, *flat)
118
+ end
119
+
120
+ # Gets the values of +fields+ and sets (:ex/:px/:exat/:pxat) or removes
121
+ # (:persist) their TTLs (HGETEX).
122
+ def hgetex(key, *fields, ex: nil, px: nil, exat: nil, pxat: nil, persist: false)
123
+ flat = fields.flatten
124
+ args = ["HGETEX", key]
125
+ args.push("EX", ex) if ex
126
+ args.push("PX", px) if px
127
+ args.push("EXAT", exat) if exat
128
+ args.push("PXAT", pxat) if pxat
129
+ args.push("PERSIST") if persist
130
+ args.push("FIELDS", flat.length, *flat)
131
+ invoke(*args)
132
+ end
133
+
134
+ # Sets fields, all or none, with an optional TTL (HSETEX).
135
+ # +hash_or_pairs+ is a Hash or a flat [field, value, ...] list. :fnx
136
+ # sets only when none of the fields exists, :fxx only when all do; TTL
137
+ # is one of :ex/:px/:exat/:pxat/:keepttl. Returns true when set.
138
+ def hsetex(key, hash_or_pairs, fnx: false, fxx: false, ex: nil, px: nil, exat: nil, pxat: nil, keepttl: false)
139
+ flat = flatten_pairs(hash_or_pairs)
140
+ args = ["HSETEX", key]
141
+ args.push("FNX") if fnx
142
+ args.push("FXX") if fxx
143
+ args.push("EX", ex) if ex
144
+ args.push("PX", px) if px
145
+ args.push("EXAT", exat) if exat
146
+ args.push("PXAT", pxat) if pxat
147
+ args.push("KEEPTTL") if keepttl
148
+ args.push("FIELDS", flat.length / 2, *flat)
149
+ invoke(*args) { |r| r == 1 }
150
+ end
151
+
152
+ private
153
+
154
+ def flatten_pairs(hash_or_pairs)
155
+ hash_or_pairs.is_a?(Hash) ? hash_or_pairs.flatten(1) : hash_or_pairs.flatten(1)
156
+ end
157
+
158
+ def hfield_ttl_args(cmd, key, ttl, fields, nx, xx, gt, lt)
159
+ args = [cmd, key, ttl]
160
+ args.push("NX") if nx
161
+ args.push("XX") if xx
162
+ args.push("GT") if gt
163
+ args.push("LT") if lt
164
+ args.push("FIELDS", fields.length, *fields)
165
+ args
166
+ end
167
+ end
168
+ end
169
+ end
@@ -0,0 +1,21 @@
1
+ # Copyright (c) 2026 HotKV Ltd
2
+ # Licensed under the Apache License, Version 2.0. See LICENSE in the project root.
3
+
4
+ module HotKV
5
+ module Commands
6
+ # HyperLogLog commands.
7
+ module HyperLogLog
8
+ def pfadd(key, *elements)
9
+ invoke("PFADD", key, *elements.flatten) { |r| r == 1 }
10
+ end
11
+
12
+ def pfcount(*keys)
13
+ invoke("PFCOUNT", *keys.flatten)
14
+ end
15
+
16
+ def pfmerge(destination, *source_keys)
17
+ invoke("PFMERGE", destination, *source_keys.flatten) { |r| r == "OK" }
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,170 @@
1
+ # Copyright (c) 2026 HotKV Ltd
2
+ # Licensed under the Apache License, Version 2.0. See LICENSE in the project root.
3
+
4
+ module HotKV
5
+ module Commands
6
+ # Generic key space and expiry commands.
7
+ module Keys
8
+ def del(*keys)
9
+ invoke("DEL", *keys.flatten)
10
+ end
11
+
12
+ def unlink(*keys)
13
+ invoke("UNLINK", *keys.flatten)
14
+ end
15
+
16
+ def exists(*keys)
17
+ invoke("EXISTS", *keys.flatten)
18
+ end
19
+
20
+ def type(key)
21
+ invoke("TYPE", key)
22
+ end
23
+
24
+ # Sets a relative TTL, in seconds. :nx/:xx/:gt/:lt mirror EXPIRE's
25
+ # write condition flags.
26
+ def expire(key, seconds, nx: false, xx: false, gt: false, lt: false)
27
+ invoke(*expire_args("EXPIRE", key, seconds, nx, xx, gt, lt)) { |r| r == 1 }
28
+ end
29
+
30
+ def pexpire(key, milliseconds, nx: false, xx: false, gt: false, lt: false)
31
+ invoke(*expire_args("PEXPIRE", key, milliseconds, nx, xx, gt, lt)) { |r| r == 1 }
32
+ end
33
+
34
+ def expireat(key, unix_seconds, nx: false, xx: false, gt: false, lt: false)
35
+ invoke(*expire_args("EXPIREAT", key, unix_seconds, nx, xx, gt, lt)) { |r| r == 1 }
36
+ end
37
+
38
+ def pexpireat(key, unix_milliseconds, nx: false, xx: false, gt: false, lt: false)
39
+ invoke(*expire_args("PEXPIREAT", key, unix_milliseconds, nx, xx, gt, lt)) { |r| r == 1 }
40
+ end
41
+
42
+ def ttl(key)
43
+ invoke("TTL", key)
44
+ end
45
+
46
+ def pttl(key)
47
+ invoke("PTTL", key)
48
+ end
49
+
50
+ def expiretime(key)
51
+ invoke("EXPIRETIME", key)
52
+ end
53
+
54
+ def pexpiretime(key)
55
+ invoke("PEXPIRETIME", key)
56
+ end
57
+
58
+ def persist(key)
59
+ invoke("PERSIST", key) { |r| r == 1 }
60
+ end
61
+
62
+ def rename(key, new_key)
63
+ invoke("RENAME", key, new_key) { |r| r == "OK" }
64
+ end
65
+
66
+ def renamenx(key, new_key)
67
+ invoke("RENAMENX", key, new_key) { |r| r == 1 }
68
+ end
69
+
70
+ def keys(pattern = "*")
71
+ invoke("KEYS", pattern)
72
+ end
73
+
74
+ def randomkey
75
+ invoke("RANDOMKEY")
76
+ end
77
+
78
+ def dump(key)
79
+ invoke("DUMP", key)
80
+ end
81
+
82
+ def restore(key, ttl, serialized_value, replace: false, absttl: false, idletime: nil, freq: nil)
83
+ args = ["RESTORE", key, ttl, serialized_value]
84
+ args.push("REPLACE") if replace
85
+ args.push("ABSTTL") if absttl
86
+ args.push("IDLETIME", idletime) if idletime
87
+ args.push("FREQ", freq) if freq
88
+ invoke(*args) { |r| r == "OK" }
89
+ end
90
+
91
+ def copy(source, destination, db: nil, replace: false)
92
+ args = ["COPY", source, destination]
93
+ args.push("DB", db) if db
94
+ args.push("REPLACE") if replace
95
+ invoke(*args) { |r| r == 1 }
96
+ end
97
+
98
+ def move(key, db)
99
+ invoke("MOVE", key, db) { |r| r == 1 }
100
+ end
101
+
102
+ def touch(*keys)
103
+ invoke("TOUCH", *keys.flatten)
104
+ end
105
+
106
+ def object_encoding(key)
107
+ invoke("OBJECT", "ENCODING", key)
108
+ end
109
+
110
+ def object_refcount(key)
111
+ invoke("OBJECT", "REFCOUNT", key)
112
+ end
113
+
114
+ def object_idletime(key)
115
+ invoke("OBJECT", "IDLETIME", key)
116
+ end
117
+
118
+ def object_freq(key)
119
+ invoke("OBJECT", "FREQ", key)
120
+ end
121
+
122
+ # SORT of a list, set or sorted set. :by is a pattern ("nosort" skips
123
+ # sorting), :limit an [offset, count] pair, :get a pattern or Array of
124
+ # patterns, :order "ASC"/"DESC", :alpha sorts lexicographically, :store
125
+ # a destination key (then the reply is the stored element count).
126
+ def sort(key, by: nil, limit: nil, get: nil, order: nil, alpha: false, store: nil)
127
+ invoke(*sort_args("SORT", key, by, limit, get, order, alpha), *(store ? ["STORE", store] : []))
128
+ end
129
+
130
+ # Read-only SORT (no :store).
131
+ def sort_ro(key, by: nil, limit: nil, get: nil, order: nil, alpha: false)
132
+ invoke(*sort_args("SORT_RO", key, by, limit, get, order, alpha))
133
+ end
134
+
135
+ # Blocks until +numreplicas+ replicas acknowledge this connection's
136
+ # writes or +timeout+ milliseconds pass. Returns the replica count.
137
+ def wait(numreplicas, timeout)
138
+ invoke("WAIT", numreplicas, timeout)
139
+ end
140
+
141
+ # WAITAOF: waits until writes are fsynced to the local AOF
142
+ # (+numlocal+ 0 or 1) and to +numreplicas+ replicas' AOFs. Returns
143
+ # [local_acks, replica_acks].
144
+ def waitaof(numlocal, numreplicas, timeout)
145
+ invoke("WAITAOF", numlocal, numreplicas, timeout)
146
+ end
147
+
148
+ private
149
+
150
+ def sort_args(cmd, key, by, limit, get, order, alpha)
151
+ args = [cmd, key]
152
+ args.push("BY", by) unless by.nil?
153
+ args.push("LIMIT", limit[0], limit[1]) if limit
154
+ Array(get).each { |pattern| args.push("GET", pattern) }
155
+ args.push(order.to_s.upcase) if order
156
+ args.push("ALPHA") if alpha
157
+ args
158
+ end
159
+
160
+ def expire_args(cmd, key, ttl, nx, xx, gt, lt)
161
+ args = [cmd, key, ttl]
162
+ args.push("NX") if nx
163
+ args.push("XX") if xx
164
+ args.push("GT") if gt
165
+ args.push("LT") if lt
166
+ args
167
+ end
168
+ end
169
+ end
170
+ end