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,181 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module XitDB
|
|
4
|
+
module Core
|
|
5
|
+
# Reads and writes inline or external byte payloads.
|
|
6
|
+
module Bytes
|
|
7
|
+
CHUNK_SIZE = 64 * 1024
|
|
8
|
+
|
|
9
|
+
# A bounded reader for one immutable byte payload.
|
|
10
|
+
class Source
|
|
11
|
+
attr_reader :size, :format_tag
|
|
12
|
+
|
|
13
|
+
def initialize(database, location, size, format_tag, bound)
|
|
14
|
+
@database = database
|
|
15
|
+
@location = location
|
|
16
|
+
@size = size
|
|
17
|
+
@format_tag = format_tag
|
|
18
|
+
@bound = bound
|
|
19
|
+
freeze
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def read(position, length)
|
|
23
|
+
@database.check_open!
|
|
24
|
+
unless position.is_a?(Integer) && length.is_a?(Integer)
|
|
25
|
+
raise TypeError, "byte positions and lengths must be Integers"
|
|
26
|
+
end
|
|
27
|
+
if position.negative? || length.negative? || position + length > size
|
|
28
|
+
raise InvalidValueError, "truncated byte payload"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
location = @location
|
|
32
|
+
if location.is_a?(String)
|
|
33
|
+
Format.slice(location, position, length)
|
|
34
|
+
else
|
|
35
|
+
@database.read_at(location + position, length, bound: @bound)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
private_constant :Source
|
|
40
|
+
|
|
41
|
+
def self.open(database, slot, bound:)
|
|
42
|
+
location, size, format_tag = parse(database, slot, bound)
|
|
43
|
+
Source.new(database, location, size, format_tag, bound)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def self.append(transaction, size:, format_tag: nil, prefix: "".b, representation: :automatic, &reader)
|
|
47
|
+
transaction.check_active!
|
|
48
|
+
validate_source(size, prefix, representation, format_tag)
|
|
49
|
+
format_tag = format_tag&.b
|
|
50
|
+
total_size = prefix.bytesize + size
|
|
51
|
+
raise RangeError, "byte payload exceeds unsigned 64-bit size" if total_size >= (1 << 64)
|
|
52
|
+
|
|
53
|
+
capacity = format_tag ? 6 : 8
|
|
54
|
+
captured = capture_inline(size, prefix, representation, capacity, &reader)
|
|
55
|
+
if captured && !captured.include?("\0")
|
|
56
|
+
payload = captured.ljust(capacity, "\0") + (format_tag || "")
|
|
57
|
+
return Slot.new(tag: :short_bytes, value: Format.unsigned(payload), full: !format_tag.nil?)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
offset = transaction.append([total_size].pack("Q>"))
|
|
61
|
+
if captured
|
|
62
|
+
transaction.append(captured)
|
|
63
|
+
else
|
|
64
|
+
append_source(transaction, prefix, size, &reader)
|
|
65
|
+
end
|
|
66
|
+
transaction.append(format_tag) if format_tag
|
|
67
|
+
Slot.new(tag: :bytes, value: offset, full: !format_tag.nil?)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def self.validate_source(size, prefix, representation, format_tag)
|
|
71
|
+
raise TypeError, "size must be an Integer" unless size.is_a?(Integer)
|
|
72
|
+
raise ArgumentError, "size must be nonnegative" if size.negative?
|
|
73
|
+
raise TypeError, "prefix must be a String" unless prefix.is_a?(String)
|
|
74
|
+
raise ArgumentError, "unknown byte representation" unless %i[automatic external].include?(representation)
|
|
75
|
+
return if format_tag.nil? || (format_tag.is_a?(String) && format_tag.bytesize == 2)
|
|
76
|
+
|
|
77
|
+
raise ArgumentError, "format_tag must contain two bytes"
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def self.capture_inline(size, prefix, representation, capacity, &)
|
|
81
|
+
return nil unless representation == :automatic && prefix.bytesize + size <= capacity
|
|
82
|
+
|
|
83
|
+
prefix.b + (size.zero? ? "".b : read_chunk(0, size, &))
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def self.append_source(transaction, prefix, size, &)
|
|
87
|
+
position = 0
|
|
88
|
+
while position < prefix.bytesize
|
|
89
|
+
chunk = Format.slice(prefix, position, CHUNK_SIZE).b
|
|
90
|
+
transaction.append(chunk)
|
|
91
|
+
position += chunk.bytesize
|
|
92
|
+
end
|
|
93
|
+
position = 0
|
|
94
|
+
while position < size
|
|
95
|
+
length = [size - position, CHUNK_SIZE].min
|
|
96
|
+
transaction.append(read_chunk(position, length, &))
|
|
97
|
+
position += length
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
private_class_method :validate_source, :capture_inline, :append_source
|
|
101
|
+
|
|
102
|
+
def self.read_chunk(position, length)
|
|
103
|
+
chunk = yield(position, length) if block_given?
|
|
104
|
+
raise EOFError, "byte source ended before its declared size" if chunk.nil?
|
|
105
|
+
raise TypeError, "byte source must return a String" unless chunk.is_a?(String)
|
|
106
|
+
raise EOFError, "byte source ended before its declared size" if chunk.bytesize < length
|
|
107
|
+
raise ArgumentError, "byte source returned more than requested" if chunk.bytesize > length
|
|
108
|
+
|
|
109
|
+
chunk.b
|
|
110
|
+
end
|
|
111
|
+
private_class_method :read_chunk
|
|
112
|
+
|
|
113
|
+
def self.parse(database, slot, bound)
|
|
114
|
+
database.check_open!
|
|
115
|
+
case slot.tag
|
|
116
|
+
when :short_bytes
|
|
117
|
+
payload = [slot.value].pack("Q>")
|
|
118
|
+
capacity = slot.full ? 6 : 8
|
|
119
|
+
data = (Format.slice(payload, 0, capacity).split("\0", 2).first || "".b).freeze
|
|
120
|
+
tag = Format.slice(payload, 6, 2).freeze if slot.full
|
|
121
|
+
[data, data.bytesize, tag]
|
|
122
|
+
when :bytes
|
|
123
|
+
size = Format.unsigned(database.read_at(slot.value, 8, bound: bound))
|
|
124
|
+
offset = slot.value + 8
|
|
125
|
+
tail = database.read_at(offset + size, slot.full ? 2 : 0, bound: bound)
|
|
126
|
+
tag = tail.freeze if slot.full
|
|
127
|
+
[offset, size, tag]
|
|
128
|
+
else
|
|
129
|
+
raise InvalidValueError, "slot does not contain bytes"
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
private_class_method :parse
|
|
133
|
+
|
|
134
|
+
# Return inline data or its file offset, without materializing a scalar.
|
|
135
|
+
def self.locate(database, slot, bound:)
|
|
136
|
+
unless %i[bytes short_bytes].include?(slot.tag) && !slot.full
|
|
137
|
+
raise InvalidValueError, "stream requires untagged bytes"
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
location, size = parse(database, slot, bound)
|
|
141
|
+
[location.is_a?(String) ? location.dup : location, size]
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def self.copy(source, size:, database:, transaction:)
|
|
145
|
+
if size <= 8
|
|
146
|
+
bytes = source.read(size)
|
|
147
|
+
raise EOFError, "byte source ended before its declared size" unless bytes
|
|
148
|
+
|
|
149
|
+
return database.bytes(bytes)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
append(transaction, size: size, representation: :external) do |_position, length|
|
|
153
|
+
chunk = "".b
|
|
154
|
+
while chunk.bytesize < length
|
|
155
|
+
part = source.read(length - chunk.bytesize)
|
|
156
|
+
raise EOFError, "byte source ended before its declared size" if part.nil? || part.empty?
|
|
157
|
+
|
|
158
|
+
chunk << part
|
|
159
|
+
end
|
|
160
|
+
chunk
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# An immutable byte payload and its optional format tag.
|
|
166
|
+
class ByteValue
|
|
167
|
+
attr_reader :bytes, :format_tag
|
|
168
|
+
|
|
169
|
+
def initialize(bytes, format_tag: nil)
|
|
170
|
+
raise TypeError, "bytes must be a String" unless bytes.is_a?(String)
|
|
171
|
+
unless format_tag.nil? || (format_tag.is_a?(String) && format_tag.bytesize == 2)
|
|
172
|
+
raise ArgumentError, "format_tag must contain two bytes"
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
@bytes = bytes.b.freeze
|
|
176
|
+
@format_tag = format_tag&.b&.freeze
|
|
177
|
+
freeze
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "offset_map"
|
|
4
|
+
|
|
5
|
+
module XitDB
|
|
6
|
+
module Core
|
|
7
|
+
# Copies reachable records into a new database.
|
|
8
|
+
class Compaction
|
|
9
|
+
INLINE_TAGS = %i[none uint int float short_bytes].freeze
|
|
10
|
+
BLOCK_SIZE = 16 * Slot::SIZE
|
|
11
|
+
|
|
12
|
+
def initialize(source, cursor, transaction, offsets)
|
|
13
|
+
@source = source
|
|
14
|
+
@bound = cursor.bound
|
|
15
|
+
@transaction = transaction
|
|
16
|
+
@offsets = offsets
|
|
17
|
+
@stack = []
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def copy(slot)
|
|
21
|
+
result = remap(slot)
|
|
22
|
+
@stack.pop&.call until @stack.empty?
|
|
23
|
+
result
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def read(offset, size)
|
|
29
|
+
@source.read_at(offset, size, bound: @bound)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def parse(bytes)
|
|
33
|
+
slot = Slot.parse(bytes)
|
|
34
|
+
raise CorruptionError, "unknown compaction slot tag" unless Slot::TAGS.include?(slot.tag)
|
|
35
|
+
|
|
36
|
+
slot
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def remap(slot)
|
|
40
|
+
return slot if INLINE_TAGS.include?(slot.tag)
|
|
41
|
+
|
|
42
|
+
Slot.new(tag: slot.tag, full: slot.full, value: remap_pointer(slot))
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def remap_pointer(slot)
|
|
46
|
+
case slot.tag
|
|
47
|
+
when :bytes then copy_bytes(slot)
|
|
48
|
+
when :index, :hash_map, :hash_set then reserve_slots(slot.value, BLOCK_SIZE, 0, 16)
|
|
49
|
+
when :counted_hash_map, :counted_hash_set then reserve_slots(slot.value, BLOCK_SIZE + 8, 8, 16)
|
|
50
|
+
when :array_list then reserve(slot.value, 16) { |source, target| array_header(source, target) }
|
|
51
|
+
when :linked_array_list then reserve(slot.value, 16) do |source, target|
|
|
52
|
+
tree_header(source, target, false)
|
|
53
|
+
end
|
|
54
|
+
when :sorted_map, :sorted_set then reserve(slot.value, 16) do |source, target|
|
|
55
|
+
tree_header(source, target, true)
|
|
56
|
+
end
|
|
57
|
+
when :kv_pair then reserve_slots(slot.value, @source.header.hash_size + 18,
|
|
58
|
+
@source.header.hash_size, 2)
|
|
59
|
+
when :sorted_kv_pair then reserve_slots(slot.value, 18, 0, 2)
|
|
60
|
+
else raise CorruptionError, "unknown compaction slot tag"
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def reserve_slots(source, size, start, count)
|
|
65
|
+
reserve(source, size) { |origin, target| slots(origin, target, start, count) }
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def reserve(source, size, cache: true)
|
|
69
|
+
mapped = @offsets.fetch(source) if cache
|
|
70
|
+
return mapped if mapped
|
|
71
|
+
|
|
72
|
+
bytes = read(source, size)
|
|
73
|
+
target = @transaction.append(bytes)
|
|
74
|
+
@offsets.store(source, target) if cache
|
|
75
|
+
@stack << -> { yield(source, target) }
|
|
76
|
+
target
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def copy_bytes(slot)
|
|
80
|
+
mapped = @offsets.fetch(slot.value)
|
|
81
|
+
return mapped if mapped
|
|
82
|
+
|
|
83
|
+
source = Bytes.open(@source, slot, bound: @bound)
|
|
84
|
+
copied = Bytes.append(@transaction, size: source.size, format_tag: source.format_tag,
|
|
85
|
+
representation: :external) do |position, length|
|
|
86
|
+
source.read(position,
|
|
87
|
+
length)
|
|
88
|
+
end
|
|
89
|
+
target = copied.value
|
|
90
|
+
@offsets.store(slot.value, target)
|
|
91
|
+
target
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def slots(source, target, start, count)
|
|
95
|
+
count.times do |index|
|
|
96
|
+
offset = start + (index * Slot::SIZE)
|
|
97
|
+
slot = parse(read(source + offset, Slot::SIZE))
|
|
98
|
+
@transaction.write_at(target + offset, remap(slot).to_bytes)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def array_header(source, target)
|
|
103
|
+
size, pointer = Format.pair(read(source, 16), "Q>Q>")
|
|
104
|
+
shift = [((size - 1).bit_length - 1) / 4, 0].max
|
|
105
|
+
mapped = array_index(pointer, size, shift)
|
|
106
|
+
@transaction.write_at(target, [size, mapped].pack("Q>Q>"))
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def array_index(source, size, shift)
|
|
110
|
+
return reserve_slots(source, BLOCK_SIZE, 0, 16) if size == 1 << ((shift + 1) * 4)
|
|
111
|
+
|
|
112
|
+
reserve(source, BLOCK_SIZE, cache: false) { |origin, target| partial_array(origin, target, size, shift) }
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def partial_array(source, target, size, shift)
|
|
116
|
+
# Partial blocks can be shared by arrays with different visible sizes.
|
|
117
|
+
# Copy them separately and clear every slot outside this array's size.
|
|
118
|
+
@transaction.write_at(target, "\0".b * BLOCK_SIZE)
|
|
119
|
+
child_size = 1 << (shift * 4)
|
|
120
|
+
16.times do |index|
|
|
121
|
+
break unless size.positive?
|
|
122
|
+
|
|
123
|
+
slot = parse(read(source + (index * Slot::SIZE), Slot::SIZE))
|
|
124
|
+
mapped = if shift.zero? || slot.empty?
|
|
125
|
+
remap(slot)
|
|
126
|
+
else
|
|
127
|
+
raise CorruptionError, "invalid array branch" unless slot.tag == :index
|
|
128
|
+
|
|
129
|
+
Slot.new(tag: :index, full: slot.full,
|
|
130
|
+
value: array_index(slot.value, [size, child_size].min, shift - 1))
|
|
131
|
+
end
|
|
132
|
+
@transaction.write_at(target + (index * Slot::SIZE), mapped.to_bytes)
|
|
133
|
+
size -= [size, child_size].min
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def tree_header(source, target, sorted)
|
|
138
|
+
size, pointer = Format.pair(read(source, 16), "Q>Q>")
|
|
139
|
+
@transaction.write_at(target, [size, tree_node(pointer, sorted)].pack("Q>Q>"))
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def tree_node(source, sorted)
|
|
143
|
+
mapped = @offsets.fetch(source)
|
|
144
|
+
return mapped if mapped
|
|
145
|
+
|
|
146
|
+
kind, count = Format.pair(read(source, 2), "CC")
|
|
147
|
+
raise CorruptionError, "invalid compaction B-tree node" unless kind <= 1 && count <= 16
|
|
148
|
+
|
|
149
|
+
size = 2 + BLOCK_SIZE + (if kind == 1
|
|
150
|
+
128 + (sorted ? BLOCK_SIZE : 0)
|
|
151
|
+
else
|
|
152
|
+
0
|
|
153
|
+
end)
|
|
154
|
+
if kind.zero?
|
|
155
|
+
reserve_slots(source, size, 2, 16)
|
|
156
|
+
else
|
|
157
|
+
reserve(source, size) { |origin, target| tree_branch(origin, target, sorted) }
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def tree_branch(source, target, sorted)
|
|
162
|
+
16.times do |index|
|
|
163
|
+
offset = 2 + (index * Slot::SIZE)
|
|
164
|
+
slot = parse(read(source + offset, Slot::SIZE))
|
|
165
|
+
slot = Slot.new(tag: :index, full: slot.full, value: tree_node(slot.value, sorted)) if slot.tag == :index
|
|
166
|
+
@transaction.write_at(target + offset, slot.to_bytes)
|
|
167
|
+
end
|
|
168
|
+
slots(source, target, 2 + BLOCK_SIZE, 16) if sorted
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module XitDB
|
|
4
|
+
module Core
|
|
5
|
+
# Calculates the checksum used by root trailers.
|
|
6
|
+
module CRC64
|
|
7
|
+
MASK = 0xffffffffffffffff
|
|
8
|
+
POLYNOMIAL = 0xc96c5795d7870f42
|
|
9
|
+
|
|
10
|
+
def self.call(bytes)
|
|
11
|
+
raise TypeError, "expected String, got #{bytes.class}" unless bytes.is_a?(String)
|
|
12
|
+
|
|
13
|
+
crc = MASK
|
|
14
|
+
bytes.each_byte do |byte|
|
|
15
|
+
crc ^= byte
|
|
16
|
+
8.times { crc = (crc >> 1) ^ (crc.odd? ? POLYNOMIAL : 0) }
|
|
17
|
+
end
|
|
18
|
+
crc ^ MASK
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module XitDB
|
|
4
|
+
module Core
|
|
5
|
+
# Tracks a bounded collection slot and its ancestor path.
|
|
6
|
+
class Cursor
|
|
7
|
+
attr_reader :database, :transaction
|
|
8
|
+
|
|
9
|
+
def initialize(database:, slot:, bound:, transaction: nil)
|
|
10
|
+
@database = database
|
|
11
|
+
@slot = slot
|
|
12
|
+
@bound = bound
|
|
13
|
+
@transaction = transaction
|
|
14
|
+
@valid = true
|
|
15
|
+
@children = ObjectSpace::WeakMap.new
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def slot
|
|
19
|
+
database.check_open!
|
|
20
|
+
# @type var path: Array[Cursor]
|
|
21
|
+
path = []
|
|
22
|
+
ancestor = self
|
|
23
|
+
loop do
|
|
24
|
+
transaction&.check_cursor!(ancestor)
|
|
25
|
+
raise ExpiredViewError, "cursor ancestor was replaced" unless ancestor.valid
|
|
26
|
+
break unless ancestor.parent
|
|
27
|
+
|
|
28
|
+
path << ancestor
|
|
29
|
+
ancestor = ancestor.parent
|
|
30
|
+
end
|
|
31
|
+
value = ancestor.local_slot
|
|
32
|
+
limit = transaction ? transaction.length : @bound
|
|
33
|
+
path.reverse_each do |child|
|
|
34
|
+
reader = self.class.new(database: database, slot: value, bound: limit)
|
|
35
|
+
value = reader.collection_get(child.selector)
|
|
36
|
+
raise ExpiredViewError, "cursor element was removed" unless value
|
|
37
|
+
|
|
38
|
+
child.instance_variable_set(:@slot, value)
|
|
39
|
+
end
|
|
40
|
+
value
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def bound
|
|
44
|
+
slot
|
|
45
|
+
transaction ? transaction.length : @bound
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def replace(slot)
|
|
49
|
+
database.check_open!
|
|
50
|
+
raise ReadOnlyError, "cursor is read-only" unless transaction
|
|
51
|
+
|
|
52
|
+
transaction.check_replacement!(self, slot)
|
|
53
|
+
self.slot
|
|
54
|
+
parent = @parent
|
|
55
|
+
parent&.collection_replace(@selector, slot, owner: self)
|
|
56
|
+
@slot = slot
|
|
57
|
+
invalidate_children
|
|
58
|
+
self
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def child(kind:, selector:)
|
|
62
|
+
unless %i[array_list linked_array_list sorted_map sorted_set].include?(kind) || HashMap::TAGS.include?(kind)
|
|
63
|
+
raise ArgumentError, "unsupported child kind: #{kind.inspect}"
|
|
64
|
+
end
|
|
65
|
+
return nil if slot.tag == :none
|
|
66
|
+
|
|
67
|
+
if kind == :array_list
|
|
68
|
+
ArrayList.new(self).cursor_at(integer_selector(selector))
|
|
69
|
+
elsif kind == :linked_array_list
|
|
70
|
+
LinkedArrayList.new(self).cursor_at(integer_selector(selector))
|
|
71
|
+
elsif %i[sorted_map sorted_set].include?(kind)
|
|
72
|
+
SortedMap.new(self).cursor_at(string_selector(selector))
|
|
73
|
+
else
|
|
74
|
+
HashMap.from_cursor(self).cursor_at(string_selector(selector))
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Collection updates keep child paths live while replacing copied ancestors.
|
|
79
|
+
def update_collection(slot, propagate: true)
|
|
80
|
+
transaction = self.transaction
|
|
81
|
+
raise ReadOnlyError, "cursor is read-only" unless transaction
|
|
82
|
+
|
|
83
|
+
transaction.check_replacement!(self, slot)
|
|
84
|
+
self.slot
|
|
85
|
+
@slot = slot
|
|
86
|
+
if propagate
|
|
87
|
+
child = self
|
|
88
|
+
while child.parent
|
|
89
|
+
parent = child.parent
|
|
90
|
+
parent.collection_replace(child.selector, child.local_slot, collection: true, propagate: false)
|
|
91
|
+
child = parent
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
self
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def array_child(index, slot)
|
|
98
|
+
cached_child(index, slot)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def hash_child(hash_bytes, slot)
|
|
102
|
+
cached_child(hash_bytes, slot)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def cached_child(index, slot)
|
|
106
|
+
@children.each_pair do |child, selector|
|
|
107
|
+
next unless selector == index && child.valid
|
|
108
|
+
|
|
109
|
+
begin
|
|
110
|
+
child.slot
|
|
111
|
+
rescue ExpiredViewError
|
|
112
|
+
next
|
|
113
|
+
end
|
|
114
|
+
return child
|
|
115
|
+
end
|
|
116
|
+
child = if transaction
|
|
117
|
+
transaction.cursor(slot: slot)
|
|
118
|
+
else
|
|
119
|
+
self.class.new(database: database, slot: slot, bound: bound)
|
|
120
|
+
end
|
|
121
|
+
child.instance_variable_set(:@parent, self)
|
|
122
|
+
child.instance_variable_set(:@selector, index)
|
|
123
|
+
@children[child] = index
|
|
124
|
+
child
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def invalidate_children(index: nil, from: nil, except: nil)
|
|
128
|
+
@children.each_pair do |child, selector|
|
|
129
|
+
next if child.equal?(except)
|
|
130
|
+
next if index && selector != index
|
|
131
|
+
next if from && selector < from
|
|
132
|
+
|
|
133
|
+
child.instance_variable_set(:@valid, false)
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# These states are internal transaction snapshots, including invalid views.
|
|
138
|
+
def checkpoint_state
|
|
139
|
+
[@slot, @valid]
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def restore_checkpoint_state(state)
|
|
143
|
+
@slot, @valid = state
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def read_bytes
|
|
147
|
+
database.read_bytes(slot, bound: bound)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
protected
|
|
151
|
+
|
|
152
|
+
def collection_get(selector)
|
|
153
|
+
target = collection
|
|
154
|
+
if target.is_a?(ArrayList) || target.is_a?(LinkedArrayList)
|
|
155
|
+
target.get(integer_selector(selector))
|
|
156
|
+
else
|
|
157
|
+
target.get(string_selector(selector))
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def collection_replace(selector, slot, owner: nil, collection: false, propagate: true)
|
|
162
|
+
target = self.collection
|
|
163
|
+
if target.is_a?(ArrayList) || target.is_a?(LinkedArrayList)
|
|
164
|
+
target.replace_child(integer_selector(selector), slot, owner: owner, collection: collection,
|
|
165
|
+
propagate: propagate)
|
|
166
|
+
else
|
|
167
|
+
target.replace_child(string_selector(selector), slot, owner: owner, collection: collection,
|
|
168
|
+
propagate: propagate)
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def integer_selector(selector)
|
|
173
|
+
raise TypeError, "array index must be an Integer" unless selector.is_a?(Integer)
|
|
174
|
+
|
|
175
|
+
selector
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def string_selector(selector)
|
|
179
|
+
raise TypeError, "collection key must be a String" unless selector.is_a?(String)
|
|
180
|
+
|
|
181
|
+
selector
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
attr_reader :parent, :selector, :valid
|
|
185
|
+
|
|
186
|
+
def collection
|
|
187
|
+
case slot.tag
|
|
188
|
+
when :array_list then ArrayList.new(self)
|
|
189
|
+
when :linked_array_list then LinkedArrayList.new(self)
|
|
190
|
+
when :sorted_map, :sorted_set then SortedMap.new(self)
|
|
191
|
+
when :hash_map, :hash_set, :counted_hash_map, :counted_hash_set then HashMap.from_cursor(self)
|
|
192
|
+
else raise InvalidValueError, "slot does not contain a supported collection"
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def local_slot
|
|
197
|
+
@slot
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|