slatedb 0.3.2.beta.3-x86_64-linux
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 +176 -0
- data/README.md +632 -0
- data/lib/slatedb/3.3/slatedb.so +0 -0
- data/lib/slatedb/3.4/slatedb.so +0 -0
- data/lib/slatedb/4.0/slatedb.so +0 -0
- data/lib/slatedb/admin.rb +122 -0
- data/lib/slatedb/database.rb +498 -0
- data/lib/slatedb/iterator.rb +31 -0
- data/lib/slatedb/metrics.rb +20 -0
- data/lib/slatedb/reader.rb +154 -0
- data/lib/slatedb/snapshot.rb +86 -0
- data/lib/slatedb/transaction.rb +174 -0
- data/lib/slatedb/version.rb +5 -0
- data/lib/slatedb/write_batch.rb +58 -0
- data/lib/slatedb.rb +21 -0
- metadata +121 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SlateDb
|
|
4
|
+
class Admin
|
|
5
|
+
class << self
|
|
6
|
+
# Create an admin handle for a database path/object store.
|
|
7
|
+
#
|
|
8
|
+
# @param path [String] Database path
|
|
9
|
+
# @param url [String, nil] Optional object store URL
|
|
10
|
+
# @return [Admin] The admin handle
|
|
11
|
+
#
|
|
12
|
+
# @example
|
|
13
|
+
# admin = SlateDb::Admin.new("/tmp/mydb")
|
|
14
|
+
# checkpoints = admin.list_checkpoints
|
|
15
|
+
#
|
|
16
|
+
def new(path, url: nil)
|
|
17
|
+
_new(path, url)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Read the latest or a specific manifest as a JSON string.
|
|
22
|
+
#
|
|
23
|
+
# @param id [Integer, nil] Optional manifest id to read. If nil, reads the latest.
|
|
24
|
+
# @return [String, nil] JSON string of the manifest, or nil if no manifests exist
|
|
25
|
+
#
|
|
26
|
+
# @example
|
|
27
|
+
# json = admin.read_manifest
|
|
28
|
+
# json = admin.read_manifest(123)
|
|
29
|
+
#
|
|
30
|
+
def read_manifest(id = nil)
|
|
31
|
+
_read_manifest(id)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# List manifests within an optional [start, end) range as JSON.
|
|
35
|
+
#
|
|
36
|
+
# @param start [Integer, nil] Optional inclusive start id
|
|
37
|
+
# @param end_id [Integer, nil] Optional exclusive end id
|
|
38
|
+
# @return [String] JSON string containing a list of manifest metadata
|
|
39
|
+
#
|
|
40
|
+
# @example
|
|
41
|
+
# json = admin.list_manifests
|
|
42
|
+
# json = admin.list_manifests(start: 1, end_id: 10)
|
|
43
|
+
#
|
|
44
|
+
def list_manifests(start: nil, end_id: nil)
|
|
45
|
+
_list_manifests(start, end_id)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Create a detached checkpoint.
|
|
49
|
+
#
|
|
50
|
+
# @param lifetime [Integer, nil] Checkpoint lifetime in milliseconds
|
|
51
|
+
# @param source [String, nil] Source checkpoint UUID string to extend/refresh
|
|
52
|
+
# @param name [String, nil] Checkpoint name
|
|
53
|
+
# @return [Hash] Hash with :id (UUID string) and :manifest_id (Integer)
|
|
54
|
+
#
|
|
55
|
+
# @example
|
|
56
|
+
# result = admin.create_checkpoint(name: "my_checkpoint")
|
|
57
|
+
# puts result[:id] # => "uuid-string"
|
|
58
|
+
# puts result[:manifest_id] # => 7
|
|
59
|
+
#
|
|
60
|
+
def create_checkpoint(lifetime: nil, source: nil, name: nil)
|
|
61
|
+
opts = {}
|
|
62
|
+
opts[:lifetime] = lifetime if lifetime
|
|
63
|
+
opts[:source] = source if source
|
|
64
|
+
opts[:name] = name if name
|
|
65
|
+
_create_checkpoint(opts)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# List known checkpoints for the database.
|
|
69
|
+
#
|
|
70
|
+
# @param name [String, nil] Optional checkpoint name filter
|
|
71
|
+
# @return [Array<Hash>] Array of checkpoint hashes
|
|
72
|
+
#
|
|
73
|
+
# @example
|
|
74
|
+
# checkpoints = admin.list_checkpoints
|
|
75
|
+
# checkpoints.each do |cp|
|
|
76
|
+
# puts "#{cp[:id]}: #{cp[:name]}"
|
|
77
|
+
# end
|
|
78
|
+
#
|
|
79
|
+
def list_checkpoints(name: nil)
|
|
80
|
+
_list_checkpoints(name)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Refresh a checkpoint's lifetime.
|
|
84
|
+
#
|
|
85
|
+
# @param id [String] Checkpoint UUID string
|
|
86
|
+
# @param lifetime [Integer, nil] New lifetime in milliseconds
|
|
87
|
+
# @return [void]
|
|
88
|
+
#
|
|
89
|
+
# @example
|
|
90
|
+
# admin.refresh_checkpoint("uuid-here", lifetime: 60_000)
|
|
91
|
+
#
|
|
92
|
+
def refresh_checkpoint(id, lifetime: nil)
|
|
93
|
+
_refresh_checkpoint(id, lifetime)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Delete a checkpoint.
|
|
97
|
+
#
|
|
98
|
+
# @param id [String] Checkpoint UUID string
|
|
99
|
+
# @return [void]
|
|
100
|
+
#
|
|
101
|
+
# @example
|
|
102
|
+
# admin.delete_checkpoint("uuid-here")
|
|
103
|
+
#
|
|
104
|
+
def delete_checkpoint(id)
|
|
105
|
+
_delete_checkpoint(id)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Run garbage collection once.
|
|
109
|
+
#
|
|
110
|
+
# @param min_age [Integer, nil] Minimum age in milliseconds for objects to be collected
|
|
111
|
+
# @return [void]
|
|
112
|
+
#
|
|
113
|
+
# @example
|
|
114
|
+
# admin.run_gc(min_age: 3600_000) # 1 hour
|
|
115
|
+
#
|
|
116
|
+
def run_gc(min_age: nil)
|
|
117
|
+
opts = {}
|
|
118
|
+
opts[:min_age] = min_age if min_age
|
|
119
|
+
_run_gc(opts)
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
@@ -0,0 +1,498 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SlateDb
|
|
4
|
+
class Database # rubocop:disable Metrics/ClassLength
|
|
5
|
+
private_class_method :new
|
|
6
|
+
|
|
7
|
+
class << self
|
|
8
|
+
# Open a database at the given path.
|
|
9
|
+
#
|
|
10
|
+
# @param path [String] The path identifier for the database
|
|
11
|
+
# @param url [String, nil] Optional object store URL (e.g., "s3://bucket/path")
|
|
12
|
+
# @param merge_operator [Symbol, String, Proc, nil] Optional merge operator.
|
|
13
|
+
# Can be a symbol/string ("string_concat" or "concat") or a Proc/lambda
|
|
14
|
+
# that takes (key, existing_value, new_value) and returns the merged value.
|
|
15
|
+
# @yield [db] If a block is given, yields the database and ensures it's closed
|
|
16
|
+
# @return [Database] The opened database (or block result if block given)
|
|
17
|
+
#
|
|
18
|
+
# @example Open a database
|
|
19
|
+
# db = SlateDb::Database.open("/tmp/mydb")
|
|
20
|
+
# db.put("key", "value")
|
|
21
|
+
# db.close
|
|
22
|
+
#
|
|
23
|
+
# @example Open with block (auto-close)
|
|
24
|
+
# SlateDb::Database.open("/tmp/mydb") do |db|
|
|
25
|
+
# db.put("key", "value")
|
|
26
|
+
# end # automatically closed
|
|
27
|
+
#
|
|
28
|
+
# @example Open with S3 backend
|
|
29
|
+
# db = SlateDb::Database.open("/tmp/mydb", url: "s3://mybucket/path")
|
|
30
|
+
#
|
|
31
|
+
# @example Open with a custom merge operator (Proc)
|
|
32
|
+
# # Custom merge that adds numbers
|
|
33
|
+
# db = SlateDb::Database.open("/tmp/mydb", merge_operator: ->(key, existing, new_val) {
|
|
34
|
+
# existing_num = existing ? existing.to_i : 0
|
|
35
|
+
# (existing_num + new_val.to_i).to_s
|
|
36
|
+
# })
|
|
37
|
+
# db.merge("counter", "5")
|
|
38
|
+
# db.merge("counter", "3")
|
|
39
|
+
# db.get("counter") # => "8"
|
|
40
|
+
#
|
|
41
|
+
def open(path, url: nil, merge_operator: nil)
|
|
42
|
+
opts = {}
|
|
43
|
+
|
|
44
|
+
case merge_operator
|
|
45
|
+
when Symbol, String
|
|
46
|
+
opts[:merge_operator] = merge_operator.to_s
|
|
47
|
+
when Proc
|
|
48
|
+
# Store the proc to prevent GC and pass to Rust
|
|
49
|
+
@_merge_operator_proc = merge_operator
|
|
50
|
+
opts[:merge_operator_proc] = merge_operator
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
db = _open(path, url, opts)
|
|
54
|
+
|
|
55
|
+
if block_given?
|
|
56
|
+
begin
|
|
57
|
+
yield db
|
|
58
|
+
ensure
|
|
59
|
+
begin
|
|
60
|
+
db.close
|
|
61
|
+
rescue StandardError
|
|
62
|
+
nil
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
else
|
|
66
|
+
db
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Get a value by key.
|
|
72
|
+
#
|
|
73
|
+
# @param key [String] The key to look up
|
|
74
|
+
# @param durability_filter [String, nil] Filter by durability level ("remote" or "memory")
|
|
75
|
+
# @param dirty [Boolean, nil] Whether to include uncommitted data
|
|
76
|
+
# @param cache_blocks [Boolean, nil] Whether to cache blocks
|
|
77
|
+
# @return [String, nil] The value, or nil if not found
|
|
78
|
+
#
|
|
79
|
+
# @example Basic get
|
|
80
|
+
# value = db.get("mykey")
|
|
81
|
+
#
|
|
82
|
+
# @example Get with options
|
|
83
|
+
# value = db.get("mykey", durability_filter: "memory", dirty: true)
|
|
84
|
+
#
|
|
85
|
+
def get(key, durability_filter: nil, dirty: nil, cache_blocks: nil)
|
|
86
|
+
opts = {}
|
|
87
|
+
opts[:durability_filter] = durability_filter.to_s if durability_filter
|
|
88
|
+
opts[:dirty] = dirty unless dirty.nil?
|
|
89
|
+
opts[:cache_blocks] = cache_blocks unless cache_blocks.nil?
|
|
90
|
+
|
|
91
|
+
if opts.empty?
|
|
92
|
+
_get(key)
|
|
93
|
+
else
|
|
94
|
+
_get_with_options(key, opts)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Get a key-value pair with SlateDB metadata.
|
|
99
|
+
#
|
|
100
|
+
# @param key [String] The key to look up
|
|
101
|
+
# @param durability_filter [String, nil] Filter by durability level ("remote" or "memory")
|
|
102
|
+
# @param dirty [Boolean, nil] Whether to include uncommitted data
|
|
103
|
+
# @param cache_blocks [Boolean, nil] Whether to cache blocks
|
|
104
|
+
# @return [Hash, nil] A hash with :key, :value, :seq, :create_ts, and :expire_ts, or nil if not found
|
|
105
|
+
#
|
|
106
|
+
# @example Inspect metadata
|
|
107
|
+
# entry = db.get_key_value("mykey")
|
|
108
|
+
# entry[:value] # => "myvalue"
|
|
109
|
+
# entry[:seq] # => sequence number
|
|
110
|
+
#
|
|
111
|
+
def get_key_value(key, durability_filter: nil, dirty: nil, cache_blocks: nil)
|
|
112
|
+
opts = {}
|
|
113
|
+
opts[:durability_filter] = durability_filter.to_s if durability_filter
|
|
114
|
+
opts[:dirty] = dirty unless dirty.nil?
|
|
115
|
+
opts[:cache_blocks] = cache_blocks unless cache_blocks.nil?
|
|
116
|
+
|
|
117
|
+
if opts.empty?
|
|
118
|
+
_get_key_value(key)
|
|
119
|
+
else
|
|
120
|
+
_get_key_value_with_options(key, opts)
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
alias get_entry get_key_value
|
|
125
|
+
|
|
126
|
+
# Store a key-value pair.
|
|
127
|
+
#
|
|
128
|
+
# @param key [String] The key to store
|
|
129
|
+
# @param value [String] The value to store
|
|
130
|
+
# @param ttl [Integer, nil] Time-to-live in milliseconds
|
|
131
|
+
# @param await_durable [Boolean] Whether to wait for durability (default: true)
|
|
132
|
+
# @param seqnum [Integer, nil] User-supplied sequence number for this write.
|
|
133
|
+
# When provided (and non-zero), it is used instead of the internally
|
|
134
|
+
# generated sequence number. It must be strictly greater than the current
|
|
135
|
+
# maximum sequence number or the write fails. (Requires SlateDB >= 0.13.0)
|
|
136
|
+
# @return [void]
|
|
137
|
+
#
|
|
138
|
+
# @example Basic put
|
|
139
|
+
# db.put("mykey", "myvalue")
|
|
140
|
+
#
|
|
141
|
+
# @example Put with TTL
|
|
142
|
+
# db.put("mykey", "myvalue", ttl: 60_000) # expires in 60 seconds
|
|
143
|
+
#
|
|
144
|
+
# @example Put without waiting for durability
|
|
145
|
+
# db.put("mykey", "myvalue", await_durable: false)
|
|
146
|
+
#
|
|
147
|
+
# @example Put with an explicit sequence number
|
|
148
|
+
# db.put("mykey", "myvalue", seqnum: 42)
|
|
149
|
+
#
|
|
150
|
+
def put(key, value, ttl: nil, await_durable: nil, seqnum: nil)
|
|
151
|
+
opts = {}
|
|
152
|
+
opts[:ttl] = ttl if ttl
|
|
153
|
+
opts[:await_durable] = await_durable unless await_durable.nil?
|
|
154
|
+
opts[:seqnum] = seqnum if seqnum
|
|
155
|
+
|
|
156
|
+
if opts.empty?
|
|
157
|
+
_put(key, value)
|
|
158
|
+
else
|
|
159
|
+
_put_with_options(key, value, opts)
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Delete a key.
|
|
164
|
+
#
|
|
165
|
+
# @param key [String] The key to delete
|
|
166
|
+
# @param await_durable [Boolean] Whether to wait for durability (default: true)
|
|
167
|
+
# @param seqnum [Integer, nil] User-supplied sequence number for this write.
|
|
168
|
+
# See {#put} for semantics. (Requires SlateDB >= 0.13.0)
|
|
169
|
+
# @return [void]
|
|
170
|
+
#
|
|
171
|
+
# @example Basic delete
|
|
172
|
+
# db.delete("mykey")
|
|
173
|
+
#
|
|
174
|
+
# @example Delete without waiting for durability
|
|
175
|
+
# db.delete("mykey", await_durable: false)
|
|
176
|
+
#
|
|
177
|
+
def delete(key, await_durable: nil, seqnum: nil)
|
|
178
|
+
opts = {}
|
|
179
|
+
opts[:await_durable] = await_durable unless await_durable.nil?
|
|
180
|
+
opts[:seqnum] = seqnum if seqnum
|
|
181
|
+
|
|
182
|
+
if opts.empty?
|
|
183
|
+
_delete(key)
|
|
184
|
+
else
|
|
185
|
+
_delete_with_options(key, opts)
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Scan a range of keys.
|
|
190
|
+
#
|
|
191
|
+
# @param start_key [String] The start key (inclusive)
|
|
192
|
+
# @param end_key [String, nil] The end key (exclusive). If nil, scans to end.
|
|
193
|
+
# @param durability_filter [String, nil] Filter by durability level ("remote" or "memory")
|
|
194
|
+
# @param dirty [Boolean, nil] Whether to include uncommitted data
|
|
195
|
+
# @param read_ahead_bytes [Integer, nil] Number of bytes to read ahead
|
|
196
|
+
# @param cache_blocks [Boolean, nil] Whether to cache blocks
|
|
197
|
+
# @param max_fetch_tasks [Integer, nil] Maximum number of fetch tasks
|
|
198
|
+
# @param order [Symbol, String, nil] Iteration order (:asc/:ascending or :desc/:descending)
|
|
199
|
+
# @return [Iterator] An iterator over key-value pairs
|
|
200
|
+
#
|
|
201
|
+
# @example Basic scan
|
|
202
|
+
# iter = db.scan("a")
|
|
203
|
+
# while entry = iter.next_entry
|
|
204
|
+
# key, value = entry
|
|
205
|
+
# puts "#{key}: #{value}"
|
|
206
|
+
# end
|
|
207
|
+
#
|
|
208
|
+
# @example Scan with range
|
|
209
|
+
# iter = db.scan("a", "z")
|
|
210
|
+
#
|
|
211
|
+
# @example Scan with block
|
|
212
|
+
# db.scan("user:") do |key, value|
|
|
213
|
+
# puts "#{key}: #{value}"
|
|
214
|
+
# end
|
|
215
|
+
#
|
|
216
|
+
def scan(start_key, end_key = nil, durability_filter: nil, dirty: nil,
|
|
217
|
+
read_ahead_bytes: nil, cache_blocks: nil, max_fetch_tasks: nil, order: nil, &)
|
|
218
|
+
opts = scan_options(
|
|
219
|
+
durability_filter: durability_filter,
|
|
220
|
+
dirty: dirty,
|
|
221
|
+
read_ahead_bytes: read_ahead_bytes,
|
|
222
|
+
cache_blocks: cache_blocks,
|
|
223
|
+
max_fetch_tasks: max_fetch_tasks,
|
|
224
|
+
order: order
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
iter = if opts.empty?
|
|
228
|
+
_scan(start_key, end_key)
|
|
229
|
+
else
|
|
230
|
+
_scan_with_options(start_key, end_key, opts)
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
if block_given?
|
|
234
|
+
iter.each(&)
|
|
235
|
+
else
|
|
236
|
+
iter
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# Scan all keys with a given prefix.
|
|
241
|
+
#
|
|
242
|
+
# @param prefix [String] The key prefix to scan
|
|
243
|
+
# @param durability_filter [String, nil] Filter by durability level ("remote" or "memory")
|
|
244
|
+
# @param dirty [Boolean, nil] Whether to include uncommitted data
|
|
245
|
+
# @param read_ahead_bytes [Integer, nil] Number of bytes to read ahead
|
|
246
|
+
# @param cache_blocks [Boolean, nil] Whether to cache blocks
|
|
247
|
+
# @param max_fetch_tasks [Integer, nil] Maximum number of fetch tasks
|
|
248
|
+
# @param order [Symbol, String, nil] Iteration order (:asc/:ascending or :desc/:descending)
|
|
249
|
+
# @return [Iterator] An iterator over key-value pairs
|
|
250
|
+
#
|
|
251
|
+
# @example Scan all user keys
|
|
252
|
+
# db.scan_prefix("user:") do |key, value|
|
|
253
|
+
# puts "#{key}: #{value}"
|
|
254
|
+
# end
|
|
255
|
+
#
|
|
256
|
+
def scan_prefix(prefix, durability_filter: nil, dirty: nil,
|
|
257
|
+
read_ahead_bytes: nil, cache_blocks: nil, max_fetch_tasks: nil, order: nil, &)
|
|
258
|
+
opts = scan_options(
|
|
259
|
+
durability_filter: durability_filter,
|
|
260
|
+
dirty: dirty,
|
|
261
|
+
read_ahead_bytes: read_ahead_bytes,
|
|
262
|
+
cache_blocks: cache_blocks,
|
|
263
|
+
max_fetch_tasks: max_fetch_tasks,
|
|
264
|
+
order: order
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
iter = if opts.empty?
|
|
268
|
+
_scan_prefix(prefix)
|
|
269
|
+
else
|
|
270
|
+
_scan_prefix_with_options(prefix, opts)
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
if block_given?
|
|
274
|
+
iter.each(&)
|
|
275
|
+
else
|
|
276
|
+
iter
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def scan_options(durability_filter:, dirty:, read_ahead_bytes:, cache_blocks:,
|
|
281
|
+
max_fetch_tasks:, order:)
|
|
282
|
+
opts = {}
|
|
283
|
+
opts[:durability_filter] = durability_filter.to_s if durability_filter
|
|
284
|
+
opts[:dirty] = dirty unless dirty.nil?
|
|
285
|
+
opts[:read_ahead_bytes] = read_ahead_bytes if read_ahead_bytes
|
|
286
|
+
opts[:cache_blocks] = cache_blocks unless cache_blocks.nil?
|
|
287
|
+
opts[:max_fetch_tasks] = max_fetch_tasks if max_fetch_tasks
|
|
288
|
+
opts[:order] = order.to_s if order
|
|
289
|
+
opts
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
private :scan_options
|
|
293
|
+
|
|
294
|
+
# Write a batch of operations atomically.
|
|
295
|
+
#
|
|
296
|
+
# @param batch [WriteBatch] The batch to write
|
|
297
|
+
# @param await_durable [Boolean] Whether to wait for durability (default: true)
|
|
298
|
+
# @param seqnum [Integer, nil] User-supplied sequence number applied to the
|
|
299
|
+
# batch. See {#put} for semantics. (Requires SlateDB >= 0.13.0)
|
|
300
|
+
# @return [void]
|
|
301
|
+
#
|
|
302
|
+
# @example Write a batch
|
|
303
|
+
# batch = SlateDb::WriteBatch.new
|
|
304
|
+
# batch.put("key1", "value1")
|
|
305
|
+
# batch.put("key2", "value2")
|
|
306
|
+
# batch.delete("key3")
|
|
307
|
+
# db.write(batch)
|
|
308
|
+
#
|
|
309
|
+
# @example Using batch block helper
|
|
310
|
+
# db.batch do |b|
|
|
311
|
+
# b.put("key1", "value1")
|
|
312
|
+
# b.put("key2", "value2")
|
|
313
|
+
# end
|
|
314
|
+
#
|
|
315
|
+
def write(batch, await_durable: nil, seqnum: nil)
|
|
316
|
+
opts = {}
|
|
317
|
+
opts[:await_durable] = await_durable unless await_durable.nil?
|
|
318
|
+
opts[:seqnum] = seqnum if seqnum
|
|
319
|
+
|
|
320
|
+
if opts.empty?
|
|
321
|
+
_write(batch)
|
|
322
|
+
else
|
|
323
|
+
_write_with_options(batch, opts)
|
|
324
|
+
end
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
# Merge a value into the database.
|
|
328
|
+
#
|
|
329
|
+
# @param key [String] The key to merge into
|
|
330
|
+
# @param value [String] The merge operand to apply
|
|
331
|
+
# @param ttl [Integer, nil] Time-to-live in milliseconds
|
|
332
|
+
# @param await_durable [Boolean] Whether to wait for durability (default: true)
|
|
333
|
+
# @param seqnum [Integer, nil] User-supplied sequence number for this write.
|
|
334
|
+
# See {#put} for semantics. (Requires SlateDB >= 0.13.0)
|
|
335
|
+
# @return [void]
|
|
336
|
+
#
|
|
337
|
+
# @example Merge with string concatenation operator
|
|
338
|
+
# db = SlateDb::Database.open("/tmp/mydb", merge_operator: :string_concat)
|
|
339
|
+
# db.merge("key", "part1")
|
|
340
|
+
# db.merge("key", "part2")
|
|
341
|
+
#
|
|
342
|
+
def merge(key, value, ttl: nil, await_durable: nil, seqnum: nil)
|
|
343
|
+
opts = {}
|
|
344
|
+
opts[:ttl] = ttl if ttl
|
|
345
|
+
opts[:await_durable] = await_durable unless await_durable.nil?
|
|
346
|
+
opts[:seqnum] = seqnum if seqnum
|
|
347
|
+
|
|
348
|
+
if opts.empty?
|
|
349
|
+
_merge(key, value)
|
|
350
|
+
else
|
|
351
|
+
_merge_with_options(key, value, opts)
|
|
352
|
+
end
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
# Create and write a batch using a block.
|
|
356
|
+
#
|
|
357
|
+
# @param await_durable [Boolean] Whether to wait for durability (default: true)
|
|
358
|
+
# @param seqnum [Integer, nil] User-supplied sequence number applied to the
|
|
359
|
+
# batch. See {#put} for semantics. (Requires SlateDB >= 0.13.0)
|
|
360
|
+
# @yield [batch] Yields a WriteBatch to the block
|
|
361
|
+
# @return [void]
|
|
362
|
+
#
|
|
363
|
+
# @example
|
|
364
|
+
# db.batch do |b|
|
|
365
|
+
# b.put("key1", "value1")
|
|
366
|
+
# b.put("key2", "value2")
|
|
367
|
+
# b.delete("old_key")
|
|
368
|
+
# end
|
|
369
|
+
#
|
|
370
|
+
def batch(await_durable: nil, seqnum: nil)
|
|
371
|
+
b = WriteBatch.new
|
|
372
|
+
yield b
|
|
373
|
+
write(b, await_durable: await_durable, seqnum: seqnum)
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
# Begin a new transaction.
|
|
377
|
+
#
|
|
378
|
+
# @param isolation [Symbol, String] Isolation level (:snapshot or :serializable)
|
|
379
|
+
# @yield [txn] If a block is given, yields the transaction and auto-commits/rollbacks
|
|
380
|
+
# @return [Transaction, Object] The transaction (or block result if block given)
|
|
381
|
+
#
|
|
382
|
+
# @example Manual transaction management
|
|
383
|
+
# txn = db.begin_transaction
|
|
384
|
+
# txn.put("key", "value")
|
|
385
|
+
# txn.commit
|
|
386
|
+
#
|
|
387
|
+
# @example Block-based transaction (auto-commit)
|
|
388
|
+
# db.transaction do |txn|
|
|
389
|
+
# txn.put("key", "value")
|
|
390
|
+
# txn.get("other_key")
|
|
391
|
+
# end # automatically committed
|
|
392
|
+
#
|
|
393
|
+
# @example Serializable isolation
|
|
394
|
+
# db.transaction(isolation: :serializable) do |txn|
|
|
395
|
+
# val = txn.get("counter")
|
|
396
|
+
# txn.put("counter", (val.to_i + 1).to_s)
|
|
397
|
+
# end
|
|
398
|
+
#
|
|
399
|
+
def begin_transaction(isolation: nil)
|
|
400
|
+
isolation_str = isolation&.to_s
|
|
401
|
+
_begin_transaction(isolation_str)
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
# Execute a block within a transaction.
|
|
405
|
+
#
|
|
406
|
+
# The transaction is automatically committed if the block succeeds,
|
|
407
|
+
# or rolled back if an exception is raised.
|
|
408
|
+
#
|
|
409
|
+
# @param isolation [Symbol, String] Isolation level (:snapshot or :serializable)
|
|
410
|
+
# @yield [txn] Yields the transaction to the block
|
|
411
|
+
# @return [Object] The result of the block
|
|
412
|
+
#
|
|
413
|
+
# @example
|
|
414
|
+
# result = db.transaction do |txn|
|
|
415
|
+
# old_val = txn.get("counter") || "0"
|
|
416
|
+
# new_val = (old_val.to_i + 1).to_s
|
|
417
|
+
# txn.put("counter", new_val)
|
|
418
|
+
# new_val
|
|
419
|
+
# end
|
|
420
|
+
#
|
|
421
|
+
def transaction(isolation: nil)
|
|
422
|
+
txn = begin_transaction(isolation: isolation)
|
|
423
|
+
begin
|
|
424
|
+
result = yield txn
|
|
425
|
+
txn.commit
|
|
426
|
+
result
|
|
427
|
+
rescue StandardError
|
|
428
|
+
begin
|
|
429
|
+
txn.rollback
|
|
430
|
+
rescue StandardError
|
|
431
|
+
nil
|
|
432
|
+
end
|
|
433
|
+
raise
|
|
434
|
+
end
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
# Create a snapshot for consistent reads.
|
|
438
|
+
#
|
|
439
|
+
# @yield [snapshot] If a block is given, yields the snapshot and auto-closes
|
|
440
|
+
# @return [Snapshot, Object] The snapshot (or block result if block given)
|
|
441
|
+
#
|
|
442
|
+
# @example Manual snapshot management
|
|
443
|
+
# snapshot = db.snapshot
|
|
444
|
+
# snapshot.get("key")
|
|
445
|
+
# snapshot.close
|
|
446
|
+
#
|
|
447
|
+
# @example Block-based snapshot (auto-close)
|
|
448
|
+
# db.snapshot do |snap|
|
|
449
|
+
# snap.get("key1")
|
|
450
|
+
# snap.get("key2")
|
|
451
|
+
# end # automatically closed
|
|
452
|
+
#
|
|
453
|
+
def snapshot
|
|
454
|
+
snap = _snapshot
|
|
455
|
+
|
|
456
|
+
if block_given?
|
|
457
|
+
begin
|
|
458
|
+
yield snap
|
|
459
|
+
ensure
|
|
460
|
+
begin
|
|
461
|
+
snap.close
|
|
462
|
+
rescue StandardError
|
|
463
|
+
nil
|
|
464
|
+
end
|
|
465
|
+
end
|
|
466
|
+
else
|
|
467
|
+
snap
|
|
468
|
+
end
|
|
469
|
+
end
|
|
470
|
+
|
|
471
|
+
# Create a checkpoint of the database.
|
|
472
|
+
#
|
|
473
|
+
# @param lifetime [Integer, nil] Checkpoint lifetime in milliseconds
|
|
474
|
+
# @param name [String, nil] Optional name for the checkpoint
|
|
475
|
+
# @return [Hash] Hash with :id (UUID string) and :manifest_id (integer)
|
|
476
|
+
#
|
|
477
|
+
# @example Create a named checkpoint
|
|
478
|
+
# checkpoint = db.create_checkpoint(name: "before-migration")
|
|
479
|
+
# puts "Checkpoint ID: #{checkpoint[:id]}"
|
|
480
|
+
#
|
|
481
|
+
# @example Create a checkpoint with lifetime
|
|
482
|
+
# checkpoint = db.create_checkpoint(lifetime: 3600_000) # 1 hour
|
|
483
|
+
#
|
|
484
|
+
def create_checkpoint(lifetime: nil, name: nil)
|
|
485
|
+
opts = {}
|
|
486
|
+
opts[:lifetime] = lifetime if lifetime
|
|
487
|
+
opts[:name] = name if name
|
|
488
|
+
_create_checkpoint(opts)
|
|
489
|
+
end
|
|
490
|
+
|
|
491
|
+
# Get database metrics registry.
|
|
492
|
+
#
|
|
493
|
+
# @return [Metrics] Metrics registry
|
|
494
|
+
def metrics
|
|
495
|
+
_metrics
|
|
496
|
+
end
|
|
497
|
+
end
|
|
498
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SlateDb
|
|
4
|
+
class Iterator
|
|
5
|
+
include Enumerable
|
|
6
|
+
|
|
7
|
+
# Iterate over all entries.
|
|
8
|
+
#
|
|
9
|
+
# @yield [key, value] Yields each key-value pair
|
|
10
|
+
# @return [self, Enumerator] Returns self if block given, otherwise an Enumerator
|
|
11
|
+
#
|
|
12
|
+
# @example
|
|
13
|
+
# iter.each do |key, value|
|
|
14
|
+
# puts "#{key}: #{value}"
|
|
15
|
+
# end
|
|
16
|
+
#
|
|
17
|
+
# @example With Enumerable methods
|
|
18
|
+
# iter.map { |k, v| [k.upcase, v] }
|
|
19
|
+
# iter.select { |k, v| k.start_with?("user:") }
|
|
20
|
+
#
|
|
21
|
+
def each
|
|
22
|
+
return to_enum(:each) unless block_given?
|
|
23
|
+
|
|
24
|
+
while (entry = next_entry)
|
|
25
|
+
yield entry
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
self
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SlateDb
|
|
4
|
+
class Metrics
|
|
5
|
+
# Get a metric value by name.
|
|
6
|
+
#
|
|
7
|
+
# @param name [String] Metric name
|
|
8
|
+
# @return [Integer, nil] Current value or nil if not found
|
|
9
|
+
def [](name)
|
|
10
|
+
get(name)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Convert all metrics to a hash.
|
|
14
|
+
#
|
|
15
|
+
# @return [Hash] Map of metric name to value
|
|
16
|
+
def to_h
|
|
17
|
+
names.to_h { |metric_name| [metric_name, get(metric_name)] }
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|