xitdb 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.
Files changed (66) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +28 -0
  3. data/LICENSE +21 -0
  4. data/README.md +131 -0
  5. data/docs/benchmarks.md +135 -0
  6. data/docs/core.md +123 -0
  7. data/docs/portable.md +99 -0
  8. data/docs/releasing.md +37 -0
  9. data/docs/ruby-profile-v1.md +226 -0
  10. data/docs/ruby.md +61 -0
  11. data/examples/indexed_users.rb +23 -0
  12. data/examples/portable.rb +62 -0
  13. data/examples/ruby.rb +56 -0
  14. data/lib/xitdb/context.rb +360 -0
  15. data/lib/xitdb/conversion.rb +222 -0
  16. data/lib/xitdb/core/array_list.rb +282 -0
  17. data/lib/xitdb/core/b_tree.rb +546 -0
  18. data/lib/xitdb/core/bytes.rb +181 -0
  19. data/lib/xitdb/core/compaction.rb +172 -0
  20. data/lib/xitdb/core/crc64.rb +22 -0
  21. data/lib/xitdb/core/cursor.rb +201 -0
  22. data/lib/xitdb/core/database.rb +359 -0
  23. data/lib/xitdb/core/file_store.rb +194 -0
  24. data/lib/xitdb/core/format.rb +148 -0
  25. data/lib/xitdb/core/hash_map.rb +371 -0
  26. data/lib/xitdb/core/hasher.rb +49 -0
  27. data/lib/xitdb/core/linked_array_list.rb +85 -0
  28. data/lib/xitdb/core/memory_store.rb +277 -0
  29. data/lib/xitdb/core/offset_map.rb +85 -0
  30. data/lib/xitdb/core/replication.rb +242 -0
  31. data/lib/xitdb/core/sorted_map.rb +361 -0
  32. data/lib/xitdb/core/transaction.rb +159 -0
  33. data/lib/xitdb/core.rb +17 -0
  34. data/lib/xitdb/database.rb +332 -0
  35. data/lib/xitdb/declarations.rb +73 -0
  36. data/lib/xitdb/errors.rb +84 -0
  37. data/lib/xitdb/history.rb +58 -0
  38. data/lib/xitdb/input_guard.rb +39 -0
  39. data/lib/xitdb/profiles/portable.rb +407 -0
  40. data/lib/xitdb/profiles/ruby.rb +173 -0
  41. data/lib/xitdb/revision.rb +42 -0
  42. data/lib/xitdb/ruby/codec.rb +123 -0
  43. data/lib/xitdb/ruby/containers.rb +495 -0
  44. data/lib/xitdb/ruby/key_encoding.rb +60 -0
  45. data/lib/xitdb/ruby/ordered_keys.rb +87 -0
  46. data/lib/xitdb/ruby/scalars.rb +296 -0
  47. data/lib/xitdb/ruby/wire.rb +146 -0
  48. data/lib/xitdb/ruby.rb +46 -0
  49. data/lib/xitdb/snapshot.rb +13 -0
  50. data/lib/xitdb/streams.rb +148 -0
  51. data/lib/xitdb/value_walker.rb +192 -0
  52. data/lib/xitdb/version.rb +5 -0
  53. data/lib/xitdb/views/array.rb +303 -0
  54. data/lib/xitdb/views/base.rb +271 -0
  55. data/lib/xitdb/views/linked_array.rb +18 -0
  56. data/lib/xitdb/views/map.rb +162 -0
  57. data/lib/xitdb/views/set.rb +41 -0
  58. data/lib/xitdb/views/sorted.rb +77 -0
  59. data/lib/xitdb.rb +20 -0
  60. data/sig/base.rbs +76 -0
  61. data/sig/core.rbs +697 -0
  62. data/sig/manifest.yaml +10 -0
  63. data/sig/native.rbs +118 -0
  64. data/sig/public.rbs +427 -0
  65. data/sig/ruby.rbs +337 -0
  66. metadata +137 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 7ff2dced063b5fa4980d7e5f30c71d016a338b6986980b6a8003156b3b23c2d0
4
+ data.tar.gz: aa3fac0c1fbed621cbbe30bf059b2ea038a8fcd4660052d02c5a0d17648b7c78
5
+ SHA512:
6
+ metadata.gz: cdd84a81d4ebe0faa251f986f58eaacabdda3a1fa24968c8c8147ae1d75d3402e796854d6441f963d5a5310fe692153b4e898713810cac41a13e0e0686651b4c
7
+ data.tar.gz: 693ded1aa3dd47139bf04f99f139e5f5396810ab80b0038d911f190368b3c086d3102f26fdcaba7a95d5d85176fe69e192e9e95755a02378453eeab846d98368
data/CHANGELOG.md ADDED
@@ -0,0 +1,28 @@
1
+ # Changelog
2
+
3
+ ## Unreleased
4
+
5
+ ## 0.1.0 - 2026-11-05
6
+
7
+ Initial release of the `xitdb` gem. Requires MRI 3.3 or later.
8
+
9
+ ### Added
10
+
11
+ - Pure Ruby storage for upstream xitdb maps, sets, counted collections, array lists, linked array lists, and sorted collections.
12
+ - Portable `sha1` and `sha2` files with the full signed and unsigned upstream integer ranges.
13
+ - Transactions with coordinated writers, lock timeouts, rollback, commit outcome reporting, and synchronized file publication.
14
+ - Stable snapshots, incremental collection views, history, revision tokens, and revert operations.
15
+ - Sorted range queries, rank lookup, and pagination through storage indexes.
16
+ - Byte streams with bounded transfers, compaction, and byte-preserving full and incremental replication.
17
+ - The `rb01` Ruby profile with typed keys, insertion-ordered maps and sets, and typed sorted indexes.
18
+ - Exact encodings for arbitrary-size integers, booleans, Symbols, String encodings, temporal values, BigDecimal, and Rational.
19
+ - Per-handle custom value codecs and immutable preservation of unknown encoded values.
20
+ - Explicit profile conversion and context-level copying between databases.
21
+ - RBS signatures, packaged examples, and guides for portable storage, Ruby values, the core API, and the Ruby wire format.
22
+ - Package installation tests, reciprocal checks against pinned Go, TypeScript, Java, and Zig references, and Linux memory workloads.
23
+
24
+ ### Limitations
25
+
26
+ - Portable keys and set members must be Strings. Booleans, Symbols, and native temporal values require the Ruby profile.
27
+ - Conversion can duplicate data shared across histories, increasing runtime and output size. See [the measurements](docs/benchmarks.md).
28
+ - The public API can change before version 1.0.
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (Expat)
2
+
3
+ Copyright (c) contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,131 @@
1
+ # xitdb for Ruby
2
+
3
+ [`xitdb`](https://rubygems.org/gems/xitdb) provides transactional collections in one file, implemented in Ruby. It requires MRI 3.3 or later.
4
+ It requires no compiler or database service. Runtime dependencies are `date` and `bigdecimal`.
5
+
6
+ This is a Ruby implementation of [xitdb](https://github.com/xit-vcs/xitdb), a binary format for immutable database structures.
7
+ The repository is named `xitdb-ruby`; the gem and require path are `xitdb`.
8
+
9
+ The portable profile reads and writes upstream xitdb structures. The Ruby profile adds typed keys, ordered maps, and Ruby values.
10
+ Both profiles use the same storage core. Requiring the Ruby extension does not change portable behavior.
11
+
12
+ ## Install
13
+
14
+ Add the gem to your application's bundle:
15
+
16
+ ```sh
17
+ bundle add xitdb
18
+ ```
19
+
20
+ Or install it directly:
21
+
22
+ ```sh
23
+ gem install xitdb
24
+ ```
25
+
26
+ ## Quick start
27
+
28
+ Use String keys and portable values to share files with other xitdb implementations:
29
+
30
+ ```ruby
31
+ require "xitdb"
32
+
33
+ XitDB.open("shared.xit") do |db|
34
+ db.transaction { |root| root["profile"] = { "name" => "Ada", "visits" => 0 } }
35
+ before = db.snapshot
36
+ db.transaction { |root| root["profile"]["visits"] += 1 }
37
+ p before.dig("profile", "visits") # 0
38
+ p db.snapshot.dig("profile", "visits") # 1
39
+ end
40
+ ```
41
+
42
+ Use the Ruby profile for Symbol keys, booleans, arbitrary-size integers, and other Ruby values:
43
+
44
+ ```ruby
45
+ require "xitdb/ruby"
46
+
47
+ XitDB::Ruby.memory do |db|
48
+ db.transaction do |root|
49
+ root[:enabled] = true
50
+ root[:large] = 2**200
51
+ end
52
+ p db.snapshot[:enabled] # true
53
+ end
54
+ ```
55
+
56
+ ## Choose a profile
57
+
58
+ | Entry point | Values and keys | File identifier |
59
+ | --- | --- | --- |
60
+ | `XitDB.open`, `XitDB.memory` | Portable integers, floats, bytes, collections, and String keys | `sha1` by default; `hash: :sha256` selects `sha2` |
61
+ | `XitDB::Ruby.open`, `XitDB::Ruby.memory` | Ruby scalar values, typed keys, and ordered maps and sets | `rb01` |
62
+ | `XitDB::Core::Database` | Raw slots, every upstream collection, and arbitrary roots | Explicit storage configuration |
63
+
64
+ Opening never converts a profile. Public entry points reject a mismatched profile and pre-existing zero-byte files.
65
+ Missing paths are created. Block forms close the handle; callers close handles returned without a block.
66
+
67
+ ## Transactions and limits
68
+
69
+ Write through `db.transaction`. Normal block completion commits; an exception aborts the transaction.
70
+ Snapshots retain their committed values through later writes and remain usable until the database handle closes.
71
+ Transaction views expire when their block ends. Use `to_ruby` to obtain independent Ruby containers.
72
+
73
+ Writers serialize across cooperating threads and processes. Lock timeout raises `XitDB::BusyError` before the transaction block runs.
74
+ File commits synchronize data and root publication. A `XitDB::CommitError` reports whether publication failed, succeeded, or has an unknown outcome.
75
+ See [locking and durability](docs/core.md#locking-and-durability) before implementing retries.
76
+
77
+ Portable maps and sets require String keys or members; portable values exclude booleans and Symbols.
78
+ The Ruby profile supports these types and preserves insertion order in ordinary maps and sets.
79
+ Profile conversion is explicit. Conversion can duplicate shared data across histories; see [measurements and costs](docs/benchmarks.md).
80
+
81
+ Version 0.1.0 is the initial release. The public API can change before 1.0.
82
+
83
+ ## Documentation
84
+
85
+ Read the [portable guide](docs/portable.md), [Ruby guide](docs/ruby.md), and [core guide](docs/core.md).
86
+ The [Ruby wire contract](docs/ruby-profile-v1.md) defines cross-language interpretation of `rb01`.
87
+ Other languages can preserve Ruby-profile bytes without decoding their logical values.
88
+
89
+ ## Run the workflows
90
+
91
+ 1. Run `ruby examples/portable.rb` for nested updates, snapshots, history, streams, compaction, and replication.
92
+ 2. Run `ruby examples/ruby.rb` for native values, codecs, cross-file copying, and explicit conversion.
93
+ 3. Run `ruby examples/indexed_users.rb` for atomic record/index updates and sorted pagination.
94
+
95
+ Examples require the installed package and write temporary files only. Each emits one JSON object.
96
+ From a checkout, run them with `bundle exec ruby -Ilib examples/portable.rb`, substituting the example filename.
97
+
98
+ ## Verify a checkout
99
+
100
+ Run `bundle install` to install the locked development dependencies.
101
+ Run `bundle exec ruby script/ci.rb` to check the checkout.
102
+ It checks Ruby style, library types, and Change Risk Anti-Patterns (CRAP), then inspects the locally built package.
103
+ The tests execute packaged examples from temporary storage and inspect package contents.
104
+ The package check uses an isolated application bundle and local gem resolution. It does not install dependencies from the network.
105
+
106
+ `bundle exec rake` runs the same checks using the locked development dependencies.
107
+ Plain `rake` also works when those versions are installed on Ruby's gem paths.
108
+ RuboCop loads the Minitest, Performance, and Rake plugins. Individual checks are available:
109
+
110
+ | Command | Check |
111
+ | --- | --- |
112
+ | `bundle exec rake rubocop` | Project style rules, with size metrics exempted |
113
+ | `bundle exec rake types` | Steep diagnostics and 100% typed calls in every library file |
114
+ | `bundle exec rake crap` | Fast tests with fresh SimpleCov coverage, then a CRAP limit of 10 |
115
+ | `bundle exec rake test` | Fast tests without the analysis tools |
116
+
117
+ RBS signatures ship in `sig/`. Custom codec results have an unknown type until callers validate their class.
118
+ Type coverage measures checked method calls. It does not prove that arbitrary codec output meets an application's domain constraints.
119
+
120
+ The workflow defines separate `reference` and `linux-memory` jobs alongside Linux MRI 3.3, 3.4, and 4.0 and macOS jobs.
121
+ Configure every job as a required branch check. Workflow definitions alone do not enable repository branch protection.
122
+ Reference tools are mandatory for `ruby script/interop.rb --all`; unavailable tools fail that gate.
123
+ The [measurement guide](docs/benchmarks.md) records bounded-memory evidence and conversion costs.
124
+
125
+ ## Releases
126
+
127
+ See [CHANGELOG.md](CHANGELOG.md) for release changes and [the release guide](docs/releasing.md) for the publishing procedure.
128
+
129
+ ## License
130
+
131
+ The package preserves the [MIT license notice](LICENSE). Upstream xitdb references remain pinned in `test/interop/manifest.json`.
@@ -0,0 +1,135 @@
1
+ # Memory and performance measurements
2
+
3
+ The stress command measures file databases inside an isolated Linux container with a hard memory budget.
4
+ The benchmark command measures fixed workloads without timing pass thresholds.
5
+ Both commands require Docker and the cached `ruby:4.0.9-slim` image.
6
+ They use `--pull=never` and `--network=none` and fail when the required capability is unavailable.
7
+ They install nothing.
8
+
9
+ ## Run the workloads
10
+
11
+ 1. Run `ruby script/stress.rb --limit-mib 256 --data-mib 768`.
12
+ 2. Run `ruby script/benchmark.rb --output pkg/benchmark.json`.
13
+ 3. Run `bundle exec ruby -Ilib:test test/stress/memory_test.rb` for the covering tests.
14
+
15
+ The covering stress test executes the same 768 MiB command and saves its report in `pkg/stress.json`.
16
+ It therefore supplies the command evidence without a second standalone stress run.
17
+ The stress suite is separate from `rake test`.
18
+ Set `XITDB_STRESS_REPORT=PATH` to validate a previously captured report and republish it without starting another full workload.
19
+ This explicit mode checks recorded measurements and the report publication path; it does not provide a fresh Linux execution result.
20
+
21
+ Docker sets the container's memory and combined memory-plus-swap limits to the requested budget.
22
+ The child verifies Linux cgroup v2 `memory.max` and `memory.swap.max` before creating data.
23
+ Swap must be zero.
24
+ The report records these actual limits, `memory.peak`, and `memory.events`.
25
+ The cgroup peak includes kernel charges and file cache, not only Ruby's resident memory.
26
+ The budget applies during fixture creation, operations, and verification.
27
+
28
+ With Docker's systemd cgroup driver, the harness sets `memory.high` to 75% of the hard limit through runc's systemd annotation.
29
+ For the 256 MiB workload, this starts reclamation at 192 MiB while retaining the 256 MiB maximum and zero swap.
30
+ This gives the kernel time to reclaim filesystem cache before the hard limit can trigger an OOM kill.
31
+ The child verifies the configured threshold and records `memory_high_bytes` in its report.
32
+ Docker's cgroupfs driver retains its hard-limit behavior and records `memory_high_bytes` as `"max"`.
33
+ See [the kernel's memory controller guide](https://docs.kernel.org/admin-guide/cgroup-v2.html#usage-guidelines)
34
+ and [runc's systemd properties](https://github.com/opencontainers/runc/blob/main/docs/systemd.md#auxiliary-properties).
35
+
36
+ Each command checks available scratch space before starting.
37
+ Datasets use one task-owned temporary directory.
38
+ Profiles run sequentially, and each destination closes and unlinks before the next maintenance operation.
39
+ The parent removes the directory on normal completion or failure.
40
+ If the child fails, the parent removes its own container and reports the failure.
41
+ It does not use a weaker memory limit or skip a workload.
42
+
43
+ ## Stress workload
44
+
45
+ Each profile stores 8,192 deterministic sorted rows and a 768 MiB byte value.
46
+ The generator writes one reusable 64 KiB chunk and retains one digest.
47
+ Two histories share the byte value and sorted rows, with different revision scalars.
48
+ `dataset_bytes` comes from the committed source file's actual size.
49
+ `payload_bytes` records the complete generated byte count.
50
+
51
+ Lookup checks present and absent keys.
52
+ Pagination checks every sorted row through pages of at most 37 entries.
53
+ Streaming checks the complete byte count and SHA-256 digest through public `open_bytes`.
54
+ Conversion copies both histories to the other profile.
55
+ Compaction retains the latest history in the original profile.
56
+ Both outputs receive lookup, pagination, history, revision, byte-count, and digest checks.
57
+ Verification times are separate from conversion and compaction times.
58
+ Destination file sizes record the output cost of each operation.
59
+
60
+ Converted Ruby Strings can use tagged envelopes that public `open_bytes` does not accept.
61
+ Their verification uses the existing profile byte descriptor and bounded range reads.
62
+ Original explicit byte streams use public `open_bytes` in both profiles.
63
+ Source reads exceeding 64 KiB fail throughout measured operations and destination verification.
64
+ The workload never decodes the large scalar or materializes a database root.
65
+
66
+ The offset-cache observer reads `OffsetMap#peak_cache_size` when each map closes.
67
+ It retains one maximum integer, without retaining maps, entries, or per-access records.
68
+ The covering test requires a positive observed peak no greater than 16,384 entries.
69
+
70
+ ## Benchmark fields and interpretation
71
+
72
+ Each benchmark workload uses a 1 MiB payload, 256 sorted rows, and two histories.
73
+ Portable and Ruby profiles run against file and memory sources.
74
+ Every operation has three elapsed samples and three allocation counts.
75
+ The output records minimum, median, and maximum elapsed seconds, Ruby version, source sizes, destination sizes, and fixture times.
76
+ Allocation counts use `GC.stat(:total_allocated_objects)` differences.
77
+ They count Ruby objects, not bytes or retained memory.
78
+ Maintenance verification runs outside the operation's elapsed and allocation sample.
79
+
80
+ File workloads separate freshly reopened handles from warm handles.
81
+ Memory workloads separate newly constructed handles from warm handles.
82
+ A warm handle receives one complete unmeasured workload before sampling.
83
+ Each memory first-handle sample receives a separately generated fixture.
84
+ File reopen samples reuse the source file and reopen its database handle.
85
+ All states use identical logical parameters and checks.
86
+
87
+ Operating-system cache control is unavailable and recorded as `os_cache_control: unavailable`.
88
+ Reopening a file does not establish a cold filesystem cache.
89
+ The output makes no cold-cache timing claim.
90
+ The samples describe one run, without comparisons to other implementations or portable performance guarantees.
91
+
92
+ ## Shared-history characterization
93
+
94
+ The benchmark also measures 25 shared histories in each profile.
95
+ One fixture shares a 128-element array.
96
+ Another retains a depth-600 array chain and a 64-element array in every history.
97
+ Fixture creation, conversion, and successful verification have separate timings.
98
+ Each record includes history length, source size, destination size, and their ratio.
99
+
100
+ Conversion has a ten-second diagnostic timeout for these fixtures.
101
+ A timeout records elapsed time through abort cleanup and the actual remaining destination size.
102
+ It has status `timeout`, no verification sample, and no successful conversion sample.
103
+ The cutoff bounds this diagnostic; it is not a performance pass threshold.
104
+ Conversion copies shared logical nodes separately, so retained histories can increase runtime and output size substantially.
105
+
106
+ ## Observed run
107
+
108
+ An aarch64 Linux run used MRI 4.0.9 and `ruby:4.0.9-slim` on November 5, 2026.
109
+ The 768 MiB workload completed in both profiles under a 268,435,456-byte cgroup limit with zero swap.
110
+ The observed cgroup peak equaled that limit; OOM and OOM-kill event counts were zero.
111
+ Both offset caches peaked at 16,384 entries. Independent inspection directly observed disk spill for the Ruby profile.
112
+ Other validation jobs ran concurrently, and operating-system cache state was uncontrolled.
113
+ These measurements establish this workload's completion and output cost.
114
+
115
+ | Source profile | Source bytes | Fixture seconds | Conversion seconds | Conversion output bytes | Compaction seconds | Compaction output bytes |
116
+ | --- | ---: | ---: | ---: | ---: | ---: | ---: |
117
+ | Portable | 818,396,326 | 39.962 | 53.047 | 1,644,815,197 | 8.370 | 805,827,485 |
118
+ | Ruby | 833,361,937 | 130.068 | 32.251 | 1,633,809,495 | 8.866 | 807,393,651 |
119
+
120
+ Conversion preserved both histories and independently copied their shared payloads.
121
+ Compaction preserved one latest history.
122
+ Conversion verification took 6.030 and 4.601 seconds; compaction verification took 4.636 and 5.970 seconds, respectively.
123
+
124
+ The separate 1 MiB benchmark peaked at 197,292,032 cgroup bytes and recorded zero OOM events.
125
+ Its shared-history results were:
126
+
127
+ | Source profile | Depth | Fixture seconds | Source bytes | Conversion result | Destination bytes |
128
+ | --- | ---: | ---: | ---: | --- | ---: |
129
+ | Portable | 0 | 0.069 | 48,219 | Completed in 1.303 s | 1,560,437 |
130
+ | Ruby | 0 | 0.148 | 100,661 | Completed in 1.030 s | 941,187 |
131
+ | Portable | 600 | 0.328 | 220,945 | Timed out after 10.019 s including cleanup | 0 |
132
+ | Ruby | 600 | 1.391 | 648,584 | Timed out after 10.018 s including cleanup | 0 |
133
+
134
+ The shallow conversions expanded output to approximately 32.36 and 9.35 times source size.
135
+ The deep cases provide interrupted diagnostic results, not completed conversion timings.
data/docs/core.md ADDED
@@ -0,0 +1,123 @@
1
+ # Storage core and maintenance
2
+
3
+ `require "xitdb/core"` exposes raw upstream storage. Public profile handles also expose `db.core`.
4
+ Core accepts arbitrary valid root layouts; public profiles require a history array containing logical root maps.
5
+ Unknown format tags remain uninterpreted bytes. Raw graphs can contain sharing and cycles that public materialization rejects.
6
+
7
+ ## Raw collections
8
+
9
+ ```ruby
10
+ require "xitdb/core"
11
+
12
+ db = XitDB::Core::Database.memory
13
+ begin
14
+ db.transaction do |root|
15
+ list = XitDB::Core::ArrayList.new(root)
16
+ list.append(XitDB::Core::Slot.uint(42))
17
+ end
18
+ list = XitDB::Core::ArrayList.new(db.root_cursor)
19
+ p list.cursor_at(0).slot.value # 42
20
+ ensure
21
+ db.close
22
+ end
23
+ ```
24
+
25
+ Core constructors include `ArrayList`, `LinkedArrayList`, `HashMap`, and `SortedMap`.
26
+ `HashMap` supports counted and set variants; `SortedMap` supports set mode.
27
+ Callers provide raw digest bytes to hash collections and raw key bytes to sorted collections.
28
+ Slots contain a tag, full flag, and unsigned 64-bit payload. Use `db.bytes(data, format_tag:)` during a transaction for byte slots.
29
+ Core cursor ownership and transaction-local references are lower-level contracts; public views provide value capture and operation rollback.
30
+ Core empty stores may initialize. Public opening rejects pre-existing zero-byte files.
31
+
32
+ ## Conversion and copying
33
+
34
+ `db.convert(to:, profile:, history: :all, transform: nil)` returns an open writable destination handle owned by the caller.
35
+ Always close that handle. Destinations must be missing or zero bytes, including for empty sources.
36
+ `history:` accepts only `:all` and `:latest`. Empty sources retain zero history entries.
37
+ Same-profile symbols retain source configuration; cross-profile symbols select defaults.
38
+ Exact configured `XitDB::Profiles::Portable` and `XitDB::Profiles::Ruby` instances create fresh target profiles and registries.
39
+
40
+ ```ruby
41
+ require "xitdb/ruby"
42
+
43
+ XitDB::Ruby.memory do |db|
44
+ db.transaction { |root| root[:enabled] = true }
45
+ portable = db.convert(to: "converted.xit", profile: :portable, history: :latest,
46
+ transform: ->(value:, path:, role:) { role == :key ? value.to_s : (value == true ? 1 : value) })
47
+ begin
48
+ p portable.snapshot["enabled"] # 1
49
+ ensure
50
+ portable.close
51
+ end
52
+ end
53
+ ```
54
+
55
+ Transforms run once for each source scalar or logical key. Keywords are `value:`, `path:`, and `role:` (`:key` or `:value`).
56
+ Paths are immutable and relative to the copied source root. Returned products are encoded without recursively applying the transform.
57
+ Ruby-only keys, booleans, native numeric sorted keys, and unknown custom codecs need explicit portable representations.
58
+ Collapsing distinct keys raises `KeyCollisionError`. Opaque portable `rb` payloads also require an explicit transform for Ruby targets.
59
+ Sorted kinds remain sorted. Portable-to-Ruby sorted keys use the String kind.
60
+
61
+ Inside a destination transaction, use `root.context.copy_from(source_view, at: ["parent", "copy"], transform: nil)`.
62
+ It returns the installed destination view. `at:` requires a full Array path with existing parents.
63
+ `at: []` explicitly replaces the root with a Map or sorted Map. Previous child views expire; the transaction root remains usable.
64
+ No view-level copy wrapper exists. Direct foreign-view assignment remains an error.
65
+
66
+ Conversion traverses roots and histories incrementally. Transform-free Strings and opaque bytes stream in 64 KiB chunks.
67
+ A transform may allocate its complete scalar input. Shared data is copied independently across histories.
68
+ The [measurements](benchmarks.md) record 25-history shallow conversions expanding to 32.36 and 9.35 times source size.
69
+ Depth-600 repeated-history diagnostics exceeded ten seconds and were interrupted; those are not completed conversion timings.
70
+ Compaction retains only the latest root and can preserve physical sharing. It is separate from logical conversion.
71
+
72
+ ## Replication
73
+
74
+ Run [the portable example](../examples/portable.rb) for a complete bootstrap and incremental update.
75
+ Use binary caller-owned IO. Transport objects stay open and input consumes only the declared length.
76
+
77
+ 1. Capture `finish = source.replica_state.committed_size` from a Core handle.
78
+ 2. Export `source.read_committed(0, finish, io)`, then rewind the transport.
79
+ 3. Bootstrap with `Core::Database.copy_replica(to: missing_path, source_io: io, length: finish)`.
80
+ 4. Negotiate updates with `start = source.replication_start(replica.replica_state)`.
81
+ 5. For positive start, export through a captured finish and call `replica.apply_replicated(start, io, finish - start)`.
82
+
83
+ `ReplicaState` is immutable with `committed_size` and `checksum`. `replication_start` returns the matched size or zero.
84
+ `read_committed` and `apply_replicated` return nil. `copy_replica` returns a separate validated open writable Core handle; its caller closes it.
85
+ A zero negotiation requires a full copy to another missing or zero-byte path. `apply_replicated(0, ...)` raises `ArgumentError`.
86
+ Application code controls replacement and reader reopening. No API renames a staged file over a live replica.
87
+ Replication preserves bytes and profile metadata. It does not convert profiles or merge independent writers.
88
+
89
+ Known incremental aborts restore the previous size. Known staging aborts attempt to restore zero, subject to storage cleanup success.
90
+ Staging synchronization failure raises `CommitError` with outcome `:unknown` and retains on-disk output, which may be partial.
91
+ Do not assume that an uncertain staged image is complete or valid. Transport failures preserve their original exception identity.
92
+ Short input raises `EOFError`; invalid trailers raise `CorruptionError`.
93
+
94
+ ## Locking and durability
95
+
96
+ Writers serialize across cooperating processes and threads. The writer refreshes committed state after acquiring its lock.
97
+ `lock_timeout:` defaults to five seconds. `BusyError` occurs before the transaction callback runs.
98
+ Snapshot reads use positional access and do not acquire the exclusive writer lock.
99
+ Closing waits for active operations; closing inside that handle's transaction raises `TransactionStateError`.
100
+
101
+ File publication synchronizes data before publishing and synchronizing the root header. A successful transaction returns after confirmed synchronization.
102
+ Each writable pathname handle synchronizes its containing directory after its first file synchronization, including handles opened on existing files.
103
+ This lets cooperating initializers and writers establish pathname durability independently. Later commits on that handle synchronize only the file.
104
+ Opening an existing file validates its contents; it does not confirm durability of bytes left by an earlier failed publication.
105
+ Creation rejects dangling final symlinks with `Errno::ENOENT`; open the intended target path directly. Symlinks to existing files remain supported.
106
+ Memory handles provide atomic visibility without disk durability.
107
+ `CommitError#outcome` is `:not_committed` before publication, `:unknown` during uncertain publication, or `:committed` after confirmed synchronization.
108
+ Uncertain publication, failed rollback, or lock-release failure requires reopening before further writes. Callbacks are never automatically retried.
109
+ Library errors inherit `XitDB::Error`; standard argument, missing-key, index, and range failures retain Ruby error classes.
110
+
111
+ ## Physical contract
112
+
113
+ The header occupies 20 bytes. The root header adds a nine-byte slot and eight-byte committed size.
114
+ A trailer contains a root slot, commit identifier, and CRC-64/XZ checksum, occupying 25 bytes.
115
+ An initialized empty database occupies 62 bytes. Parsing checks object ranges against committed boundaries before allocating.
116
+ Graph traversal uses an explicit stack. Write buffers are bounded at 2 MiB, byte transfers at 64 KiB, and offset caches spill after 16,384 entries.
117
+
118
+ `XitDB::Core::Bytes` owns physical byte storage, including inline selection, lengths, format tags, and bounded transfers.
119
+ Scalar materialization, streams, conversion, and compaction use that implementation. Profiles retain payload interpretation and encoding rules.
120
+ Compaction preserves external byte records, including short payloads and unknown format tags.
121
+
122
+ The port preserves the package's [MIT license notice](../LICENSE).
123
+ Reference revisions and source hashes are recorded in `test/interop/manifest.json` in the source checkout.
data/docs/portable.md ADDED
@@ -0,0 +1,99 @@
1
+ # Portable profile
2
+
3
+ `require "xitdb"` provides `XitDB.open(path, readonly: false, hash: nil, lock_timeout: 5.0)` and `XitDB.memory(hash: :sha1)`.
4
+ New files use SHA-1 with identifier `sha1`. Use `hash: :sha256` to create SHA-256 files with identifier `sha2`.
5
+ Hash collections inherit upstream collision semantics. Bulk input and merge reject distinct keys that collapse to one byte key with `KeyCollisionError`.
6
+ Recognized file hashing is selected on open. Unknown or absent hash identifiers require explicit compatible configuration.
7
+
8
+ ## Transactions and lifetime
9
+
10
+ A transaction block receives a writable root map. Normal completion publishes its mutations and returns a committed `Snapshot`.
11
+ The block runs once after the writer lock succeeds. Exceptions, `break`, `throw`, and non-local `return` abort publication.
12
+ `next` completes normally. Its return value does not replace the root.
13
+ A transaction without mutations reuses the previous snapshot. Reassigning an equal value can append history.
14
+
15
+ Nested writes to the same file raise `TransactionStateError`. Each assignment is atomic even if its error is rescued inside the transaction.
16
+ Coercion and codec callbacks may read transaction views. They cannot start another logical mutation while one is applying, including through another Context.
17
+ Byte-stream callbacks run outside finalization and permit non-overlapping sibling writes.
18
+
19
+ `db.snapshot` captures committed data, including inside a transaction. Transaction views expose the uncommitted data.
20
+ Snapshot views remain valid until handle close. Transaction views expire when their block ends and belong to their thread.
21
+ Replacing or deleting an ancestor expires earlier child views. Errors are `ClosedError`, `ExpiredViewError`, or `ReadOnlyError` as applicable.
22
+
23
+ Assignment captures input immediately. Assigning a view from the same transaction captures its value without later aliasing.
24
+ Foreign views require explicit copying or `to_ruby`; direct assignment raises `ForeignValueError`.
25
+ Read Strings are frozen. `to_ruby` creates independent mutable containers and Strings.
26
+ Cyclic inputs or materialization of cyclic graphs raise `CyclicValueError` with the offending path.
27
+
28
+ ## Values
29
+
30
+ | Input | Representation |
31
+ | --- | --- |
32
+ | `nil` | Occupied null slot, distinguishable from absence with `key?` |
33
+ | Integer from `-2**63` through `2**64 - 1` | Exact signed or unsigned upstream integer |
34
+ | `XitDB.uint(n)` | Explicit unsigned slot |
35
+ | Float | IEEE 64-bit float |
36
+ | String | Untagged bytes |
37
+ | Hash, Array, Set | Ordinary upstream map, array list, or set |
38
+ | `XitDB.bytes(data, format_tag: "zz")` | Opaque tagged bytes |
39
+
40
+ Map keys and set members must be Strings. Boolean, Symbol, temporal, and arbitrary object values require the Ruby profile or an explicit application encoding.
41
+ Strings preserve bytes. Reads use UTF-8 for valid UTF-8 bytes and ASCII-8BIT otherwise; original encoding metadata is not retained.
42
+ Hash defaults, default procs, and unsupported input behavior raise `UnsupportedValueError`.
43
+
44
+ ## Collections and costs
45
+
46
+ Declarations include `XitDB.map(counted:)`, `XitDB.set(counted:)`, `XitDB.array`, `XitDB.linked_array`, `XitDB.sorted_map`, and `XitDB.sorted_set`.
47
+ Assign a declaration, then mutate its returned view. Native Hash, Array, and Set select ordinary representations.
48
+ Views include `Enumerable`; they do not subclass native collections.
49
+
50
+ | View | Reads | Writes inside a transaction |
51
+ | --- | --- | --- |
52
+ | Map | `[]`, `fetch`, `dig`, `key?`, `each`, `each_key`, `each_value`, `keys`, `values` | `[]=`, `delete`, `clear`, `merge!` |
53
+ | Array | `[]`, `fetch`, `first`, `last`, `length`, `each` | `[]=`, `<<`, `push`, `pop`, `insert`, `delete_at`, `concat`, `clear` |
54
+ | Set | `include?`, `each` | `add`, `delete`, `clear` |
55
+ | Sorted map/set | Corresponding reads, `rank`, `from`, `from_index`, `page` | Corresponding writes |
56
+ | Linked array | Array reads, slices, `split(index)` | Array writes and concatenation |
57
+
58
+ All views support `size`, `empty?`, `to_ruby`, and bounded `inspect`. Missing map keys return nil; `fetch` raises `KeyError`.
59
+ Array positions support negative indexes. Assignment beyond the end fills the gap with stored nil.
60
+ Reads never create missing parents. Unimplemented operations raise ordinary `NoMethodError`.
61
+ Plain hash map/set size requires traversal. Counted maps/sets, arrays, and sorted collections have constant-time size.
62
+ Equality can traverse the subtree. Scalar reads allocate that scalar; `keys`, `values`, and eager enumeration allocate their results.
63
+
64
+ Enumeration is incremental; `each` without a block returns an enumerator. Snapshot enumeration stays read-only and stable.
65
+ Transaction enumeration permits replacing values and editing descendants. Changing membership or order invalidates its iterator with `ConcurrentModificationError`.
66
+
67
+ Sorted collections compare raw bytes lexicographically. `from(key)` is inclusive; `from_index(index)` seeks by rank.
68
+ `page(offset:, limit:)` returns an incremental enumerator and seeks without scanning skipped entries.
69
+ Keep one snapshot for stable pagination. Applications maintain secondary indexes in the same transaction as their records.
70
+ See [indexed users](../examples/indexed_users.rb).
71
+
72
+ ## History and streams
73
+
74
+ `db.history[index]` returns a snapshot or nil. Negative indexes count backward; `fetch` raises `IndexError` for a missing index.
75
+ Enumeration is incremental. A file without history has a virtual empty map and no revision.
76
+ `snapshot.revision` is opaque and immutable. `db.snapshot(revision: token)` validates its ancestor proof and reopens that history entry.
77
+ Tokens survive ordinary appends but identify a file generation, not a globally canonical value hash.
78
+ `db.revert(snapshot_or_revision)` appends the selected root and retains subsequent history. Foreign tokens raise `ForeignRevisionError`.
79
+
80
+ `view.write_bytes(key) { |io| io.write(chunk) }` streams an untagged value. A callback failure discards that assignment.
81
+ `view.open_bytes(key) { |io| ... }` yields a bounded reader with `read`, `readpartial`, `seek`, `rewind`, `pos`, `size`, and `eof?`.
82
+ Use these block-scoped IO objects inside their blocks. Reads do not exceed the selected value.
83
+ Byte transfers use 64 KiB chunks; writes buffer at most 2 MiB. Ordinary String lookup still allocates the complete String.
84
+ Pass a length to `read` for bounded application buffers; `read` without a length allocates the entire remaining byte value.
85
+
86
+ ## Maintenance and compatibility
87
+
88
+ `db.compact(to: path)` returns an open writable handle to a separate file with one latest root, preserving profile and hashing.
89
+ The caller owns and closes the returned handle.
90
+ The destination must be missing or zero bytes; an initialized empty database is nonempty and rejected.
91
+ Source snapshots remain valid. Application code controls file replacement and reader reopening.
92
+ See [conversion and replication](core.md) for separate-destination ownership and failure contracts.
93
+
94
+ The pinned Go, TypeScript, Java, and Zig references have reciprocal storage and maintenance checks.
95
+ TypeScript's Number convenience reader loses precision outside the exact Number integer range.
96
+ Its signed `Slot.fromBytes` carrier also makes `readUint` reject values above `2**63 - 1` with `ExpectedUnsignedLongException`.
97
+ Java's convenience `Database.Uint` writer rejects negative long carriers, and `readUint` rejects those stored carriers.
98
+ Java's public raw Slot writer preserves these unsigned bits. TypeScript can also write and retain the full unsigned bits.
99
+ Raw-bit, compaction, and replication checks pass separately from these convenience API limits. Ruby retains the full upstream integer range.
data/docs/releasing.md ADDED
@@ -0,0 +1,37 @@
1
+ # Releasing
2
+
3
+ The gem is named `xitdb`. Releases use the `vVERSION` tag and RubyGems trusted publishing from GitHub Actions.
4
+ The release workflow requires every CI job to pass before publishing: Ruby quality checks, four reference implementations, and Linux memory tests.
5
+
6
+ ## Publisher setup
7
+
8
+ Register a [pending trusted publisher](https://rubygems.org/profile/oidc/pending_trusted_publishers/new) for the first release:
9
+
10
+ | Field | Value |
11
+ | --- | --- |
12
+ | Gem name | `xitdb` |
13
+ | Repository owner | `svyatov` |
14
+ | Repository name | `xitdb-ruby` |
15
+ | Workflow filename | `release.yml` |
16
+ | Environment | `release` |
17
+
18
+ Leave the optional workflow repository fields empty. Create the `release` environment in the GitHub repository settings.
19
+ After the first publication, RubyGems converts the pending publisher into a publisher for the gem.
20
+ See [RubyGems trusted publishing](https://guides.rubygems.org/trusted-publishing/) for authentication details.
21
+
22
+ ## Release procedure
23
+
24
+ 1. Set `XitDB::VERSION` in `lib/xitdb/version.rb` to the release version.
25
+ 2. Update README examples and move changelog entries into a dated version section.
26
+ 3. Run `bundle exec ruby script/ci.rb` to verify quality and the installed package.
27
+ 4. Commit the release changes and push them to GitHub.
28
+ 5. Create the matching tag, for example `git tag -a v0.1.0 -m "Release 0.1.0"`.
29
+ 6. Push the tag with `git push origin v0.1.0`.
30
+ 7. Check the Release workflow and confirm the version and checksum on RubyGems before announcing publication.
31
+
32
+ The workflow rejects tags that differ from the packaged version.
33
+ It builds the gem before requesting credentials, then authenticates through GitHub OIDC with permission to publish `xitdb`.
34
+ No persistent RubyGems API key is required.
35
+
36
+ If publisher registration fails, correct the RubyGems settings and rerun the failed job on the same tag.
37
+ For a published version, make corrections in a subsequent release; RubyGems does not permit replacement of a version.