slatedb 0.2.0 → 0.4.4

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0026c9e2a5afd6e0d7fa35c10b037791910a2438cf0bcec73c9935db557c9a85
4
- data.tar.gz: 140bdf13e24d576e87165a63f31e3a0130244f36ea1278da7da5e6703347f7c5
3
+ metadata.gz: 5ea58f5cc108fef4066004dce3db7641d979589c6afa56023845b4e568198630
4
+ data.tar.gz: c5e52bded1c840e6d51ba420b673c5f03e3f9450dd78476dc129a45cab02b11b
5
5
  SHA512:
6
- metadata.gz: ae8fe00e7069801bfdf8cf90ea48bfee6a60ed1413cdbc8bef5b9a083744436a0b3d3c1e46ef51c5cbd5de85bd46c6c332ec4b016ffd6482a729fa0b3a50e531
7
- data.tar.gz: 5e60f6509c2eae4c948751d941f6cd7389dae8e43e241d02d559c47251b26383c6a59fcedb50133d54759d9d791a9f07079f6eed37106a386358094f2f3a5b7e
6
+ metadata.gz: 715e35df65d25a1487f648718ec72e593fed5e22289f47c2017aa8eac24ccda5d5df00151ec618b1d732550be2f8b6ac5d8136b9358d18c197ae11b5e437e8a9
7
+ data.tar.gz: 92780ae93284906f92a4e4550d885c281af32f7cd1f91642e6ca9ebbe1f114d574d20a17be3f0605b5caaaafe64e27e70f12a96b5fcc6255fd8f0e210f0fa9b1
data/README.md CHANGED
@@ -8,10 +8,6 @@ Ruby bindings for [SlateDB](https://slatedb.io), a cloud-native embedded key-val
8
8
 
9
9
  These bindings are still in early development, and while SlateDB itself is used in Production, these bindings have yet to be. Contributions are welcome!
10
10
 
11
- ### TODO
12
-
13
- - [ ] Cross-compile native extensions
14
-
15
11
  ## Installation
16
12
 
17
13
  Add this line to your application's Gemfile:
@@ -32,8 +28,7 @@ Or install it yourself as:
32
28
  gem install slatedb
33
29
  ```
34
30
 
35
- > [!IMPORTANT]
36
- > This gem currently requires a working Rust toolchain to install until the dependencies are cross-compiled.
31
+ Precompiled native gems are published for common Linux and Darwin platforms. Building from source still requires a Rust toolchain.
37
32
 
38
33
  ## Usage
39
34
 
@@ -138,6 +133,34 @@ db.put("key", "value", ttl: 60_000) # expires in 60 seconds
138
133
 
139
134
  # Don't wait for durability
140
135
  db.put("key", "value", await_durable: false)
136
+
137
+ # Supply an explicit sequence number (SlateDB >= 0.13.0)
138
+ db.put("key", "value", seqnum: 42)
139
+ ```
140
+
141
+ #### User-Supplied Sequence Numbers
142
+
143
+ By default SlateDB assigns a monotonically increasing sequence number to every
144
+ write. Since SlateDB 0.13.0 you can instead supply your own via `seqnum:`. The
145
+ value must be **strictly greater** than the current maximum sequence number, or
146
+ the write is rejected with `SlateDb::InvalidArgumentError`. This is useful when
147
+ replaying an external log or coordinating sequence numbers across systems.
148
+
149
+ ```ruby
150
+ db.put("key", "value", seqnum: 1_000)
151
+ db.delete("old", seqnum: 1_001)
152
+ db.merge("counter", "5", seqnum: 1_002) # requires a merge operator
153
+ db.write(batch, seqnum: 1_003) # applied across the batch
154
+ db.batch(seqnum: 1_004) { |b| b.put("k", "v") }
155
+
156
+ # The sequence number is reflected in the stored record
157
+ db.put("key", "value", seqnum: 2_000)
158
+ db.get_key_value("key")[:seq] # => 2000
159
+
160
+ # On a transaction it is supplied at commit time
161
+ txn = db.begin_transaction
162
+ txn.put("k", "v")
163
+ txn.commit(seqnum: 3_000)
141
164
  ```
142
165
 
143
166
  #### Get Options
@@ -151,11 +174,37 @@ db.get("key", durability_filter: "remote")
151
174
  db.get("key", dirty: true)
152
175
  ```
153
176
 
177
+ #### Key-Value Metadata
178
+
179
+ SlateDB can return the full key-value record, including storage metadata:
180
+
181
+ ```ruby
182
+ db.put("key", "value")
183
+ entry = db.get_key_value("key")
184
+ # => { key: "key", value: "value", seq: 1, create_ts: 1_765_000_000_000, expire_ts: nil }
185
+
186
+ entry[:value] # => "value"
187
+ entry[:seq] # SlateDB sequence number
188
+ entry[:create_ts] # creation timestamp in milliseconds
189
+ entry[:expire_ts] # expiration timestamp in milliseconds, or nil
190
+
191
+ # Alias for the same API
192
+ db.get_entry("key")
193
+
194
+ # The same read options accepted by #get are supported
195
+ db.get_key_value("key", durability_filter: "memory", cache_blocks: false)
196
+ ```
197
+
198
+ Missing keys return `nil`, matching `#get`.
199
+
154
200
  #### Delete Options
155
201
 
156
202
  ```ruby
157
203
  # Don't wait for durability
158
204
  db.delete("key", await_durable: false)
205
+
206
+ # Supply an explicit sequence number (SlateDB >= 0.13.0)
207
+ db.delete("key", seqnum: 42)
159
208
  ```
160
209
 
161
210
  ### Scanning
@@ -173,6 +222,11 @@ db.scan("a", "z").each do |key, value|
173
222
  puts "#{key}: #{value}"
174
223
  end
175
224
 
225
+ # Scan in descending key order
226
+ db.scan("a", "z", order: :desc).each do |key, value|
227
+ puts "#{key}: #{value}"
228
+ end
229
+
176
230
  # Use Enumerable methods
177
231
  keys = db.scan("user:").map { |k, v| k }
178
232
  users = db.scan("user:").select { |k, v| v.include?("active") }
@@ -196,6 +250,11 @@ db.scan_prefix("order:") do |key, value|
196
250
  puts "#{key}: #{value}"
197
251
  end
198
252
 
253
+ # Prefix scans can also run in descending key order
254
+ db.scan_prefix("user:", order: :desc).each do |key, value|
255
+ puts "#{key}: #{value}"
256
+ end
257
+
199
258
  # Works with transactions, snapshots, and readers too
200
259
  db.transaction do |txn|
201
260
  txn.scan_prefix("item:").each do |k, v|
@@ -204,6 +263,27 @@ db.transaction do |txn|
204
263
  end
205
264
  ```
206
265
 
266
+ You can narrow a prefix scan to a sub-range using the `from:` (inclusive) and
267
+ `to:` (exclusive) options (SlateDB >= 0.14.0). Both are key *suffixes* that are
268
+ appended to the prefix, so they let you paginate or resume within a prefix
269
+ without building full-key ranges by hand. The scan never escapes the prefix.
270
+
271
+ ```ruby
272
+ # Keys "user:100" (inclusive) up to "user:200" (exclusive)
273
+ db.scan_prefix("user:", from: "100", to: "200").each do |key, value|
274
+ puts "#{key}: #{value}"
275
+ end
276
+
277
+ # Only a lower bound: from "user:500" to the end of the prefix
278
+ db.scan_prefix("user:", from: "500")
279
+
280
+ # Only an upper bound: from the start of the prefix up to (but not including) "user:100"
281
+ db.scan_prefix("user:", to: "100")
282
+
283
+ # Sub-ranges compose with order: and work on transactions, snapshots, and readers
284
+ db.scan_prefix("user:", from: "100", to: "200", order: :desc)
285
+ ```
286
+
207
287
  ### Merge Operations
208
288
 
209
289
  Merge operations allow you to combine values without reading them first, useful for counters, append-only logs, and similar patterns:
@@ -423,12 +503,33 @@ SlateDb::Reader.open("/tmp/mydb", url: "s3://bucket/path") do |reader|
423
503
  end
424
504
  end
425
505
 
426
- # Open at a specific checkpoint
506
+ # Open at a specific checkpoint (pins the reader to that checkpoint's state)
427
507
  SlateDb::Reader.open("/tmp/mydb",
428
508
  url: "s3://bucket/path",
429
509
  checkpoint_id: "uuid-here") do |reader|
430
510
  reader.get("key")
431
511
  end
512
+
513
+ # Follow the latest state without managing a checkpoint (SlateDB >= 0.15.0).
514
+ # This performs no object-store writes but offers no protection from garbage
515
+ # collection, so it suits read-only or mirrored databases. By default (neither
516
+ # checkpoint_id nor follow_latest set) the reader creates and periodically
517
+ # refreshes its own checkpoint instead.
518
+ SlateDb::Reader.open("/tmp/mydb",
519
+ url: "s3://bucket/path",
520
+ follow_latest: true) do |reader|
521
+ reader.get("key")
522
+ end
523
+
524
+ # Enable the reader's on-disk cache and cap its open file handles
525
+ # (max_open_file_handles, added in SlateDB 0.13.0, only takes effect when
526
+ # cache_root is set, since that is what enables the cached object store).
527
+ SlateDb::Reader.open("/tmp/mydb",
528
+ url: "s3://bucket/path",
529
+ cache_root: "/var/cache/slatedb",
530
+ max_open_file_handles: 256) do |reader|
531
+ reader.get("key")
532
+ end
432
533
  ```
433
534
 
434
535
  ### Admin Operations
@@ -527,9 +628,40 @@ Exception hierarchy:
527
628
 
528
629
  ## Requirements
529
630
 
530
- - Ruby 3.1+
631
+ - Ruby 3.3+
531
632
  - Rust toolchain (for building from source)
532
633
 
634
+ ## Releasing
635
+
636
+ Releases publish a generic `ruby` platform gem and **precompiled native gems**
637
+ for six platforms (`x86_64-linux`, `aarch64-linux`, `x86_64-linux-musl`,
638
+ `aarch64-linux-musl`, `arm64-darwin`, `x86_64-darwin`) to
639
+ [rubygems.org](https://rubygems.org/gems/slatedb). The generic gem keeps
640
+ RubyGems' latest-version metadata aligned; supported platforms should still
641
+ resolve to their matching precompiled native gem.
642
+
643
+ ### Cutting a release
644
+
645
+ The gem version comes from `lib/slatedb/version.rb`, not from the tag. Use the
646
+ mise helper to bump, commit, tag, and push in one step (must be on `main` with a
647
+ clean working tree):
648
+
649
+ ```bash
650
+ mise run release:cut 0.4.3
651
+ # or: mise run release:cut 0.4.3 --no-push
652
+ ```
653
+
654
+ That updates `SlateDb::VERSION`, commits `Release v0.4.3`, creates tag `v0.4.3`,
655
+ and pushes the commit + tag (unless `--no-push`). The tag triggers the release
656
+ pipeline.
657
+
658
+ The `release:verify-tag` step fails the build if the tag and
659
+ `SlateDb::VERSION` disagree, so the tag (`v0.4.3`) must match `version.rb`
660
+ (`0.4.3`).
661
+
662
+ To exercise packaging without publishing, trigger a manual build on the
663
+ `slatedb-rb-release` pipeline or set `DRY_RUN=true`.
664
+
533
665
  ## Development
534
666
 
535
667
  After checking out the repo, run:
@@ -11,12 +11,13 @@ name = "slatedb"
11
11
  crate-type = ["cdylib"]
12
12
 
13
13
  [dependencies]
14
- slatedb = "0.10"
14
+ slatedb = "0.15.0"
15
15
  magnus = { version = "0.8.2", features = ["rb-sys"] }
16
- rb-sys = { version = "0.9.123", features = ["stable-api-compiled-fallback"] }
17
- tokio = { version = "1.47.2", features = ["rt-multi-thread", "sync"] }
18
- bytes = "1.11.0"
19
- url = "2.5.7"
20
- once_cell = "1.21.3"
16
+ rb-sys = { version = "0.9.128", features = ["stable-api-compiled-fallback"] }
17
+ tokio = { version = "1.52.3", features = ["rt-multi-thread", "sync"] }
18
+ bytes = "1.11.1"
19
+ serde_json = "1.0.145"
20
+ url = "2.5.8"
21
+ once_cell = "1.21.4"
21
22
  log = "0.4.29"
22
- uuid = "1.19.0"
23
+ uuid = "1.23.1"
@@ -43,10 +43,18 @@ impl Admin {
43
43
  /// # Returns
44
44
  /// JSON string of the manifest, or None if no manifests exist.
45
45
  pub fn read_manifest(&self, id: Option<u64>) -> Result<Option<String>, Error> {
46
- block_on(async { self.inner.read_manifest(id).await }).map_err(|e| {
46
+ let manifest = block_on(async { self.inner.read_manifest(id).await }).map_err(|e| {
47
47
  let ruby = Ruby::get().expect("Ruby runtime not available");
48
48
  Error::new(ruby.exception_runtime_error(), format!("{}", e))
49
- })
49
+ })?;
50
+
51
+ match manifest {
52
+ Some(manifest) => Ok(Some(serde_json::to_string(&manifest).map_err(|e| {
53
+ let ruby = Ruby::get().expect("Ruby runtime not available");
54
+ Error::new(ruby.exception_runtime_error(), format!("{}", e))
55
+ })?)),
56
+ None => Ok(None),
57
+ }
50
58
  }
51
59
 
52
60
  /// List manifests within an optional [start, end) range as JSON.
@@ -65,7 +73,12 @@ impl Admin {
65
73
  (None, None) => 0..u64::MAX,
66
74
  };
67
75
 
68
- block_on(async { self.inner.list_manifests(range).await }).map_err(|e| {
76
+ let manifests = block_on(async { self.inner.list_manifests(range).await }).map_err(|e| {
77
+ let ruby = Ruby::get().expect("Ruby runtime not available");
78
+ Error::new(ruby.exception_runtime_error(), format!("{}", e))
79
+ })?;
80
+
81
+ serde_json::to_string(&manifests).map_err(|e| {
69
82
  let ruby = Ruby::get().expect("Ruby runtime not available");
70
83
  Error::new(ruby.exception_runtime_error(), format!("{}", e))
71
84
  })
@@ -217,6 +230,7 @@ impl Admin {
217
230
  Some(GarbageCollectorDirectoryOptions {
218
231
  interval: default_opts.as_ref().and_then(|o| o.interval),
219
232
  min_age: std::time::Duration::from_millis(ms),
233
+ dry_run: default_opts.as_ref().map(|o| o.dry_run).unwrap_or(false),
220
234
  })
221
235
  } else {
222
236
  default_opts
@@ -230,12 +244,17 @@ impl Admin {
230
244
  default_opts.manifest_options,
231
245
  ),
232
246
  wal_options: make_dir_opts(wal_min_age, min_age, default_opts.wal_options),
247
+ wal_fence_options: default_opts.wal_fence_options,
233
248
  compacted_options: make_dir_opts(
234
249
  compacted_min_age,
235
250
  min_age,
236
251
  default_opts.compacted_options,
237
252
  ),
238
253
  compactions_options: default_opts.compactions_options,
254
+ detach_options: default_opts.detach_options,
255
+ metric_level: default_opts.metric_level,
256
+ boundary_files_enabled: default_opts.boundary_files_enabled,
257
+ object_store_max_retries: default_opts.object_store_max_retries,
239
258
  }
240
259
  };
241
260