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,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
|