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,360 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module XitDB
|
|
4
|
+
# Owns one captured root or one transaction's logical root.
|
|
5
|
+
class Context
|
|
6
|
+
Location = Struct.new(:path, :membership, :expired)
|
|
7
|
+
private_constant :Location
|
|
8
|
+
|
|
9
|
+
attr_reader :database, :profile, :transaction, :root, :reference_proof
|
|
10
|
+
|
|
11
|
+
def initialize(database:, root:, bound:, transaction: nil, readonly: false, reference_proof: nil)
|
|
12
|
+
@database = database
|
|
13
|
+
@profile = database.profile
|
|
14
|
+
@root = root
|
|
15
|
+
@bound = bound
|
|
16
|
+
@transaction = transaction
|
|
17
|
+
@readonly = readonly
|
|
18
|
+
@changed = false
|
|
19
|
+
@locations = ObjectSpace::WeakMap.new if transaction && !readonly
|
|
20
|
+
return if transaction
|
|
21
|
+
|
|
22
|
+
@reference_proof = reference_proof || database.core.read_at(
|
|
23
|
+
bound - Core::Database::TRAILER_SIZE, Core::Database::TRAILER_SIZE, bound: bound
|
|
24
|
+
).freeze
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def check!
|
|
28
|
+
database.core.check_open!
|
|
29
|
+
transaction&.check_active!
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def read
|
|
33
|
+
database.core.with_operation do
|
|
34
|
+
check!
|
|
35
|
+
yield
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def writable!
|
|
40
|
+
check!
|
|
41
|
+
raise ReadOnlyError, "view is read-only" if @readonly || !transaction
|
|
42
|
+
|
|
43
|
+
transaction.check_logical_mutation!
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def writable_transaction
|
|
47
|
+
check!
|
|
48
|
+
raise ReadOnlyError, "view is read-only" if @readonly
|
|
49
|
+
|
|
50
|
+
transaction || raise(ReadOnlyError, "view is read-only")
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def collection(slot, path:)
|
|
54
|
+
profile.container(slot, context: self, path: path) || raise(ExpiredViewError, "view location is not a collection")
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def read_bound
|
|
58
|
+
check!
|
|
59
|
+
transaction ? transaction.length : @bound
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def reference_compatible?(destination)
|
|
63
|
+
check!
|
|
64
|
+
destination.check!
|
|
65
|
+
target = destination.database.core
|
|
66
|
+
return false unless database.core.storage_identity == target.storage_identity
|
|
67
|
+
unless profile.instance_of?(destination.profile.class) && database.core.header.to_bytes == target.header.to_bytes
|
|
68
|
+
return false
|
|
69
|
+
end
|
|
70
|
+
return transaction.equal?(destination.transaction) if transaction
|
|
71
|
+
return false if @bound > destination.read_bound
|
|
72
|
+
|
|
73
|
+
reference_proof == target.read_at(@bound - Core::Database::TRAILER_SIZE,
|
|
74
|
+
Core::Database::TRAILER_SIZE, bound: destination.read_bound)
|
|
75
|
+
rescue CorruptionError
|
|
76
|
+
false
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def cursor(slot)
|
|
80
|
+
check!
|
|
81
|
+
if transaction
|
|
82
|
+
transaction.cursor(slot: slot)
|
|
83
|
+
else
|
|
84
|
+
Core::Cursor.new(database: database.core, slot: slot, bound: read_bound)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def resolve(path)
|
|
89
|
+
check!
|
|
90
|
+
slot = root
|
|
91
|
+
path.each_with_index do |selector, index|
|
|
92
|
+
slot = collection(slot, path: path.first(index)).lookup(selector)
|
|
93
|
+
raise ExpiredViewError, "view location no longer exists" unless slot
|
|
94
|
+
end
|
|
95
|
+
slot
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def view(_slot, path:, kind:)
|
|
99
|
+
klass = { map: Views::Map, sorted_map: Views::SortedMap, array: Views::Array,
|
|
100
|
+
linked_array: Views::LinkedArray, set: Views::Set, sorted_set: Views::SortedSet }.fetch(kind)
|
|
101
|
+
klass.new(context: self, path: path, kind: kind)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def changed?
|
|
105
|
+
@changed
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def copy_from(view, at:, transform: nil)
|
|
109
|
+
writable!
|
|
110
|
+
raise TypeError, "copy source must be a view" unless Object.instance_method(:is_a?).bind_call(view, Views::Base)
|
|
111
|
+
raise TypeError, "copy path must be an Array" unless at.instance_of?(Array)
|
|
112
|
+
raise TypeError, "transform must respond to call" unless transform.nil? || transform.respond_to?(:call)
|
|
113
|
+
|
|
114
|
+
read do
|
|
115
|
+
view.context.read do
|
|
116
|
+
source, source_adapter = capture_copy_source(view, at)
|
|
117
|
+
location = copy_location(at)
|
|
118
|
+
parent = location.take([location.size - 1, 0].max)
|
|
119
|
+
present = !location.empty? && !collection(resolve(parent), path: parent).lookup(location.last).nil?
|
|
120
|
+
perform_copy(source, source_adapter, location, parent, transform)
|
|
121
|
+
refresh_copy_location(location, parent, present)
|
|
122
|
+
profile.decode(resolve(location), context: self, path: location)
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Publish the logical root only after the complete operation succeeds.
|
|
128
|
+
def mutate(path, targets: nil, positions: nil)
|
|
129
|
+
writable!
|
|
130
|
+
transaction = writable_transaction
|
|
131
|
+
transaction.with_logical_mutation do
|
|
132
|
+
check_stream_mutation!(path, targets: targets, positions: positions)
|
|
133
|
+
checkpoint = transaction.checkpoint
|
|
134
|
+
completed = false
|
|
135
|
+
begin
|
|
136
|
+
# @type var ancestors: ::Array[[adapter, selector]]
|
|
137
|
+
ancestors = []
|
|
138
|
+
slot = root
|
|
139
|
+
path.each_with_index do |selector, index|
|
|
140
|
+
adapter = collection(slot, path: path.first(index))
|
|
141
|
+
ancestors << [adapter, selector]
|
|
142
|
+
slot = adapter.lookup(selector)
|
|
143
|
+
raise ExpiredViewError, "view location no longer exists" unless slot
|
|
144
|
+
end
|
|
145
|
+
target = cursor(slot)
|
|
146
|
+
result = yield target
|
|
147
|
+
if target.slot == slot
|
|
148
|
+
completed = true
|
|
149
|
+
return result
|
|
150
|
+
end
|
|
151
|
+
slot = target.slot
|
|
152
|
+
ancestors.reverse_each do |adapter, selector|
|
|
153
|
+
adapter.assign(selector, slot)
|
|
154
|
+
slot = adapter.slot
|
|
155
|
+
end
|
|
156
|
+
@root = slot
|
|
157
|
+
@changed = true
|
|
158
|
+
completed = true
|
|
159
|
+
result
|
|
160
|
+
ensure
|
|
161
|
+
completed ? transaction.release_checkpoint(checkpoint) : transaction.restore(checkpoint)
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def register_stream(path, write:)
|
|
167
|
+
check!
|
|
168
|
+
key = path_key(path)
|
|
169
|
+
# @type var streams: ::Array[registration]
|
|
170
|
+
streams = @streams ||= []
|
|
171
|
+
if streams.any? { |other, writing| (write || writing) && overlapping_paths?(key, other) }
|
|
172
|
+
raise ConcurrentModificationError, "value has an active byte stream"
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# @type var registration: registration
|
|
176
|
+
registration = [key, write]
|
|
177
|
+
streams << registration
|
|
178
|
+
registration
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def release_stream(registration)
|
|
182
|
+
@streams&.delete_if { |item| item.equal?(registration) }
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def observe(path)
|
|
186
|
+
check!
|
|
187
|
+
locations = @locations
|
|
188
|
+
return unless locations
|
|
189
|
+
|
|
190
|
+
location = Location.new(path_key(path), 0, false)
|
|
191
|
+
locations[location] = true
|
|
192
|
+
location
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def replace_location(path)
|
|
196
|
+
each_location(path) { |location| location.expired = true }
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def invalidate_positions(path, start)
|
|
200
|
+
each_location(path) do |location|
|
|
201
|
+
index = location.path[path.size]
|
|
202
|
+
location.expired = true if index.is_a?(Integer) && index >= start
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def change_membership(path)
|
|
207
|
+
each_location(path) { |location| location.membership += 1 if location.path.size == path.size }
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def invalidate_children(path)
|
|
211
|
+
each_location(path) { |location| location.expired = true if location.path.size > path.size }
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def check_stream_mutation!(path, targets: nil, positions: nil)
|
|
215
|
+
streams = @streams
|
|
216
|
+
return if !streams || streams.empty?
|
|
217
|
+
|
|
218
|
+
keys = mutation_keys(path, targets, positions)
|
|
219
|
+
parent = path_key(path) if positions
|
|
220
|
+
conflict = streams.any? do |active, _writing|
|
|
221
|
+
keys.any? { |key| overlapping_paths?(key, active) } ||
|
|
222
|
+
position_conflict?(parent, active, positions)
|
|
223
|
+
end
|
|
224
|
+
raise ConcurrentModificationError, "value has an active byte stream" if conflict
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
private
|
|
228
|
+
|
|
229
|
+
def mutation_keys(path, targets, positions)
|
|
230
|
+
(targets || (positions ? [] : [path])).map { |target| path_key(target) }
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def capture_copy_source(view, at)
|
|
234
|
+
view.check!
|
|
235
|
+
source_slot = view.slot
|
|
236
|
+
view.context.transaction&.freeze
|
|
237
|
+
source_adapter = view.adapter
|
|
238
|
+
if at.empty? && !%i[map sorted_map].include?(source_adapter.kind)
|
|
239
|
+
raise ProfileMismatchError, "context root must be a map"
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
source = Context.new(database: view.context.database, root: source_slot, bound: view.context.read_bound,
|
|
243
|
+
transaction: view.context.transaction, readonly: true,
|
|
244
|
+
reference_proof: view.context.reference_proof)
|
|
245
|
+
[source, source_adapter]
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def copy_location(at)
|
|
249
|
+
# @type var location: path
|
|
250
|
+
location = []
|
|
251
|
+
set_kinds = %i[set sorted_set]
|
|
252
|
+
array_kinds = %i[array linked_array]
|
|
253
|
+
at.each_with_index do |selector, index|
|
|
254
|
+
adapter = profile.container(resolve(location), context: self, path: location)
|
|
255
|
+
raise TypeError, "copy parent must be a collection" unless adapter
|
|
256
|
+
raise TypeError, "cannot assign a collection to a set member" if set_kinds.include?(adapter.kind)
|
|
257
|
+
|
|
258
|
+
selector = copy_array_position(selector, adapter.size) if array_kinds.include?(adapter.kind)
|
|
259
|
+
adapter.lookup(selector)
|
|
260
|
+
location << case selector
|
|
261
|
+
when Time then selector.dup.freeze
|
|
262
|
+
when String then String.new(selector).freeze
|
|
263
|
+
else selector
|
|
264
|
+
end
|
|
265
|
+
resolve(location) if index < at.size - 1
|
|
266
|
+
end
|
|
267
|
+
location
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def copy_array_position(selector, size)
|
|
271
|
+
selector = Integer.try_convert(selector) || raise(TypeError, "array position must be an Integer")
|
|
272
|
+
long_max = (1 << (([0].pack("l!").bytesize * 8) - 1)) - 1
|
|
273
|
+
raise RangeError, "array position is too large" unless ((-long_max - 1)..long_max).cover?(selector)
|
|
274
|
+
|
|
275
|
+
selector += size if selector.negative?
|
|
276
|
+
raise IndexError, "array index outside list" if selector.negative?
|
|
277
|
+
raise IndexError, "array size too big" if selector + 1 > long_max / 0.size
|
|
278
|
+
|
|
279
|
+
selector
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def perform_copy(source, source_adapter, location, parent, transform)
|
|
283
|
+
operation = lambda do
|
|
284
|
+
mutate(location.empty? ? [] : parent, targets: [location]) do |cursor|
|
|
285
|
+
assign_copy(cursor, source, source_adapter, location, parent, transform)
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
source_transaction = source.transaction
|
|
289
|
+
if source_transaction && !source_transaction.equal?(transaction)
|
|
290
|
+
source_transaction.with_logical_mutation(&operation)
|
|
291
|
+
else
|
|
292
|
+
operation.call
|
|
293
|
+
end
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def assign_copy(cursor, source, source_adapter, location, parent, transform)
|
|
297
|
+
copied = Conversion.new(source, self, transform: transform).copy(source.root, adapter: source_adapter)
|
|
298
|
+
if location.empty?
|
|
299
|
+
cursor.replace(copied)
|
|
300
|
+
else
|
|
301
|
+
adapter = collection(cursor.slot, path: parent)
|
|
302
|
+
if %i[array linked_array].include?(adapter.kind)
|
|
303
|
+
position = Integer.try_convert(location.last) || raise(TypeError, "array position must be an Integer")
|
|
304
|
+
adapter.assign(adapter.size, Core::Slot.null) while adapter.size < position
|
|
305
|
+
end
|
|
306
|
+
adapter.assign(location.last, copied)
|
|
307
|
+
cursor.replace(adapter.slot)
|
|
308
|
+
end
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
def refresh_copy_location(location, parent, present)
|
|
312
|
+
if location.empty?
|
|
313
|
+
invalidate_children([])
|
|
314
|
+
change_membership([])
|
|
315
|
+
else
|
|
316
|
+
replace_location(location)
|
|
317
|
+
change_membership(parent) unless present
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
def position_conflict?(parent, active, positions)
|
|
322
|
+
return false unless parent && positions && overlapping_paths?(parent, active)
|
|
323
|
+
|
|
324
|
+
active.size <= parent.size || positions.cover?(active[parent.size])
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
def overlapping_paths?(left, right)
|
|
328
|
+
left.first(right.size).eql?(right) || right.first(left.size).eql?(left)
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
def each_location(path)
|
|
332
|
+
key = path_key(path)
|
|
333
|
+
retained = ObjectSpace::WeakMap.new
|
|
334
|
+
@locations&.each_key do |location|
|
|
335
|
+
next if location.expired
|
|
336
|
+
|
|
337
|
+
yield location if location.path.first(key.size).eql?(key)
|
|
338
|
+
retained[location] = true unless location.expired
|
|
339
|
+
end
|
|
340
|
+
@locations = retained
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
def path_key(path)
|
|
344
|
+
slot = root
|
|
345
|
+
path.each_with_index.map do |selector, index|
|
|
346
|
+
adapter = collection(slot, path: path.first(index))
|
|
347
|
+
key = if defined?(Ruby::Containers) && adapter.is_a?(Ruby::Containers)
|
|
348
|
+
adapter.selector_identity(selector)
|
|
349
|
+
else
|
|
350
|
+
case selector
|
|
351
|
+
when Integer then selector
|
|
352
|
+
else profile.key_bytes(selector)
|
|
353
|
+
end
|
|
354
|
+
end
|
|
355
|
+
slot = adapter.lookup(selector) if index < path.size - 1
|
|
356
|
+
key
|
|
357
|
+
end
|
|
358
|
+
end
|
|
359
|
+
end
|
|
360
|
+
end
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module XitDB
|
|
4
|
+
# Copies logical entries with one iterator per ancestor and no node cache.
|
|
5
|
+
class Conversion
|
|
6
|
+
def initialize(source, destination, transform: nil)
|
|
7
|
+
@source = source
|
|
8
|
+
@destination = destination
|
|
9
|
+
@transform = transform
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def copy(slot, path: [], adapter: nil)
|
|
13
|
+
# @type var stack: ::Array[conversion_frame]
|
|
14
|
+
stack = []
|
|
15
|
+
# @type var active: ::Hash[[Symbol, Integer], bool]
|
|
16
|
+
active = {}
|
|
17
|
+
# @type var current: [Core::Slot, path]?
|
|
18
|
+
current = [slot, path]
|
|
19
|
+
selected = adapter
|
|
20
|
+
# @type var result: Core::Slot?
|
|
21
|
+
result = nil
|
|
22
|
+
while true # rubocop:disable Style/InfiniteLoop -- Callback StopIteration must propagate.
|
|
23
|
+
if current
|
|
24
|
+
result = begin_value(current[0], current[1], selected, stack, active)
|
|
25
|
+
current = nil
|
|
26
|
+
selected = nil
|
|
27
|
+
end
|
|
28
|
+
if result
|
|
29
|
+
break if stack.empty?
|
|
30
|
+
|
|
31
|
+
assign_parent(stack, result)
|
|
32
|
+
end
|
|
33
|
+
frame = stack.last || raise(CorruptionError, "missing conversion frame")
|
|
34
|
+
entry = next_entry(frame)
|
|
35
|
+
if entry
|
|
36
|
+
current = select_child(frame, entry)
|
|
37
|
+
else
|
|
38
|
+
result = finish_frame(frame, stack, active)
|
|
39
|
+
break if stack.empty?
|
|
40
|
+
end
|
|
41
|
+
result = nil
|
|
42
|
+
end
|
|
43
|
+
result || raise(CorruptionError, "conversion produced no value")
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def begin_value(stored, location, selected, stack, active)
|
|
49
|
+
adapter = selected || @source.profile.container(stored, context: @source, path: location)
|
|
50
|
+
return copy_scalar(stored, location) unless adapter
|
|
51
|
+
|
|
52
|
+
# @type var identity: [Symbol, Integer]
|
|
53
|
+
identity = [stored.tag, stored.value]
|
|
54
|
+
raise CyclicValueError.new("cyclic stored value", path: location) if active[identity]
|
|
55
|
+
|
|
56
|
+
active[identity] = true
|
|
57
|
+
kind = @source.profile.conversion_kind(adapter)
|
|
58
|
+
output = @destination.profile.conversion_container(kind, context: @destination, path: location)
|
|
59
|
+
stack << [identity, location, adapter.each_entry, output, nil]
|
|
60
|
+
nil
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def copy_scalar(stored, location)
|
|
64
|
+
bytes = @source.profile.conversion_bytes(stored, context: @source) unless @transform
|
|
65
|
+
if bytes
|
|
66
|
+
return @destination.profile.copy_conversion_bytes(bytes[0], bytes[1], context: @destination, path: location)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
value = @source.profile.decode(stored, context: @source, path: location)
|
|
70
|
+
value = transform(value, location, :value)
|
|
71
|
+
if @source.profile.instance_of?(Profiles::Portable) && !@destination.profile.instance_of?(Profiles::Portable)
|
|
72
|
+
case value
|
|
73
|
+
when Core::ByteValue
|
|
74
|
+
if value.format_tag == "rb"
|
|
75
|
+
raise UnsupportedValueError.new("portable rb bytes require an explicit transform", path: location)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
@destination.profile.encode(value, context: @destination, path: location)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def assign_parent(stack, stored)
|
|
83
|
+
parent = stack.last || raise(CorruptionError, "missing conversion parent")
|
|
84
|
+
parent[3].assign(parent[4], stored)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def finish_frame(frame, stack, active)
|
|
88
|
+
stored = frame[3].slot
|
|
89
|
+
active.delete(frame[0])
|
|
90
|
+
stack.pop
|
|
91
|
+
assign_parent(stack, stored) unless stack.empty?
|
|
92
|
+
stored
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Callback StopIteration must abort the operation, so rescue only #next.
|
|
96
|
+
def next_entry(frame)
|
|
97
|
+
frame[2].next
|
|
98
|
+
rescue StopIteration
|
|
99
|
+
nil
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def select_child(frame, entry)
|
|
103
|
+
selector, child = entry
|
|
104
|
+
child_path = frame[1] + [selector]
|
|
105
|
+
if %i[map sorted_map set sorted_set].include?(frame[3].kind)
|
|
106
|
+
selector = transform_key(selector, child_path)
|
|
107
|
+
if frame[3].lookup(selector)
|
|
108
|
+
raise KeyCollisionError.new("converted keys have equal destination identity", path: child_path)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
selector = case selector
|
|
112
|
+
when String then String.new(selector).freeze
|
|
113
|
+
when Time then selector.dup.freeze
|
|
114
|
+
else selector
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
frame[4] = selector
|
|
118
|
+
if %i[set sorted_set].include?(frame[3].kind)
|
|
119
|
+
frame[3].assign(selector, Core::Slot.null)
|
|
120
|
+
nil
|
|
121
|
+
else
|
|
122
|
+
[child, child_path]
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def transform_key(value, path)
|
|
127
|
+
converted = transform(value, path, :key)
|
|
128
|
+
case converted
|
|
129
|
+
when String, Integer, Float, Time, Symbol, TrueClass, FalseClass, NilClass, UInt
|
|
130
|
+
converted
|
|
131
|
+
else
|
|
132
|
+
raise UnsupportedValueError.new("unsupported converted key", path: path)
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def transform(value, path, role)
|
|
137
|
+
callback = @transform
|
|
138
|
+
return value unless callback
|
|
139
|
+
|
|
140
|
+
immutable_path = path.map do |part|
|
|
141
|
+
case part
|
|
142
|
+
when String then String.new(part).freeze
|
|
143
|
+
else part
|
|
144
|
+
end
|
|
145
|
+
end.freeze
|
|
146
|
+
callback.call(value: value, path: immutable_path, role: role)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# A bounded byte range, including tagged bytes and Ruby String payloads.
|
|
150
|
+
class ByteSource
|
|
151
|
+
attr_reader :size, :format_tag
|
|
152
|
+
|
|
153
|
+
def initialize(context, slot)
|
|
154
|
+
@source = Core::Bytes.open(context.database.core, slot, bound: context.read_bound)
|
|
155
|
+
@start = 0
|
|
156
|
+
@size = @source.size
|
|
157
|
+
@format_tag = @source.format_tag
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def read(position, length)
|
|
161
|
+
unless position >= 0 && length >= 0 && position + length <= size
|
|
162
|
+
raise InvalidValueError, "truncated byte payload"
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
@source.read(@start + position, length)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def unsigned(position)
|
|
169
|
+
value = shift = 0
|
|
170
|
+
while true
|
|
171
|
+
part = read(position, 1).getbyte(0) || raise(InvalidValueError, "truncated byte payload")
|
|
172
|
+
position += 1
|
|
173
|
+
value |= (part & 0x7f) << shift
|
|
174
|
+
if part < 0x80
|
|
175
|
+
raise InvalidValueError, "nonminimal unsigned field" if shift.positive? && part.zero?
|
|
176
|
+
|
|
177
|
+
break
|
|
178
|
+
end
|
|
179
|
+
shift += 7
|
|
180
|
+
raise InvalidValueError, "byte field length exceeds storage range" if shift >= 70
|
|
181
|
+
end
|
|
182
|
+
[value, position]
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def slice(position, length)
|
|
186
|
+
read(position + length, 0)
|
|
187
|
+
output = dup
|
|
188
|
+
output.instance_variable_set(:@size, length)
|
|
189
|
+
output.instance_variable_set(:@format_tag, nil)
|
|
190
|
+
output.instance_variable_set(:@start, @start + position)
|
|
191
|
+
output
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def portable_encoding
|
|
195
|
+
converter = Encoding::Converter.new(Encoding::UTF_8, Encoding::UTF_16LE)
|
|
196
|
+
each_chunk { |chunk| converter.convert(chunk) }
|
|
197
|
+
converter.finish
|
|
198
|
+
Encoding::UTF_8
|
|
199
|
+
rescue Encoding::InvalidByteSequenceError, Encoding::UndefinedConversionError
|
|
200
|
+
Encoding::BINARY
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def write_to(context, prefix: "".b, format_tag: @format_tag)
|
|
204
|
+
Core::Bytes.append(context.writable_transaction, size: size, prefix: prefix, format_tag: format_tag,
|
|
205
|
+
representation: :external) do |position, length|
|
|
206
|
+
read(position, length)
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
private
|
|
211
|
+
|
|
212
|
+
def each_chunk
|
|
213
|
+
position = 0
|
|
214
|
+
while position < size
|
|
215
|
+
chunk = read(position, [size - position, Core::Bytes::CHUNK_SIZE].min)
|
|
216
|
+
yield chunk
|
|
217
|
+
position += chunk.bytesize
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
end
|