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,113 @@
|
|
|
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
|
+
# List commands.
|
|
7
|
+
module Lists
|
|
8
|
+
def lpush(key, *values)
|
|
9
|
+
invoke("LPUSH", key, *values.flatten)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def rpush(key, *values)
|
|
13
|
+
invoke("RPUSH", key, *values.flatten)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def lpushx(key, *values)
|
|
17
|
+
invoke("LPUSHX", key, *values.flatten)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def rpushx(key, *values)
|
|
21
|
+
invoke("RPUSHX", key, *values.flatten)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Without :count, returns the popped element or nil. With :count,
|
|
25
|
+
# returns an Array of popped elements (possibly empty) or nil when
|
|
26
|
+
# the key does not exist.
|
|
27
|
+
def lpop(key, count = nil)
|
|
28
|
+
count ? invoke("LPOP", key, count) : invoke("LPOP", key)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def rpop(key, count = nil)
|
|
32
|
+
count ? invoke("RPOP", key, count) : invoke("RPOP", key)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def llen(key)
|
|
36
|
+
invoke("LLEN", key)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def lrange(key, start, stop)
|
|
40
|
+
invoke("LRANGE", key, start, stop)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def lindex(key, index)
|
|
44
|
+
invoke("LINDEX", key, index)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def lset(key, index, value)
|
|
48
|
+
invoke("LSET", key, index, value) { |r| r == "OK" }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def linsert(key, where, pivot, value)
|
|
52
|
+
invoke("LINSERT", key, where, pivot, value)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def lrem(key, count, value)
|
|
56
|
+
invoke("LREM", key, count, value)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def ltrim(key, start, stop)
|
|
60
|
+
invoke("LTRIM", key, start, stop) { |r| r == "OK" }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def rpoplpush(source, destination)
|
|
64
|
+
invoke("RPOPLPUSH", source, destination)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def lmove(source, destination, wherefrom, whereto)
|
|
68
|
+
invoke("LMOVE", source, destination, wherefrom, whereto)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def lpos(key, element, rank: nil, count: nil, maxlen: nil)
|
|
72
|
+
args = ["LPOS", key, element]
|
|
73
|
+
args.push("RANK", rank) if rank
|
|
74
|
+
args.push("COUNT", count) if count
|
|
75
|
+
args.push("MAXLEN", maxlen) if maxlen
|
|
76
|
+
invoke(*args)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# -> [key, values] on success, or nil when every source list was empty.
|
|
80
|
+
def lmpop(keys, where, count: nil)
|
|
81
|
+
args = ["LMPOP", keys.length, *keys, where]
|
|
82
|
+
args.push("COUNT", count) if count
|
|
83
|
+
invoke(*args)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def blpop(*keys, timeout: 0)
|
|
87
|
+
invoke("BLPOP", *keys.flatten, timeout)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def brpop(*keys, timeout: 0)
|
|
91
|
+
invoke("BRPOP", *keys.flatten, timeout)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def blmove(source, destination, wherefrom, whereto, timeout: 0)
|
|
95
|
+
invoke("BLMOVE", source, destination, wherefrom, whereto, timeout)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Pops from the first non-empty list of +keys+ (LMPOP) or waits up to
|
|
99
|
+
# +timeout+ seconds (BLMPOP). +where+ is "LEFT" or "RIGHT". Returns
|
|
100
|
+
# [key, [elements]] or nil on timeout.
|
|
101
|
+
def blmpop(keys, where, timeout: 0, count: nil)
|
|
102
|
+
args = ["BLMPOP", timeout, keys.length, *keys, where]
|
|
103
|
+
args.push("COUNT", count) if count
|
|
104
|
+
invoke(*args)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Deprecated blocking RPOPLPUSH.
|
|
108
|
+
def brpoplpush(source, destination, timeout: 0)
|
|
109
|
+
invoke("BRPOPLPUSH", source, destination, timeout)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
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/probabilistic"
|
|
5
|
+
|
|
6
|
+
module HotKV
|
|
7
|
+
module Commands
|
|
8
|
+
# Bloom filter, Count-Min Sketch and Top-K accessors.
|
|
9
|
+
module Probabilistic
|
|
10
|
+
def bf
|
|
11
|
+
@hotkv_ns_bf ||= Namespaces::BF.new(self)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def cms
|
|
15
|
+
@hotkv_ns_cms ||= Namespaces::CMS.new(self)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def topk
|
|
19
|
+
@hotkv_ns_topk ||= Namespaces::TopK.new(self)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
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
|
+
# Publishing and Pub/Sub introspection. Subscribing is not here: it needs
|
|
7
|
+
# a connection of its own, see Client#subscribe.
|
|
8
|
+
module Publishing
|
|
9
|
+
# Publishes +message+ to +channel+. Returns the number of subscribers
|
|
10
|
+
# that received it.
|
|
11
|
+
def publish(channel, message)
|
|
12
|
+
invoke("PUBLISH", channel, message)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Publishes +message+ to a shard channel (SPUBLISH).
|
|
16
|
+
def spublish(channel, message)
|
|
17
|
+
invoke("SPUBLISH", channel, message)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Active channels, optionally only those matching the glob +pattern+.
|
|
21
|
+
def pubsub_channels(pattern = nil)
|
|
22
|
+
pattern.nil? ? invoke("PUBSUB", "CHANNELS") : invoke("PUBSUB", "CHANNELS", pattern)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Hash of channel => subscriber count for the given channels.
|
|
26
|
+
def pubsub_numsub(*channels)
|
|
27
|
+
invoke("PUBSUB", "NUMSUB", *channels.flatten) { |r| r.is_a?(Hash) ? r : r.each_slice(2).to_h }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Number of pattern subscriptions.
|
|
31
|
+
def pubsub_numpat
|
|
32
|
+
invoke("PUBSUB", "NUMPAT")
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Active shard channels, optionally only those matching +pattern+.
|
|
36
|
+
def pubsub_shardchannels(pattern = nil)
|
|
37
|
+
pattern.nil? ? invoke("PUBSUB", "SHARDCHANNELS") : invoke("PUBSUB", "SHARDCHANNELS", pattern)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Hash of shard channel => subscriber count.
|
|
41
|
+
def pubsub_shardnumsub(*channels)
|
|
42
|
+
invoke("PUBSUB", "SHARDNUMSUB", *channels.flatten) { |r| r.is_a?(Hash) ? r : r.each_slice(2).to_h }
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
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
|
+
# SCAN family: one-shot cursor calls plus Enumerator based iterators
|
|
7
|
+
# that page through an entire key space, hash, set or sorted set.
|
|
8
|
+
module Scan
|
|
9
|
+
# -> [next_cursor, keys]. next_cursor is "0" once the scan completes.
|
|
10
|
+
def scan(cursor = "0", match: nil, count: nil, type: nil)
|
|
11
|
+
args = ["SCAN", cursor]
|
|
12
|
+
args.push("MATCH", match) if match
|
|
13
|
+
args.push("COUNT", count) if count
|
|
14
|
+
args.push("TYPE", type) if type
|
|
15
|
+
invoke(*args)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Enumerator over every key, paging with SCAN until exhausted.
|
|
19
|
+
def scan_each(match: nil, count: nil, type: nil, &block)
|
|
20
|
+
return to_enum(:scan_each, match: match, count: count, type: type) unless block
|
|
21
|
+
|
|
22
|
+
each_scan_page(:scan, [], match: match, count: count, type: type) { |page| page.each(&block) }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def hscan(key, cursor = "0", match: nil, count: nil, novalues: false)
|
|
26
|
+
args = ["HSCAN", key, cursor]
|
|
27
|
+
args.push("MATCH", match) if match
|
|
28
|
+
args.push("COUNT", count) if count
|
|
29
|
+
args.push("NOVALUES") if novalues
|
|
30
|
+
invoke(*args)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def hscan_each(key, match: nil, count: nil, &block)
|
|
34
|
+
return to_enum(:hscan_each, key, match: match, count: count) unless block
|
|
35
|
+
|
|
36
|
+
each_scan_page(:hscan, [key], match: match, count: count) { |page| page.each_slice(2) { |(f, v)| yield f, v } }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def sscan(key, cursor = "0", match: nil, count: nil)
|
|
40
|
+
args = ["SSCAN", key, cursor]
|
|
41
|
+
args.push("MATCH", match) if match
|
|
42
|
+
args.push("COUNT", count) if count
|
|
43
|
+
invoke(*args)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def sscan_each(key, match: nil, count: nil, &block)
|
|
47
|
+
return to_enum(:sscan_each, key, match: match, count: count) unless block
|
|
48
|
+
|
|
49
|
+
each_scan_page(:sscan, [key], match: match, count: count) { |page| page.each(&block) }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def zscan(key, cursor = "0", match: nil, count: nil)
|
|
53
|
+
args = ["ZSCAN", key, cursor]
|
|
54
|
+
args.push("MATCH", match) if match
|
|
55
|
+
args.push("COUNT", count) if count
|
|
56
|
+
invoke(*args)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def zscan_each(key, match: nil, count: nil, &block)
|
|
60
|
+
return to_enum(:zscan_each, key, match: match, count: count) unless block
|
|
61
|
+
|
|
62
|
+
each_scan_page(:zscan, [key], match: match, count: count) do |page|
|
|
63
|
+
page.each_slice(2) { |(m, s)| yield m, Wire.parse_float(s) }
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
def each_scan_page(method, prefix_args, match:, count:, type: nil, novalues: nil)
|
|
70
|
+
cursor = "0"
|
|
71
|
+
loop do
|
|
72
|
+
kwargs = { match: match, count: count }
|
|
73
|
+
kwargs[:type] = type if method == :scan
|
|
74
|
+
kwargs[:novalues] = novalues if method == :hscan && !novalues.nil?
|
|
75
|
+
cursor, page = public_send(method, *prefix_args, cursor, **kwargs)
|
|
76
|
+
yield page
|
|
77
|
+
break if cursor == "0"
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# Copyright (c) 2026 HotKV Ltd
|
|
2
|
+
# Licensed under the Apache License, Version 2.0. See LICENSE in the project root.
|
|
3
|
+
|
|
4
|
+
require "digest"
|
|
5
|
+
|
|
6
|
+
module HotKV
|
|
7
|
+
module Commands
|
|
8
|
+
# Lua scripting commands.
|
|
9
|
+
module Scripting
|
|
10
|
+
def eval_script(script, keys: [], args: [])
|
|
11
|
+
invoke("EVAL", script, keys.length, *keys, *args)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Runs EVALSHA for +sha+ (or Digest::SHA1.hexdigest(script) when
|
|
15
|
+
# :script is given instead of :sha); on NOSCRIPT, falls back to a
|
|
16
|
+
# plain EVAL of :script if one was given. The fallback only fires
|
|
17
|
+
# for an immediate call (HotKV::Client, inside a #watch block); a
|
|
18
|
+
# queued Pipeline/Transaction call cannot inspect its own reply
|
|
19
|
+
# before the batch runs, so there it behaves like a plain EVALSHA.
|
|
20
|
+
def evalsha(sha = nil, script: nil, keys: [], args: [])
|
|
21
|
+
sha ||= Digest::SHA1.hexdigest(script)
|
|
22
|
+
invoke("EVALSHA", sha, keys.length, *keys, *args)
|
|
23
|
+
rescue Error::NoScriptError
|
|
24
|
+
raise unless script
|
|
25
|
+
|
|
26
|
+
invoke("EVAL", script, keys.length, *keys, *args)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def script_load(script)
|
|
30
|
+
invoke("SCRIPT", "LOAD", script)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def script_exists(*shas)
|
|
34
|
+
invoke("SCRIPT", "EXISTS", *shas.flatten) { |r| r.map { |v| v == 1 } }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def script_flush
|
|
38
|
+
invoke("SCRIPT", "FLUSH") { |r| r == "OK" }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Read-only EVAL (the script may not write).
|
|
42
|
+
def eval_ro(script, keys: [], args: [])
|
|
43
|
+
invoke("EVAL_RO", script, keys.length, *keys, *args)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Read-only EVALSHA.
|
|
47
|
+
def evalsha_ro(sha, keys: [], args: [])
|
|
48
|
+
invoke("EVALSHA_RO", sha, keys.length, *keys, *args)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Stops a running script (NOTBUSY if none is running).
|
|
52
|
+
def script_kill
|
|
53
|
+
invoke("SCRIPT", "KILL") { |r| r == "OK" }
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Calls a function registered by FUNCTION LOAD.
|
|
57
|
+
def fcall(function, keys: [], args: [])
|
|
58
|
+
invoke("FCALL", function, keys.length, *keys, *args)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# FCALL for a function flagged no-writes.
|
|
62
|
+
def fcall_ro(function, keys: [], args: [])
|
|
63
|
+
invoke("FCALL_RO", function, keys.length, *keys, *args)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Loads a function library (`#!lua name=<lib>` header). Returns the
|
|
67
|
+
# library name. :replace overwrites an existing library of that name.
|
|
68
|
+
def function_load(code, replace: false)
|
|
69
|
+
replace ? invoke("FUNCTION", "LOAD", "REPLACE", code) : invoke("FUNCTION", "LOAD", code)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def function_delete(library)
|
|
73
|
+
invoke("FUNCTION", "DELETE", library) { |r| r == "OK" }
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Libraries as an Array of Hashes ("library_name", "engine",
|
|
77
|
+
# "functions" => [{"name", "description", "flags"}], plus "library_code"
|
|
78
|
+
# with :withcode).
|
|
79
|
+
def function_list(libraryname: nil, withcode: false)
|
|
80
|
+
args = ["FUNCTION", "LIST"]
|
|
81
|
+
args.push("LIBRARYNAME", libraryname) if libraryname
|
|
82
|
+
args.push("WITHCODE") if withcode
|
|
83
|
+
invoke(*args) { |r| r.map { |lib| lib.is_a?(Hash) ? lib : function_entry(lib) } }
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Deletes every library. +mode+ is "ASYNC" or "SYNC".
|
|
87
|
+
def function_flush(mode = nil)
|
|
88
|
+
mode ? invoke("FUNCTION", "FLUSH", mode) { |r| r == "OK" } : invoke("FUNCTION", "FLUSH") { |r| r == "OK" }
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Serialized libraries (binary String) for #function_restore.
|
|
92
|
+
def function_dump
|
|
93
|
+
invoke("FUNCTION", "DUMP")
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Restores a #function_dump payload. +policy+: "APPEND", "REPLACE" or "FLUSH".
|
|
97
|
+
def function_restore(payload, policy = nil)
|
|
98
|
+
args = ["FUNCTION", "RESTORE", payload]
|
|
99
|
+
args.push(policy) if policy
|
|
100
|
+
invoke(*args) { |r| r == "OK" }
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Hash with "running_script" and "engines".
|
|
104
|
+
def function_stats
|
|
105
|
+
invoke("FUNCTION", "STATS") { |r| r.is_a?(Hash) ? r : { "running_script" => r[1], "engines" => engines_hash(r[3]) } }
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Stops a running function.
|
|
109
|
+
def function_kill
|
|
110
|
+
invoke("FUNCTION", "KILL") { |r| r == "OK" }
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
private
|
|
114
|
+
|
|
115
|
+
def function_entry(flat)
|
|
116
|
+
entry = flat.each_slice(2).to_h
|
|
117
|
+
entry["functions"] = entry["functions"].map { |f| f.is_a?(Hash) ? f : f.each_slice(2).to_h } if entry["functions"]
|
|
118
|
+
entry
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def engines_hash(flat)
|
|
122
|
+
return flat if flat.is_a?(Hash)
|
|
123
|
+
|
|
124
|
+
flat.each_slice(2).to_h { |(name, stats)| [name, stats.is_a?(Hash) ? stats : stats.each_slice(2).to_h] }
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
@@ -0,0 +1,175 @@
|
|
|
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
|
+
# Server and connection commands.
|
|
7
|
+
module Server
|
|
8
|
+
def ping(message = nil)
|
|
9
|
+
message ? invoke("PING", message) : invoke("PING")
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def echo(message)
|
|
13
|
+
invoke("ECHO", message)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def info(section = nil)
|
|
17
|
+
section ? invoke("INFO", section) : invoke("INFO")
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def dbsize
|
|
21
|
+
invoke("DBSIZE")
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def flushdb(async: false)
|
|
25
|
+
args = ["FLUSHDB"]
|
|
26
|
+
args.push(async ? "ASYNC" : "SYNC")
|
|
27
|
+
invoke(*args) { |r| r == "OK" }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def flushall(async: false)
|
|
31
|
+
args = ["FLUSHALL"]
|
|
32
|
+
args.push(async ? "ASYNC" : "SYNC")
|
|
33
|
+
invoke(*args) { |r| r == "OK" }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def time
|
|
37
|
+
invoke("TIME") { |r| [Integer(r[0]), Integer(r[1])] }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def lastsave
|
|
41
|
+
invoke("LASTSAVE")
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def config_get(parameter)
|
|
45
|
+
invoke("CONFIG", "GET", parameter) { |r| r.is_a?(Hash) ? r : r.each_slice(2).to_h }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def config_set(parameter, value)
|
|
49
|
+
invoke("CONFIG", "SET", parameter, value) { |r| r == "OK" }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def config_resetstat
|
|
53
|
+
invoke("CONFIG", "RESETSTAT") { |r| r == "OK" }
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def client_setname(name)
|
|
57
|
+
invoke("CLIENT", "SETNAME", name) { |r| r == "OK" }
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def client_getname
|
|
61
|
+
invoke("CLIENT", "GETNAME")
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def client_id
|
|
65
|
+
invoke("CLIENT", "ID")
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def client_info
|
|
69
|
+
invoke("CLIENT", "INFO")
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def client_list
|
|
73
|
+
invoke("CLIENT", "LIST")
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def select(db)
|
|
77
|
+
invoke("SELECT", db) { |r| r == "OK" }
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def swapdb(index1, index2)
|
|
81
|
+
invoke("SWAPDB", index1, index2) { |r| r == "OK" }
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Replication role: ["master", offset, [[host, port, offset], ...]] or
|
|
85
|
+
# ["slave", host, port, state, offset].
|
|
86
|
+
def role
|
|
87
|
+
invoke("ROLE")
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Saves a snapshot now, blocking the server until done.
|
|
91
|
+
def save
|
|
92
|
+
invoke("SAVE") { |r| r == "OK" }
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Saves a snapshot in the background. Returns the status line.
|
|
96
|
+
def bgsave(schedule: false)
|
|
97
|
+
schedule ? invoke("BGSAVE", "SCHEDULE") : invoke("BGSAVE")
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Rewrites the append-only file in the background. Returns the status line.
|
|
101
|
+
def bgrewriteaof
|
|
102
|
+
invoke("BGREWRITEAOF")
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Slow log entries (the newest +count+, default 10).
|
|
106
|
+
def slowlog_get(count = nil)
|
|
107
|
+
count ? invoke("SLOWLOG", "GET", count) : invoke("SLOWLOG", "GET")
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def slowlog_len
|
|
111
|
+
invoke("SLOWLOG", "LEN")
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def slowlog_reset
|
|
115
|
+
invoke("SLOWLOG", "RESET") { |r| r == "OK" }
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Bytes used by the value at +key+ (nil if missing); :samples limits
|
|
119
|
+
# the nested elements sampled.
|
|
120
|
+
def memory_usage(key, samples: nil)
|
|
121
|
+
samples ? invoke("MEMORY", "USAGE", key, "SAMPLES", samples) : invoke("MEMORY", "USAGE", key)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Number of commands the server knows.
|
|
125
|
+
def command_count
|
|
126
|
+
invoke("COMMAND", "COUNT")
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# The keys +command+ (a full command line as separate arguments) touches.
|
|
130
|
+
def command_getkeys(*command)
|
|
131
|
+
invoke("COMMAND", "GETKEYS", *command.flatten)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Suspends client command processing for +milliseconds+ on the whole
|
|
135
|
+
# server. +mode+ is "WRITE" or "ALL". Lift it with #client_unpause.
|
|
136
|
+
def client_pause(milliseconds, mode = nil)
|
|
137
|
+
mode ? invoke("CLIENT", "PAUSE", milliseconds, mode) { |r| r == "OK" } : invoke("CLIENT", "PAUSE", milliseconds) { |r| r == "OK" }
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def client_unpause
|
|
141
|
+
invoke("CLIENT", "UNPAUSE") { |r| r == "OK" }
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Marks the pooled connection that runs it as exempt from (or subject
|
|
145
|
+
# to) client eviction. +enabled+ true/false. Applies to one pooled
|
|
146
|
+
# connection only, so it is mostly useful on a pool_size 1 client.
|
|
147
|
+
def client_no_evict(enabled)
|
|
148
|
+
invoke("CLIENT", "NO-EVICT", enabled ? "ON" : "OFF") { |r| r == "OK" }
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# As #client_no_evict, for the LRU/LFU touch on reads.
|
|
152
|
+
def client_no_touch(enabled)
|
|
153
|
+
invoke("CLIENT", "NO-TOUCH", enabled ? "ON" : "OFF") { |r| r == "OK" }
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# CLIENT SETINFO LIB-NAME / LIB-VER on the pooled connection that runs it.
|
|
157
|
+
def client_setinfo(attribute, value)
|
|
158
|
+
invoke("CLIENT", "SETINFO", attribute, value) { |r| r == "OK" }
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Unblocks the client blocked with id +client_id+. +reason+ is
|
|
162
|
+
# "TIMEOUT" or "ERROR". Returns true when a client was unblocked.
|
|
163
|
+
def client_unblock(client_id, reason = nil)
|
|
164
|
+
args = ["CLIENT", "UNBLOCK", client_id]
|
|
165
|
+
args.push(reason) if reason
|
|
166
|
+
invoke(*args) { |r| r == 1 }
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Closes the connection with id +client_id+. Returns the number closed.
|
|
170
|
+
def client_kill_id(client_id)
|
|
171
|
+
invoke("CLIENT", "KILL", "ID", client_id)
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
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
|
+
# Set commands.
|
|
7
|
+
module Sets
|
|
8
|
+
def sadd(key, *members)
|
|
9
|
+
invoke("SADD", key, *members.flatten)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def srem(key, *members)
|
|
13
|
+
invoke("SREM", key, *members.flatten)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def smembers(key)
|
|
17
|
+
invoke("SMEMBERS", key)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def sismember(key, member)
|
|
21
|
+
invoke("SISMEMBER", key, member) { |r| Support.truthy(r) }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def smismember(key, *members)
|
|
25
|
+
invoke("SMISMEMBER", key, *members.flatten) { |r| r.map { |m| Support.truthy(m) } }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def scard(key)
|
|
29
|
+
invoke("SCARD", key)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def spop(key, count = nil)
|
|
33
|
+
count ? invoke("SPOP", key, count) : invoke("SPOP", key)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def srandmember(key, count = nil)
|
|
37
|
+
count ? invoke("SRANDMEMBER", key, count) : invoke("SRANDMEMBER", key)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def sinter(*keys)
|
|
41
|
+
invoke("SINTER", *keys.flatten)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def sunion(*keys)
|
|
45
|
+
invoke("SUNION", *keys.flatten)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def sdiff(*keys)
|
|
49
|
+
invoke("SDIFF", *keys.flatten)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def sintercard(keys, limit: nil)
|
|
53
|
+
args = ["SINTERCARD", keys.length, *keys]
|
|
54
|
+
args.push("LIMIT", limit) if limit
|
|
55
|
+
invoke(*args)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def sinterstore(destination, *keys)
|
|
59
|
+
invoke("SINTERSTORE", destination, *keys.flatten)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def sunionstore(destination, *keys)
|
|
63
|
+
invoke("SUNIONSTORE", destination, *keys.flatten)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def sdiffstore(destination, *keys)
|
|
67
|
+
invoke("SDIFFSTORE", destination, *keys.flatten)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def smove(source, destination, member)
|
|
71
|
+
invoke("SMOVE", source, destination, member) { |r| r == 1 }
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|