slatedb 0.4.2-aarch64-linux-musl

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.
data/README.md ADDED
@@ -0,0 +1,690 @@
1
+ # SlateDB Ruby
2
+
3
+ Ruby bindings for [SlateDB](https://slatedb.io), a cloud-native embedded key-value store built on object storage.
4
+
5
+ [![Build status](https://badge.buildkite.com/a7ae51f3a0bc7809cf66981641ec47b3c70db8cf349a5e462f.svg)](https://buildkite.com/catkins-test/slatedb-rb) [![Gem Version](https://badge.fury.io/rb/slatedb.svg)](https://badge.fury.io/rb/slatedb)
6
+
7
+ ## Production Readiness
8
+
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
+
11
+ ## Installation
12
+
13
+ Add this line to your application's Gemfile:
14
+
15
+ ```ruby
16
+ gem 'slatedb'
17
+ ```
18
+
19
+ And then execute:
20
+
21
+ ```bash
22
+ bundle install
23
+ ```
24
+
25
+ Or install it yourself as:
26
+
27
+ ```bash
28
+ gem install slatedb
29
+ ```
30
+
31
+ Precompiled native gems are published for common Linux and Darwin platforms. Building from source still requires a Rust toolchain.
32
+
33
+ ## Usage
34
+
35
+ ### Basic Operations
36
+
37
+ ```ruby
38
+ require 'slatedb'
39
+
40
+ # Open a database with in-memory storage (for testing)
41
+ db = SlateDb::Database.open("/tmp/mydb")
42
+
43
+ # Store a value
44
+ db.put("hello", "world")
45
+
46
+ # Retrieve a value
47
+ value = db.get("hello") # => "world"
48
+
49
+ # Delete a value
50
+ db.delete("hello")
51
+
52
+ # Close the database
53
+ db.close
54
+ ```
55
+
56
+ ### Block Form (Recommended)
57
+
58
+ The block form automatically closes the database when the block exits:
59
+
60
+ ```ruby
61
+ SlateDb::Database.open("/tmp/mydb") do |db|
62
+ db.put("key", "value")
63
+ db.get("key") # => "value"
64
+ end # automatically closed
65
+ ```
66
+
67
+ ### Persistent Storage
68
+
69
+ For persistent storage, provide an object store URL:
70
+
71
+ ```ruby
72
+ # Local filesystem
73
+ SlateDb::Database.open("/tmp/mydb", url: "file:///tmp/mydb") do |db|
74
+ db.put("key", "value")
75
+ end
76
+
77
+ # S3 (requires AWS credentials)
78
+ SlateDb::Database.open("mydb", url: "s3://mybucket/path") do |db|
79
+ db.put("key", "value")
80
+ end
81
+
82
+ # Azure Blob Storage
83
+ SlateDb::Database.open("mydb", url: "az://container/path") do |db|
84
+ db.put("key", "value")
85
+ end
86
+
87
+ # Google Cloud Storage
88
+ SlateDb::Database.open("mydb", url: "gs://bucket/path") do |db|
89
+ db.put("key", "value")
90
+ end
91
+ ```
92
+
93
+ #### Cloud Storage Credentials
94
+
95
+ SlateDB uses the [object_store](https://docs.rs/object_store) crate, which automatically discovers credentials from standard environment variables and configuration files:
96
+
97
+ **AWS S3:**
98
+ - Environment variables: `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_SESSION_TOKEN`, `AWS_REGION`
99
+ - Credential files: `~/.aws/credentials`, `~/.aws/config`
100
+ - IAM roles (when running on EC2/ECS/EKS)
101
+ - Web identity tokens (for IRSA on EKS)
102
+
103
+ **Azure Blob Storage:**
104
+ - Environment variables: `AZURE_STORAGE_ACCOUNT_NAME`, `AZURE_STORAGE_ACCOUNT_KEY`, `AZURE_STORAGE_SAS_TOKEN`
105
+ - Azure CLI credentials: `az login`
106
+ - Managed Identity (when running on Azure)
107
+
108
+ **Google Cloud Storage:**
109
+ - Environment variables: `GOOGLE_SERVICE_ACCOUNT`, `GOOGLE_SERVICE_ACCOUNT_PATH`, `GOOGLE_SERVICE_ACCOUNT_KEY`
110
+ - Application Default Credentials: `gcloud auth application-default login`
111
+ - Service account key file: `GOOGLE_APPLICATION_CREDENTIALS=/path/to/key.json`
112
+
113
+ Example with explicit AWS credentials:
114
+
115
+ ```ruby
116
+ # Set credentials via environment
117
+ ENV['AWS_ACCESS_KEY_ID'] = 'your-access-key'
118
+ ENV['AWS_SECRET_ACCESS_KEY'] = 'your-secret-key'
119
+ ENV['AWS_REGION'] = 'us-east-1'
120
+
121
+ SlateDb::Database.open("mydb", url: "s3://mybucket/path") do |db|
122
+ db.put("key", "value")
123
+ end
124
+ ```
125
+
126
+ ### Options
127
+
128
+ #### Put Options
129
+
130
+ ```ruby
131
+ # Set TTL (time-to-live) in milliseconds
132
+ db.put("key", "value", ttl: 60_000) # expires in 60 seconds
133
+
134
+ # Don't wait for durability
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)
164
+ ```
165
+
166
+ #### Get Options
167
+
168
+ ```ruby
169
+ # Filter by durability level
170
+ db.get("key", durability_filter: "memory")
171
+ db.get("key", durability_filter: "remote")
172
+
173
+ # Include uncommitted data
174
+ db.get("key", dirty: true)
175
+ ```
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
+
200
+ #### Delete Options
201
+
202
+ ```ruby
203
+ # Don't wait for durability
204
+ db.delete("key", await_durable: false)
205
+
206
+ # Supply an explicit sequence number (SlateDB >= 0.13.0)
207
+ db.delete("key", seqnum: 42)
208
+ ```
209
+
210
+ ### Scanning
211
+
212
+ Iterate over key ranges using the `scan` method:
213
+
214
+ ```ruby
215
+ # Scan all keys from "a" onwards
216
+ db.scan("a").each do |key, value|
217
+ puts "#{key}: #{value}"
218
+ end
219
+
220
+ # Scan a specific range [start, end)
221
+ db.scan("a", "z").each do |key, value|
222
+ puts "#{key}: #{value}"
223
+ end
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
+
230
+ # Use Enumerable methods
231
+ keys = db.scan("user:").map { |k, v| k }
232
+ users = db.scan("user:").select { |k, v| v.include?("active") }
233
+
234
+ # Convert to array
235
+ all_entries = db.scan("").to_a
236
+ ```
237
+
238
+ #### Prefix Scanning
239
+
240
+ Scan all keys with a given prefix using `scan_prefix`:
241
+
242
+ ```ruby
243
+ # Scan all keys starting with "user:"
244
+ db.scan_prefix("user:").each do |key, value|
245
+ puts "#{key}: #{value}"
246
+ end
247
+
248
+ # Block form
249
+ db.scan_prefix("order:") do |key, value|
250
+ puts "#{key}: #{value}"
251
+ end
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
+
258
+ # Works with transactions, snapshots, and readers too
259
+ db.transaction do |txn|
260
+ txn.scan_prefix("item:").each do |k, v|
261
+ puts "#{k}: #{v}"
262
+ end
263
+ end
264
+ ```
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
+
287
+ ### Merge Operations
288
+
289
+ Merge operations allow you to combine values without reading them first, useful for counters, append-only logs, and similar patterns:
290
+
291
+ ```ruby
292
+ # Open with a built-in merge operator
293
+ SlateDb::Database.open("/tmp/mydb", merge_operator: :string_concat) do |db|
294
+ # Merge appends to existing values (or creates if key doesn't exist)
295
+ db.merge("log", "line1\n")
296
+ db.merge("log", "line2\n")
297
+ db.merge("log", "line3\n")
298
+
299
+ db.get("log") # => "line1\nline2\nline3\n"
300
+ end
301
+
302
+ # Merge with options
303
+ db.merge("key", "value", ttl: 60_000, await_durable: false)
304
+
305
+ # Works in transactions and batches
306
+ db.transaction do |txn|
307
+ txn.merge("counter", "1")
308
+ end
309
+
310
+ db.batch do |b|
311
+ b.merge("key", "a")
312
+ .merge("key", "b")
313
+ end
314
+ ```
315
+
316
+ #### Custom Merge Operators
317
+
318
+ You can provide a Ruby Proc/lambda as a custom merge operator:
319
+
320
+ ```ruby
321
+ # Counter merge operator (adds numbers)
322
+ counter_merge = ->(key, existing, new_value) {
323
+ existing_num = existing ? existing.to_i : 0
324
+ (existing_num + new_value.to_i).to_s
325
+ }
326
+
327
+ SlateDb::Database.open("/tmp/mydb", merge_operator: counter_merge) do |db|
328
+ db.merge("visits", "1")
329
+ db.merge("visits", "1")
330
+ db.merge("visits", "1")
331
+
332
+ db.get("visits") # => "3"
333
+ end
334
+
335
+ # Max value merge operator
336
+ max_merge = ->(key, existing, new_value) {
337
+ existing_num = existing ? existing.to_i : 0
338
+ new_num = new_value.to_i
339
+ [existing_num, new_num].max.to_s
340
+ }
341
+
342
+ SlateDb::Database.open("/tmp/mydb", merge_operator: max_merge) do |db|
343
+ db.merge("high_score", "100")
344
+ db.merge("high_score", "250")
345
+ db.merge("high_score", "150")
346
+
347
+ db.get("high_score") # => "250"
348
+ end
349
+ ```
350
+
351
+ The proc receives three arguments:
352
+ - `key` - The key being merged
353
+ - `existing` - The existing value (nil if no value exists)
354
+ - `new_value` - The new merge operand
355
+
356
+ **Note:** Custom Proc merge operators work best with direct `db.merge()` calls. When used with transactions or batches, some merge operations may be processed on background threads and fall back to string concatenation.
357
+
358
+ #### Available Merge Operators
359
+
360
+ - `:string_concat` (or `:concat`) - Concatenates byte values (built-in)
361
+ - Any `Proc` or `lambda` - Custom merge logic
362
+
363
+ ### Write Batches
364
+
365
+ Perform multiple writes atomically:
366
+
367
+ ```ruby
368
+ # Create a batch manually
369
+ batch = SlateDb::WriteBatch.new
370
+ batch.put("key1", "value1")
371
+ batch.put("key2", "value2", ttl: 60_000)
372
+ batch.delete("old_key")
373
+ db.write(batch)
374
+
375
+ # Or use the block helper
376
+ db.batch do |b|
377
+ b.put("key1", "value1")
378
+ b.put("key2", "value2")
379
+ b.delete("old_key")
380
+ end
381
+ ```
382
+
383
+ ### Transactions
384
+
385
+ ACID transactions with snapshot or serializable isolation:
386
+
387
+ ```ruby
388
+ # Block form (recommended) - auto-commits on success, rolls back on exception
389
+ db.transaction do |txn|
390
+ balance = txn.get("balance").to_i
391
+ txn.put("balance", (balance - 100).to_s)
392
+ txn.put("withdrawal", "100")
393
+ end
394
+
395
+ # With serializable isolation for strict consistency
396
+ db.transaction(isolation: :serializable) do |txn|
397
+ counter = txn.get("counter").to_i
398
+ txn.put("counter", (counter + 1).to_s)
399
+ end
400
+
401
+ # Manual transaction management
402
+ txn = db.begin_transaction(isolation: :snapshot)
403
+ txn.put("key", "value")
404
+ txn.commit # or txn.rollback
405
+ ```
406
+
407
+ Transaction operations:
408
+
409
+ ```ruby
410
+ db.transaction do |txn|
411
+ # Read
412
+ value = txn.get("key")
413
+
414
+ # Write
415
+ txn.put("key", "value")
416
+ txn.put("expiring", "data", ttl: 30_000)
417
+
418
+ # Delete
419
+ txn.delete("old_key")
420
+
421
+ # Scan
422
+ txn.scan("prefix:").each do |k, v|
423
+ puts "#{k}: #{v}"
424
+ end
425
+
426
+ # Scan with prefix
427
+ txn.scan_prefix("user:").each do |k, v|
428
+ puts "#{k}: #{v}"
429
+ end
430
+ end
431
+ ```
432
+
433
+ #### Explicit Read Tracking
434
+
435
+ In serializable transactions, use `mark_read` to explicitly track keys for conflict detection without actually reading them:
436
+
437
+ ```ruby
438
+ db.transaction(isolation: :serializable) do |txn|
439
+ # Mark keys as read for conflict detection
440
+ txn.mark_read(["key1", "key2", "key3"])
441
+
442
+ # Now if another transaction modifies key1/key2/key3,
443
+ # this transaction will fail on commit
444
+ txn.put("result", "computed_value")
445
+ end
446
+ ```
447
+
448
+ ### Checkpoints
449
+
450
+ Create durable checkpoints for backup or read replica purposes:
451
+
452
+ ```ruby
453
+ SlateDb::Database.open("/tmp/mydb", url: "file:///tmp/mydb") do |db|
454
+ db.put("key", "value")
455
+ db.flush
456
+
457
+ # Create a checkpoint
458
+ checkpoint = db.create_checkpoint
459
+ puts "Checkpoint ID: #{checkpoint[:id]}"
460
+ puts "Manifest ID: #{checkpoint[:manifest_id]}"
461
+
462
+ # Create a named checkpoint with lifetime
463
+ checkpoint = db.create_checkpoint(
464
+ name: "before-migration",
465
+ lifetime: 3_600_000 # 1 hour in milliseconds
466
+ )
467
+ end
468
+ ```
469
+
470
+ ### Snapshots
471
+
472
+ Point-in-time consistent reads:
473
+
474
+ ```ruby
475
+ # Block form (recommended)
476
+ db.snapshot do |snap|
477
+ # All reads see the same consistent state
478
+ value1 = snap.get("key1")
479
+ value2 = snap.get("key2")
480
+
481
+ snap.scan("prefix:").each do |k, v|
482
+ puts "#{k}: #{v}"
483
+ end
484
+ end # automatically closed
485
+
486
+ # Manual management
487
+ snap = db.snapshot
488
+ value = snap.get("key")
489
+ snap.close
490
+ ```
491
+
492
+ ### Reader (Read-Only Access)
493
+
494
+ Open a database in read-only mode, useful for replicas:
495
+
496
+ ```ruby
497
+ # Basic read-only access
498
+ SlateDb::Reader.open("/tmp/mydb", url: "s3://bucket/path") do |reader|
499
+ value = reader.get("key")
500
+
501
+ reader.scan("prefix:").each do |k, v|
502
+ puts "#{k}: #{v}"
503
+ end
504
+ end
505
+
506
+ # Open at a specific checkpoint (pins the reader to that checkpoint's state)
507
+ SlateDb::Reader.open("/tmp/mydb",
508
+ url: "s3://bucket/path",
509
+ checkpoint_id: "uuid-here") do |reader|
510
+ reader.get("key")
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
533
+ ```
534
+
535
+ ### Admin Operations
536
+
537
+ Administrative operations for database management:
538
+
539
+ ```ruby
540
+ admin = SlateDb::Admin.new("/tmp/mydb", url: "s3://bucket/path")
541
+
542
+ # Manifests
543
+ json = admin.read_manifest # Latest manifest as JSON
544
+ json = admin.read_manifest(123) # Specific manifest by ID
545
+ json = admin.list_manifests # List all manifests
546
+ json = admin.list_manifests(start: 1, end_id: 10) # Range query
547
+
548
+ # Checkpoints
549
+ result = admin.create_checkpoint(name: "backup-2024")
550
+ # => { id: "uuid-string", manifest_id: 7 }
551
+
552
+ checkpoints = admin.list_checkpoints
553
+ checkpoints = admin.list_checkpoints(name: "backup") # Filter by name
554
+
555
+ admin.refresh_checkpoint("uuid", lifetime: 3600_000) # Extend lifetime
556
+ admin.delete_checkpoint("uuid")
557
+
558
+ # Garbage Collection
559
+ admin.run_gc # Run with default settings
560
+ admin.run_gc(min_age: 3600_000) # Set min age for all directories (1 hour)
561
+ admin.run_gc(manifest_min_age: 86400_000) # Custom age for manifest (1 day)
562
+ admin.run_gc(wal_min_age: 60_000) # Custom age for WAL (1 minute)
563
+ admin.run_gc(compacted_min_age: 60_000) # Custom age for compacted (1 minute)
564
+ ```
565
+
566
+ ### Flushing
567
+
568
+ Ensure all writes are persisted:
569
+
570
+ ```ruby
571
+ db.put("key", "value")
572
+ db.flush
573
+ ```
574
+
575
+ ## Thread Safety
576
+
577
+ **SlateDB is fully thread-safe and optimized for concurrent access.**
578
+
579
+ - The `Database` class can be safely shared across multiple Ruby threads
580
+ - All operations (get, put, delete, scan, transactions) are thread-safe
581
+ - The Ruby bindings release the Global VM Lock (GVL) during I/O operations, allowing other Ruby threads to run concurrently
582
+ - Perfect for use with multi-threaded Ruby applications like Puma, Sidekiq, and concurrent test suites
583
+
584
+ ```ruby
585
+ db = SlateDb::Database.open("/tmp/mydb")
586
+
587
+ # Safe to use from multiple threads
588
+ threads = 10.times.map do |i|
589
+ Thread.new do
590
+ db.put("key-#{i}", "value-#{i}")
591
+ db.get("key-#{i}")
592
+ end
593
+ end
594
+
595
+ threads.each(&:join)
596
+ ```
597
+
598
+ **Implementation details:**
599
+ - The underlying SlateDB library uses `Arc` (atomic reference counting) and `RwLock` for internal state management
600
+ - I/O operations release the Ruby GVL using `rb_thread_call_without_gvl`, preventing blocking other threads
601
+ - A shared Tokio multi-threaded runtime handles all async operations efficiently
602
+
603
+ ## Error Handling
604
+
605
+ SlateDB defines several exception classes:
606
+
607
+ ```ruby
608
+ begin
609
+ db.put("", "value") # empty key
610
+ rescue SlateDb::InvalidArgumentError => e
611
+ puts "Invalid argument: #{e.message}"
612
+ rescue SlateDb::TransactionError => e
613
+ puts "Transaction conflict: #{e.message}"
614
+ rescue SlateDb::Error => e
615
+ puts "SlateDB error: #{e.message}"
616
+ end
617
+ ```
618
+
619
+ Exception hierarchy:
620
+
621
+ - `SlateDb::Error` - Base class (inherits from `StandardError`)
622
+ - `SlateDb::TransactionError` - Transaction conflicts
623
+ - `SlateDb::ClosedError` - Database has been closed
624
+ - `SlateDb::UnavailableError` - Storage/network unavailable
625
+ - `SlateDb::InvalidArgumentError` - Invalid arguments
626
+ - `SlateDb::DataError` - Data corruption or format errors
627
+ - `SlateDb::InternalError` - Internal errors
628
+
629
+ ## Requirements
630
+
631
+ - Ruby 3.3+
632
+ - Rust toolchain (for building from source)
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
+
665
+ ## Development
666
+
667
+ After checking out the repo, run:
668
+
669
+ ```bash
670
+ bundle install
671
+ bundle exec rake compile
672
+ bundle exec rake spec
673
+ ```
674
+
675
+ To run specific tests:
676
+
677
+ ```bash
678
+ bundle exec rspec spec/database_spec.rb
679
+ bundle exec rspec spec/transaction_spec.rb
680
+ ```
681
+
682
+ ## Contributing
683
+
684
+ Bug reports and pull requests are welcome on GitHub at https://github.com/catkins/slatedb-rb.
685
+
686
+ Also, find me on the [SlateDB Discord Server](https://discord.gg/mHYmGy5MgA).
687
+
688
+ ## License
689
+
690
+ Apache-2.0
Binary file
Binary file
Binary file