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,371 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module XitDB
|
|
4
|
+
module Core
|
|
5
|
+
# A persistent hash trie with optional entry counts.
|
|
6
|
+
class HashMap
|
|
7
|
+
BLOCK_SIZE = 16 * Slot::SIZE
|
|
8
|
+
EMPTY_BLOCK = ("\0".b * BLOCK_SIZE).freeze
|
|
9
|
+
TAGS = %i[hash_map hash_set counted_hash_map counted_hash_set].freeze
|
|
10
|
+
|
|
11
|
+
attr_reader :cursor
|
|
12
|
+
|
|
13
|
+
def initialize(cursor, counted: false, set: false)
|
|
14
|
+
@cursor = cursor
|
|
15
|
+
@tag = collection_tag(counted, set)
|
|
16
|
+
slot = cursor.slot
|
|
17
|
+
unless slot.tag == :none || (TAGS.include?(slot.tag) && counted == counted_tag?(slot.tag))
|
|
18
|
+
raise InvalidValueError, "slot does not contain the requested hash collection"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
@tag = slot.tag unless slot.tag == :none
|
|
22
|
+
return unless slot.tag == :none && cursor.transaction
|
|
23
|
+
|
|
24
|
+
mutate { |transaction| publish(writable_root(transaction)) }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def self.from_cursor(cursor)
|
|
28
|
+
new(cursor, counted: %i[counted_hash_map counted_hash_set].include?(cursor.slot.tag))
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def get(hash_bytes)
|
|
32
|
+
entry(hash_bytes)&.last
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def key(hash_bytes)
|
|
36
|
+
entry(hash_bytes)&.first
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def include?(hash_bytes)
|
|
40
|
+
!entry(hash_bytes).nil?
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def size
|
|
44
|
+
start = root_pointer
|
|
45
|
+
return 0 unless start
|
|
46
|
+
|
|
47
|
+
counted? ? Format.unsigned(read(start, 8)) : each_entry.count
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def put(hash_bytes, key:, value:)
|
|
51
|
+
hash_bytes = validate_hash(hash_bytes)
|
|
52
|
+
validate_slot(key)
|
|
53
|
+
validate_slot(value)
|
|
54
|
+
write_entry(hash_bytes, key: key, value: value)
|
|
55
|
+
self
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def add(hash_bytes, key:)
|
|
59
|
+
hash_bytes = validate_hash(hash_bytes)
|
|
60
|
+
validate_slot(key)
|
|
61
|
+
write_entry(hash_bytes, key: key, value: nil)
|
|
62
|
+
self
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def cursor_at(hash_bytes, create: false)
|
|
66
|
+
hash_bytes = validate_hash(hash_bytes)
|
|
67
|
+
value = get(hash_bytes)
|
|
68
|
+
if value.nil? && create
|
|
69
|
+
put(hash_bytes, key: Slot.new, value: Slot.new)
|
|
70
|
+
value = get(hash_bytes)
|
|
71
|
+
end
|
|
72
|
+
return nil unless value
|
|
73
|
+
|
|
74
|
+
cursor.hash_child(hash_bytes.freeze, value)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def replace_child(hash_bytes, slot, owner: nil, collection: false, propagate: true)
|
|
78
|
+
hash_bytes = validate_hash(hash_bytes)
|
|
79
|
+
validate_slot(slot)
|
|
80
|
+
raise ExpiredViewError, "hash entry was removed" unless include?(hash_bytes)
|
|
81
|
+
|
|
82
|
+
write_entry(hash_bytes, key: nil, value: slot, owner: owner, collection: collection, propagate: propagate)
|
|
83
|
+
self
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def remove(hash_bytes) # rubocop:disable Naming/PredicateMethod -- This public method removes an entry.
|
|
87
|
+
hash_bytes = validate_hash(hash_bytes)
|
|
88
|
+
writable!
|
|
89
|
+
return false unless include?(hash_bytes)
|
|
90
|
+
|
|
91
|
+
mutate do |transaction|
|
|
92
|
+
start = writable_root(transaction)
|
|
93
|
+
pointer = start + count_bytes
|
|
94
|
+
depth = 0
|
|
95
|
+
# @type var path: Array[[Integer, String, Integer]]
|
|
96
|
+
path = []
|
|
97
|
+
loop do
|
|
98
|
+
block = read(pointer, BLOCK_SIZE)
|
|
99
|
+
index = nibble(hash_bytes, depth)
|
|
100
|
+
slot = parse_slot(Format.slice(block, index * Slot::SIZE, Slot::SIZE))
|
|
101
|
+
path << [pointer, block, index]
|
|
102
|
+
break unless slot.tag == :index
|
|
103
|
+
|
|
104
|
+
pointer = slot.value
|
|
105
|
+
depth += 1
|
|
106
|
+
end
|
|
107
|
+
collapse_path(transaction, start, path)
|
|
108
|
+
adjust_count(transaction, start, -1)
|
|
109
|
+
publish(start)
|
|
110
|
+
cursor.invalidate_children(index: hash_bytes)
|
|
111
|
+
end
|
|
112
|
+
true
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def each_entry
|
|
116
|
+
return enum_for(:each_entry) unless block_given?
|
|
117
|
+
|
|
118
|
+
start = root_pointer
|
|
119
|
+
return self unless start
|
|
120
|
+
|
|
121
|
+
# @type var stack: Array[[Integer, Integer]]
|
|
122
|
+
stack = [[start + count_bytes, 0]]
|
|
123
|
+
until stack.empty?
|
|
124
|
+
pointer, depth = stack.pop || raise(CorruptionError, "missing hash branch")
|
|
125
|
+
raise CorruptionError, "hash trie exceeds hash width" if depth >= hash_size * 2
|
|
126
|
+
|
|
127
|
+
block = read(pointer, BLOCK_SIZE)
|
|
128
|
+
# @type var children: Array[[Integer, Integer]]
|
|
129
|
+
children = []
|
|
130
|
+
16.times do |i|
|
|
131
|
+
slot = parse_slot(Format.slice(block, i * Slot::SIZE, Slot::SIZE))
|
|
132
|
+
if slot.tag == :index
|
|
133
|
+
children << [slot.value, depth + 1]
|
|
134
|
+
elsif slot.tag == :kv_pair
|
|
135
|
+
hash, key, value = read_record(slot)
|
|
136
|
+
yield hash, key, value
|
|
137
|
+
elsif !slot.empty?
|
|
138
|
+
raise CorruptionError, "invalid hash trie entry"
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
stack.concat(children.reverse)
|
|
142
|
+
end
|
|
143
|
+
self
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
private
|
|
147
|
+
|
|
148
|
+
def collection_tag(counted, set)
|
|
149
|
+
if counted
|
|
150
|
+
set ? :counted_hash_set : :counted_hash_map
|
|
151
|
+
else
|
|
152
|
+
set ? :hash_set : :hash_map
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def collapsible?(pointer, start, entries)
|
|
157
|
+
pointer != start + count_bytes && entries.size <= 1 && entries.none? { |slot| slot.tag == :index }
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def collapse_path(transaction, start, path)
|
|
161
|
+
replacement = Slot.new
|
|
162
|
+
path.reverse_each do |old_pointer, block, index|
|
|
163
|
+
block[index * Slot::SIZE, Slot::SIZE] = replacement.to_bytes
|
|
164
|
+
entries = Array.new(16) { |i| parse_slot(Format.slice(block, i * Slot::SIZE, Slot::SIZE)) }.reject(&:empty?)
|
|
165
|
+
if collapsible?(old_pointer, start, entries)
|
|
166
|
+
replacement = entries.first || Slot.new
|
|
167
|
+
else
|
|
168
|
+
new_pointer = old_pointer < transaction.mutable_start ? transaction.append(block) : old_pointer
|
|
169
|
+
transaction.write_at(new_pointer, block)
|
|
170
|
+
replacement = Slot.new(tag: :index, value: new_pointer)
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def counted_tag?(tag)
|
|
176
|
+
%i[counted_hash_map counted_hash_set].include?(tag)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def counted?
|
|
180
|
+
counted_tag?(@tag)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def count_bytes
|
|
184
|
+
counted? ? 8 : 0
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def hash_size
|
|
188
|
+
cursor.database.header.hash_size
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def validate_hash(hash)
|
|
192
|
+
raise TypeError, "hash must be a String" unless hash.is_a?(String)
|
|
193
|
+
raise ArgumentError, "hash has incorrect width" unless hash.bytesize == hash_size && hash_size.positive?
|
|
194
|
+
|
|
195
|
+
hash.b
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def validate_slot(slot)
|
|
199
|
+
raise TypeError, "expected Slot" unless slot.is_a?(Slot)
|
|
200
|
+
raise CorruptionError, "unknown hash record slot tag" unless Slot::TAGS.include?(slot.tag)
|
|
201
|
+
raise InvalidValueError, "index slot is internal to hash branches" if slot.tag == :index
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def nibble(hash, depth)
|
|
205
|
+
raise CorruptionError, "hash trie exceeds hash width" if depth >= hash_size * 2
|
|
206
|
+
|
|
207
|
+
byte = hash.getbyte(hash_size - 1 - (depth / 2))
|
|
208
|
+
raise CorruptionError, "hash trie exceeds hash width" unless byte
|
|
209
|
+
|
|
210
|
+
(byte >> (depth % 2 * 4)) & 15
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def root_pointer
|
|
214
|
+
slot = cursor.slot
|
|
215
|
+
return nil if slot.tag == :none
|
|
216
|
+
raise InvalidValueError, "slot does not contain the requested hash collection" unless slot.tag == @tag
|
|
217
|
+
|
|
218
|
+
read(slot.value, count_bytes + BLOCK_SIZE)
|
|
219
|
+
slot.value
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def read(offset, length)
|
|
223
|
+
cursor.database.read_at(offset, length, bound: cursor.bound)
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def parse_slot(bytes)
|
|
227
|
+
slot = Slot.parse(bytes)
|
|
228
|
+
raise CorruptionError, "unknown hash slot tag" unless Slot::TAGS.include?(slot.tag)
|
|
229
|
+
|
|
230
|
+
slot
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def read_record(slot)
|
|
234
|
+
bytes = read(slot.value, hash_size + (Slot::SIZE * 2))
|
|
235
|
+
key = parse_slot(Format.slice(bytes, hash_size, Slot::SIZE))
|
|
236
|
+
value = parse_slot(Format.slice(bytes, hash_size + Slot::SIZE, Slot::SIZE))
|
|
237
|
+
raise CorruptionError, "index slot in hash record" if key.tag == :index || value.tag == :index
|
|
238
|
+
|
|
239
|
+
[Format.slice(bytes, 0, hash_size).freeze, key, value]
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def entry(hash)
|
|
243
|
+
hash = validate_hash(hash)
|
|
244
|
+
start = root_pointer
|
|
245
|
+
return nil unless start
|
|
246
|
+
|
|
247
|
+
pointer = start + count_bytes
|
|
248
|
+
depth = 0
|
|
249
|
+
loop do
|
|
250
|
+
block = read(pointer, BLOCK_SIZE)
|
|
251
|
+
slot = parse_slot(Format.slice(block, nibble(hash, depth) * Slot::SIZE, Slot::SIZE))
|
|
252
|
+
return nil if slot.empty?
|
|
253
|
+
|
|
254
|
+
if slot.tag == :kv_pair
|
|
255
|
+
stored_hash, key, value = read_record(slot)
|
|
256
|
+
return stored_hash == hash ? [key, value] : nil
|
|
257
|
+
end
|
|
258
|
+
raise CorruptionError, "invalid hash trie entry" unless slot.tag == :index
|
|
259
|
+
|
|
260
|
+
pointer = slot.value
|
|
261
|
+
depth += 1
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def writable!
|
|
266
|
+
cursor.slot
|
|
267
|
+
raise ReadOnlyError, "hash collection is read-only" unless cursor.transaction
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def mutate
|
|
271
|
+
writable!
|
|
272
|
+
transaction = cursor.transaction
|
|
273
|
+
raise ReadOnlyError, "hash collection is read-only" unless transaction
|
|
274
|
+
|
|
275
|
+
checkpoint = transaction.checkpoint
|
|
276
|
+
begin
|
|
277
|
+
yield transaction
|
|
278
|
+
rescue Exception # rubocop:disable Lint/RescueException -- Cleanup must also preserve nonstandard exceptions.
|
|
279
|
+
transaction.restore(checkpoint)
|
|
280
|
+
raise
|
|
281
|
+
else
|
|
282
|
+
transaction.release_checkpoint(checkpoint)
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def writable_root(transaction)
|
|
287
|
+
start = root_pointer
|
|
288
|
+
return transaction.append(("\0".b * count_bytes) + EMPTY_BLOCK) unless start
|
|
289
|
+
|
|
290
|
+
bytes = read(start, count_bytes + BLOCK_SIZE)
|
|
291
|
+
start < transaction.mutable_start ? transaction.append(bytes) : start
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def publish(start, propagate: true)
|
|
295
|
+
cursor.update_collection(Slot.new(tag: @tag, value: start), propagate: propagate)
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
def adjust_count(transaction, start, delta)
|
|
299
|
+
return unless counted?
|
|
300
|
+
|
|
301
|
+
count = Format.unsigned(read(start, 8)) + delta
|
|
302
|
+
Format.integer(count, 0..Format::UINT64_MAX)
|
|
303
|
+
transaction.write_at(start, [count].pack("Q>"))
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
def write_entry(hash, key:, value:, owner: nil, collection: false, propagate: true)
|
|
307
|
+
mutate do |transaction|
|
|
308
|
+
start = writable_root(transaction)
|
|
309
|
+
pointer = start + count_bytes
|
|
310
|
+
depth = 0
|
|
311
|
+
loop do
|
|
312
|
+
position = pointer + (nibble(hash, depth) * Slot::SIZE)
|
|
313
|
+
slot = parse_slot(read(position, Slot::SIZE))
|
|
314
|
+
if slot.empty? || slot.tag == :kv_pair
|
|
315
|
+
next_pointer = write_leaf(transaction, position, slot, hash, depth, start, key, value)
|
|
316
|
+
break unless next_pointer
|
|
317
|
+
elsif slot.tag == :index
|
|
318
|
+
bytes = read(slot.value, BLOCK_SIZE)
|
|
319
|
+
next_pointer = slot.value < transaction.mutable_start ? transaction.append(bytes) : slot.value
|
|
320
|
+
else
|
|
321
|
+
raise CorruptionError, "invalid hash trie entry"
|
|
322
|
+
end
|
|
323
|
+
transaction.write_at(position, Slot.new(tag: :index, value: next_pointer).to_bytes)
|
|
324
|
+
pointer = next_pointer
|
|
325
|
+
depth += 1
|
|
326
|
+
end
|
|
327
|
+
publish(start, propagate: propagate)
|
|
328
|
+
cursor.invalidate_children(index: hash, except: owner) unless collection || value.nil?
|
|
329
|
+
end
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
def write_leaf(transaction, position, slot, hash, depth, start, key, value)
|
|
333
|
+
stored_hash, stored_key, stored_value = slot.empty? ? [nil, Slot.new, Slot.new] : read_record(slot)
|
|
334
|
+
if stored_hash.nil? || stored_hash == hash
|
|
335
|
+
next_key = stored_key.empty? && key ? key : stored_key
|
|
336
|
+
next_value = value || stored_value
|
|
337
|
+
record = transaction.append(hash.b + next_key.to_bytes + next_value.to_bytes)
|
|
338
|
+
transaction.write_at(position, Slot.new(tag: :kv_pair, value: record).to_bytes)
|
|
339
|
+
adjust_count(transaction, start, 1) if stored_hash.nil?
|
|
340
|
+
return nil
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
next_pointer = transaction.append(EMPTY_BLOCK)
|
|
344
|
+
previous_position = next_pointer + (nibble(stored_hash, depth + 1) * Slot::SIZE)
|
|
345
|
+
transaction.write_at(previous_position, slot.to_bytes)
|
|
346
|
+
next_pointer
|
|
347
|
+
end
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
# A hash trie that stores keys without values.
|
|
351
|
+
class HashSet < HashMap
|
|
352
|
+
def initialize(cursor, counted: false)
|
|
353
|
+
super(cursor, counted: counted, set: true)
|
|
354
|
+
end
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
# A hash trie with a stored entry count.
|
|
358
|
+
class CountedHashMap < HashMap
|
|
359
|
+
def initialize(cursor)
|
|
360
|
+
super(cursor, counted: true)
|
|
361
|
+
end
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
# A hash set with a stored entry count.
|
|
365
|
+
class CountedHashSet < HashSet
|
|
366
|
+
def initialize(cursor)
|
|
367
|
+
super(cursor, counted: true)
|
|
368
|
+
end
|
|
369
|
+
end
|
|
370
|
+
end
|
|
371
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
|
|
5
|
+
module XitDB
|
|
6
|
+
module Core
|
|
7
|
+
# Validates the identity and width of a digest implementation.
|
|
8
|
+
class Hasher
|
|
9
|
+
attr_reader :id, :size
|
|
10
|
+
|
|
11
|
+
def initialize(id:, size:, &digest)
|
|
12
|
+
raise TypeError, "id must be a String" unless id.is_a?(String)
|
|
13
|
+
raise ArgumentError, "id must contain four bytes" unless id.bytesize == 4
|
|
14
|
+
raise ArgumentError, "digest block is required" unless digest
|
|
15
|
+
|
|
16
|
+
@id = id.b.freeze
|
|
17
|
+
@size = Format.integer(size, 1..65_535)
|
|
18
|
+
@digest = digest
|
|
19
|
+
freeze
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def digest(bytes)
|
|
23
|
+
raise TypeError, "bytes must be a String" unless bytes.is_a?(String)
|
|
24
|
+
|
|
25
|
+
result = @digest.call(bytes.b.freeze)
|
|
26
|
+
raise TypeError, "digest must return a String" unless result.is_a?(String)
|
|
27
|
+
raise ArgumentError, "digest has incorrect width" unless result.bytesize == size
|
|
28
|
+
|
|
29
|
+
result.b.freeze
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def self.sha1
|
|
33
|
+
new(id: "sha1", size: 20) { |bytes| Digest::SHA1.digest(bytes) }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def self.sha256
|
|
37
|
+
new(id: "sha2", size: 32) { |bytes| Digest::SHA256.digest(bytes) }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def self.sha384
|
|
41
|
+
new(id: "sha2", size: 48) { |bytes| Digest::SHA384.digest(bytes) }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def self.sha512
|
|
45
|
+
new(id: "sha2", size: 64) { |bytes| Digest::SHA512.digest(bytes) }
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module XitDB
|
|
4
|
+
module Core
|
|
5
|
+
# An indexed list backed by a counted B-tree.
|
|
6
|
+
class LinkedArrayList
|
|
7
|
+
include Enumerable
|
|
8
|
+
|
|
9
|
+
attr_reader :cursor
|
|
10
|
+
|
|
11
|
+
def initialize(cursor)
|
|
12
|
+
@cursor = cursor
|
|
13
|
+
@tree = BTree.new(cursor)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def size
|
|
17
|
+
@tree.size
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def get(index)
|
|
21
|
+
@tree.at(index)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def append(slot)
|
|
25
|
+
@tree.insert(size, slot)
|
|
26
|
+
self
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def put(index, slot)
|
|
30
|
+
replace_child(index, slot)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def delete_at(index)
|
|
34
|
+
@tree.delete_at(index)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def slice(start, length)
|
|
38
|
+
self.class.new(@tree.slice(start, length).cursor)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def split(index)
|
|
42
|
+
@tree.split(index).map { |tree| self.class.new(tree.cursor) }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def concat(other)
|
|
46
|
+
raise TypeError, "expected LinkedArrayList" unless other.is_a?(LinkedArrayList)
|
|
47
|
+
|
|
48
|
+
self.class.new(@tree.concat(BTree.new(other.cursor)).cursor)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def replace_child(index, slot, **)
|
|
52
|
+
@tree.replace(index, slot, **)
|
|
53
|
+
self
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def cursor_at(index, create: false)
|
|
57
|
+
slot = get(index)
|
|
58
|
+
if !slot && create
|
|
59
|
+
raise IndexError, "array index outside list" unless index == size
|
|
60
|
+
|
|
61
|
+
append(Slot.new)
|
|
62
|
+
slot = get(index)
|
|
63
|
+
end
|
|
64
|
+
return nil unless slot
|
|
65
|
+
|
|
66
|
+
index += size if index.negative?
|
|
67
|
+
cursor.array_child(index, slot)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def each(&block)
|
|
71
|
+
return enum_for(:each) unless block
|
|
72
|
+
|
|
73
|
+
@tree.each_from_index(0, &block)
|
|
74
|
+
self
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def each_from_index(index, &block)
|
|
78
|
+
return enum_for(:each_from_index, index) unless block
|
|
79
|
+
|
|
80
|
+
@tree.each_from_index(index, &block)
|
|
81
|
+
self
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|