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,407 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../input_guard"
|
|
4
|
+
|
|
5
|
+
module XitDB
|
|
6
|
+
module Profiles
|
|
7
|
+
# Maps portable values to native xitdb storage layouts.
|
|
8
|
+
class Portable
|
|
9
|
+
KINDS = {
|
|
10
|
+
hash_map: :map, counted_hash_map: :map, sorted_map: :sorted_map,
|
|
11
|
+
hash_set: :set, counted_hash_set: :set, sorted_set: :sorted_set,
|
|
12
|
+
array_list: :array, linked_array_list: :linked_array
|
|
13
|
+
}.freeze
|
|
14
|
+
|
|
15
|
+
attr_reader :hasher
|
|
16
|
+
|
|
17
|
+
def initialize(hasher: Core::Hasher.sha1)
|
|
18
|
+
raise TypeError, "hasher must be a Core::Hasher" unless hasher.is_a?(Core::Hasher)
|
|
19
|
+
|
|
20
|
+
@hasher = hasher
|
|
21
|
+
freeze
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def encode_scalar(value, core:)
|
|
25
|
+
InputGuard.check!(value)
|
|
26
|
+
case value
|
|
27
|
+
when nil then Core::Slot.null
|
|
28
|
+
when UInt then Core::Slot.uint(value.value)
|
|
29
|
+
when Integer
|
|
30
|
+
value > (1 << 63) - 1 ? Core::Slot.uint(value) : Core::Slot.int(value)
|
|
31
|
+
when Float then Core::Slot.float(value)
|
|
32
|
+
when String then core.bytes(value)
|
|
33
|
+
when Core::ByteValue then core.bytes(value.bytes, format_tag: value.format_tag)
|
|
34
|
+
else
|
|
35
|
+
raise UnsupportedValueError, "unsupported portable scalar: #{Object.instance_method(:class).bind_call(value)}"
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def decode_scalar(slot, core:, bound: nil, immutable: true)
|
|
40
|
+
case slot.tag
|
|
41
|
+
when :none then nil
|
|
42
|
+
when :uint then slot.value
|
|
43
|
+
when :int then slot.value >= (1 << 63) ? slot.value - (1 << 64) : slot.value
|
|
44
|
+
when :float then [slot.value].pack("Q>").unpack1("G")
|
|
45
|
+
when :bytes, :short_bytes
|
|
46
|
+
value = core.read_bytes(slot, bound: bound)
|
|
47
|
+
return value if value.format_tag
|
|
48
|
+
|
|
49
|
+
bytes = value.bytes.dup.force_encoding(Encoding::UTF_8)
|
|
50
|
+
bytes.force_encoding(Encoding::BINARY) unless bytes.valid_encoding?
|
|
51
|
+
immutable ? bytes.freeze : bytes
|
|
52
|
+
else
|
|
53
|
+
raise UnsupportedValueError, "unsupported portable scalar tag: #{slot.tag}"
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def key_bytes(value)
|
|
58
|
+
InputGuard.check!(value)
|
|
59
|
+
case value
|
|
60
|
+
when String then value.b.freeze
|
|
61
|
+
else raise UnsupportedValueError, "portable keys must be strings"
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def encode(value, context:, path:)
|
|
66
|
+
ValueWalker.encode(value, context: context, path: path)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def decode(slot, context:, path:, immutable: true)
|
|
70
|
+
if (kind = KINDS[slot.tag])
|
|
71
|
+
context.view(slot, path: path, kind: kind)
|
|
72
|
+
else
|
|
73
|
+
decode_scalar(slot, core: context.database.core, bound: context.read_bound, immutable: immutable)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def container(slot, context:, path:)
|
|
78
|
+
return nil unless KINDS.key?(slot.tag) || (slot.empty? && path.empty?)
|
|
79
|
+
|
|
80
|
+
Adapter.new(self, context.cursor(slot), context: context, path: path)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def empty_container(kind, context:, path:)
|
|
84
|
+
kind = kind.first if kind.is_a?(Array)
|
|
85
|
+
Adapter.new(self, context.cursor(Core::Slot.new), context: context, path: path, tag: kind)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def conversion_kind(adapter)
|
|
89
|
+
adapter.kind
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def conversion_container(kind, context:, path:)
|
|
93
|
+
kind = kind.first if kind.is_a?(Array)
|
|
94
|
+
tag = { map: :hash_map, set: :hash_set, array: :array_list, linked_array: :linked_array_list }.fetch(kind, kind)
|
|
95
|
+
empty_container(tag, context: context, path: path)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def conversion_bytes(slot, context:)
|
|
99
|
+
return unless %i[bytes short_bytes].include?(slot.tag)
|
|
100
|
+
|
|
101
|
+
source = Conversion::ByteSource.new(context, slot)
|
|
102
|
+
[source, source.format_tag ? nil : :portable]
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def copy_conversion_bytes(source, encoding, context:, path:)
|
|
106
|
+
source.write_to(context, format_tag: encoding ? nil : source.format_tag)
|
|
107
|
+
rescue ValueError => e
|
|
108
|
+
raise e.class.new(e.message, path: path), cause: e
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def input_kind(value, path:)
|
|
112
|
+
InputGuard.check!(value, path: path)
|
|
113
|
+
klass = Object.instance_method(:class).bind_call(value)
|
|
114
|
+
case value
|
|
115
|
+
when Hash, Array, Set
|
|
116
|
+
validate_container(value, klass, path)
|
|
117
|
+
{ Hash => :hash_map, Array => :array_list, Set => :hash_set }.fetch(klass)
|
|
118
|
+
when CollectionDeclaration
|
|
119
|
+
if value.key_kind
|
|
120
|
+
raise UnsupportedValueError.new("typed sorted declarations require the Ruby profile", path: path)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
value.kind
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def hash_defaults?(value)
|
|
128
|
+
default = !BasicObject.instance_method(:equal?).bind_call(value.default, nil)
|
|
129
|
+
!value.default_proc.nil? || default || value.compare_by_identity?
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def validate_container(value, klass, path)
|
|
133
|
+
unless [Hash, Array, Set].include?(klass)
|
|
134
|
+
raise UnsupportedValueError.new("container subclasses and singleton behavior are unsupported", path: path)
|
|
135
|
+
end
|
|
136
|
+
if value.is_a?(Hash) && hash_defaults?(value)
|
|
137
|
+
raise UnsupportedValueError.new("Hash defaults and identity comparison are unsupported", path: path)
|
|
138
|
+
end
|
|
139
|
+
return unless value.is_a?(Set) && value.compare_by_identity?
|
|
140
|
+
|
|
141
|
+
raise UnsupportedValueError.new("Set identity comparison is unsupported", path: path)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def input_entries(value, path:)
|
|
145
|
+
Enumerator.new do |output|
|
|
146
|
+
# @type var seen: Hash[String, bool]
|
|
147
|
+
seen = {}
|
|
148
|
+
case value
|
|
149
|
+
when Hash, Set
|
|
150
|
+
entries = value.is_a?(Hash) ? value.each_pair : value.each.lazy.map { |member| [member, member] }
|
|
151
|
+
entries.each do |key, child|
|
|
152
|
+
begin
|
|
153
|
+
bytes = key_bytes(key)
|
|
154
|
+
rescue ValueError => e
|
|
155
|
+
raise e.class.new(e.message, path: path + [key]), cause: e
|
|
156
|
+
end
|
|
157
|
+
if seen.key?(bytes)
|
|
158
|
+
raise KeyCollisionError.new("distinct input keys have equal portable bytes", path: path + [key])
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
seen[bytes] = true
|
|
162
|
+
output << [key, child]
|
|
163
|
+
end
|
|
164
|
+
when Array
|
|
165
|
+
value.each_with_index { |child, index| output << [index, child] }
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# All knowledge of portable storage layouts stays inside this adapter.
|
|
171
|
+
class Adapter
|
|
172
|
+
attr_reader :kind, :cursor
|
|
173
|
+
|
|
174
|
+
def initialize(profile, cursor, context:, path:, tag: nil)
|
|
175
|
+
@profile = profile
|
|
176
|
+
@cursor = cursor
|
|
177
|
+
@context = context
|
|
178
|
+
@path = path
|
|
179
|
+
tag ||= cursor.slot.tag == :none ? :hash_map : cursor.slot.tag
|
|
180
|
+
@kind = KINDS.fetch(tag)
|
|
181
|
+
@collection = case tag
|
|
182
|
+
when :hash_map, :hash_set, :counted_hash_map, :counted_hash_set
|
|
183
|
+
Core::HashMap.new(cursor, counted: %i[counted_hash_map counted_hash_set].include?(tag),
|
|
184
|
+
set: %i[hash_set counted_hash_set].include?(tag))
|
|
185
|
+
when :array_list then Core::ArrayList.new(cursor)
|
|
186
|
+
when :linked_array_list then Core::LinkedArrayList.new(cursor)
|
|
187
|
+
when :sorted_map, :sorted_set then Core::SortedMap.new(cursor, set: tag == :sorted_set)
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def slot
|
|
192
|
+
cursor.slot
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def size
|
|
196
|
+
@collection.size
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def empty?
|
|
200
|
+
size.zero?
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def lookup(selector)
|
|
204
|
+
collection = @collection
|
|
205
|
+
if collection.is_a?(Core::ArrayList) || collection.is_a?(Core::LinkedArrayList)
|
|
206
|
+
collection.get(index_selector(selector))
|
|
207
|
+
elsif collection.is_a?(Core::SortedMap)
|
|
208
|
+
collection.get(key_bytes(selector))
|
|
209
|
+
else
|
|
210
|
+
collection.get(@profile.hasher.digest(key_bytes(selector)))
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def assign(selector, stored)
|
|
215
|
+
case kind
|
|
216
|
+
when :array, :linked_array
|
|
217
|
+
if selector == size
|
|
218
|
+
sequence_collection.append(stored)
|
|
219
|
+
else
|
|
220
|
+
sequence_collection.put(index_selector(selector),
|
|
221
|
+
stored)
|
|
222
|
+
end
|
|
223
|
+
when :sorted_map
|
|
224
|
+
sorted_collection.put(key_bytes(selector), value: stored)
|
|
225
|
+
when :sorted_set
|
|
226
|
+
sorted_collection.add(key_bytes(selector))
|
|
227
|
+
when :map, :set
|
|
228
|
+
bytes = key_bytes(selector)
|
|
229
|
+
digest = @profile.hasher.digest(bytes)
|
|
230
|
+
key = @context.database.core.bytes(bytes)
|
|
231
|
+
kind == :map ? hash_collection.put(digest, key: key, value: stored) : hash_collection.add(digest, key: key)
|
|
232
|
+
end
|
|
233
|
+
self
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def delete(selector)
|
|
237
|
+
case kind
|
|
238
|
+
when :array
|
|
239
|
+
index = selector
|
|
240
|
+
count = size
|
|
241
|
+
raise IndexError, "array index outside list" unless index.is_a?(Integer) && index >= 0 && index < count
|
|
242
|
+
|
|
243
|
+
(index...(count - 1)).each do |position|
|
|
244
|
+
array_collection.put(position, required_slot(array_collection.get(position + 1)))
|
|
245
|
+
end
|
|
246
|
+
array_collection.truncate(count - 1)
|
|
247
|
+
when :linked_array then linked_collection.delete_at(index_selector(selector))
|
|
248
|
+
when :map, :set then hash_collection.remove(@profile.hasher.digest(key_bytes(selector)))
|
|
249
|
+
else sorted_collection.remove(key_bytes(selector))
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# Radix insertion moves the suffix and performs one radix write per slot.
|
|
254
|
+
# Linked insertion follows one count-guided tree path.
|
|
255
|
+
def insert(index, stored)
|
|
256
|
+
if kind == :linked_array
|
|
257
|
+
Core::BTree.new(cursor).insert(index, stored)
|
|
258
|
+
else
|
|
259
|
+
count = size
|
|
260
|
+
array_collection.append(stored)
|
|
261
|
+
(count - 1).downto(index) do |position|
|
|
262
|
+
array_collection.put(position + 1, required_slot(array_collection.get(position)))
|
|
263
|
+
end
|
|
264
|
+
array_collection.put(index, stored)
|
|
265
|
+
end
|
|
266
|
+
self
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def truncate(count)
|
|
270
|
+
if kind == :linked_array
|
|
271
|
+
cursor.replace(linked_collection.slice(0, count).cursor.slot)
|
|
272
|
+
else
|
|
273
|
+
array_collection.truncate(count)
|
|
274
|
+
end
|
|
275
|
+
self
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def slice(start, count)
|
|
279
|
+
return linked_collection.slice(start, count).cursor.slot if kind == :linked_array
|
|
280
|
+
|
|
281
|
+
output = @profile.empty_container(:array_list, context: @context, path: @path)
|
|
282
|
+
count.times { |index| output.assign(index, required_slot(lookup(start + index))) }
|
|
283
|
+
output.slot
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def concat(other)
|
|
287
|
+
if kind == :linked_array && other.kind == :linked_array
|
|
288
|
+
cursor.replace(linked_collection.concat(Core::LinkedArrayList.new(other.cursor)).cursor.slot)
|
|
289
|
+
else
|
|
290
|
+
other.each_entry { |_index, stored| assign(size, stored) }
|
|
291
|
+
end
|
|
292
|
+
self
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def rank(key)
|
|
296
|
+
sorted_collection.rank(@profile.key_bytes(key))
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
def each_from(key)
|
|
300
|
+
return enum_for(:each_from, key) unless block_given?
|
|
301
|
+
|
|
302
|
+
sorted_collection.each_from(@profile.key_bytes(key)) { |bytes, stored| yield decode_key(bytes), stored }
|
|
303
|
+
self
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
def each_from_index(index)
|
|
307
|
+
return enum_for(:each_from_index, index) unless block_given?
|
|
308
|
+
|
|
309
|
+
index += size if index.negative?
|
|
310
|
+
return self unless index >= 0 && index < size
|
|
311
|
+
|
|
312
|
+
case kind
|
|
313
|
+
when :array
|
|
314
|
+
(index...size).each { |position| yield position, required_slot(lookup(position)) }
|
|
315
|
+
when :linked_array
|
|
316
|
+
linked_collection.each_from_index(index).with_index(index) { |stored, position| yield position, stored }
|
|
317
|
+
else
|
|
318
|
+
sorted_collection.each_from_index(index) { |bytes, stored| yield decode_key(bytes), stored }
|
|
319
|
+
end
|
|
320
|
+
self
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
def each_entry
|
|
324
|
+
return enum_for(:each_entry) unless block_given?
|
|
325
|
+
|
|
326
|
+
case kind
|
|
327
|
+
when :array, :linked_array
|
|
328
|
+
sequence_collection.each.with_index { |stored, index| yield index, stored }
|
|
329
|
+
when :sorted_map, :sorted_set
|
|
330
|
+
sorted_collection.each { |bytes, stored| yield decode_key(bytes), stored }
|
|
331
|
+
else
|
|
332
|
+
hash_collection.each_entry do |_digest, key, stored|
|
|
333
|
+
value = @profile.decode_scalar(key, core: @context.database.core, bound: @context.read_bound)
|
|
334
|
+
raise CorruptionError, "portable map key is not an untagged string" unless value.is_a?(String)
|
|
335
|
+
|
|
336
|
+
yield value, stored
|
|
337
|
+
end
|
|
338
|
+
end
|
|
339
|
+
self
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
private
|
|
343
|
+
|
|
344
|
+
def index_selector(selector)
|
|
345
|
+
raise TypeError, "index must be an Integer" unless selector.is_a?(Integer)
|
|
346
|
+
|
|
347
|
+
selector
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
def required_slot(slot)
|
|
351
|
+
raise CorruptionError, "missing collection slot" unless slot
|
|
352
|
+
|
|
353
|
+
slot
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
def array_collection
|
|
357
|
+
collection = @collection
|
|
358
|
+
raise CorruptionError, "unexpected collection layout" unless collection.is_a?(Core::ArrayList)
|
|
359
|
+
|
|
360
|
+
collection
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
def linked_collection
|
|
364
|
+
collection = @collection
|
|
365
|
+
raise CorruptionError, "unexpected collection layout" unless collection.is_a?(Core::LinkedArrayList)
|
|
366
|
+
|
|
367
|
+
collection
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
def sorted_collection
|
|
371
|
+
collection = @collection
|
|
372
|
+
raise CorruptionError, "unexpected collection layout" unless collection.is_a?(Core::SortedMap)
|
|
373
|
+
|
|
374
|
+
collection
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
def hash_collection
|
|
378
|
+
collection = @collection
|
|
379
|
+
raise CorruptionError, "unexpected collection layout" unless collection.is_a?(Core::HashMap)
|
|
380
|
+
|
|
381
|
+
collection
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
def sequence_collection
|
|
385
|
+
collection = @collection
|
|
386
|
+
unless collection.is_a?(Core::ArrayList) || collection.is_a?(Core::LinkedArrayList)
|
|
387
|
+
raise CorruptionError, "unexpected sequence layout"
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
collection
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
def key_bytes(selector)
|
|
394
|
+
@profile.key_bytes(selector)
|
|
395
|
+
rescue ValueError => e
|
|
396
|
+
raise e.class.new(e.message, path: @path + [selector]), cause: e
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
def decode_key(bytes)
|
|
400
|
+
value = bytes.dup.force_encoding(Encoding::UTF_8)
|
|
401
|
+
value.force_encoding(Encoding::BINARY) unless value.valid_encoding?
|
|
402
|
+
value.freeze
|
|
403
|
+
end
|
|
404
|
+
end
|
|
405
|
+
end
|
|
406
|
+
end
|
|
407
|
+
end
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../ruby/codec"
|
|
4
|
+
|
|
5
|
+
module XitDB
|
|
6
|
+
module Profiles
|
|
7
|
+
# Preserves Ruby scalar types and collection ordering.
|
|
8
|
+
class Ruby
|
|
9
|
+
attr_reader :hasher, :codecs
|
|
10
|
+
|
|
11
|
+
def initialize(codecs: [])
|
|
12
|
+
@registry = XitDB::Ruby::CodecRegistry.new(codecs)
|
|
13
|
+
@codecs = @registry.codecs
|
|
14
|
+
@hasher = Core::Hasher.new(id: "rb01", size: 32) { |bytes| Digest::SHA256.digest(bytes) }
|
|
15
|
+
freeze
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def encode(value, context:, path:)
|
|
19
|
+
ValueWalker.encode(value, context: context, path: path)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def encode_scalar(value, core:)
|
|
23
|
+
XitDB::Ruby::Scalars.encode(value, core: core, registry: @registry)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def decode(slot, context:, path:, immutable: true)
|
|
27
|
+
if (adapter = container(slot, context: context, path: path))
|
|
28
|
+
context.view(slot, path: path, kind: adapter.kind)
|
|
29
|
+
else
|
|
30
|
+
XitDB::Ruby::Scalars.decode(slot, core: context.database.core, bound: context.read_bound,
|
|
31
|
+
registry: @registry, immutable: immutable)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def key_bytes(value)
|
|
36
|
+
XitDB::Ruby::KeyEncoding.dump(value)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def container(slot, context:, path:)
|
|
40
|
+
return nil unless slot.tag == :array_list || (slot.empty? && path.empty?)
|
|
41
|
+
|
|
42
|
+
XitDB::Ruby::Containers.new(self, context.cursor(slot), context: context, path: path)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def empty_container(kind, context:, path:)
|
|
46
|
+
kind, key_kind = kind if kind.is_a?(::Array)
|
|
47
|
+
XitDB::Ruby::Containers.new(self, context.cursor(Core::Slot.new), context: context, path: path,
|
|
48
|
+
kind: kind, key_kind: key_kind)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def conversion_kind(adapter)
|
|
52
|
+
return adapter.kind unless %i[sorted_map sorted_set].include?(adapter.kind)
|
|
53
|
+
|
|
54
|
+
stored = Core::ArrayList.new(adapter.cursor).get(1)
|
|
55
|
+
raise InvalidValueError, "missing sorted key kind" unless stored
|
|
56
|
+
|
|
57
|
+
key = stored.value
|
|
58
|
+
[adapter.kind, XitDB::Ruby::Containers::KEY_KINDS.key(key)]
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def conversion_container(kind, context:, path:)
|
|
62
|
+
# @type var kind: Symbol | [Symbol, Symbol?]
|
|
63
|
+
kind = [kind, :string] if kind.is_a?(Symbol) && %i[sorted_map sorted_set].include?(kind)
|
|
64
|
+
empty_container(kind, context: context, path: path)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def conversion_bytes(slot, context:)
|
|
68
|
+
return unless %i[bytes short_bytes].include?(slot.tag)
|
|
69
|
+
|
|
70
|
+
source = Conversion::ByteSource.new(context, slot)
|
|
71
|
+
return [source, :binary] unless source.format_tag
|
|
72
|
+
return [source, nil] unless source.format_tag == "rb"
|
|
73
|
+
return unless source.read(0, [source.size, 2].min) == "\x01\x01".b
|
|
74
|
+
|
|
75
|
+
length, position = source.unsigned(2)
|
|
76
|
+
raise UnsupportedEncodingError, "encoding name is too long" if length > Core::Bytes::CHUNK_SIZE
|
|
77
|
+
|
|
78
|
+
name = source.read(position, length).force_encoding(Encoding::UTF_8)
|
|
79
|
+
raise InvalidValueError, "invalid encoding name" unless name.valid_encoding?
|
|
80
|
+
|
|
81
|
+
begin
|
|
82
|
+
encoding = Encoding.find(name)
|
|
83
|
+
rescue ArgumentError => e
|
|
84
|
+
raise UnsupportedEncodingError, e.message
|
|
85
|
+
end
|
|
86
|
+
size, position = source.unsigned(position + length)
|
|
87
|
+
raise InvalidValueError, "trailing or truncated String bytes" unless position + size == source.size
|
|
88
|
+
|
|
89
|
+
[source.slice(position, size), encoding]
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def copy_conversion_bytes(source, encoding, context:, path:)
|
|
93
|
+
if source.format_tag == "rb"
|
|
94
|
+
raise UnsupportedValueError.new("portable rb bytes require an explicit transform", path: path)
|
|
95
|
+
end
|
|
96
|
+
return source.write_to(context) if !encoding || encoding == :binary
|
|
97
|
+
|
|
98
|
+
encoding = source.portable_encoding if encoding == :portable
|
|
99
|
+
prefix = "\x01\x01".b + XitDB::Ruby::Wire.t(encoding.name) + XitDB::Ruby::Wire.u(source.size)
|
|
100
|
+
source.write_to(context, prefix: prefix, format_tag: "rb")
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def input_kind(value, path:)
|
|
104
|
+
InputGuard.check!(value, path: path)
|
|
105
|
+
return nil if @registry.for_value(value)
|
|
106
|
+
|
|
107
|
+
klass = Object.instance_method(:class).bind_call(value)
|
|
108
|
+
case value
|
|
109
|
+
when Hash, ::Array, ::Set
|
|
110
|
+
validate_container(value, klass, path)
|
|
111
|
+
{ Hash => :map, ::Array => :array, ::Set => :set }.fetch(klass)
|
|
112
|
+
when CollectionDeclaration
|
|
113
|
+
declaration_kind(value, klass, path)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def hash_defaults?(value)
|
|
118
|
+
default = !BasicObject.instance_method(:equal?).bind_call(value.default, nil)
|
|
119
|
+
!value.default_proc.nil? || default || value.compare_by_identity?
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def validate_container(value, klass, path)
|
|
123
|
+
unless [Hash, ::Array, ::Set].include?(klass)
|
|
124
|
+
raise UnsupportedValueError.new("container subclasses are unsupported", path: path)
|
|
125
|
+
end
|
|
126
|
+
if value.is_a?(Hash) && hash_defaults?(value)
|
|
127
|
+
raise UnsupportedValueError.new("Hash defaults and identity comparison are unsupported", path: path)
|
|
128
|
+
end
|
|
129
|
+
return unless value.is_a?(::Set) && value.compare_by_identity?
|
|
130
|
+
|
|
131
|
+
raise UnsupportedValueError.new("Set identity comparison is unsupported", path: path)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def declaration_kind(value, klass, path)
|
|
135
|
+
unless CollectionDeclaration.equal?(klass)
|
|
136
|
+
raise UnsupportedValueError.new("collection declaration subclasses are unsupported", path: path)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
if %i[sorted_map sorted_set].include?(value.kind)
|
|
140
|
+
unless value.key_kind
|
|
141
|
+
raise UnsupportedValueError.new("Ruby sorted collections require an explicit key: kind", path: path)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
return [value.kind, value.key_kind]
|
|
145
|
+
end
|
|
146
|
+
kinds = { hash_map: :map, counted_hash_map: :map, hash_set: :set, counted_hash_set: :set,
|
|
147
|
+
array_list: :array, linked_array_list: :linked_array }
|
|
148
|
+
kinds.fetch(value.kind) do
|
|
149
|
+
raise UnsupportedValueError.new("unsupported Ruby collection declaration", path: path)
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def input_entries(value, path:)
|
|
154
|
+
Enumerator.new do |output|
|
|
155
|
+
case value
|
|
156
|
+
when Hash, ::Set
|
|
157
|
+
entries = value.is_a?(Hash) ? value.each_pair : value.each.lazy.map { |member| [member, nil] }
|
|
158
|
+
entries.each do |key, child|
|
|
159
|
+
begin
|
|
160
|
+
key_bytes(key)
|
|
161
|
+
rescue ValueError => e
|
|
162
|
+
raise e.class.new(e.message, path: path + [key]), cause: e
|
|
163
|
+
end
|
|
164
|
+
output << [key, child]
|
|
165
|
+
end
|
|
166
|
+
when ::Array
|
|
167
|
+
value.each_with_index { |child, index| output << [index, child] }
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module XitDB
|
|
4
|
+
# Identifies a history entry and verifies its file generation and trailer.
|
|
5
|
+
class Revision
|
|
6
|
+
def initialize(file_id:, trailer_position:, checksum:, index:)
|
|
7
|
+
@file_id = file_id
|
|
8
|
+
@trailer_position = trailer_position
|
|
9
|
+
@checksum = checksum
|
|
10
|
+
@index = index
|
|
11
|
+
freeze
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def resolve(database)
|
|
15
|
+
core = database.core
|
|
16
|
+
current = core.root_cursor
|
|
17
|
+
unless @file_id == core.header.file_id && @trailer_position + Core::Database::TRAILER_SIZE <= current.bound
|
|
18
|
+
raise ForeignRevisionError, "revision does not belong to this file generation"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
trailer = core.read_at(@trailer_position, Core::Database::TRAILER_SIZE, bound: current.bound)
|
|
22
|
+
checksum_bytes = trailer.byteslice(17, 8) || raise(CorruptionError, "truncated revision trailer")
|
|
23
|
+
checksum = checksum_bytes.unpack1("Q>")
|
|
24
|
+
payload = trailer.byteslice(0, 17) || raise(CorruptionError, "truncated revision trailer")
|
|
25
|
+
expected = Core::CRC64.call([@file_id, @trailer_position].pack("Q>Q>") + payload)
|
|
26
|
+
unless checksum == @checksum && checksum == expected
|
|
27
|
+
raise ForeignRevisionError, "revision trailer proof is no longer valid"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
slot_bytes = trailer.byteslice(0, 9) || raise(CorruptionError, "truncated revision trailer")
|
|
31
|
+
cursor = Core::Cursor.new(database: core, slot: Core::Slot.parse(slot_bytes),
|
|
32
|
+
bound: @trailer_position + Core::Database::TRAILER_SIZE)
|
|
33
|
+
database.snapshot_from_history(cursor, @index, revision: self)
|
|
34
|
+
rescue CorruptionError, IndexError, InvalidValueError
|
|
35
|
+
raise ForeignRevisionError, "revision history is no longer valid"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def inspect
|
|
39
|
+
"#<XitDB::Revision>"
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|