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.
Files changed (66) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +28 -0
  3. data/LICENSE +21 -0
  4. data/README.md +131 -0
  5. data/docs/benchmarks.md +135 -0
  6. data/docs/core.md +123 -0
  7. data/docs/portable.md +99 -0
  8. data/docs/releasing.md +37 -0
  9. data/docs/ruby-profile-v1.md +226 -0
  10. data/docs/ruby.md +61 -0
  11. data/examples/indexed_users.rb +23 -0
  12. data/examples/portable.rb +62 -0
  13. data/examples/ruby.rb +56 -0
  14. data/lib/xitdb/context.rb +360 -0
  15. data/lib/xitdb/conversion.rb +222 -0
  16. data/lib/xitdb/core/array_list.rb +282 -0
  17. data/lib/xitdb/core/b_tree.rb +546 -0
  18. data/lib/xitdb/core/bytes.rb +181 -0
  19. data/lib/xitdb/core/compaction.rb +172 -0
  20. data/lib/xitdb/core/crc64.rb +22 -0
  21. data/lib/xitdb/core/cursor.rb +201 -0
  22. data/lib/xitdb/core/database.rb +359 -0
  23. data/lib/xitdb/core/file_store.rb +194 -0
  24. data/lib/xitdb/core/format.rb +148 -0
  25. data/lib/xitdb/core/hash_map.rb +371 -0
  26. data/lib/xitdb/core/hasher.rb +49 -0
  27. data/lib/xitdb/core/linked_array_list.rb +85 -0
  28. data/lib/xitdb/core/memory_store.rb +277 -0
  29. data/lib/xitdb/core/offset_map.rb +85 -0
  30. data/lib/xitdb/core/replication.rb +242 -0
  31. data/lib/xitdb/core/sorted_map.rb +361 -0
  32. data/lib/xitdb/core/transaction.rb +159 -0
  33. data/lib/xitdb/core.rb +17 -0
  34. data/lib/xitdb/database.rb +332 -0
  35. data/lib/xitdb/declarations.rb +73 -0
  36. data/lib/xitdb/errors.rb +84 -0
  37. data/lib/xitdb/history.rb +58 -0
  38. data/lib/xitdb/input_guard.rb +39 -0
  39. data/lib/xitdb/profiles/portable.rb +407 -0
  40. data/lib/xitdb/profiles/ruby.rb +173 -0
  41. data/lib/xitdb/revision.rb +42 -0
  42. data/lib/xitdb/ruby/codec.rb +123 -0
  43. data/lib/xitdb/ruby/containers.rb +495 -0
  44. data/lib/xitdb/ruby/key_encoding.rb +60 -0
  45. data/lib/xitdb/ruby/ordered_keys.rb +87 -0
  46. data/lib/xitdb/ruby/scalars.rb +296 -0
  47. data/lib/xitdb/ruby/wire.rb +146 -0
  48. data/lib/xitdb/ruby.rb +46 -0
  49. data/lib/xitdb/snapshot.rb +13 -0
  50. data/lib/xitdb/streams.rb +148 -0
  51. data/lib/xitdb/value_walker.rb +192 -0
  52. data/lib/xitdb/version.rb +5 -0
  53. data/lib/xitdb/views/array.rb +303 -0
  54. data/lib/xitdb/views/base.rb +271 -0
  55. data/lib/xitdb/views/linked_array.rb +18 -0
  56. data/lib/xitdb/views/map.rb +162 -0
  57. data/lib/xitdb/views/set.rb +41 -0
  58. data/lib/xitdb/views/sorted.rb +77 -0
  59. data/lib/xitdb.rb +20 -0
  60. data/sig/base.rbs +76 -0
  61. data/sig/core.rbs +697 -0
  62. data/sig/manifest.yaml +10 -0
  63. data/sig/native.rbs +118 -0
  64. data/sig/public.rbs +427 -0
  65. data/sig/ruby.rbs +337 -0
  66. metadata +137 -0
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../xitdb"
4
+ require_relative "scalars"
5
+
6
+ module XitDB
7
+ module Ruby
8
+ # Captures immutable application serialization callbacks.
9
+ class Codec
10
+ attr_reader :id, :type
11
+
12
+ def initialize(id:, type:, dump:, load:)
13
+ unless Object.instance_method(:instance_of?).bind_call(id, String) &&
14
+ Object.instance_method(:is_a?).bind_call(type, Class)
15
+ raise CodecConfigurationError, "codec requires a String identifier and a Class"
16
+ end
17
+
18
+ InputGuard.check!(id)
19
+ @id = String.new(id).encode(Encoding::UTF_8).freeze
20
+ unless @id.valid_encoding? && !@id.empty?
21
+ raise CodecConfigurationError, "codec identifier must be nonempty UTF-8"
22
+ end
23
+
24
+ @type = type
25
+ @dump = capture_callback(dump)
26
+ @load = capture_callback(load)
27
+ freeze
28
+ rescue UnsupportedValueError, EncodingError => e
29
+ raise CodecConfigurationError, e.message
30
+ end
31
+
32
+ def dump(value)
33
+ bytes = invoke(@dump, value, :dump)
34
+ case bytes
35
+ when String
36
+ unless Object.instance_method(:instance_of?).bind_call(bytes, String)
37
+ raise CodecError, "codec #{id} dump must return a byte String"
38
+ end
39
+ else
40
+ raise CodecError, "codec #{id} dump must return a byte String"
41
+ end
42
+
43
+ String.new(bytes).b
44
+ end
45
+
46
+ def load(bytes)
47
+ invoke(@load, String.new(bytes).b.freeze, :load)
48
+ end
49
+
50
+ private
51
+
52
+ def capture_callback(callback)
53
+ unless Object.instance_method(:is_a?).bind_call(callback, Object) &&
54
+ Object.instance_method(:public_methods).bind_call(callback).include?(:call)
55
+ raise CodecConfigurationError, "codec callbacks must implement public #call"
56
+ end
57
+
58
+ Object.instance_method(:public_method).bind_call(callback, :call)
59
+ end
60
+
61
+ def invoke(callback, value, phase)
62
+ callback.call(value)
63
+ rescue XitDB::Error
64
+ raise
65
+ rescue StandardError => e
66
+ raise CodecError, "codec #{id} #{phase} failed: #{e.message}"
67
+ end
68
+ end
69
+
70
+ # Resolves codecs by their exact Ruby class and wire identifier.
71
+ class CodecRegistry
72
+ attr_reader :codecs
73
+
74
+ def initialize(codecs)
75
+ unless Object.instance_method(:instance_of?).bind_call(codecs, ::Array)
76
+ raise CodecConfigurationError, "codecs must be an Array"
77
+ end
78
+
79
+ @codecs = ::Array.instance_method(:map).bind_call(codecs) { |codec| codec }.freeze
80
+ # @type var by_type: Hash[Class, Codec]
81
+ by_type = {}
82
+ @by_type = by_type.compare_by_identity
83
+ # @type ivar @by_id: Hash[String, Codec]
84
+ @by_id = {}
85
+ @codecs.each do |codec|
86
+ validate_registration(codec)
87
+ @by_type[codec.type] = codec
88
+ @by_id[codec.id] = codec
89
+ end
90
+ @by_type.freeze
91
+ @by_id.freeze
92
+ freeze
93
+ rescue UnsupportedValueError => e
94
+ raise CodecConfigurationError, e.message
95
+ end
96
+
97
+ def validate_registration(codec)
98
+ unless Object.instance_method(:instance_of?).bind_call(codec, Codec) &&
99
+ Object.instance_method(:frozen?).bind_call(codec)
100
+ raise CodecConfigurationError, "registrations must be immutable Codec values"
101
+ end
102
+
103
+ InputGuard.check!(codec)
104
+ unless Module.instance_method(:<=).bind_call(codec.type, Object)
105
+ raise CodecConfigurationError, "codec types must inherit Object"
106
+ end
107
+ if Scalars.builtin_type?(codec.type) || [Hash, ::Array, ::Set, CollectionDeclaration].include?(codec.type) ||
108
+ Module.instance_method(:<=).bind_call(codec.type, Views::Base) ||
109
+ @by_type.key?(codec.type) || @by_id.key?(codec.id)
110
+ raise CodecConfigurationError, "duplicate codec or builtin override: #{codec.id}"
111
+ end
112
+ end
113
+
114
+ def for_value(value)
115
+ @by_type[Object.instance_method(:class).bind_call(value)]
116
+ end
117
+
118
+ def for_id(id)
119
+ @by_id[id]
120
+ end
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,495 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "ordered_keys"
4
+
5
+ module XitDB
6
+ module Ruby
7
+ # Maintains Ruby collection envelopes and their storage indexes.
8
+ class Containers
9
+ MARKERS = { array: 0x52420101, linked_array: 0x52420102, map: 0x52420103, set: 0x52420104,
10
+ sorted_map: 0x52420105, sorted_set: 0x52420106 }.freeze
11
+ KEY_KINDS = { string: 1, integer: 2, float: 3, time: 4 }.freeze
12
+
13
+ attr_reader :kind, :cursor
14
+
15
+ def initialize(profile, cursor, context:, path:, kind: nil, key_kind: nil)
16
+ @profile = profile
17
+ @cursor = cursor
18
+ @context = context
19
+ @path = path
20
+ if kind
21
+ @kind = kind
22
+ @key_kind = key_kind
23
+ initialize_empty
24
+ elsif cursor.slot.empty?
25
+ @kind = :map
26
+ else
27
+ read_envelope
28
+ end
29
+ end
30
+
31
+ def slot
32
+ cursor.slot
33
+ end
34
+
35
+ def size
36
+ return 0 unless @envelope
37
+
38
+ ordered? ? @lookup.size : @collection.size
39
+ end
40
+
41
+ def empty?
42
+ size.zero?
43
+ end
44
+
45
+ def lookup(selector)
46
+ if sorted?
47
+ bytes = key_bytes(selector)
48
+ stored = sorted_collection.get(bytes)
49
+ return sorted_entry(bytes, stored).last if stored
50
+
51
+ return nil
52
+ end
53
+ return sequence_collection.get(index_selector(selector)) unless ordered?
54
+
55
+ bytes = key_bytes(selector)
56
+ return nil unless @envelope
57
+
58
+ record_for(@profile.hasher.digest(bytes), expected_bytes: bytes)&.get(1)
59
+ end
60
+
61
+ def assign(selector, stored)
62
+ if sorted?
63
+ assign_sorted(selector, stored)
64
+ elsif ordered?
65
+ assign_ordered(selector, stored)
66
+ elsif selector == size
67
+ sequence_collection.append(stored)
68
+ else
69
+ sequence_collection.put(index_selector(selector),
70
+ stored)
71
+ end
72
+ publish
73
+ end
74
+
75
+ def delete(selector)
76
+ if sorted?
77
+ bytes = key_bytes(selector)
78
+ stored = sorted_collection.get(bytes)
79
+ return self unless stored
80
+
81
+ sorted_entry(bytes, stored)
82
+ sorted_collection.remove(bytes)
83
+ elsif ordered?
84
+ bytes = key_bytes(selector)
85
+ digest = @profile.hasher.digest(bytes)
86
+ record = record_for(digest, expected_bytes: bytes)
87
+ return self unless record
88
+
89
+ @order.remove([fetch_slot(record, 2).value].pack("Q>"))
90
+ @lookup.remove(digest)
91
+ elsif kind == :linked_array
92
+ linked_collection.delete_at(index_selector(selector))
93
+ else
94
+ delete_array(selector)
95
+ end
96
+ publish
97
+ end
98
+
99
+ def clear_needed?
100
+ size.positive? || (ordered? && @next_id&.positive? == true)
101
+ end
102
+
103
+ def clear
104
+ initialize_empty
105
+ self
106
+ end
107
+
108
+ def insert(index, stored)
109
+ if kind == :linked_array
110
+ Core::BTree.new(linked_collection.cursor).insert(index, stored)
111
+ else
112
+ count = size
113
+ array_collection.append(stored)
114
+ (count - 1).downto(index) do |position|
115
+ array_collection.put(position + 1, required_slot(array_collection.get(position)))
116
+ end
117
+ array_collection.put(index, stored)
118
+ end
119
+ publish
120
+ end
121
+
122
+ def truncate(count)
123
+ if kind == :linked_array
124
+ @collection = linked_collection.slice(0, count)
125
+ else
126
+ array_collection.truncate(count)
127
+ end
128
+ publish
129
+ end
130
+
131
+ def slice(start, count)
132
+ if kind == :linked_array
133
+ return array([Core::Slot.uint(MARKERS.fetch(kind)),
134
+ linked_collection.slice(start, count).cursor.slot]).cursor.slot
135
+ end
136
+
137
+ output = @profile.empty_container(kind, context: @context, path: @path)
138
+ count.times { |index| output.assign(index, required_slot(lookup(start + index))) }
139
+ output.slot
140
+ end
141
+
142
+ def concat(other)
143
+ if kind == :linked_array && other.is_a?(Containers) && other.kind == :linked_array
144
+ @collection = linked_collection.concat(other.linked_collection)
145
+ publish
146
+ else
147
+ other.each_entry { |_index, stored| assign(size, stored) }
148
+ end
149
+ self
150
+ end
151
+
152
+ def each_from_index(index, &block)
153
+ return enum_for(:each_from_index, index) unless block
154
+
155
+ index += size if index.negative?
156
+ return self unless index >= 0 && index < size
157
+
158
+ each_normalized_index(index, &block)
159
+ self
160
+ end
161
+
162
+ def each_normalized_index(index)
163
+ if sorted?
164
+ sorted_collection.each_from_index(index) do |bytes, stored|
165
+ key, value = sorted_entry(bytes, stored)
166
+ yield key, value
167
+ end
168
+ elsif kind == :linked_array
169
+ linked_collection.each_from_index(index).with_index(index) { |stored, position| yield position, stored }
170
+ else
171
+ (index...size).each { |position| yield position, required_slot(lookup(position)) }
172
+ end
173
+ self
174
+ end
175
+ private :each_normalized_index
176
+
177
+ def each_entry
178
+ return enum_for(:each_entry) unless block_given?
179
+ return self unless @envelope
180
+
181
+ if sorted?
182
+ sorted_collection.each do |bytes, stored|
183
+ key, value = sorted_entry(bytes, stored)
184
+ yield key, value
185
+ end
186
+ elsif ordered?
187
+ @order.each do |id_bytes, hash_slot|
188
+ key, stored = ordered_entry(id_bytes, hash_slot)
189
+ yield key, stored
190
+ end
191
+ else
192
+ sequence_collection.each.with_index { |stored, index| yield index, stored }
193
+ end
194
+ self
195
+ end
196
+
197
+ def rank(selector)
198
+ sorted_collection.rank(key_bytes(selector))
199
+ end
200
+
201
+ def each_from(selector)
202
+ return enum_for(:each_from, selector) unless block_given?
203
+
204
+ sorted_collection.each_from(key_bytes(selector)) do |bytes, stored|
205
+ key, value = sorted_entry(bytes, stored)
206
+ yield key, value
207
+ end
208
+ self
209
+ end
210
+
211
+ def selector_identity(selector)
212
+ %i[array linked_array].include?(kind) ? selector : key_bytes(selector)
213
+ end
214
+
215
+ protected
216
+
217
+ attr_reader :collection
218
+
219
+ def linked_collection
220
+ collection = @collection
221
+ invalid! unless collection.is_a?(Core::LinkedArrayList)
222
+
223
+ collection
224
+ end
225
+
226
+ private
227
+
228
+ def index_selector(selector)
229
+ raise TypeError, "index must be an Integer" unless selector.is_a?(Integer)
230
+
231
+ selector
232
+ end
233
+
234
+ def required_slot(slot)
235
+ invalid! unless slot
236
+
237
+ slot
238
+ end
239
+
240
+ def fetch_slot(collection, index)
241
+ required_slot(collection.get(index))
242
+ end
243
+
244
+ def envelope
245
+ value = @envelope
246
+ invalid! unless value
247
+
248
+ value
249
+ end
250
+
251
+ def declared_key_kind
252
+ value = @key_kind
253
+ invalid! unless value
254
+
255
+ value
256
+ end
257
+
258
+ def array_collection
259
+ collection = @collection
260
+ invalid! unless collection.is_a?(Core::ArrayList)
261
+
262
+ collection
263
+ end
264
+
265
+ def sorted_collection
266
+ collection = @collection
267
+ invalid! unless collection.is_a?(Core::SortedMap)
268
+
269
+ collection
270
+ end
271
+
272
+ def sequence_collection
273
+ collection = @collection
274
+ invalid! unless collection.is_a?(Core::ArrayList) || collection.is_a?(Core::LinkedArrayList)
275
+
276
+ collection
277
+ end
278
+
279
+ def ordered?
280
+ %i[map set].include?(kind)
281
+ end
282
+
283
+ def sorted?
284
+ %i[sorted_map sorted_set].include?(kind)
285
+ end
286
+
287
+ def array(fields)
288
+ output = Core::ArrayList.new(@context.cursor(Core::Slot.new))
289
+ fields.each { |field| output.append(field) }
290
+ output
291
+ end
292
+
293
+ def initialize_empty
294
+ marker = Core::Slot.uint(MARKERS.fetch(kind))
295
+ if sorted?
296
+ @collection = Core::SortedMap.new(@context.cursor(Core::Slot.new))
297
+ @envelope = array([marker, Core::Slot.uint(KEY_KINDS.fetch(@key_kind)), @collection.cursor.slot])
298
+ elsif ordered?
299
+ @next_id = 0
300
+ @lookup = Core::HashMap.new(@context.cursor(Core::Slot.new), counted: true)
301
+ @order = Core::SortedMap.new(@context.cursor(Core::Slot.new))
302
+ @envelope = array([marker, Core::Slot.uint(0), @lookup.cursor.slot, @order.cursor.slot])
303
+ else
304
+ klass = kind == :array ? Core::ArrayList : Core::LinkedArrayList
305
+ @collection = klass.new(@context.cursor(Core::Slot.new))
306
+ @envelope = array([marker, @collection.cursor.slot])
307
+ end
308
+ cursor.replace(envelope.cursor.slot)
309
+ end
310
+
311
+ def read_envelope
312
+ @envelope = Core::ArrayList.new(cursor)
313
+ marker = fetch_slot(envelope, 0)
314
+ invalid! unless marker.tag == :uint && (@kind = MARKERS.key(marker.value))
315
+ invalid! unless envelope.size == envelope_size
316
+ if sorted?
317
+ read_sorted_envelope
318
+ elsif ordered?
319
+ read_ordered_envelope
320
+ else
321
+ read_sequence_envelope
322
+ end
323
+ end
324
+
325
+ def publish
326
+ if sorted?
327
+ envelope.put(2, @collection.cursor.slot)
328
+ elsif ordered?
329
+ envelope.put(1, Core::Slot.uint(@next_id))
330
+ envelope.put(2, @lookup.cursor.slot)
331
+ envelope.put(3, @order.cursor.slot)
332
+ else
333
+ envelope.put(1, @collection.cursor.slot)
334
+ end
335
+ cursor.replace(envelope.cursor.slot)
336
+ self
337
+ end
338
+
339
+ def key_bytes(selector)
340
+ sorted? ? OrderedKeys.dump(selector, kind: declared_key_kind) : @profile.key_bytes(selector)
341
+ rescue ValueError => e
342
+ raise e.class.new(e.message, path: @path + [selector]), cause: e
343
+ end
344
+
345
+ def raw_bytes(slot)
346
+ invalid! unless slot && %i[bytes short_bytes].include?(slot.tag)
347
+ value = @context.database.core.read_bytes(slot, bound: @context.read_bound)
348
+ invalid! if value.format_tag
349
+ value.bytes
350
+ end
351
+
352
+ def decode_key(slot)
353
+ value = Scalars.decode(slot, core: @context.database.core, bound: @context.read_bound)
354
+ case value
355
+ when String, Integer, Float, Time, Symbol, true, false, nil, UInt then value
356
+ else invalid!
357
+ end
358
+ end
359
+
360
+ def record_for(digest, expected_bytes: nil)
361
+ stored = @lookup.get(digest)
362
+ return nil unless stored
363
+
364
+ invalid! unless stored.tag == :array_list
365
+ record = Core::ArrayList.new(@context.cursor(stored))
366
+ invalid! unless record.size == 3
367
+ id = fetch_slot(record, 2)
368
+ invalid! unless id.tag == :uint && id.value < @next_id
369
+ validate_record(record, digest, id, expected_bytes)
370
+ record
371
+ end
372
+
373
+ def sorted_entry(bytes, stored)
374
+ if kind == :sorted_map
375
+ invalid! unless stored.tag == :array_list
376
+ record = Core::ArrayList.new(@context.cursor(stored))
377
+ invalid! unless record.size == 2
378
+ original = fetch_slot(record, 0)
379
+ value = fetch_slot(record, 1)
380
+ else
381
+ original = stored
382
+ value = Core::Slot.null
383
+ end
384
+ key = decode_key(original)
385
+ begin
386
+ invalid! unless OrderedKeys.dump(key, kind: declared_key_kind) == bytes
387
+ rescue UnsupportedValueError, UnsupportedEncodingError
388
+ invalid!
389
+ end
390
+ [key, value]
391
+ end
392
+
393
+ def assign_sorted(selector, stored)
394
+ bytes = key_bytes(selector)
395
+ previous = sorted_collection.get(bytes)
396
+ if previous
397
+ sorted_entry(bytes, previous)
398
+ original = kind == :sorted_set ? previous : fetch_slot(Core::ArrayList.new(@context.cursor(previous)), 0)
399
+ else
400
+ original = @profile.encode_scalar(selector, core: @context.database.core)
401
+ end
402
+ value = kind == :sorted_set ? original : array([original, stored]).cursor.slot
403
+ sorted_collection.put(bytes, value: value)
404
+ end
405
+
406
+ def assign_ordered(selector, stored)
407
+ bytes = key_bytes(selector)
408
+ initialize_empty unless @envelope
409
+ digest = @profile.hasher.digest(bytes)
410
+ previous = record_for(digest, expected_bytes: bytes)
411
+ if previous
412
+ record = array([fetch_slot(previous, 0), kind == :set ? Core::Slot.null : stored, fetch_slot(previous, 2)])
413
+ else
414
+ raise RangeError, "insertion identifier exhausted" if @next_id == Core::Format::UINT64_MAX
415
+
416
+ key = @profile.encode_scalar(selector, core: @context.database.core)
417
+ record = array([key, kind == :set ? Core::Slot.null : stored, Core::Slot.uint(@next_id)])
418
+ @order.put([@next_id].pack("Q>"), value: @context.database.core.bytes(digest))
419
+ @next_id += 1
420
+ end
421
+ @lookup.put(digest, key: @context.database.core.bytes(bytes), value: record.cursor.slot)
422
+ end
423
+
424
+ def delete_array(selector)
425
+ count = size
426
+ unless selector.is_a?(Integer) && selector >= 0 && selector < count
427
+ raise IndexError,
428
+ "array index outside list"
429
+ end
430
+
431
+ (selector...(count - 1)).each do |index|
432
+ array_collection.put(index, required_slot(array_collection.get(index + 1)))
433
+ end
434
+ array_collection.truncate(count - 1)
435
+ end
436
+
437
+ def envelope_size
438
+ return 4 if ordered?
439
+
440
+ sorted? ? 3 : 2
441
+ end
442
+
443
+ def read_sequence_envelope
444
+ child = fetch_slot(envelope, 1)
445
+ expected = kind == :array ? :array_list : :linked_array_list
446
+ invalid! unless child.tag == expected
447
+ klass = kind == :array ? Core::ArrayList : Core::LinkedArrayList
448
+ @collection = klass.new(@context.cursor(child))
449
+ end
450
+
451
+ def read_sorted_envelope
452
+ key = fetch_slot(envelope, 1)
453
+ child = fetch_slot(envelope, 2)
454
+ invalid! unless key.tag == :uint && (@key_kind = KEY_KINDS.key(key.value)) && child.tag == :sorted_map
455
+ @collection = Core::SortedMap.new(@context.cursor(child))
456
+ end
457
+
458
+ def read_ordered_envelope
459
+ id = fetch_slot(envelope, 1)
460
+ lookup = fetch_slot(envelope, 2)
461
+ order = fetch_slot(envelope, 3)
462
+ invalid! unless id.tag == :uint && lookup.tag == :counted_hash_map && order.tag == :sorted_map
463
+ @next_id = id.value
464
+ @lookup = Core::HashMap.new(@context.cursor(lookup), counted: true)
465
+ @order = Core::SortedMap.new(@context.cursor(order))
466
+ invalid! unless @lookup.size == @order.size && @lookup.size <= @next_id
467
+ end
468
+
469
+ def ordered_entry(id_bytes, hash_slot)
470
+ invalid! unless id_bytes.bytesize == 8
471
+ digest = raw_bytes(hash_slot)
472
+ invalid! unless digest.bytesize == 32
473
+ record = record_for(digest)
474
+ invalid! unless record && fetch_slot(record, 2).value == id_bytes.unpack1("Q>")
475
+ [decode_key(fetch_slot(record, 0)), fetch_slot(record, 1)]
476
+ end
477
+
478
+ def validate_record(record, digest, id, expected_bytes)
479
+ bytes = raw_bytes(@lookup.key(digest))
480
+ invalid! unless @profile.hasher.digest(bytes) == digest && (!expected_bytes || bytes == expected_bytes)
481
+ begin
482
+ invalid! unless @profile.key_bytes(decode_key(fetch_slot(record, 0))) == bytes
483
+ rescue UnsupportedValueError
484
+ invalid!
485
+ end
486
+ invalid! unless raw_bytes(@order.get([id.value].pack("Q>"))) == digest
487
+ invalid! if kind == :set && fetch_slot(record, 1) != Core::Slot.null
488
+ end
489
+
490
+ def invalid!
491
+ raise InvalidValueError, "invalid Ruby container envelope or index record"
492
+ end
493
+ end
494
+ end
495
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "wire"
4
+ require_relative "../input_guard"
5
+
6
+ module XitDB
7
+ module Ruby
8
+ # Encodes Ruby hash keys without invoking application equality methods.
9
+ module KeyEncoding
10
+ TYPES = [NilClass, FalseClass, TrueClass, Integer, Float, String, Symbol].freeze
11
+ private_constant :TYPES
12
+
13
+ def self.dump(value)
14
+ InputGuard.check!(value)
15
+ unless TYPES.include?(Object.instance_method(:class).bind_call(value))
16
+ raise UnsupportedValueError, "unsupported Ruby key class"
17
+ end
18
+
19
+ scalar_bytes(value).freeze
20
+ end
21
+
22
+ def self.scalar_bytes(value)
23
+ case value
24
+ when nil then "\x00".b
25
+ when false then "\x01".b
26
+ when true then "\x02".b
27
+ when Integer then "\x03".b + Wire.z(value)
28
+ when Float then float_bytes(value)
29
+ when String then "\x05".b + string_body(value)
30
+ when Symbol then "\x06".b + string_body(value.to_s)
31
+ else
32
+ raise UnsupportedValueError, "unsupported Ruby key: #{value.class}"
33
+ end
34
+ end
35
+
36
+ def self.float_bytes(value)
37
+ raise UnsupportedValueError, "NaN keys are unsupported" if value.nan?
38
+
39
+ "\x04".b + Wire.f64(value.zero? ? 0.0 : value)
40
+ end
41
+
42
+ def self.checked_key(value)
43
+ case value
44
+ when NilClass, FalseClass, TrueClass, String, Integer, Float, Symbol then value
45
+ else raise UnsupportedValueError, "unsupported Ruby key class"
46
+ end
47
+ end
48
+
49
+ def self.string_body(value)
50
+ label = if value.empty? || (value.encoding.ascii_compatible? && value.ascii_only?)
51
+ ""
52
+ else
53
+ value.encoding.name
54
+ end
55
+ Wire.t(label) + Wire.b(value)
56
+ end
57
+ private_class_method :string_body, :scalar_bytes, :float_bytes
58
+ end
59
+ end
60
+ end