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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +28 -0
- data/LICENSE +21 -0
- data/README.md +131 -0
- data/docs/benchmarks.md +135 -0
- data/docs/core.md +123 -0
- data/docs/portable.md +99 -0
- data/docs/releasing.md +37 -0
- data/docs/ruby-profile-v1.md +226 -0
- data/docs/ruby.md +61 -0
- data/examples/indexed_users.rb +23 -0
- data/examples/portable.rb +62 -0
- data/examples/ruby.rb +56 -0
- data/lib/xitdb/context.rb +360 -0
- data/lib/xitdb/conversion.rb +222 -0
- data/lib/xitdb/core/array_list.rb +282 -0
- data/lib/xitdb/core/b_tree.rb +546 -0
- data/lib/xitdb/core/bytes.rb +181 -0
- data/lib/xitdb/core/compaction.rb +172 -0
- data/lib/xitdb/core/crc64.rb +22 -0
- data/lib/xitdb/core/cursor.rb +201 -0
- data/lib/xitdb/core/database.rb +359 -0
- data/lib/xitdb/core/file_store.rb +194 -0
- data/lib/xitdb/core/format.rb +148 -0
- data/lib/xitdb/core/hash_map.rb +371 -0
- data/lib/xitdb/core/hasher.rb +49 -0
- data/lib/xitdb/core/linked_array_list.rb +85 -0
- data/lib/xitdb/core/memory_store.rb +277 -0
- data/lib/xitdb/core/offset_map.rb +85 -0
- data/lib/xitdb/core/replication.rb +242 -0
- data/lib/xitdb/core/sorted_map.rb +361 -0
- data/lib/xitdb/core/transaction.rb +159 -0
- data/lib/xitdb/core.rb +17 -0
- data/lib/xitdb/database.rb +332 -0
- data/lib/xitdb/declarations.rb +73 -0
- data/lib/xitdb/errors.rb +84 -0
- data/lib/xitdb/history.rb +58 -0
- data/lib/xitdb/input_guard.rb +39 -0
- data/lib/xitdb/profiles/portable.rb +407 -0
- data/lib/xitdb/profiles/ruby.rb +173 -0
- data/lib/xitdb/revision.rb +42 -0
- data/lib/xitdb/ruby/codec.rb +123 -0
- data/lib/xitdb/ruby/containers.rb +495 -0
- data/lib/xitdb/ruby/key_encoding.rb +60 -0
- data/lib/xitdb/ruby/ordered_keys.rb +87 -0
- data/lib/xitdb/ruby/scalars.rb +296 -0
- data/lib/xitdb/ruby/wire.rb +146 -0
- data/lib/xitdb/ruby.rb +46 -0
- data/lib/xitdb/snapshot.rb +13 -0
- data/lib/xitdb/streams.rb +148 -0
- data/lib/xitdb/value_walker.rb +192 -0
- data/lib/xitdb/version.rb +5 -0
- data/lib/xitdb/views/array.rb +303 -0
- data/lib/xitdb/views/base.rb +271 -0
- data/lib/xitdb/views/linked_array.rb +18 -0
- data/lib/xitdb/views/map.rb +162 -0
- data/lib/xitdb/views/set.rb +41 -0
- data/lib/xitdb/views/sorted.rb +77 -0
- data/lib/xitdb.rb +20 -0
- data/sig/base.rbs +76 -0
- data/sig/core.rbs +697 -0
- data/sig/manifest.yaml +10 -0
- data/sig/native.rbs +118 -0
- data/sig/public.rbs +427 -0
- data/sig/ruby.rbs +337 -0
- metadata +137 -0
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
# Ruby profile version 1 wire contract
|
|
2
|
+
|
|
3
|
+
This contract defines the `rb01` profile without changing upstream format version 1.
|
|
4
|
+
|
|
5
|
+
## File identity
|
|
6
|
+
|
|
7
|
+
The upstream header has hash size 32 and hash identifier `rb01`. Hash indexes use SHA-256 over the canonical key bytes defined below.
|
|
8
|
+
|
|
9
|
+
The physical root is an upstream array list of history entries. Each entry references an encoded logical root map.
|
|
10
|
+
|
|
11
|
+
The portable profile uses its upstream header and layout. It does not inspect or decode Ruby envelopes.
|
|
12
|
+
|
|
13
|
+
## Scalar notation
|
|
14
|
+
|
|
15
|
+
- W1: `U(n)` is minimal unsigned LEB128 for a nonnegative integer.
|
|
16
|
+
- W2: `B(bytes)` is `U(bytes.bytesize)` followed by those bytes.
|
|
17
|
+
- W3: `T(text)` is `B(text.encode("UTF-8"))`, with valid UTF-8 required.
|
|
18
|
+
- W4: `Z(n)` is one sign byte, then `B(magnitude)`. The sign is 0 for nonnegative and 1 for negative.
|
|
19
|
+
- W5: `Q(r)` is `Z(r.numerator)` followed by `U(r.denominator)`, with a positive, reduced denominator.
|
|
20
|
+
|
|
21
|
+
Integer magnitudes use minimal big-endian bytes. Zero has an empty magnitude and sign 0. A zero integer therefore has `Z(0) = 00 00`.
|
|
22
|
+
|
|
23
|
+
`F64(f)` is the eight bytes from Ruby's `pack("G")`. Multi-byte fixed integers use big-endian order.
|
|
24
|
+
|
|
25
|
+
Every field length must fit inside its enclosing byte payload. Nonminimal integers, truncated fields, invalid flags, and trailing bytes raise `InvalidValueError`.
|
|
26
|
+
|
|
27
|
+
## Encoded scalar values
|
|
28
|
+
|
|
29
|
+
Upstream byte values carry format tag `rb`, hexadecimal `72 62`. Their payload starts with version byte `01` and a type byte.
|
|
30
|
+
|
|
31
|
+
| Type byte | Ruby value | Remaining payload |
|
|
32
|
+
| --- | --- | --- |
|
|
33
|
+
| `01` | String | `T(encoding.name)`, `B(bytes)` |
|
|
34
|
+
| `02` | Symbol | `T(name.encoding.name)`, `B(name.bytes)` |
|
|
35
|
+
| `03` | Boolean | One byte, `00` for false or `01` for true |
|
|
36
|
+
| `04` | Large Integer | `Z(value)` |
|
|
37
|
+
| `05` | Time | `Q(value.to_r)`, `Z(value.utc_offset)`, one byte for `utc?` |
|
|
38
|
+
| `06` | Date | `Z(value.jd)`, `F64(value.start)` |
|
|
39
|
+
| `07` | DateTime | `Q(value.ajd)`, `Q(value.offset)`, `F64(value.start)` |
|
|
40
|
+
| `08` | BigDecimal | Decimal encoding defined below |
|
|
41
|
+
| `09` | Rational | `Q(value)` |
|
|
42
|
+
| `0a` | Registered custom value | `T(codec_id)`, `B(codec_bytes)` |
|
|
43
|
+
|
|
44
|
+
Integers fitting a native upstream integer slot use that slot. Signed 64-bit values use `int`; remaining unsigned 64-bit values use `uint`.
|
|
45
|
+
|
|
46
|
+
Values outside those ranges use type `04`. Floats use upstream float slots. Stored null uses an occupied upstream `none` slot.
|
|
47
|
+
|
|
48
|
+
Time decoding uses the exact rational instant and recorded UTC offset. The UTC flag distinguishes UTC presentation from a fixed zero offset.
|
|
49
|
+
|
|
50
|
+
The instant retains subnanosecond precision. The offset field accepts integer seconds only; fractional-second UTC offsets raise `UnsupportedValueError`.
|
|
51
|
+
|
|
52
|
+
Date and DateTime decoding restores the recorded calendar start. Their date fields use the public `date` library constructors.
|
|
53
|
+
|
|
54
|
+
Untagged byte values represent explicit binary data. Ruby reads them as frozen ASCII-8BIT strings. Public byte streams address these untagged values.
|
|
55
|
+
|
|
56
|
+
Other format tags remain opaque tagged-byte values. An unknown custom codec retains its complete encoded payload and codec identifier.
|
|
57
|
+
|
|
58
|
+
An unsupported Ruby payload version or builtin type raises `UnsupportedEncodingError`. A registered custom loader never runs during file opening or unrelated traversal.
|
|
59
|
+
|
|
60
|
+
### Decimal encoding
|
|
61
|
+
|
|
62
|
+
The first decimal byte is 0 for finite, 1 for positive infinity, 2 for negative infinity, or 3 for NaN.
|
|
63
|
+
|
|
64
|
+
Infinite and NaN encodings end after that byte. A finite value adds one sign byte, `T(digits)`, and `Z(exponent)`.
|
|
65
|
+
|
|
66
|
+
Sign 0 is positive; sign 1 is negative. Digits and exponent come from `BigDecimal#split`, whose base must be 10.
|
|
67
|
+
|
|
68
|
+
Digits contain normalized ASCII decimal digits. Zero uses digits `0` and exponent 0. Its sign preserves positive or negative zero.
|
|
69
|
+
|
|
70
|
+
## Canonical hash keys
|
|
71
|
+
|
|
72
|
+
These bytes determine key equality and hashing. The stored entry separately retains the original encoded key for enumeration.
|
|
73
|
+
|
|
74
|
+
| Prefix | Accepted key | Remaining canonical bytes |
|
|
75
|
+
| --- | --- | --- |
|
|
76
|
+
| `00` | nil | None |
|
|
77
|
+
| `01` | false | None |
|
|
78
|
+
| `02` | true | None |
|
|
79
|
+
| `03` | Integer | `Z(value)` |
|
|
80
|
+
| `04` | Float | `F64(value)`, with signed zero normalized to positive zero |
|
|
81
|
+
| `05` | String | Canonical string body defined below |
|
|
82
|
+
| `06` | Symbol | Canonical string body for `value.to_s` |
|
|
83
|
+
|
|
84
|
+
NaN keys raise `UnsupportedValueError`. Infinite float keys are accepted in hash indexes.
|
|
85
|
+
|
|
86
|
+
A canonical string body is `T(encoding_label)` followed by `B(bytes)`. The encoding label is empty for every empty string.
|
|
87
|
+
|
|
88
|
+
For nonempty ASCII-only strings with an ASCII-compatible encoding, the label is also empty. Other strings use their canonical `Encoding#name`.
|
|
89
|
+
|
|
90
|
+
This preserves Ruby's empty-string equality across encodings. It also preserves the distinction between non-ASCII binary and UTF-8 strings.
|
|
91
|
+
|
|
92
|
+
String and Symbol prefixes remain distinct. Integer and Float prefixes remain distinct.
|
|
93
|
+
|
|
94
|
+
Golden vectors include:
|
|
95
|
+
|
|
96
|
+
| Input | Hexadecimal canonical bytes |
|
|
97
|
+
| --- | --- |
|
|
98
|
+
| nil | `00` |
|
|
99
|
+
| false | `01` |
|
|
100
|
+
| true | `02` |
|
|
101
|
+
| 0 | `03 00 00` |
|
|
102
|
+
| 1 | `03 00 01 01` |
|
|
103
|
+
| -1 | `03 01 01 01` |
|
|
104
|
+
| Empty String in any encoding | `05 00 00` |
|
|
105
|
+
| ASCII String `name` | `05 00 04 6e 61 6d 65` |
|
|
106
|
+
| Symbol `name` | `06 00 04 6e 61 6d 65` |
|
|
107
|
+
|
|
108
|
+
The decoder does not reconstruct keys from canonical bytes. Entry records retain their original Ruby representation.
|
|
109
|
+
|
|
110
|
+
## Container envelopes
|
|
111
|
+
|
|
112
|
+
Each envelope is an upstream array list with the exact fields shown below. Marker fields are upstream unsigned integer slots.
|
|
113
|
+
|
|
114
|
+
Encoded child values can be native scalar slots, encoded scalar bytes, or container envelopes. Internal indexes are ordinary upstream containers.
|
|
115
|
+
|
|
116
|
+
| Marker | Logical type | Envelope fields |
|
|
117
|
+
| --- | --- | --- |
|
|
118
|
+
| `0x52420101` | Array | `[marker, array_list]` |
|
|
119
|
+
| `0x52420102` | Linked array | `[marker, linked_array_list]` |
|
|
120
|
+
| `0x52420103` | Ordered map | `[marker, next_id, lookup, order]` |
|
|
121
|
+
| `0x52420104` | Ordered set | `[marker, next_id, lookup, order]` |
|
|
122
|
+
| `0x52420105` | Sorted map | `[marker, key_kind, sorted_map]` |
|
|
123
|
+
| `0x52420106` | Sorted set | `[marker, key_kind, sorted_map]` |
|
|
124
|
+
|
|
125
|
+
Arrays and linked arrays contain encoded children. Their envelopes do not require materializing those children.
|
|
126
|
+
|
|
127
|
+
Unexpected markers, field counts, or physical collection kinds raise `InvalidValueError`. The core remains able to inspect the physical values.
|
|
128
|
+
|
|
129
|
+
### Ordered maps and sets
|
|
130
|
+
|
|
131
|
+
`next_id` is an upstream unsigned integer starting at zero. `lookup` is a counted upstream hash map. `order` is an upstream sorted map.
|
|
132
|
+
|
|
133
|
+
A lookup entry uses `SHA256(canonical_key_bytes)` as its hash. Its physical key is the canonical byte string.
|
|
134
|
+
|
|
135
|
+
Its physical value is a three-element upstream array list: `[encoded_key, encoded_value, insertion_id]`. Sets store occupied null as `encoded_value`.
|
|
136
|
+
|
|
137
|
+
An order entry maps the eight-byte big-endian insertion identifier to the 32-byte lookup hash. Both indexes reference the same logical entry.
|
|
138
|
+
|
|
139
|
+
Insertion creates one entry and increments `next_id`. Updating an equal key retains its original encoded key and insertion identifier.
|
|
140
|
+
|
|
141
|
+
Deletion removes both index entries. Reinsertion receives a fresh identifier. Clearing resets both indexes and `next_id`.
|
|
142
|
+
|
|
143
|
+
When `next_id` equals `2**64 - 1`, another insertion raises `RangeError`. Updates and deletions remain valid.
|
|
144
|
+
|
|
145
|
+
The logical size comes from the counted lookup map. Iteration follows the order index and resolves its referenced entries.
|
|
146
|
+
|
|
147
|
+
### Sorted maps and sets
|
|
148
|
+
|
|
149
|
+
Key kind is 1 for String, 2 for Integer, 3 for Float, or 4 for Time.
|
|
150
|
+
|
|
151
|
+
A sorted map stores order-preserving key bytes and a two-element record: `[encoded_original_key, encoded_value]`.
|
|
152
|
+
|
|
153
|
+
A sorted set also uses an upstream sorted map. Its value is the encoded original member. This retains representation beyond the comparison key.
|
|
154
|
+
|
|
155
|
+
Updating an equal comparison key retains its original encoded key or member. Every operation uses the declared key kind.
|
|
156
|
+
|
|
157
|
+
## Ordered key encoding
|
|
158
|
+
|
|
159
|
+
Ordered encodings use unsigned byte comparison. They contain no locale-dependent data and do not use Ruby's randomized hash.
|
|
160
|
+
|
|
161
|
+
### Integers
|
|
162
|
+
|
|
163
|
+
For nonnegative `n`, define `O(n)` as an eight-byte magnitude length followed by minimal big-endian magnitude bytes.
|
|
164
|
+
|
|
165
|
+
`O(0)` has length zero and no magnitude bytes. The length field is an unsigned 64-bit integer.
|
|
166
|
+
|
|
167
|
+
Ordered signed integers use prefix `00` for negative, `01` for zero, and `02` for positive.
|
|
168
|
+
|
|
169
|
+
Negative values append the bytewise complement of `O(abs(n))`. Positive values append `O(n)`. Zero ends after its prefix.
|
|
170
|
+
|
|
171
|
+
These encodings are prefix-free and compare in numeric order. Magnitude lengths exceeding the physical file range raise `RangeError`.
|
|
172
|
+
|
|
173
|
+
### Floats
|
|
174
|
+
|
|
175
|
+
Reject NaN. Normalize either signed zero to positive zero. Read the IEEE-754 bits as an unsigned 64-bit integer.
|
|
176
|
+
|
|
177
|
+
For negative values, complement all 64 bits. For nonnegative values, toggle the sign bit. Write the resulting eight bytes big-endian.
|
|
178
|
+
|
|
179
|
+
This places negative infinity first and positive infinity last. It gives either zero representation one lookup key.
|
|
180
|
+
|
|
181
|
+
### Strings
|
|
182
|
+
|
|
183
|
+
String keys must convert to valid UTF-8. Their ordered bytes are the resulting UTF-8 bytes without a length prefix.
|
|
184
|
+
|
|
185
|
+
The entry record retains the original encoding and bytes. Invalid or unconvertible keys raise `UnsupportedEncodingError`.
|
|
186
|
+
|
|
187
|
+
This comparison deliberately uses UTF-8 text order. Ordinary Ruby hash-key encoding retains its separate equality contract.
|
|
188
|
+
|
|
189
|
+
### Exact time instants
|
|
190
|
+
|
|
191
|
+
Time keys compare `value.to_r`, including fractions smaller than a nanosecond. Their comparison ignores presentation offsets.
|
|
192
|
+
|
|
193
|
+
For nonnegative rational `x`, define a prefix-free encoding `R(x)`:
|
|
194
|
+
|
|
195
|
+
1. Compute `q, remainder = x.numerator.divmod(x.denominator)`.
|
|
196
|
+
2. Write `O(q)`.
|
|
197
|
+
3. For zero remainder, append byte `00` and stop.
|
|
198
|
+
4. Otherwise, append byte `01` and the complement of `R(x.denominator / remainder)`.
|
|
199
|
+
|
|
200
|
+
The division in step 4 denotes an exact Rational. An iterative continued-fraction implementation avoids Ruby stack growth.
|
|
201
|
+
|
|
202
|
+
For signed rational instants, use the signed prefixes from integer encoding. Negative instants append the complement of `R(abs(x))`.
|
|
203
|
+
|
|
204
|
+
Positive instants append `R(x)`. Zero ends after its prefix. Prefix-free child encodings make byte complementation reverse their order.
|
|
205
|
+
|
|
206
|
+
This requires work proportional to the continued-fraction representation. It does not emit one byte per denominator unit.
|
|
207
|
+
|
|
208
|
+
## Ownership and loader behavior
|
|
209
|
+
|
|
210
|
+
View reads return frozen scalar values. Materialization creates independent values and mutable containers and strings where Ruby permits mutation.
|
|
211
|
+
|
|
212
|
+
Custom loaders receive a frozen copy of their payload. They must return a fresh owned value, without shared mutable state between calls.
|
|
213
|
+
|
|
214
|
+
The profile freezes the returned object's outer state for view reads. A custom codec owns the immutability of references inside that value.
|
|
215
|
+
|
|
216
|
+
Materialization invokes the loader independently. No implicit constant lookup or arbitrary object loader interprets stored class names.
|
|
217
|
+
|
|
218
|
+
## Required fixtures
|
|
219
|
+
|
|
220
|
+
Golden fixtures cover every scalar type, every container envelope, key equality, signed zero, and unknown custom codecs.
|
|
221
|
+
|
|
222
|
+
Ordered-key fixtures compare encoded byte order with numeric or temporal order. They include negative values, huge integers, infinities, and exact subnanosecond instants.
|
|
223
|
+
|
|
224
|
+
Malformed fixtures cover truncated lengths, nonminimal integers, invalid encodings, invalid fields, and conflicting index records.
|
|
225
|
+
|
|
226
|
+
Changes to canonical keys, payload interpretation, or container layouts require a different profile identifier. Version-one readers never infer a changed layout from data shape.
|
data/docs/ruby.md
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Ruby profile
|
|
2
|
+
|
|
3
|
+
`require "xitdb/ruby"` provides `XitDB::Ruby.open(path, readonly: false, lock_timeout: 5.0, codecs: [])` and `XitDB::Ruby.memory(codecs: [])`.
|
|
4
|
+
Configuration belongs to each handle. The file identifier `rb01` selects SHA-256 over canonical typed keys and version-one container envelopes.
|
|
5
|
+
The [portable guide](portable.md) defines the shared transaction, snapshot, history, ownership, stream, and view contracts.
|
|
6
|
+
|
|
7
|
+
## Native values and keys
|
|
8
|
+
|
|
9
|
+
Supported exact builtin value classes include nil, booleans, Integer, Float, String, Symbol, Hash, Array, Set, Time, Date, DateTime, BigDecimal, and Rational.
|
|
10
|
+
Integers have arbitrary size. Strings preserve bytes and encoding names. Unknown encodings raise `UnsupportedEncodingError` on decoding.
|
|
11
|
+
Time preserves its exact rational instant, including subnanosecond fractions, UTC flag, and integer UTC offset.
|
|
12
|
+
The offset wire field is an integer: fractional-second UTC offsets raise `UnsupportedValueError`. Instant precision is unchanged.
|
|
13
|
+
Named timezone rules are not stored. Date and DateTime preserve their calendar start and date or instant.
|
|
14
|
+
BigDecimal preserves numeric value and signed zero. Rational preserves numerator and denominator.
|
|
15
|
+
Float values preserve IEEE representations, including infinities and NaN outside keys.
|
|
16
|
+
|
|
17
|
+
Ordinary keys and set members accept exact NilClass, TrueClass, FalseClass, Integer, Float, String, and Symbol instances.
|
|
18
|
+
Symbols and Strings remain distinct, as do Integer and Float keys. Float keys normalize signed zero and reject NaN.
|
|
19
|
+
ASCII-compatible String keys with equal ASCII bytes compare equally. Non-ASCII keys retain encoding-sensitive equality.
|
|
20
|
+
Ordinary keys never use custom codecs. Containers, temporal values, and custom objects cannot be ordinary keys.
|
|
21
|
+
|
|
22
|
+
Maps and sets preserve insertion order. Updating retains position; deleting and reinserting moves an entry to the end.
|
|
23
|
+
Native containers select the ordinary Ruby-profile representation. Counts are available without scanning members.
|
|
24
|
+
Hash defaults, default procs, identity comparison, singleton behavior, executable objects, and cyclic inputs are unsupported.
|
|
25
|
+
Scalar and container subclasses need an explicit value codec. Unregistered subclasses reject before overridden methods execute.
|
|
26
|
+
|
|
27
|
+
## Sorted collections
|
|
28
|
+
|
|
29
|
+
Declare `XitDB.sorted_map(key: :integer)` or `XitDB.sorted_set(key: :string)` explicitly.
|
|
30
|
+
Key kinds are `:string`, `:integer`, `:float`, and `:time`.
|
|
31
|
+
Strings use valid UTF-8 byte order. Integers include arbitrary-size values; Float rejects NaN.
|
|
32
|
+
Time keys compare exact rational instants, independent of presentation offset.
|
|
33
|
+
`rank`, inclusive `from`, `from_index`, and `page` use the storage tree. Snapshot pagination remains stable through later commits.
|
|
34
|
+
|
|
35
|
+
## Custom values
|
|
36
|
+
|
|
37
|
+
```ruby
|
|
38
|
+
Money = Struct.new(:cents)
|
|
39
|
+
codec = XitDB::Ruby::Codec.new(
|
|
40
|
+
id: "example/money/v1", type: Money,
|
|
41
|
+
dump: ->(money) { money.cents.to_s },
|
|
42
|
+
load: ->(bytes) { Money.new(Integer(bytes)) }
|
|
43
|
+
)
|
|
44
|
+
XitDB::Ruby.memory(codecs: [codec]) do |db|
|
|
45
|
+
db.transaction { |root| root[:price] = Money.new(125) }
|
|
46
|
+
p db.snapshot[:price].cents # 125
|
|
47
|
+
end
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Dumps return byte Strings. Use stable, schema-versioned identifiers. Duplicate IDs, overlapping registrations, and builtin overrides raise `CodecConfigurationError`.
|
|
51
|
+
Register subclass values explicitly; registering codecs never extends key support.
|
|
52
|
+
Loader input is a frozen copy. Return a fresh owned value for each call.
|
|
53
|
+
The profile freezes the returned object's outer state. The codec must control mutable references inside that object.
|
|
54
|
+
`to_ruby` invokes loaders independently. Unknown codecs return immutable `EncodedValue` objects that retain complete payload bytes.
|
|
55
|
+
Opening and unrelated traversal never load classes or run arbitrary object deserialization.
|
|
56
|
+
|
|
57
|
+
Callbacks can read transaction views. Coercion, dump, load, and conversion callbacks cannot start a second logical mutation during one Core transaction's active mutation.
|
|
58
|
+
The guard applies across Context instances; nested writes raise `TransactionStateError` before changing state.
|
|
59
|
+
Callback failure aborts the affected assignment. Byte-stream callbacks retain their separate non-overlapping sibling-write behavior.
|
|
60
|
+
|
|
61
|
+
See [native workflows](../examples/ruby.rb) and the [wire contract](ruby-profile-v1.md).
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "xitdb"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
XitDB.memory do |db|
|
|
7
|
+
db.transaction do |root|
|
|
8
|
+
root["users"] = {}
|
|
9
|
+
root["by_name"] = XitDB.sorted_map
|
|
10
|
+
[%w[1 Ada], %w[2 Grace]].each do |id, name|
|
|
11
|
+
root["users"][id] = { "name" => name }
|
|
12
|
+
root["by_name"][name] = id
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
snapshot = db.snapshot
|
|
16
|
+
names = snapshot["by_name"].page(offset: 0, limit: 2).map do |_name, id|
|
|
17
|
+
snapshot["users"][id]["name"]
|
|
18
|
+
end
|
|
19
|
+
raise "rank differs" unless snapshot["by_name"].rank("Grace") == 1
|
|
20
|
+
raise "range differs" unless snapshot["by_name"].from("Grace").first == %w[Grace 2]
|
|
21
|
+
|
|
22
|
+
puts JSON.generate(names: names)
|
|
23
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "xitdb"
|
|
4
|
+
require "json"
|
|
5
|
+
require "tmpdir"
|
|
6
|
+
require "stringio"
|
|
7
|
+
|
|
8
|
+
Dir.mktmpdir("xitdb-portable-") do |directory|
|
|
9
|
+
XitDB.open(File.join(directory, "source.xit"), hash: :sha256) do |db|
|
|
10
|
+
db.transaction do |root|
|
|
11
|
+
root["profile"] = { "name" => "Ada", "visits" => 0 }
|
|
12
|
+
root["tags"] = Set["ruby", "storage"]
|
|
13
|
+
root["events"] = XitDB.linked_array
|
|
14
|
+
root["events"].push("created", "visited")
|
|
15
|
+
root["counted"] = XitDB.map(counted: true)
|
|
16
|
+
root["counted"]["n"] = XitDB.uint((2**64) - 1)
|
|
17
|
+
root["opaque"] = XitDB.bytes("payload", format_tag: "zz")
|
|
18
|
+
root.write_bytes("attachment") do |io|
|
|
19
|
+
io.write("abc")
|
|
20
|
+
io.write("def")
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
before = db.snapshot
|
|
24
|
+
db.transaction { |root| root["profile"]["visits"] += 1 }
|
|
25
|
+
raise "snapshot changed" unless before.dig("profile", "visits").zero?
|
|
26
|
+
raise "revision changed" unless db.snapshot(revision: before.revision).dig("profile", "visits").zero?
|
|
27
|
+
raise "stream differs" unless db.snapshot.open_bytes("attachment", &:read) == "abcdef"
|
|
28
|
+
raise "linked slice differs" unless db.snapshot["events"].split(1).last.to_ruby == ["visited"]
|
|
29
|
+
raise "history missing" unless db.history.fetch(0).dig("profile", "visits").zero?
|
|
30
|
+
|
|
31
|
+
db.revert(before)
|
|
32
|
+
raise "revert differs" unless db.snapshot.dig("profile", "visits").zero?
|
|
33
|
+
|
|
34
|
+
db.compact(to: File.join(directory, "compact.xit")).close
|
|
35
|
+
XitDB.open(File.join(directory, "compact.xit"), readonly: true) do |compact|
|
|
36
|
+
raise "compaction differs" unless compact.snapshot.dig("profile", "name") == "Ada"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Bootstrap a separate replica, then send one incremental update.
|
|
40
|
+
full = StringIO.new("".b)
|
|
41
|
+
finish = db.core.replica_state.committed_size
|
|
42
|
+
db.core.read_committed(0, finish, full)
|
|
43
|
+
full.rewind
|
|
44
|
+
replica = XitDB::Core::Database.copy_replica(to: File.join(directory, "replica.xit"), source_io: full,
|
|
45
|
+
length: finish)
|
|
46
|
+
begin
|
|
47
|
+
db.transaction { |root| root["profile"]["visits"] = 2 }
|
|
48
|
+
start = db.core.replication_start(replica.replica_state)
|
|
49
|
+
raise "full staging required" if start.zero?
|
|
50
|
+
|
|
51
|
+
finish = db.core.replica_state.committed_size
|
|
52
|
+
delta = StringIO.new("".b)
|
|
53
|
+
db.core.read_committed(start, finish, delta)
|
|
54
|
+
delta.rewind
|
|
55
|
+
replica.apply_replicated(start, delta, finish - start)
|
|
56
|
+
raise "replica differs" unless replica.replica_state == db.core.replica_state
|
|
57
|
+
ensure
|
|
58
|
+
replica.close
|
|
59
|
+
end
|
|
60
|
+
puts JSON.generate(name: db.snapshot.dig("profile", "name"), visits: db.snapshot.dig("profile", "visits"))
|
|
61
|
+
end
|
|
62
|
+
end
|
data/examples/ruby.rb
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "xitdb/ruby"
|
|
4
|
+
require "json"
|
|
5
|
+
require "tmpdir"
|
|
6
|
+
require "bigdecimal"
|
|
7
|
+
require "date"
|
|
8
|
+
|
|
9
|
+
Money = Struct.new(:cents)
|
|
10
|
+
codec = XitDB::Ruby::Codec.new(id: "example/money/v1", type: Money,
|
|
11
|
+
dump: ->(money) { money.cents.to_s },
|
|
12
|
+
load: ->(bytes) { Money.new(Integer(bytes)) })
|
|
13
|
+
|
|
14
|
+
Dir.mktmpdir("xitdb-ruby-") do |directory|
|
|
15
|
+
XitDB::Ruby.memory(codecs: [codec]) do |db|
|
|
16
|
+
instant = Time.at(Rational(1, 10**12)).utc
|
|
17
|
+
db.transaction do |root|
|
|
18
|
+
root[:enabled] = true
|
|
19
|
+
root[:values] = [2**200, Rational(2, 3), BigDecimal("1.25"), Date.new(2026, 11, 4),
|
|
20
|
+
DateTime.new(2026, 11, 4), instant, "é".encode("UTF-16LE"), Money.new(125)]
|
|
21
|
+
root[:keys] = { name: "symbol", "name" => "string", 1 => "integer", 1.0 => "float" }
|
|
22
|
+
root[:members] = Set[:second, :first]
|
|
23
|
+
root[:times] = XitDB.sorted_map(key: :time)
|
|
24
|
+
root[:times][instant] = "created"
|
|
25
|
+
end
|
|
26
|
+
snapshot = db.snapshot
|
|
27
|
+
raise "exact instant lost" unless snapshot[:values][5].to_r == instant.to_r
|
|
28
|
+
raise "codec differs" unless snapshot[:values][7].cents == 125
|
|
29
|
+
raise "key identity lost" unless snapshot[:keys].size == 4
|
|
30
|
+
raise "set order lost" unless snapshot[:members].to_a == %i[second first]
|
|
31
|
+
raise "time lookup differs" unless snapshot[:times].from(instant).first.last == "created"
|
|
32
|
+
|
|
33
|
+
XitDB::Ruby.memory(codecs: [codec]) do |copy|
|
|
34
|
+
copy.transaction { |root| root.context.copy_from(snapshot[:values], at: [:copied]) }
|
|
35
|
+
raise "cross-file copy differs" unless copy.snapshot[:copied][0] == 2**200
|
|
36
|
+
end
|
|
37
|
+
# Convert a representable subset with an explicit key/value transform.
|
|
38
|
+
XitDB::Ruby.memory do |source|
|
|
39
|
+
source.transaction { |root| root[:enabled] = true }
|
|
40
|
+
portable = source.convert(to: File.join(directory, "portable.xit"), profile: :portable, history: :latest,
|
|
41
|
+
transform: lambda { |value:, role:, path:| # rubocop:disable Lint/UnusedBlockArgument -- Keep the callback's public keyword contract.
|
|
42
|
+
if role == :key
|
|
43
|
+
value.to_s
|
|
44
|
+
else
|
|
45
|
+
(value == true ? 1 : value)
|
|
46
|
+
end
|
|
47
|
+
})
|
|
48
|
+
begin
|
|
49
|
+
raise "conversion differs" unless portable.snapshot["enabled"] == 1
|
|
50
|
+
ensure
|
|
51
|
+
portable.close
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
puts JSON.generate(enabled: snapshot[:enabled])
|
|
55
|
+
end
|
|
56
|
+
end
|