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.
- checksums.yaml +7 -0
- data/LICENSE +202 -0
- data/NOTICE +6 -0
- data/README.md +459 -0
- data/SECURITY.md +30 -0
- data/lib/hotkv/ai/call_result.rb +25 -0
- data/lib/hotkv/ai/prompt_cache.rb +135 -0
- data/lib/hotkv/ai/semantic_cache.rb +66 -0
- data/lib/hotkv/client.rb +201 -0
- data/lib/hotkv/cluster/crc16.rb +43 -0
- data/lib/hotkv/cluster.rb +184 -0
- data/lib/hotkv/commands/admin.rb +36 -0
- data/lib/hotkv/commands/all.rb +49 -0
- data/lib/hotkv/commands/bitmaps.rb +52 -0
- data/lib/hotkv/commands/geo.rb +146 -0
- data/lib/hotkv/commands/hashes.rb +169 -0
- data/lib/hotkv/commands/hyperloglog.rb +21 -0
- data/lib/hotkv/commands/keys.rb +170 -0
- data/lib/hotkv/commands/lists.rb +113 -0
- data/lib/hotkv/commands/probabilistic.rb +23 -0
- data/lib/hotkv/commands/publishing.rb +46 -0
- data/lib/hotkv/commands/scan.rb +82 -0
- data/lib/hotkv/commands/scripting.rb +128 -0
- data/lib/hotkv/commands/server.rb +175 -0
- data/lib/hotkv/commands/sets.rb +75 -0
- data/lib/hotkv/commands/sorted_sets.rb +258 -0
- data/lib/hotkv/commands/streams.rb +207 -0
- data/lib/hotkv/commands/strings.rb +178 -0
- data/lib/hotkv/commands/support.rb +69 -0
- data/lib/hotkv/connection.rb +221 -0
- data/lib/hotkv/errors.rb +145 -0
- data/lib/hotkv/extensions.rb +118 -0
- data/lib/hotkv/future.rb +55 -0
- data/lib/hotkv/llm_price.rb +53 -0
- data/lib/hotkv/namespaces/admin.rb +261 -0
- data/lib/hotkv/namespaces/amem.rb +172 -0
- data/lib/hotkv/namespaces/caching.rb +397 -0
- data/lib/hotkv/namespaces/core.rb +190 -0
- data/lib/hotkv/namespaces/ops.rb +166 -0
- data/lib/hotkv/namespaces/probabilistic.rb +180 -0
- data/lib/hotkv/namespaces/rag.rb +63 -0
- data/lib/hotkv/pipeline.rb +57 -0
- data/lib/hotkv/pool.rb +123 -0
- data/lib/hotkv/pubsub.rb +60 -0
- data/lib/hotkv/resp.rb +176 -0
- data/lib/hotkv/transaction.rb +115 -0
- data/lib/hotkv/url.rb +100 -0
- data/lib/hotkv/usage.rb +42 -0
- data/lib/hotkv/version.rb +7 -0
- data/lib/hotkv/wire.rb +145 -0
- data/lib/hotkv.rb +28 -0
- 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
|