slatedb 0.1.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.
@@ -0,0 +1,310 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SlateDb
4
+ class Database
5
+ class << self
6
+ # Open a database at the given path.
7
+ #
8
+ # @param path [String] The path identifier for the database
9
+ # @param url [String, nil] Optional object store URL (e.g., "s3://bucket/path")
10
+ # @yield [db] If a block is given, yields the database and ensures it's closed
11
+ # @return [Database] The opened database (or block result if block given)
12
+ #
13
+ # @example Open a database
14
+ # db = SlateDb::Database.open("/tmp/mydb")
15
+ # db.put("key", "value")
16
+ # db.close
17
+ #
18
+ # @example Open with block (auto-close)
19
+ # SlateDb::Database.open("/tmp/mydb") do |db|
20
+ # db.put("key", "value")
21
+ # end # automatically closed
22
+ #
23
+ # @example Open with S3 backend
24
+ # db = SlateDb::Database.open("/tmp/mydb", url: "s3://mybucket/path")
25
+ #
26
+ def open(path, url: nil)
27
+ db = _open(path, url)
28
+
29
+ if block_given?
30
+ begin
31
+ yield db
32
+ ensure
33
+ begin
34
+ db.close
35
+ rescue StandardError
36
+ nil
37
+ end
38
+ end
39
+ else
40
+ db
41
+ end
42
+ end
43
+ end
44
+
45
+ # Get a value by key.
46
+ #
47
+ # @param key [String] The key to look up
48
+ # @param durability_filter [String, nil] Filter by durability level ("remote" or "memory")
49
+ # @param dirty [Boolean, nil] Whether to include uncommitted data
50
+ # @param cache_blocks [Boolean, nil] Whether to cache blocks
51
+ # @return [String, nil] The value, or nil if not found
52
+ #
53
+ # @example Basic get
54
+ # value = db.get("mykey")
55
+ #
56
+ # @example Get with options
57
+ # value = db.get("mykey", durability_filter: "memory", dirty: true)
58
+ #
59
+ def get(key, durability_filter: nil, dirty: nil, cache_blocks: nil)
60
+ opts = {}
61
+ opts[:durability_filter] = durability_filter.to_s if durability_filter
62
+ opts[:dirty] = dirty unless dirty.nil?
63
+ opts[:cache_blocks] = cache_blocks unless cache_blocks.nil?
64
+
65
+ if opts.empty?
66
+ _get(key)
67
+ else
68
+ _get_with_options(key, opts)
69
+ end
70
+ end
71
+
72
+ # Store a key-value pair.
73
+ #
74
+ # @param key [String] The key to store
75
+ # @param value [String] The value to store
76
+ # @param ttl [Integer, nil] Time-to-live in milliseconds
77
+ # @param await_durable [Boolean] Whether to wait for durability (default: true)
78
+ # @return [void]
79
+ #
80
+ # @example Basic put
81
+ # db.put("mykey", "myvalue")
82
+ #
83
+ # @example Put with TTL
84
+ # db.put("mykey", "myvalue", ttl: 60_000) # expires in 60 seconds
85
+ #
86
+ # @example Put without waiting for durability
87
+ # db.put("mykey", "myvalue", await_durable: false)
88
+ #
89
+ def put(key, value, ttl: nil, await_durable: nil)
90
+ opts = {}
91
+ opts[:ttl] = ttl if ttl
92
+ opts[:await_durable] = await_durable unless await_durable.nil?
93
+
94
+ if opts.empty?
95
+ _put(key, value)
96
+ else
97
+ _put_with_options(key, value, opts)
98
+ end
99
+ end
100
+
101
+ # Delete a key.
102
+ #
103
+ # @param key [String] The key to delete
104
+ # @param await_durable [Boolean] Whether to wait for durability (default: true)
105
+ # @return [void]
106
+ #
107
+ # @example Basic delete
108
+ # db.delete("mykey")
109
+ #
110
+ # @example Delete without waiting for durability
111
+ # db.delete("mykey", await_durable: false)
112
+ #
113
+ def delete(key, await_durable: nil)
114
+ if await_durable.nil?
115
+ _delete(key)
116
+ else
117
+ _delete_with_options(key, { await_durable: await_durable })
118
+ end
119
+ end
120
+
121
+ # Scan a range of keys.
122
+ #
123
+ # @param start_key [String] The start key (inclusive)
124
+ # @param end_key [String, nil] The end key (exclusive). If nil, scans to end.
125
+ # @param durability_filter [String, nil] Filter by durability level ("remote" or "memory")
126
+ # @param dirty [Boolean, nil] Whether to include uncommitted data
127
+ # @param read_ahead_bytes [Integer, nil] Number of bytes to read ahead
128
+ # @param cache_blocks [Boolean, nil] Whether to cache blocks
129
+ # @param max_fetch_tasks [Integer, nil] Maximum number of fetch tasks
130
+ # @return [Iterator] An iterator over key-value pairs
131
+ #
132
+ # @example Basic scan
133
+ # iter = db.scan("a")
134
+ # while entry = iter.next_entry
135
+ # key, value = entry
136
+ # puts "#{key}: #{value}"
137
+ # end
138
+ #
139
+ # @example Scan with range
140
+ # iter = db.scan("a", "z")
141
+ #
142
+ # @example Scan with block
143
+ # db.scan("user:") do |key, value|
144
+ # puts "#{key}: #{value}"
145
+ # end
146
+ #
147
+ def scan(start_key, end_key = nil, durability_filter: nil, dirty: nil,
148
+ read_ahead_bytes: nil, cache_blocks: nil, max_fetch_tasks: nil, &)
149
+ opts = {}
150
+ opts[:durability_filter] = durability_filter.to_s if durability_filter
151
+ opts[:dirty] = dirty unless dirty.nil?
152
+ opts[:read_ahead_bytes] = read_ahead_bytes if read_ahead_bytes
153
+ opts[:cache_blocks] = cache_blocks unless cache_blocks.nil?
154
+ opts[:max_fetch_tasks] = max_fetch_tasks if max_fetch_tasks
155
+
156
+ iter = if opts.empty?
157
+ _scan(start_key, end_key)
158
+ else
159
+ _scan_with_options(start_key, end_key, opts)
160
+ end
161
+
162
+ if block_given?
163
+ iter.each(&)
164
+ else
165
+ iter
166
+ end
167
+ end
168
+
169
+ # Write a batch of operations atomically.
170
+ #
171
+ # @param batch [WriteBatch] The batch to write
172
+ # @param await_durable [Boolean] Whether to wait for durability (default: true)
173
+ # @return [void]
174
+ #
175
+ # @example Write a batch
176
+ # batch = SlateDb::WriteBatch.new
177
+ # batch.put("key1", "value1")
178
+ # batch.put("key2", "value2")
179
+ # batch.delete("key3")
180
+ # db.write(batch)
181
+ #
182
+ # @example Using batch block helper
183
+ # db.batch do |b|
184
+ # b.put("key1", "value1")
185
+ # b.put("key2", "value2")
186
+ # end
187
+ #
188
+ def write(batch, await_durable: nil)
189
+ if await_durable.nil?
190
+ _write(batch)
191
+ else
192
+ _write_with_options(batch, { await_durable: await_durable })
193
+ end
194
+ end
195
+
196
+ # Create and write a batch using a block.
197
+ #
198
+ # @param await_durable [Boolean] Whether to wait for durability (default: true)
199
+ # @yield [batch] Yields a WriteBatch to the block
200
+ # @return [void]
201
+ #
202
+ # @example
203
+ # db.batch do |b|
204
+ # b.put("key1", "value1")
205
+ # b.put("key2", "value2")
206
+ # b.delete("old_key")
207
+ # end
208
+ #
209
+ def batch(await_durable: nil)
210
+ b = WriteBatch.new
211
+ yield b
212
+ write(b, await_durable: await_durable)
213
+ end
214
+
215
+ # Begin a new transaction.
216
+ #
217
+ # @param isolation [Symbol, String] Isolation level (:snapshot or :serializable)
218
+ # @yield [txn] If a block is given, yields the transaction and auto-commits/rollbacks
219
+ # @return [Transaction, Object] The transaction (or block result if block given)
220
+ #
221
+ # @example Manual transaction management
222
+ # txn = db.begin_transaction
223
+ # txn.put("key", "value")
224
+ # txn.commit
225
+ #
226
+ # @example Block-based transaction (auto-commit)
227
+ # db.transaction do |txn|
228
+ # txn.put("key", "value")
229
+ # txn.get("other_key")
230
+ # end # automatically committed
231
+ #
232
+ # @example Serializable isolation
233
+ # db.transaction(isolation: :serializable) do |txn|
234
+ # val = txn.get("counter")
235
+ # txn.put("counter", (val.to_i + 1).to_s)
236
+ # end
237
+ #
238
+ def begin_transaction(isolation: nil)
239
+ isolation_str = isolation&.to_s
240
+ _begin_transaction(isolation_str)
241
+ end
242
+
243
+ # Execute a block within a transaction.
244
+ #
245
+ # The transaction is automatically committed if the block succeeds,
246
+ # or rolled back if an exception is raised.
247
+ #
248
+ # @param isolation [Symbol, String] Isolation level (:snapshot or :serializable)
249
+ # @yield [txn] Yields the transaction to the block
250
+ # @return [Object] The result of the block
251
+ #
252
+ # @example
253
+ # result = db.transaction do |txn|
254
+ # old_val = txn.get("counter") || "0"
255
+ # new_val = (old_val.to_i + 1).to_s
256
+ # txn.put("counter", new_val)
257
+ # new_val
258
+ # end
259
+ #
260
+ def transaction(isolation: nil)
261
+ txn = begin_transaction(isolation: isolation)
262
+ begin
263
+ result = yield txn
264
+ txn.commit
265
+ result
266
+ rescue StandardError
267
+ begin
268
+ txn.rollback
269
+ rescue StandardError
270
+ nil
271
+ end
272
+ raise
273
+ end
274
+ end
275
+
276
+ # Create a snapshot for consistent reads.
277
+ #
278
+ # @yield [snapshot] If a block is given, yields the snapshot and auto-closes
279
+ # @return [Snapshot, Object] The snapshot (or block result if block given)
280
+ #
281
+ # @example Manual snapshot management
282
+ # snapshot = db.snapshot
283
+ # snapshot.get("key")
284
+ # snapshot.close
285
+ #
286
+ # @example Block-based snapshot (auto-close)
287
+ # db.snapshot do |snap|
288
+ # snap.get("key1")
289
+ # snap.get("key2")
290
+ # end # automatically closed
291
+ #
292
+ def snapshot
293
+ snap = _snapshot
294
+
295
+ if block_given?
296
+ begin
297
+ yield snap
298
+ ensure
299
+ begin
300
+ snap.close
301
+ rescue StandardError
302
+ nil
303
+ end
304
+ end
305
+ else
306
+ snap
307
+ end
308
+ end
309
+ end
310
+ 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,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SlateDb
4
+ class Reader
5
+ class << self
6
+ # Open a read-only reader at the given path.
7
+ #
8
+ # @param path [String] The path identifier for the database
9
+ # @param url [String, nil] Optional object store URL
10
+ # @param checkpoint_id [String, nil] Optional checkpoint UUID to read at
11
+ # @param manifest_poll_interval [Integer, nil] Poll interval in milliseconds
12
+ # @param checkpoint_lifetime [Integer, nil] Checkpoint lifetime in milliseconds
13
+ # @param max_memtable_bytes [Integer, nil] Maximum memtable size in bytes
14
+ # @yield [reader] If a block is given, yields the reader and ensures it's closed
15
+ # @return [Reader] The opened reader (or block result if block given)
16
+ #
17
+ # @example Open a reader
18
+ # reader = SlateDb::Reader.open("/tmp/mydb")
19
+ # value = reader.get("key")
20
+ # reader.close
21
+ #
22
+ # @example Open with block (auto-close)
23
+ # SlateDb::Reader.open("/tmp/mydb") do |reader|
24
+ # reader.get("key")
25
+ # end # automatically closed
26
+ #
27
+ # @example Open at a specific checkpoint
28
+ # reader = SlateDb::Reader.open("/tmp/mydb", checkpoint_id: "uuid-here")
29
+ #
30
+ def open(path, url: nil, checkpoint_id: nil,
31
+ manifest_poll_interval: nil, checkpoint_lifetime: nil,
32
+ max_memtable_bytes: nil)
33
+ opts = {}
34
+ opts[:manifest_poll_interval] = manifest_poll_interval if manifest_poll_interval
35
+ opts[:checkpoint_lifetime] = checkpoint_lifetime if checkpoint_lifetime
36
+ opts[:max_memtable_bytes] = max_memtable_bytes if max_memtable_bytes
37
+
38
+ reader = _open(path, url, checkpoint_id, opts)
39
+
40
+ if block_given?
41
+ begin
42
+ yield reader
43
+ ensure
44
+ begin
45
+ reader.close
46
+ rescue StandardError
47
+ nil
48
+ end
49
+ end
50
+ else
51
+ reader
52
+ end
53
+ end
54
+ end
55
+
56
+ # Get a value by key.
57
+ #
58
+ # @param key [String] The key to look up
59
+ # @param durability_filter [String, nil] Filter by durability level ("remote" or "memory")
60
+ # @param dirty [Boolean, nil] Whether to include uncommitted data
61
+ # @param cache_blocks [Boolean, nil] Whether to cache blocks
62
+ # @return [String, nil] The value, or nil if not found
63
+ #
64
+ def get(key, durability_filter: nil, dirty: nil, cache_blocks: nil)
65
+ opts = {}
66
+ opts[:durability_filter] = durability_filter.to_s if durability_filter
67
+ opts[:dirty] = dirty unless dirty.nil?
68
+ opts[:cache_blocks] = cache_blocks unless cache_blocks.nil?
69
+
70
+ if opts.empty?
71
+ _get(key)
72
+ else
73
+ _get_with_options(key, opts)
74
+ end
75
+ end
76
+
77
+ # Scan a range of keys.
78
+ #
79
+ # @param start_key [String] The start key (inclusive)
80
+ # @param end_key [String, nil] The end key (exclusive)
81
+ # @return [Iterator] An iterator over key-value pairs
82
+ #
83
+ def scan(start_key, end_key = nil, durability_filter: nil, dirty: nil,
84
+ read_ahead_bytes: nil, cache_blocks: nil, max_fetch_tasks: nil, &)
85
+ opts = {}
86
+ opts[:durability_filter] = durability_filter.to_s if durability_filter
87
+ opts[:dirty] = dirty unless dirty.nil?
88
+ opts[:read_ahead_bytes] = read_ahead_bytes if read_ahead_bytes
89
+ opts[:cache_blocks] = cache_blocks unless cache_blocks.nil?
90
+ opts[:max_fetch_tasks] = max_fetch_tasks if max_fetch_tasks
91
+
92
+ iter = if opts.empty?
93
+ _scan(start_key, end_key)
94
+ else
95
+ _scan_with_options(start_key, end_key, opts)
96
+ end
97
+
98
+ if block_given?
99
+ iter.each(&)
100
+ else
101
+ iter
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SlateDb
4
+ class Snapshot
5
+ # Get a value by key from the snapshot.
6
+ #
7
+ # @param key [String] The key to look up
8
+ # @param durability_filter [String, nil] Filter by durability level
9
+ # @param dirty [Boolean, nil] Whether to include uncommitted data
10
+ # @param cache_blocks [Boolean, nil] Whether to cache blocks
11
+ # @return [String, nil] The value, or nil if not found
12
+ #
13
+ def get(key, durability_filter: nil, dirty: nil, cache_blocks: nil)
14
+ opts = {}
15
+ opts[:durability_filter] = durability_filter.to_s if durability_filter
16
+ opts[:dirty] = dirty unless dirty.nil?
17
+ opts[:cache_blocks] = cache_blocks unless cache_blocks.nil?
18
+
19
+ if opts.empty?
20
+ _get(key)
21
+ else
22
+ _get_with_options(key, opts)
23
+ end
24
+ end
25
+
26
+ # Scan a range of keys from the snapshot.
27
+ #
28
+ # @param start_key [String] The start key (inclusive)
29
+ # @param end_key [String, nil] The end key (exclusive)
30
+ # @return [Iterator] An iterator over key-value pairs
31
+ #
32
+ def scan(start_key, end_key = nil, durability_filter: nil, dirty: nil,
33
+ read_ahead_bytes: nil, cache_blocks: nil, max_fetch_tasks: nil, &)
34
+ opts = {}
35
+ opts[:durability_filter] = durability_filter.to_s if durability_filter
36
+ opts[:dirty] = dirty unless dirty.nil?
37
+ opts[:read_ahead_bytes] = read_ahead_bytes if read_ahead_bytes
38
+ opts[:cache_blocks] = cache_blocks unless cache_blocks.nil?
39
+ opts[:max_fetch_tasks] = max_fetch_tasks if max_fetch_tasks
40
+
41
+ iter = if opts.empty?
42
+ _scan(start_key, end_key)
43
+ else
44
+ _scan_with_options(start_key, end_key, opts)
45
+ end
46
+
47
+ if block_given?
48
+ iter.each(&)
49
+ else
50
+ iter
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SlateDb
4
+ class Transaction
5
+ # Get a value by key within the transaction.
6
+ #
7
+ # @param key [String] The key to look up
8
+ # @param durability_filter [String, nil] Filter by durability level
9
+ # @param dirty [Boolean, nil] Whether to include uncommitted data
10
+ # @param cache_blocks [Boolean, nil] Whether to cache blocks
11
+ # @return [String, nil] The value, or nil if not found
12
+ #
13
+ def get(key, durability_filter: nil, dirty: nil, cache_blocks: nil)
14
+ opts = {}
15
+ opts[:durability_filter] = durability_filter.to_s if durability_filter
16
+ opts[:dirty] = dirty unless dirty.nil?
17
+ opts[:cache_blocks] = cache_blocks unless cache_blocks.nil?
18
+
19
+ if opts.empty?
20
+ _get(key)
21
+ else
22
+ _get_with_options(key, opts)
23
+ end
24
+ end
25
+
26
+ # Store a key-value pair within the transaction.
27
+ #
28
+ # @param key [String] The key to store
29
+ # @param value [String] The value to store
30
+ # @param ttl [Integer, nil] Time-to-live in milliseconds
31
+ # @return [void]
32
+ #
33
+ def put(key, value, ttl: nil)
34
+ if ttl
35
+ _put_with_options(key, value, { ttl: ttl })
36
+ else
37
+ _put(key, value)
38
+ end
39
+ end
40
+
41
+ # Delete a key within the transaction.
42
+ #
43
+ # @param key [String] The key to delete
44
+ # @return [void]
45
+ #
46
+ def delete(key)
47
+ _delete(key)
48
+ end
49
+
50
+ # Scan a range of keys within the transaction.
51
+ #
52
+ # @param start_key [String] The start key (inclusive)
53
+ # @param end_key [String, nil] The end key (exclusive)
54
+ # @return [Iterator] An iterator over key-value pairs
55
+ #
56
+ def scan(start_key, end_key = nil, durability_filter: nil, dirty: nil,
57
+ read_ahead_bytes: nil, cache_blocks: nil, max_fetch_tasks: nil, &)
58
+ opts = {}
59
+ opts[:durability_filter] = durability_filter.to_s if durability_filter
60
+ opts[:dirty] = dirty unless dirty.nil?
61
+ opts[:read_ahead_bytes] = read_ahead_bytes if read_ahead_bytes
62
+ opts[:cache_blocks] = cache_blocks unless cache_blocks.nil?
63
+ opts[:max_fetch_tasks] = max_fetch_tasks if max_fetch_tasks
64
+
65
+ iter = if opts.empty?
66
+ _scan(start_key, end_key)
67
+ else
68
+ _scan_with_options(start_key, end_key, opts)
69
+ end
70
+
71
+ if block_given?
72
+ iter.each(&)
73
+ else
74
+ iter
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SlateDb
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SlateDb
4
+ class WriteBatch
5
+ # Add a put operation to the batch.
6
+ #
7
+ # @param key [String] The key to store
8
+ # @param value [String] The value to store
9
+ # @param ttl [Integer, nil] Time-to-live in milliseconds
10
+ # @return [self] Returns self for method chaining
11
+ #
12
+ # @example
13
+ # batch.put("key", "value")
14
+ # batch.put("key2", "value2", ttl: 60_000)
15
+ #
16
+ def put(key, value, ttl: nil)
17
+ if ttl
18
+ _put_with_options(key, value, { ttl: ttl })
19
+ else
20
+ _put(key, value)
21
+ end
22
+ self
23
+ end
24
+
25
+ # Add a delete operation to the batch.
26
+ #
27
+ # @param key [String] The key to delete
28
+ # @return [self] Returns self for method chaining
29
+ #
30
+ # @example
31
+ # batch.delete("key")
32
+ #
33
+ def delete(key)
34
+ _delete(key)
35
+ self
36
+ end
37
+ end
38
+ end
data/lib/slatedb.rb ADDED
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "slatedb/version"
4
+
5
+ # Load the native extension
6
+ begin
7
+ RUBY_VERSION =~ /(\d+\.\d+)/
8
+ require "slatedb/#{Regexp.last_match(1)}/slatedb"
9
+ rescue LoadError
10
+ require "slatedb/slatedb"
11
+ end
12
+
13
+ # Load Ruby class extensions
14
+ require_relative "slatedb/database"
15
+ require_relative "slatedb/iterator"
16
+ require_relative "slatedb/write_batch"
17
+ require_relative "slatedb/transaction"
18
+ require_relative "slatedb/snapshot"
19
+ require_relative "slatedb/reader"
20
+ require_relative "slatedb/admin"