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,148 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "tempfile"
|
|
4
|
+
|
|
5
|
+
module XitDB
|
|
6
|
+
module Streams
|
|
7
|
+
# Restricts stream access to the lifetime of its owning block.
|
|
8
|
+
class Handle
|
|
9
|
+
def initialize(context)
|
|
10
|
+
@context = context
|
|
11
|
+
@active = true
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def expire!
|
|
15
|
+
@active = false
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def check!
|
|
21
|
+
raise ExpiredViewError, "byte stream block has ended" unless @active
|
|
22
|
+
|
|
23
|
+
@context.check!
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def integer(value)
|
|
27
|
+
result = Integer.try_convert(value) || raise(TypeError, "expected an Integer")
|
|
28
|
+
limit = (1 << ((0.size * 8) - 1)) - 1
|
|
29
|
+
raise RangeError, "integer too big to convert into long" unless result.between?(-limit - 1, limit)
|
|
30
|
+
|
|
31
|
+
result
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Reads a captured byte value with bounded storage reads.
|
|
36
|
+
class Reader < Handle
|
|
37
|
+
def initialize(context, slot)
|
|
38
|
+
super(context)
|
|
39
|
+
unless %i[bytes short_bytes].include?(slot.tag) && !slot.full
|
|
40
|
+
raise InvalidValueError, "stream requires untagged bytes"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
@source = Core::Bytes.open(context.database.core, slot, bound: context.read_bound)
|
|
44
|
+
@size = @source.size
|
|
45
|
+
@pos = 0
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def read(length = nil, output = nil)
|
|
49
|
+
check!
|
|
50
|
+
@context.read do
|
|
51
|
+
check!
|
|
52
|
+
count = length.nil? ? [@size - @pos, 0].max : integer(length)
|
|
53
|
+
raise ArgumentError, "negative length" if count.negative?
|
|
54
|
+
|
|
55
|
+
buffer = if output.nil?
|
|
56
|
+
+"".b
|
|
57
|
+
else
|
|
58
|
+
String.try_convert(output) || raise(TypeError, "expected a String buffer")
|
|
59
|
+
end
|
|
60
|
+
buffer.replace("".b)
|
|
61
|
+
return nil if !length.nil? && count.positive? && @pos >= @size
|
|
62
|
+
|
|
63
|
+
remaining = count.clamp(0, [@size - @pos, 0].max)
|
|
64
|
+
while remaining.positive?
|
|
65
|
+
size = [remaining, Core::Bytes::CHUNK_SIZE].min
|
|
66
|
+
buffer << @source.read(@pos, size)
|
|
67
|
+
@pos += size
|
|
68
|
+
remaining -= size
|
|
69
|
+
end
|
|
70
|
+
buffer
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def readpartial(length, output = nil)
|
|
75
|
+
check!
|
|
76
|
+
read(integer(length), output) || raise(EOFError, "end of byte stream")
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def seek(offset, whence = IO::SEEK_SET)
|
|
80
|
+
check!
|
|
81
|
+
offset = integer(offset)
|
|
82
|
+
whence = case whence
|
|
83
|
+
when :SET then IO::SEEK_SET
|
|
84
|
+
when :CUR then IO::SEEK_CUR
|
|
85
|
+
when :END then IO::SEEK_END
|
|
86
|
+
else whence
|
|
87
|
+
end
|
|
88
|
+
base = case integer(whence)
|
|
89
|
+
when IO::SEEK_SET then 0
|
|
90
|
+
when IO::SEEK_CUR then @pos
|
|
91
|
+
when IO::SEEK_END then @size
|
|
92
|
+
else raise Errno::EINVAL, "invalid seek origin"
|
|
93
|
+
end
|
|
94
|
+
position = base + offset
|
|
95
|
+
raise Errno::EINVAL, "negative seek position" if position.negative?
|
|
96
|
+
|
|
97
|
+
@pos = integer(position)
|
|
98
|
+
0
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def rewind
|
|
102
|
+
seek(0)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def pos
|
|
106
|
+
check!
|
|
107
|
+
@pos
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def size
|
|
111
|
+
check!
|
|
112
|
+
@size
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def eof?
|
|
116
|
+
check!
|
|
117
|
+
@pos >= @size
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Spools bytes before the enclosing assignment is published.
|
|
122
|
+
class Writer < Handle
|
|
123
|
+
def initialize(context, spool)
|
|
124
|
+
super(context)
|
|
125
|
+
@spool = spool
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def write(value)
|
|
129
|
+
check!
|
|
130
|
+
bytes = value.to_s
|
|
131
|
+
raise TypeError, "to_s must return a String" unless bytes.is_a?(String)
|
|
132
|
+
|
|
133
|
+
position = 0
|
|
134
|
+
while position < bytes.bytesize
|
|
135
|
+
check!
|
|
136
|
+
chunk = bytes.byteslice(position, Core::Bytes::CHUNK_SIZE) || raise(EOFError, "byte source ended")
|
|
137
|
+
position += @spool.write(chunk)
|
|
138
|
+
end
|
|
139
|
+
position
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def pos
|
|
143
|
+
check!
|
|
144
|
+
@spool.pos
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "input_guard"
|
|
4
|
+
|
|
5
|
+
module XitDB
|
|
6
|
+
# Encodes and materializes nested values without recursive tree traversal.
|
|
7
|
+
module ValueWalker
|
|
8
|
+
# Frames retain only the current ancestors and their incremental iterators.
|
|
9
|
+
def self.encode(value, context:, path:)
|
|
10
|
+
# @type var active: ::Hash[Integer, bool]
|
|
11
|
+
active = {}
|
|
12
|
+
# @type var stack: ::Array[encoding_frame]
|
|
13
|
+
stack = []
|
|
14
|
+
# @type var current: [top, path]?
|
|
15
|
+
current = [value, path]
|
|
16
|
+
# @type var result: Core::Slot?
|
|
17
|
+
result = nil
|
|
18
|
+
while true # rubocop:disable Style/InfiniteLoop -- Codec StopIteration must propagate.
|
|
19
|
+
if current
|
|
20
|
+
result = begin_encoding(current[0], current[1], context, active, stack)
|
|
21
|
+
current = nil
|
|
22
|
+
end
|
|
23
|
+
if result
|
|
24
|
+
break if stack.empty?
|
|
25
|
+
|
|
26
|
+
assign_encoding_parent(stack, result)
|
|
27
|
+
end
|
|
28
|
+
frame = stack.last || raise(CorruptionError, "missing encoding frame")
|
|
29
|
+
entry = next_input(frame)
|
|
30
|
+
if entry
|
|
31
|
+
selector, child = entry
|
|
32
|
+
frame[4] = selector
|
|
33
|
+
current = [child, frame[1] + [selector]]
|
|
34
|
+
else
|
|
35
|
+
result = finish_encoding(frame, active, stack)
|
|
36
|
+
break if stack.empty?
|
|
37
|
+
|
|
38
|
+
result = nil
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
result || raise(CorruptionError, "encoding produced no value")
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def self.materialize(view)
|
|
45
|
+
view.check!
|
|
46
|
+
context = view.context
|
|
47
|
+
# @type var active: ::Hash[[Symbol, Integer], bool]
|
|
48
|
+
active = {}
|
|
49
|
+
# @type var stack: ::Array[materialization_frame]
|
|
50
|
+
stack = []
|
|
51
|
+
# @type var current: [Core::Slot, path]?
|
|
52
|
+
current = [view.slot, view.path]
|
|
53
|
+
# @type var selected: adapter | Views::Slice::Selection | nil
|
|
54
|
+
selected = view.adapter
|
|
55
|
+
# @type var materialized: materialized?
|
|
56
|
+
materialized = nil
|
|
57
|
+
while true # rubocop:disable Style/InfiniteLoop -- Codec StopIteration must propagate.
|
|
58
|
+
if current
|
|
59
|
+
begin_materialization(current[0], current[1], selected, context, active, stack)
|
|
60
|
+
current = nil
|
|
61
|
+
selected = nil
|
|
62
|
+
end
|
|
63
|
+
frame = stack.last || raise(CorruptionError, "missing materialization frame")
|
|
64
|
+
entry = next_stored_entry(frame)
|
|
65
|
+
if entry
|
|
66
|
+
current = materialized_child(frame, entry)
|
|
67
|
+
else
|
|
68
|
+
materialized = finish_materialization(frame, active, stack)
|
|
69
|
+
break if stack.empty?
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
materialized || raise(CorruptionError, "materialization produced no value")
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def self.begin_encoding(input, location, context, active, stack)
|
|
76
|
+
InputGuard.check!(input, path: location)
|
|
77
|
+
case input
|
|
78
|
+
when Views::Base then encode_view(input, context, location)
|
|
79
|
+
else
|
|
80
|
+
profile = context.profile
|
|
81
|
+
kind = profile.input_kind(input, path: location)
|
|
82
|
+
return encode_scalar(input, context, location) unless kind
|
|
83
|
+
|
|
84
|
+
identity = BasicObject.instance_method(:__id__).bind_call(input)
|
|
85
|
+
raise CyclicValueError.new("cyclic value at #{location.inspect}", path: location) if active[identity]
|
|
86
|
+
|
|
87
|
+
active[identity] = true
|
|
88
|
+
adapter = profile.empty_container(kind, context: context, path: location)
|
|
89
|
+
stack << [input, location, adapter, profile.input_entries(input, path: location), nil]
|
|
90
|
+
nil
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def self.encode_view(input, context, location)
|
|
95
|
+
input.check!
|
|
96
|
+
unless input.context.reference_compatible?(context)
|
|
97
|
+
raise ForeignValueError.new("view belongs to another store or file generation", path: location)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
context.writable_transaction.freeze
|
|
101
|
+
assigned = input.slot_for_assignment(context: context, path: location)
|
|
102
|
+
assigned.empty? ? encode({}, context: context, path: location) : assigned
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def self.encode_scalar(input, context, location)
|
|
106
|
+
context.profile.encode_scalar(input, core: context.database.core)
|
|
107
|
+
rescue ValueError => e
|
|
108
|
+
raise e.class.new("#{e.message} at #{location.inspect}", path: location), cause: e
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def self.assign_encoding_parent(stack, stored)
|
|
112
|
+
frame = stack.last || raise(CorruptionError, "missing encoding frame")
|
|
113
|
+
frame[2].assign(frame[4], stored)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def self.finish_encoding(frame, active, stack)
|
|
117
|
+
stored = frame[2].slot
|
|
118
|
+
active.delete(BasicObject.instance_method(:__id__).bind_call(frame[0]))
|
|
119
|
+
stack.pop
|
|
120
|
+
assign_encoding_parent(stack, stored) unless stack.empty?
|
|
121
|
+
stored
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def self.next_input(frame)
|
|
125
|
+
frame[3].next
|
|
126
|
+
rescue StopIteration
|
|
127
|
+
nil
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def self.begin_materialization(slot, path, selected, context, active, stack)
|
|
131
|
+
adapter = selected || context.profile.container(slot, context: context, path: path)
|
|
132
|
+
unless adapter
|
|
133
|
+
result = context.profile.decode(slot, context: context, path: path, immutable: false)
|
|
134
|
+
frame = stack.last || raise(CorruptionError, "missing materialization frame")
|
|
135
|
+
insert(frame, result)
|
|
136
|
+
return
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# @type var identity: [Symbol, Integer]
|
|
140
|
+
identity = [slot.tag, slot.value]
|
|
141
|
+
raise CyclicValueError.new("cyclic stored value", path: path) if active[identity]
|
|
142
|
+
|
|
143
|
+
active[identity] = true
|
|
144
|
+
# @type var output: materialized
|
|
145
|
+
output = case adapter.kind
|
|
146
|
+
when :map, :sorted_map then {}
|
|
147
|
+
when :set, :sorted_set then Set.new
|
|
148
|
+
else []
|
|
149
|
+
end
|
|
150
|
+
stack << [identity, path, adapter.each_entry, output, nil]
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def self.next_stored_entry(frame)
|
|
154
|
+
frame[2].next
|
|
155
|
+
rescue StopIteration
|
|
156
|
+
nil
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def self.materialized_child(frame, entry)
|
|
160
|
+
selector, child = entry
|
|
161
|
+
frame[4] = selector.is_a?(String) ? selector.dup : selector
|
|
162
|
+
if frame[3].is_a?(Set)
|
|
163
|
+
frame[3].add(frame[4])
|
|
164
|
+
nil
|
|
165
|
+
else
|
|
166
|
+
[child, frame[1] + [selector]]
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def self.finish_materialization(frame, active, stack)
|
|
171
|
+
active.delete(frame[0])
|
|
172
|
+
stack.pop
|
|
173
|
+
output = frame[3]
|
|
174
|
+
unless stack.empty?
|
|
175
|
+
parent = stack.last || raise(CorruptionError, "missing materialization parent")
|
|
176
|
+
insert(parent, output)
|
|
177
|
+
end
|
|
178
|
+
output
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def self.insert(frame, value)
|
|
182
|
+
output = frame[3]
|
|
183
|
+
case output
|
|
184
|
+
when Hash then output[frame[4]] = value
|
|
185
|
+
when Array then output << value
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
private_class_method :insert, :begin_encoding, :encode_view, :encode_scalar, :assign_encoding_parent,
|
|
189
|
+
:finish_encoding, :next_input, :begin_materialization, :next_stored_entry,
|
|
190
|
+
:materialized_child, :finish_materialization
|
|
191
|
+
end
|
|
192
|
+
end
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module XitDB
|
|
4
|
+
module Views
|
|
5
|
+
# Provides indexed reads and writes for persistent arrays.
|
|
6
|
+
class Array < Base
|
|
7
|
+
UNSET = MissingArgument.new.freeze
|
|
8
|
+
private_constant :UNSET
|
|
9
|
+
|
|
10
|
+
alias length size
|
|
11
|
+
|
|
12
|
+
def [](index, count = UNSET)
|
|
13
|
+
context.read { read_index(index, count) }
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def read_index(index, count)
|
|
17
|
+
return read_slice(integer_argument(index), integer_argument(count)) unless count.equal?(UNSET)
|
|
18
|
+
return read_range(index) if index.is_a?(Range)
|
|
19
|
+
|
|
20
|
+
index = normalize_index(index)
|
|
21
|
+
return nil unless index >= 0 && index < size
|
|
22
|
+
|
|
23
|
+
stored = adapter.lookup(index) || raise(IndexError, "array index outside list")
|
|
24
|
+
context.profile.decode(stored, context: context, path: entry_path(index))
|
|
25
|
+
end
|
|
26
|
+
private :read_index
|
|
27
|
+
|
|
28
|
+
def read_range(index)
|
|
29
|
+
start = index.begin.nil? ? 0 : integer_argument(index.begin)
|
|
30
|
+
finish = index.end.nil? ? size : integer_argument(index.end)
|
|
31
|
+
start += size if start.negative?
|
|
32
|
+
return nil if start.negative? || start > size
|
|
33
|
+
|
|
34
|
+
finish += size if finish.negative?
|
|
35
|
+
finish += 1 unless index.exclude_end? || index.end.nil?
|
|
36
|
+
read_slice(start, [finish - start, 0].max)
|
|
37
|
+
end
|
|
38
|
+
private :read_range
|
|
39
|
+
|
|
40
|
+
def fetch(index, default = UNSET)
|
|
41
|
+
found, value = context.read do
|
|
42
|
+
position = normalize_index(index)
|
|
43
|
+
present = position >= 0 && position < size
|
|
44
|
+
[present, present ? self[position] : nil]
|
|
45
|
+
end
|
|
46
|
+
return value if found
|
|
47
|
+
return yield index if block_given?
|
|
48
|
+
|
|
49
|
+
case default
|
|
50
|
+
when MissingArgument then nil
|
|
51
|
+
else return default
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
raise IndexError, "array index outside list"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def first(count = UNSET)
|
|
58
|
+
context.read do
|
|
59
|
+
check!
|
|
60
|
+
return self[0] if count.equal?(UNSET)
|
|
61
|
+
|
|
62
|
+
requested = nonnegative_count(count)
|
|
63
|
+
each.take(requested)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def last(count = UNSET)
|
|
68
|
+
context.read do
|
|
69
|
+
return self[-1] if count.equal?(UNSET)
|
|
70
|
+
|
|
71
|
+
requested = [nonnegative_count(count), size].min
|
|
72
|
+
((size - requested)...size).map { |index| self[index] }
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def []=(index, value)
|
|
77
|
+
check!
|
|
78
|
+
context.writable!
|
|
79
|
+
index = normalize_index(index)
|
|
80
|
+
raise IndexError, "array index outside list" if index.negative?
|
|
81
|
+
|
|
82
|
+
validate_growth(index + 1)
|
|
83
|
+
|
|
84
|
+
previous_size = size
|
|
85
|
+
edit(positions: [previous_size, index].min..index) do |collection|
|
|
86
|
+
stored = context.profile.encode(value, context: context, path: path + [index])
|
|
87
|
+
collection.assign(collection.size, Core::Slot.null) while collection.size < index
|
|
88
|
+
collection.assign(index, stored)
|
|
89
|
+
end
|
|
90
|
+
context.replace_location(path + [index])
|
|
91
|
+
context.change_membership(path) if index >= previous_size
|
|
92
|
+
value
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def <<(value)
|
|
96
|
+
push(value)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def push(*values)
|
|
100
|
+
insert(size, *values)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Radix arrays shift the suffix for insertion and deletion. Linked arrays
|
|
104
|
+
# use their positional tree operations; neither operation decodes values.
|
|
105
|
+
def insert(index, *values)
|
|
106
|
+
check!
|
|
107
|
+
context.writable!
|
|
108
|
+
index = integer_argument(index)
|
|
109
|
+
return self if values.empty?
|
|
110
|
+
|
|
111
|
+
previous_size = size
|
|
112
|
+
index += previous_size + 1 if index.negative?
|
|
113
|
+
raise IndexError, "array index outside list" if index.negative?
|
|
114
|
+
|
|
115
|
+
validate_growth([previous_size, index].max + values.size)
|
|
116
|
+
edit(positions: [previous_size, index].min..) do |collection|
|
|
117
|
+
encoded = values.each_with_index.map do |value, offset|
|
|
118
|
+
context.profile.encode(value, context: context, path: path + [index + offset])
|
|
119
|
+
end
|
|
120
|
+
collection.assign(collection.size, Core::Slot.null) while collection.size < index
|
|
121
|
+
encoded.each_with_index { |stored, offset| collection.insert(index + offset, stored) }
|
|
122
|
+
end
|
|
123
|
+
invalidate_positions(index)
|
|
124
|
+
self
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def delete_at(index)
|
|
128
|
+
check!
|
|
129
|
+
context.writable!
|
|
130
|
+
index = normalize_index(index)
|
|
131
|
+
previous_size = size
|
|
132
|
+
return nil unless index >= 0 && index < previous_size
|
|
133
|
+
|
|
134
|
+
context.check_stream_mutation!(path, positions: index..)
|
|
135
|
+
|
|
136
|
+
value = nil
|
|
137
|
+
edit(positions: index..) do |collection|
|
|
138
|
+
captured = captured_context
|
|
139
|
+
stored = collection.lookup(index) || raise(IndexError, "array index outside list")
|
|
140
|
+
value = context.profile.decode(stored, context: captured, path: [index])
|
|
141
|
+
collection.delete(index)
|
|
142
|
+
end
|
|
143
|
+
invalidate_positions(index)
|
|
144
|
+
value
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def pop(count = UNSET)
|
|
148
|
+
check!
|
|
149
|
+
context.writable!
|
|
150
|
+
return delete_at(-1) if count.equal?(UNSET)
|
|
151
|
+
|
|
152
|
+
count = [nonnegative_count(count), size].min
|
|
153
|
+
return [] if count.zero?
|
|
154
|
+
|
|
155
|
+
previous_size = size
|
|
156
|
+
context.check_stream_mutation!(path, positions: (previous_size - count)..)
|
|
157
|
+
values = nil
|
|
158
|
+
edit(positions: (previous_size - count)..) do |collection|
|
|
159
|
+
captured = captured_context
|
|
160
|
+
values = ((previous_size - count)...previous_size).map do |index|
|
|
161
|
+
stored = collection.lookup(index) || raise(IndexError, "array index outside list")
|
|
162
|
+
context.profile.decode(stored, context: captured, path: [index])
|
|
163
|
+
end
|
|
164
|
+
collection.truncate(previous_size - count)
|
|
165
|
+
end
|
|
166
|
+
invalidate_positions(previous_size - count)
|
|
167
|
+
values
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def concat(*others)
|
|
171
|
+
check!
|
|
172
|
+
context.writable!
|
|
173
|
+
added = false
|
|
174
|
+
edit(positions: size..) do |collection|
|
|
175
|
+
inputs = others.map do |other|
|
|
176
|
+
case other
|
|
177
|
+
when Array then nil
|
|
178
|
+
else other = ::Array.try_convert(other) || raise(TypeError, "concat requires an array")
|
|
179
|
+
end
|
|
180
|
+
stored = context.profile.encode(other, context: context, path: path)
|
|
181
|
+
context.collection(stored, path: path)
|
|
182
|
+
end
|
|
183
|
+
inputs.each do |input|
|
|
184
|
+
next if input.empty?
|
|
185
|
+
|
|
186
|
+
validate_growth(collection.size + input.size)
|
|
187
|
+
added = true
|
|
188
|
+
collection.concat(input)
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
context.change_membership(path) if added
|
|
192
|
+
self
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def clear
|
|
196
|
+
check!
|
|
197
|
+
context.writable!
|
|
198
|
+
return self if empty?
|
|
199
|
+
|
|
200
|
+
edit { |collection| collection.truncate(0) }
|
|
201
|
+
context.invalidate_children(path)
|
|
202
|
+
context.change_membership(path)
|
|
203
|
+
self
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
protected
|
|
207
|
+
|
|
208
|
+
def validate_growth(count)
|
|
209
|
+
raise IndexError, "array size too big" if count > LONG_MAX / 0.size
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def normalize_index(index)
|
|
213
|
+
index = integer_argument(index)
|
|
214
|
+
index.negative? ? index + size : index
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def nonnegative_count(count)
|
|
218
|
+
count = integer_argument(count)
|
|
219
|
+
raise ArgumentError, "negative array size" if count.negative?
|
|
220
|
+
|
|
221
|
+
count
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def read_slice(start, count)
|
|
225
|
+
start += size if start.negative?
|
|
226
|
+
return nil if start.negative? || start > size || count.negative?
|
|
227
|
+
|
|
228
|
+
count = [count, size - start].min
|
|
229
|
+
Slice.new(context: captured_context(readonly: true), path: [], kind: kind,
|
|
230
|
+
offset: slice_offset + start, count: count)
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def slice_offset
|
|
234
|
+
0
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def invalidate_positions(start)
|
|
238
|
+
context.invalidate_positions(path, start)
|
|
239
|
+
context.change_membership(path)
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# A captured, read-only range of an array.
|
|
244
|
+
class Slice < Array
|
|
245
|
+
def initialize(offset:, count:, **)
|
|
246
|
+
super(**)
|
|
247
|
+
@offset = offset
|
|
248
|
+
@count = count
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def adapter
|
|
252
|
+
Selection.new(super, @offset, @count)
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def slot_for_assignment(context:, path:)
|
|
256
|
+
context.collection(slot, path: path).slice(@offset, @count)
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
protected
|
|
260
|
+
|
|
261
|
+
def slice_offset
|
|
262
|
+
@offset
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def entry_path(selector)
|
|
266
|
+
path + [@offset + selector]
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
# Limits the adapter's visible entries to the captured range.
|
|
270
|
+
class Selection
|
|
271
|
+
attr_reader :size
|
|
272
|
+
|
|
273
|
+
def initialize(source, offset, count)
|
|
274
|
+
@source = source
|
|
275
|
+
@offset = offset
|
|
276
|
+
@size = count
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def kind
|
|
280
|
+
@source.kind
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def lookup(index)
|
|
284
|
+
@source.lookup(@offset + index)
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def each_entry
|
|
288
|
+
return enum_for(:each_entry) unless block_given?
|
|
289
|
+
return self if size.zero?
|
|
290
|
+
|
|
291
|
+
index = 0
|
|
292
|
+
@source.each_from_index(@offset) do |_selector, stored|
|
|
293
|
+
yield index, stored
|
|
294
|
+
index += 1
|
|
295
|
+
break if index == size
|
|
296
|
+
end
|
|
297
|
+
self
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
private_constant :Selection
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
end
|