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,361 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module XitDB
|
|
4
|
+
module Core
|
|
5
|
+
# A byte-ordered map backed by a counted B-tree.
|
|
6
|
+
class SortedMap
|
|
7
|
+
include Enumerable
|
|
8
|
+
|
|
9
|
+
attr_reader :cursor
|
|
10
|
+
|
|
11
|
+
def initialize(cursor, set: false)
|
|
12
|
+
@cursor = cursor
|
|
13
|
+
tag = if cursor.slot.tag == :none
|
|
14
|
+
set ? :sorted_set : :sorted_map
|
|
15
|
+
else
|
|
16
|
+
cursor.slot.tag
|
|
17
|
+
end
|
|
18
|
+
raise InvalidValueError, "slot does not contain a sorted collection" unless %i[sorted_map
|
|
19
|
+
sorted_set].include?(tag)
|
|
20
|
+
|
|
21
|
+
@tree = Tree.new(cursor, tag: tag)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def size
|
|
25
|
+
@tree.size
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def get(key_bytes)
|
|
29
|
+
entry = @tree.find(key_bytes)
|
|
30
|
+
@tree.record(entry).last if entry
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def include?(key_bytes)
|
|
34
|
+
!@tree.find(key_bytes).nil?
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def put(key_bytes, value:)
|
|
38
|
+
replace_child(key_bytes, value)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def add(key_bytes)
|
|
42
|
+
@tree.store(key_bytes, key_only: true)
|
|
43
|
+
self
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def remove(key_bytes)
|
|
47
|
+
@tree.remove(key_bytes)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def rank(key_bytes)
|
|
51
|
+
@tree.locate(key_bytes).last
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def at(index)
|
|
55
|
+
entry = @tree.at(index)
|
|
56
|
+
decode(entry) if entry
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def each(&block)
|
|
60
|
+
return enum_for(:each) unless block
|
|
61
|
+
|
|
62
|
+
each_from_index(0, &block)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def each_from(key_bytes, &block)
|
|
66
|
+
return enum_for(:each_from, key_bytes) unless block
|
|
67
|
+
|
|
68
|
+
each_from_index(rank(key_bytes), &block)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def each_from_index(index)
|
|
72
|
+
return enum_for(:each_from_index, index) unless block_given?
|
|
73
|
+
|
|
74
|
+
@tree.each_from_index(index) { |entry| yield decode(entry) }
|
|
75
|
+
self
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def cursor_at(key_bytes, create: false)
|
|
79
|
+
slot = get(key_bytes)
|
|
80
|
+
if !slot && create
|
|
81
|
+
add(key_bytes)
|
|
82
|
+
slot = get(key_bytes)
|
|
83
|
+
end
|
|
84
|
+
cursor.hash_child(key_bytes.b.freeze, slot) if slot
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def replace_child(key_bytes, slot, **)
|
|
88
|
+
@tree.store(key_bytes, value: slot, **)
|
|
89
|
+
self
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
private
|
|
93
|
+
|
|
94
|
+
def decode(entry)
|
|
95
|
+
key, value = @tree.record(entry)
|
|
96
|
+
[cursor.database.read_bytes(key, bound: cursor.bound).bytes, value]
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Maintains sorted records and branch separators.
|
|
100
|
+
class Tree < BTree
|
|
101
|
+
BRANCH_SIZE = 418
|
|
102
|
+
# A mutable sorted node and its separators.
|
|
103
|
+
class Node
|
|
104
|
+
attr_accessor :kind, :slots, :counts, :separators
|
|
105
|
+
|
|
106
|
+
def initialize(kind, slots, counts, separators)
|
|
107
|
+
@kind = kind
|
|
108
|
+
@slots = slots
|
|
109
|
+
@counts = counts
|
|
110
|
+
@separators = separators
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def size
|
|
114
|
+
kind.zero? ? slots.size : counts.sum
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def record(entry)
|
|
119
|
+
return [entry, Slot.new] if %i[bytes short_bytes].include?(entry.tag)
|
|
120
|
+
raise CorruptionError, "invalid sorted entry" unless %i[kv_pair sorted_kv_pair].include?(entry.tag)
|
|
121
|
+
|
|
122
|
+
offset = entry.value + (entry.tag == :kv_pair ? cursor.database.header.hash_size : 0)
|
|
123
|
+
bytes = read(offset, 18)
|
|
124
|
+
key = Slot.parse(Format.slice(bytes, 0, 9))
|
|
125
|
+
value = Slot.parse(Format.slice(bytes, 9, 9))
|
|
126
|
+
unless %i[bytes short_bytes].include?(key.tag) && Slot::TAGS.include?(value.tag) && value.tag != :index
|
|
127
|
+
raise CorruptionError, "invalid sorted record slots"
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
[key, value]
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def find(key)
|
|
134
|
+
_path, node, index, found, = locate(key)
|
|
135
|
+
node.slots[index] if found
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def locate(key)
|
|
139
|
+
raise TypeError, "sorted key must be a String" unless key.is_a?(String)
|
|
140
|
+
|
|
141
|
+
key = key.b
|
|
142
|
+
count, pointer = header
|
|
143
|
+
# @type var path: sorted_path
|
|
144
|
+
path = []
|
|
145
|
+
# @type var seen: Hash[Integer?, bool]
|
|
146
|
+
seen = {}
|
|
147
|
+
rank = 0
|
|
148
|
+
loop do
|
|
149
|
+
raise CorruptionError, "cyclic sorted branch" if seen[pointer]
|
|
150
|
+
|
|
151
|
+
seen[pointer] = true
|
|
152
|
+
node = pointer ? read_node(pointer, count) : Node.new(0, [], [], [])
|
|
153
|
+
if node.kind.zero?
|
|
154
|
+
index = lower_bound(node.slots.size) { |i| key_bytes(record(node.slots[i]).first) >= key }
|
|
155
|
+
found = index < node.slots.size && key_bytes(record(node.slots[index]).first) == key
|
|
156
|
+
return [path, node, index, found, rank + index]
|
|
157
|
+
end
|
|
158
|
+
index = lower_bound(node.slots.size - 1) { |i| key_bytes(node.separators[i + 1]) > key }
|
|
159
|
+
raise CorruptionError, "missing sorted branch pointer" unless pointer
|
|
160
|
+
|
|
161
|
+
path << [pointer, node, index]
|
|
162
|
+
rank += node.counts.first(index).sum
|
|
163
|
+
pointer = node.slots[index].value
|
|
164
|
+
count = node.counts[index]
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def store(key, value: nil, key_only: false, owner: nil, collection: false, propagate: true)
|
|
169
|
+
unless key_only
|
|
170
|
+
raise TypeError, "expected Slot" unless value
|
|
171
|
+
|
|
172
|
+
validate_slot(value)
|
|
173
|
+
end
|
|
174
|
+
mutate do |transaction|
|
|
175
|
+
count, pointer = header
|
|
176
|
+
path, node, index, found, = locate(key)
|
|
177
|
+
next if found && key_only
|
|
178
|
+
raise RangeError, "sorted size exceeds unsigned 64-bit range" if !found && count == Format::UINT64_MAX
|
|
179
|
+
|
|
180
|
+
store_leaf(transaction, key, node, index, found, key_only, value)
|
|
181
|
+
root = write_path(transaction, path, node, pointer_for(path, pointer))
|
|
182
|
+
publish(transaction, count + (found ? 0 : 1), root, propagate: propagate)
|
|
183
|
+
cursor.invalidate_children(index: key.b, except: owner) unless collection || key_only
|
|
184
|
+
end
|
|
185
|
+
self
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def remove(key)
|
|
189
|
+
mutate do |transaction|
|
|
190
|
+
count, pointer = header
|
|
191
|
+
path, node, index, found, = locate(key)
|
|
192
|
+
next false unless found
|
|
193
|
+
|
|
194
|
+
node.slots.delete_at(index)
|
|
195
|
+
old_pointer = pointer_for(path, pointer)
|
|
196
|
+
path.reverse_each do |parent_pointer, parent, child_index|
|
|
197
|
+
if node.slots.empty?
|
|
198
|
+
parent.slots.delete_at(child_index)
|
|
199
|
+
parent.counts.delete_at(child_index)
|
|
200
|
+
parent.separators.delete_at(child_index)
|
|
201
|
+
parent.separators[0] = Slot.new unless parent.separators.empty?
|
|
202
|
+
else
|
|
203
|
+
parent.slots[child_index] = Slot.new(tag: :index, value: write_node(transaction, node, old_pointer))
|
|
204
|
+
parent.counts[child_index] = node.size
|
|
205
|
+
end
|
|
206
|
+
node = parent
|
|
207
|
+
old_pointer = parent_pointer
|
|
208
|
+
end
|
|
209
|
+
root_pointer = collapse_sorted_root(transaction, node, old_pointer, count - 1)
|
|
210
|
+
publish(transaction, count - 1, root_pointer)
|
|
211
|
+
cursor.invalidate_children(index: key.b)
|
|
212
|
+
true
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
private
|
|
217
|
+
|
|
218
|
+
def store_leaf(transaction, key, node, index, found, key_only, value)
|
|
219
|
+
key_slot = found ? record(node.slots[index]).first : cursor.database.bytes(key)
|
|
220
|
+
entry = if key_only
|
|
221
|
+
key_slot
|
|
222
|
+
else
|
|
223
|
+
raise TypeError, "expected Slot" unless value
|
|
224
|
+
|
|
225
|
+
Slot.new(tag: :sorted_kv_pair, value: transaction.append(key_slot.to_bytes + value.to_bytes))
|
|
226
|
+
end
|
|
227
|
+
found ? node.slots[index] = entry : node.slots.insert(index, entry)
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def write_path(transaction, path, node, pointer)
|
|
231
|
+
result = split_node(transaction, node, pointer)
|
|
232
|
+
path.reverse_each do |parent_pointer, parent, child_index|
|
|
233
|
+
parent.slots[child_index, 1] = result.map { |ptr, _, _| Slot.new(tag: :index, value: ptr) }
|
|
234
|
+
parent.counts[child_index, 1] = result.map { |_, size, _| size }
|
|
235
|
+
parent.separators.insert(child_index + 1, split_separator(result)) if result.size == 2
|
|
236
|
+
result = split_node(transaction, parent, parent_pointer)
|
|
237
|
+
end
|
|
238
|
+
return result.first.first if result.size == 1
|
|
239
|
+
|
|
240
|
+
write_node(transaction, Node.new(1, result.map { |ptr, _, _| Slot.new(tag: :index, value: ptr) },
|
|
241
|
+
result.map { |_, size, _| size }, [Slot.new, split_separator(result)]))
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def collapse_sorted_root(transaction, node, old_pointer, count)
|
|
245
|
+
unless node.kind == 1 && node.slots.size == 1
|
|
246
|
+
node = Node.new(0, [], [], []) if node.slots.empty?
|
|
247
|
+
return write_node(transaction, node, old_pointer)
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
pointer = node.slots.first.value
|
|
251
|
+
# @type var seen: Hash[Integer, bool]
|
|
252
|
+
seen = {}
|
|
253
|
+
loop do
|
|
254
|
+
raise CorruptionError, "cyclic sorted branch" if seen[pointer]
|
|
255
|
+
|
|
256
|
+
seen[pointer] = true
|
|
257
|
+
child = read_node(pointer, count)
|
|
258
|
+
return pointer unless child.kind == 1 && child.slots.size == 1
|
|
259
|
+
|
|
260
|
+
pointer = child.slots.first.value
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def split_separator(result)
|
|
265
|
+
result.last.last || raise(CorruptionError, "missing sorted split separator")
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def lower_bound(size)
|
|
269
|
+
left = 0
|
|
270
|
+
right = size
|
|
271
|
+
while left < right
|
|
272
|
+
middle = (left + right) / 2
|
|
273
|
+
yield(middle) ? right = middle : left = middle + 1
|
|
274
|
+
end
|
|
275
|
+
left
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def key_bytes(slot)
|
|
279
|
+
raise CorruptionError, "invalid sorted key" unless %i[bytes short_bytes].include?(slot.tag)
|
|
280
|
+
|
|
281
|
+
cursor.database.read_bytes(slot, bound: cursor.bound).bytes
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def read_node(pointer, expected, bound: cursor.bound)
|
|
285
|
+
kind, num = Format.pair(read(pointer, 2, bound: bound), "CC")
|
|
286
|
+
validate_header(kind, num)
|
|
287
|
+
|
|
288
|
+
bytes = read(pointer, kind.zero? ? LEAF_SIZE : BRANCH_SIZE, bound: bound)
|
|
289
|
+
slots = Array.new(num) { |i| Slot.parse(Format.slice(bytes, 2 + (i * 9), 9)) }
|
|
290
|
+
# @type var separators: Array[Slot]
|
|
291
|
+
separators = kind.zero? ? [] : Array.new(num) { |i| Slot.parse(Format.slice(bytes, LEAF_SIZE + (i * 9), 9)) }
|
|
292
|
+
# @type var counts: Array[Integer]
|
|
293
|
+
counts = kind.zero? ? [] : Format.integers(Format.slice(bytes, 290, 128), "Q>*").first(num)
|
|
294
|
+
node = Node.new(kind, slots, counts, separators)
|
|
295
|
+
raise CorruptionError, "invalid sorted node" unless valid_node?(node) && node.size == expected
|
|
296
|
+
|
|
297
|
+
node
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def validate_header(kind, count)
|
|
301
|
+
return if [0, 1].include?(kind) && count <= CAPACITY && (kind.zero? || count.positive?)
|
|
302
|
+
|
|
303
|
+
raise CorruptionError, "invalid sorted node header"
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
def valid_node?(node)
|
|
307
|
+
if node.kind.zero?
|
|
308
|
+
tags = %i[bytes short_bytes kv_pair sorted_kv_pair]
|
|
309
|
+
node.slots.all? { |slot| tags.include?(slot.tag) }
|
|
310
|
+
else
|
|
311
|
+
tags = %i[bytes short_bytes]
|
|
312
|
+
node.slots.all? { |slot| slot.tag == :index } && node.separators.drop(1).all? do |slot|
|
|
313
|
+
tags.include?(slot.tag)
|
|
314
|
+
end
|
|
315
|
+
end
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def write_node(transaction, node, old_pointer = nil)
|
|
319
|
+
bytes = [node.kind, node.slots.size].pack("CC")
|
|
320
|
+
bytes << padded_slots(node.slots)
|
|
321
|
+
unless node.kind.zero?
|
|
322
|
+
raise CorruptionError, "invalid sorted branch" unless node.is_a?(Node)
|
|
323
|
+
|
|
324
|
+
bytes << padded_slots(node.separators)
|
|
325
|
+
bytes << (node.counts + ([0] * (CAPACITY - node.counts.size))).pack("Q>*")
|
|
326
|
+
end
|
|
327
|
+
if old_pointer && old_pointer >= transaction.mutable_start
|
|
328
|
+
transaction.write_at(old_pointer, bytes)
|
|
329
|
+
old_pointer
|
|
330
|
+
else
|
|
331
|
+
transaction.append(bytes)
|
|
332
|
+
end
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
def padded_slots(slots)
|
|
336
|
+
slots.map(&:to_bytes).join + ("\0".b * ((CAPACITY - slots.size) * 9))
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
def split_node(transaction, node, old_pointer)
|
|
340
|
+
return [[write_node(transaction, node, old_pointer), node.size, nil]] if node.slots.size <= CAPACITY
|
|
341
|
+
|
|
342
|
+
separator = node.kind.zero? ? record(node.slots[SPLIT_COUNT]).first : node.separators[SPLIT_COUNT]
|
|
343
|
+
left = Node.new(node.kind, node.slots.first(SPLIT_COUNT), node.counts.first(SPLIT_COUNT),
|
|
344
|
+
node.separators.first(SPLIT_COUNT))
|
|
345
|
+
right = Node.new(node.kind, node.slots.drop(SPLIT_COUNT), node.counts.drop(SPLIT_COUNT),
|
|
346
|
+
node.separators.drop(SPLIT_COUNT))
|
|
347
|
+
[[write_node(transaction, left, old_pointer), left.size, nil],
|
|
348
|
+
[write_node(transaction, right), right.size, separator]]
|
|
349
|
+
end
|
|
350
|
+
end
|
|
351
|
+
private_constant :Tree
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
# A byte-ordered collection of keys.
|
|
355
|
+
class SortedSet < SortedMap
|
|
356
|
+
def initialize(cursor)
|
|
357
|
+
super(cursor, set: true)
|
|
358
|
+
end
|
|
359
|
+
end
|
|
360
|
+
end
|
|
361
|
+
end
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module XitDB
|
|
4
|
+
module Core
|
|
5
|
+
# Owns writable cursors and their rollback checkpoints.
|
|
6
|
+
class Transaction
|
|
7
|
+
Checkpoint = Struct.new(:owner, :slots, :length, :cancelled, keyword_init: true) # rubocop:disable Lint/StructNewOverride -- Length is the saved store boundary.
|
|
8
|
+
private_constant :Checkpoint
|
|
9
|
+
|
|
10
|
+
attr_reader :root, :mutable_start
|
|
11
|
+
|
|
12
|
+
def initialize(database, store, slot)
|
|
13
|
+
@database = database
|
|
14
|
+
@store = store
|
|
15
|
+
@owner = Thread.current
|
|
16
|
+
@active = true
|
|
17
|
+
@cancelled = false
|
|
18
|
+
@cursors = ObjectSpace::WeakMap.new
|
|
19
|
+
@checkpoints = []
|
|
20
|
+
@mutable_start = store.length
|
|
21
|
+
@root = cursor(slot: slot)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def check_active!
|
|
25
|
+
@database.check_open!
|
|
26
|
+
raise ExpiredViewError, "transaction has ended" unless @active
|
|
27
|
+
raise TransactionStateError, "transaction belongs to another thread" unless @owner == Thread.current
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def check_logical_mutation!
|
|
31
|
+
check_active!
|
|
32
|
+
raise TransactionStateError, "a logical mutation is already active" if @logical_mutation
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def with_logical_mutation
|
|
36
|
+
check_logical_mutation!
|
|
37
|
+
@logical_mutation = true
|
|
38
|
+
begin
|
|
39
|
+
yield
|
|
40
|
+
ensure
|
|
41
|
+
@logical_mutation = false
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def cursor(slot:)
|
|
46
|
+
check_active!
|
|
47
|
+
validate_slot(slot)
|
|
48
|
+
cursor = Cursor.new(database: @database, slot: slot, bound: length, transaction: self)
|
|
49
|
+
@cursors[cursor] = true
|
|
50
|
+
cursor
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def check_cursor!(cursor)
|
|
54
|
+
check_active!
|
|
55
|
+
raise ExpiredViewError, "cursor was discarded by restore" unless @cursors.key?(cursor)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def check_replacement!(cursor, value)
|
|
59
|
+
check_cursor!(cursor)
|
|
60
|
+
validate_slot(value)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def length
|
|
64
|
+
check_active!
|
|
65
|
+
@store.length
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def append(bytes)
|
|
69
|
+
check_active!
|
|
70
|
+
@store.append(bytes)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def read_at(offset, size)
|
|
74
|
+
check_active!
|
|
75
|
+
@database.read_at(offset, size, bound: length)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Collections must copy blocks below this boundary before changing them.
|
|
79
|
+
def write_at(offset, bytes)
|
|
80
|
+
check_active!
|
|
81
|
+
raise TransactionStateError, "write requires copy-on-write" if offset < @mutable_start
|
|
82
|
+
raise RangeError, "write exceeds allocated data" if offset + bytes.bytesize > length
|
|
83
|
+
|
|
84
|
+
@store.write_at(offset, bytes)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def freeze
|
|
88
|
+
check_active!
|
|
89
|
+
@mutable_start = length
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def checkpoint
|
|
93
|
+
check_active!
|
|
94
|
+
# @type var slots: Hash[Cursor, cursor_state]
|
|
95
|
+
slots = {}
|
|
96
|
+
@cursors.each_key { |cursor| slots[cursor] = cursor.checkpoint_state }
|
|
97
|
+
checkpoint = Checkpoint.new(owner: self, slots: slots, length: freeze, cancelled: @cancelled).freeze
|
|
98
|
+
@checkpoints << checkpoint
|
|
99
|
+
checkpoint
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def restore(checkpoint)
|
|
103
|
+
check_active!
|
|
104
|
+
index = @checkpoints.index { |item| item.equal?(checkpoint) }
|
|
105
|
+
unless index && checkpoint.owner.equal?(self) && checkpoint.length <= length
|
|
106
|
+
raise TransactionStateError, "checkpoint does not belong to this transaction state"
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
@store.truncate(checkpoint.length)
|
|
110
|
+
@cursors = ObjectSpace::WeakMap.new
|
|
111
|
+
checkpoint.slots.each do |cursor, state|
|
|
112
|
+
@cursors[cursor] = true
|
|
113
|
+
cursor.restore_checkpoint_state(state)
|
|
114
|
+
end
|
|
115
|
+
@mutable_start = checkpoint.length
|
|
116
|
+
@cancelled = checkpoint.cancelled
|
|
117
|
+
(@checkpoints.slice!(index..) || []).each { |item| item.slots.clear }
|
|
118
|
+
nil
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Internal operations release rollback state after a successful mutation.
|
|
122
|
+
def release_checkpoint(checkpoint)
|
|
123
|
+
check_active!
|
|
124
|
+
unless checkpoint.is_a?(Checkpoint) && checkpoint.owner.equal?(self)
|
|
125
|
+
raise TransactionStateError, "checkpoint belongs to another transaction"
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
@checkpoints.delete_if { |item| item.equal?(checkpoint) }
|
|
129
|
+
checkpoint.slots.clear
|
|
130
|
+
nil
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def cancel_publication
|
|
134
|
+
check_active!
|
|
135
|
+
@cancelled = true
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def cancelled?
|
|
139
|
+
check_active!
|
|
140
|
+
@cancelled
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def expire!
|
|
144
|
+
@active = false
|
|
145
|
+
@checkpoints.each { |checkpoint| checkpoint.slots.clear }
|
|
146
|
+
@checkpoints.clear
|
|
147
|
+
@cursors = ObjectSpace::WeakMap.new
|
|
148
|
+
@root = nil
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
private
|
|
152
|
+
|
|
153
|
+
def validate_slot(slot)
|
|
154
|
+
raise TypeError, "expected Slot" unless slot.is_a?(Slot)
|
|
155
|
+
raise CorruptionError, "unknown slot tag" unless Slot::TAGS.include?(slot.tag)
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
data/lib/xitdb/core.rb
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "errors"
|
|
4
|
+
require_relative "core/format"
|
|
5
|
+
require_relative "core/hasher"
|
|
6
|
+
require_relative "core/crc64"
|
|
7
|
+
require_relative "core/memory_store"
|
|
8
|
+
require_relative "core/file_store"
|
|
9
|
+
require_relative "core/bytes"
|
|
10
|
+
require_relative "core/cursor"
|
|
11
|
+
require_relative "core/transaction"
|
|
12
|
+
require_relative "core/database"
|
|
13
|
+
require_relative "core/array_list"
|
|
14
|
+
require_relative "core/b_tree"
|
|
15
|
+
require_relative "core/linked_array_list"
|
|
16
|
+
require_relative "core/hash_map"
|
|
17
|
+
require_relative "core/sorted_map"
|