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,181 @@
1
+ # frozen_string_literal: true
2
+
3
+ module XitDB
4
+ module Core
5
+ # Reads and writes inline or external byte payloads.
6
+ module Bytes
7
+ CHUNK_SIZE = 64 * 1024
8
+
9
+ # A bounded reader for one immutable byte payload.
10
+ class Source
11
+ attr_reader :size, :format_tag
12
+
13
+ def initialize(database, location, size, format_tag, bound)
14
+ @database = database
15
+ @location = location
16
+ @size = size
17
+ @format_tag = format_tag
18
+ @bound = bound
19
+ freeze
20
+ end
21
+
22
+ def read(position, length)
23
+ @database.check_open!
24
+ unless position.is_a?(Integer) && length.is_a?(Integer)
25
+ raise TypeError, "byte positions and lengths must be Integers"
26
+ end
27
+ if position.negative? || length.negative? || position + length > size
28
+ raise InvalidValueError, "truncated byte payload"
29
+ end
30
+
31
+ location = @location
32
+ if location.is_a?(String)
33
+ Format.slice(location, position, length)
34
+ else
35
+ @database.read_at(location + position, length, bound: @bound)
36
+ end
37
+ end
38
+ end
39
+ private_constant :Source
40
+
41
+ def self.open(database, slot, bound:)
42
+ location, size, format_tag = parse(database, slot, bound)
43
+ Source.new(database, location, size, format_tag, bound)
44
+ end
45
+
46
+ def self.append(transaction, size:, format_tag: nil, prefix: "".b, representation: :automatic, &reader)
47
+ transaction.check_active!
48
+ validate_source(size, prefix, representation, format_tag)
49
+ format_tag = format_tag&.b
50
+ total_size = prefix.bytesize + size
51
+ raise RangeError, "byte payload exceeds unsigned 64-bit size" if total_size >= (1 << 64)
52
+
53
+ capacity = format_tag ? 6 : 8
54
+ captured = capture_inline(size, prefix, representation, capacity, &reader)
55
+ if captured && !captured.include?("\0")
56
+ payload = captured.ljust(capacity, "\0") + (format_tag || "")
57
+ return Slot.new(tag: :short_bytes, value: Format.unsigned(payload), full: !format_tag.nil?)
58
+ end
59
+
60
+ offset = transaction.append([total_size].pack("Q>"))
61
+ if captured
62
+ transaction.append(captured)
63
+ else
64
+ append_source(transaction, prefix, size, &reader)
65
+ end
66
+ transaction.append(format_tag) if format_tag
67
+ Slot.new(tag: :bytes, value: offset, full: !format_tag.nil?)
68
+ end
69
+
70
+ def self.validate_source(size, prefix, representation, format_tag)
71
+ raise TypeError, "size must be an Integer" unless size.is_a?(Integer)
72
+ raise ArgumentError, "size must be nonnegative" if size.negative?
73
+ raise TypeError, "prefix must be a String" unless prefix.is_a?(String)
74
+ raise ArgumentError, "unknown byte representation" unless %i[automatic external].include?(representation)
75
+ return if format_tag.nil? || (format_tag.is_a?(String) && format_tag.bytesize == 2)
76
+
77
+ raise ArgumentError, "format_tag must contain two bytes"
78
+ end
79
+
80
+ def self.capture_inline(size, prefix, representation, capacity, &)
81
+ return nil unless representation == :automatic && prefix.bytesize + size <= capacity
82
+
83
+ prefix.b + (size.zero? ? "".b : read_chunk(0, size, &))
84
+ end
85
+
86
+ def self.append_source(transaction, prefix, size, &)
87
+ position = 0
88
+ while position < prefix.bytesize
89
+ chunk = Format.slice(prefix, position, CHUNK_SIZE).b
90
+ transaction.append(chunk)
91
+ position += chunk.bytesize
92
+ end
93
+ position = 0
94
+ while position < size
95
+ length = [size - position, CHUNK_SIZE].min
96
+ transaction.append(read_chunk(position, length, &))
97
+ position += length
98
+ end
99
+ end
100
+ private_class_method :validate_source, :capture_inline, :append_source
101
+
102
+ def self.read_chunk(position, length)
103
+ chunk = yield(position, length) if block_given?
104
+ raise EOFError, "byte source ended before its declared size" if chunk.nil?
105
+ raise TypeError, "byte source must return a String" unless chunk.is_a?(String)
106
+ raise EOFError, "byte source ended before its declared size" if chunk.bytesize < length
107
+ raise ArgumentError, "byte source returned more than requested" if chunk.bytesize > length
108
+
109
+ chunk.b
110
+ end
111
+ private_class_method :read_chunk
112
+
113
+ def self.parse(database, slot, bound)
114
+ database.check_open!
115
+ case slot.tag
116
+ when :short_bytes
117
+ payload = [slot.value].pack("Q>")
118
+ capacity = slot.full ? 6 : 8
119
+ data = (Format.slice(payload, 0, capacity).split("\0", 2).first || "".b).freeze
120
+ tag = Format.slice(payload, 6, 2).freeze if slot.full
121
+ [data, data.bytesize, tag]
122
+ when :bytes
123
+ size = Format.unsigned(database.read_at(slot.value, 8, bound: bound))
124
+ offset = slot.value + 8
125
+ tail = database.read_at(offset + size, slot.full ? 2 : 0, bound: bound)
126
+ tag = tail.freeze if slot.full
127
+ [offset, size, tag]
128
+ else
129
+ raise InvalidValueError, "slot does not contain bytes"
130
+ end
131
+ end
132
+ private_class_method :parse
133
+
134
+ # Return inline data or its file offset, without materializing a scalar.
135
+ def self.locate(database, slot, bound:)
136
+ unless %i[bytes short_bytes].include?(slot.tag) && !slot.full
137
+ raise InvalidValueError, "stream requires untagged bytes"
138
+ end
139
+
140
+ location, size = parse(database, slot, bound)
141
+ [location.is_a?(String) ? location.dup : location, size]
142
+ end
143
+
144
+ def self.copy(source, size:, database:, transaction:)
145
+ if size <= 8
146
+ bytes = source.read(size)
147
+ raise EOFError, "byte source ended before its declared size" unless bytes
148
+
149
+ return database.bytes(bytes)
150
+ end
151
+
152
+ append(transaction, size: size, representation: :external) do |_position, length|
153
+ chunk = "".b
154
+ while chunk.bytesize < length
155
+ part = source.read(length - chunk.bytesize)
156
+ raise EOFError, "byte source ended before its declared size" if part.nil? || part.empty?
157
+
158
+ chunk << part
159
+ end
160
+ chunk
161
+ end
162
+ end
163
+ end
164
+
165
+ # An immutable byte payload and its optional format tag.
166
+ class ByteValue
167
+ attr_reader :bytes, :format_tag
168
+
169
+ def initialize(bytes, format_tag: nil)
170
+ raise TypeError, "bytes must be a String" unless bytes.is_a?(String)
171
+ unless format_tag.nil? || (format_tag.is_a?(String) && format_tag.bytesize == 2)
172
+ raise ArgumentError, "format_tag must contain two bytes"
173
+ end
174
+
175
+ @bytes = bytes.b.freeze
176
+ @format_tag = format_tag&.b&.freeze
177
+ freeze
178
+ end
179
+ end
180
+ end
181
+ end
@@ -0,0 +1,172 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "offset_map"
4
+
5
+ module XitDB
6
+ module Core
7
+ # Copies reachable records into a new database.
8
+ class Compaction
9
+ INLINE_TAGS = %i[none uint int float short_bytes].freeze
10
+ BLOCK_SIZE = 16 * Slot::SIZE
11
+
12
+ def initialize(source, cursor, transaction, offsets)
13
+ @source = source
14
+ @bound = cursor.bound
15
+ @transaction = transaction
16
+ @offsets = offsets
17
+ @stack = []
18
+ end
19
+
20
+ def copy(slot)
21
+ result = remap(slot)
22
+ @stack.pop&.call until @stack.empty?
23
+ result
24
+ end
25
+
26
+ private
27
+
28
+ def read(offset, size)
29
+ @source.read_at(offset, size, bound: @bound)
30
+ end
31
+
32
+ def parse(bytes)
33
+ slot = Slot.parse(bytes)
34
+ raise CorruptionError, "unknown compaction slot tag" unless Slot::TAGS.include?(slot.tag)
35
+
36
+ slot
37
+ end
38
+
39
+ def remap(slot)
40
+ return slot if INLINE_TAGS.include?(slot.tag)
41
+
42
+ Slot.new(tag: slot.tag, full: slot.full, value: remap_pointer(slot))
43
+ end
44
+
45
+ def remap_pointer(slot)
46
+ case slot.tag
47
+ when :bytes then copy_bytes(slot)
48
+ when :index, :hash_map, :hash_set then reserve_slots(slot.value, BLOCK_SIZE, 0, 16)
49
+ when :counted_hash_map, :counted_hash_set then reserve_slots(slot.value, BLOCK_SIZE + 8, 8, 16)
50
+ when :array_list then reserve(slot.value, 16) { |source, target| array_header(source, target) }
51
+ when :linked_array_list then reserve(slot.value, 16) do |source, target|
52
+ tree_header(source, target, false)
53
+ end
54
+ when :sorted_map, :sorted_set then reserve(slot.value, 16) do |source, target|
55
+ tree_header(source, target, true)
56
+ end
57
+ when :kv_pair then reserve_slots(slot.value, @source.header.hash_size + 18,
58
+ @source.header.hash_size, 2)
59
+ when :sorted_kv_pair then reserve_slots(slot.value, 18, 0, 2)
60
+ else raise CorruptionError, "unknown compaction slot tag"
61
+ end
62
+ end
63
+
64
+ def reserve_slots(source, size, start, count)
65
+ reserve(source, size) { |origin, target| slots(origin, target, start, count) }
66
+ end
67
+
68
+ def reserve(source, size, cache: true)
69
+ mapped = @offsets.fetch(source) if cache
70
+ return mapped if mapped
71
+
72
+ bytes = read(source, size)
73
+ target = @transaction.append(bytes)
74
+ @offsets.store(source, target) if cache
75
+ @stack << -> { yield(source, target) }
76
+ target
77
+ end
78
+
79
+ def copy_bytes(slot)
80
+ mapped = @offsets.fetch(slot.value)
81
+ return mapped if mapped
82
+
83
+ source = Bytes.open(@source, slot, bound: @bound)
84
+ copied = Bytes.append(@transaction, size: source.size, format_tag: source.format_tag,
85
+ representation: :external) do |position, length|
86
+ source.read(position,
87
+ length)
88
+ end
89
+ target = copied.value
90
+ @offsets.store(slot.value, target)
91
+ target
92
+ end
93
+
94
+ def slots(source, target, start, count)
95
+ count.times do |index|
96
+ offset = start + (index * Slot::SIZE)
97
+ slot = parse(read(source + offset, Slot::SIZE))
98
+ @transaction.write_at(target + offset, remap(slot).to_bytes)
99
+ end
100
+ end
101
+
102
+ def array_header(source, target)
103
+ size, pointer = Format.pair(read(source, 16), "Q>Q>")
104
+ shift = [((size - 1).bit_length - 1) / 4, 0].max
105
+ mapped = array_index(pointer, size, shift)
106
+ @transaction.write_at(target, [size, mapped].pack("Q>Q>"))
107
+ end
108
+
109
+ def array_index(source, size, shift)
110
+ return reserve_slots(source, BLOCK_SIZE, 0, 16) if size == 1 << ((shift + 1) * 4)
111
+
112
+ reserve(source, BLOCK_SIZE, cache: false) { |origin, target| partial_array(origin, target, size, shift) }
113
+ end
114
+
115
+ def partial_array(source, target, size, shift)
116
+ # Partial blocks can be shared by arrays with different visible sizes.
117
+ # Copy them separately and clear every slot outside this array's size.
118
+ @transaction.write_at(target, "\0".b * BLOCK_SIZE)
119
+ child_size = 1 << (shift * 4)
120
+ 16.times do |index|
121
+ break unless size.positive?
122
+
123
+ slot = parse(read(source + (index * Slot::SIZE), Slot::SIZE))
124
+ mapped = if shift.zero? || slot.empty?
125
+ remap(slot)
126
+ else
127
+ raise CorruptionError, "invalid array branch" unless slot.tag == :index
128
+
129
+ Slot.new(tag: :index, full: slot.full,
130
+ value: array_index(slot.value, [size, child_size].min, shift - 1))
131
+ end
132
+ @transaction.write_at(target + (index * Slot::SIZE), mapped.to_bytes)
133
+ size -= [size, child_size].min
134
+ end
135
+ end
136
+
137
+ def tree_header(source, target, sorted)
138
+ size, pointer = Format.pair(read(source, 16), "Q>Q>")
139
+ @transaction.write_at(target, [size, tree_node(pointer, sorted)].pack("Q>Q>"))
140
+ end
141
+
142
+ def tree_node(source, sorted)
143
+ mapped = @offsets.fetch(source)
144
+ return mapped if mapped
145
+
146
+ kind, count = Format.pair(read(source, 2), "CC")
147
+ raise CorruptionError, "invalid compaction B-tree node" unless kind <= 1 && count <= 16
148
+
149
+ size = 2 + BLOCK_SIZE + (if kind == 1
150
+ 128 + (sorted ? BLOCK_SIZE : 0)
151
+ else
152
+ 0
153
+ end)
154
+ if kind.zero?
155
+ reserve_slots(source, size, 2, 16)
156
+ else
157
+ reserve(source, size) { |origin, target| tree_branch(origin, target, sorted) }
158
+ end
159
+ end
160
+
161
+ def tree_branch(source, target, sorted)
162
+ 16.times do |index|
163
+ offset = 2 + (index * Slot::SIZE)
164
+ slot = parse(read(source + offset, Slot::SIZE))
165
+ slot = Slot.new(tag: :index, full: slot.full, value: tree_node(slot.value, sorted)) if slot.tag == :index
166
+ @transaction.write_at(target + offset, slot.to_bytes)
167
+ end
168
+ slots(source, target, 2 + BLOCK_SIZE, 16) if sorted
169
+ end
170
+ end
171
+ end
172
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module XitDB
4
+ module Core
5
+ # Calculates the checksum used by root trailers.
6
+ module CRC64
7
+ MASK = 0xffffffffffffffff
8
+ POLYNOMIAL = 0xc96c5795d7870f42
9
+
10
+ def self.call(bytes)
11
+ raise TypeError, "expected String, got #{bytes.class}" unless bytes.is_a?(String)
12
+
13
+ crc = MASK
14
+ bytes.each_byte do |byte|
15
+ crc ^= byte
16
+ 8.times { crc = (crc >> 1) ^ (crc.odd? ? POLYNOMIAL : 0) }
17
+ end
18
+ crc ^ MASK
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,201 @@
1
+ # frozen_string_literal: true
2
+
3
+ module XitDB
4
+ module Core
5
+ # Tracks a bounded collection slot and its ancestor path.
6
+ class Cursor
7
+ attr_reader :database, :transaction
8
+
9
+ def initialize(database:, slot:, bound:, transaction: nil)
10
+ @database = database
11
+ @slot = slot
12
+ @bound = bound
13
+ @transaction = transaction
14
+ @valid = true
15
+ @children = ObjectSpace::WeakMap.new
16
+ end
17
+
18
+ def slot
19
+ database.check_open!
20
+ # @type var path: Array[Cursor]
21
+ path = []
22
+ ancestor = self
23
+ loop do
24
+ transaction&.check_cursor!(ancestor)
25
+ raise ExpiredViewError, "cursor ancestor was replaced" unless ancestor.valid
26
+ break unless ancestor.parent
27
+
28
+ path << ancestor
29
+ ancestor = ancestor.parent
30
+ end
31
+ value = ancestor.local_slot
32
+ limit = transaction ? transaction.length : @bound
33
+ path.reverse_each do |child|
34
+ reader = self.class.new(database: database, slot: value, bound: limit)
35
+ value = reader.collection_get(child.selector)
36
+ raise ExpiredViewError, "cursor element was removed" unless value
37
+
38
+ child.instance_variable_set(:@slot, value)
39
+ end
40
+ value
41
+ end
42
+
43
+ def bound
44
+ slot
45
+ transaction ? transaction.length : @bound
46
+ end
47
+
48
+ def replace(slot)
49
+ database.check_open!
50
+ raise ReadOnlyError, "cursor is read-only" unless transaction
51
+
52
+ transaction.check_replacement!(self, slot)
53
+ self.slot
54
+ parent = @parent
55
+ parent&.collection_replace(@selector, slot, owner: self)
56
+ @slot = slot
57
+ invalidate_children
58
+ self
59
+ end
60
+
61
+ def child(kind:, selector:)
62
+ unless %i[array_list linked_array_list sorted_map sorted_set].include?(kind) || HashMap::TAGS.include?(kind)
63
+ raise ArgumentError, "unsupported child kind: #{kind.inspect}"
64
+ end
65
+ return nil if slot.tag == :none
66
+
67
+ if kind == :array_list
68
+ ArrayList.new(self).cursor_at(integer_selector(selector))
69
+ elsif kind == :linked_array_list
70
+ LinkedArrayList.new(self).cursor_at(integer_selector(selector))
71
+ elsif %i[sorted_map sorted_set].include?(kind)
72
+ SortedMap.new(self).cursor_at(string_selector(selector))
73
+ else
74
+ HashMap.from_cursor(self).cursor_at(string_selector(selector))
75
+ end
76
+ end
77
+
78
+ # Collection updates keep child paths live while replacing copied ancestors.
79
+ def update_collection(slot, propagate: true)
80
+ transaction = self.transaction
81
+ raise ReadOnlyError, "cursor is read-only" unless transaction
82
+
83
+ transaction.check_replacement!(self, slot)
84
+ self.slot
85
+ @slot = slot
86
+ if propagate
87
+ child = self
88
+ while child.parent
89
+ parent = child.parent
90
+ parent.collection_replace(child.selector, child.local_slot, collection: true, propagate: false)
91
+ child = parent
92
+ end
93
+ end
94
+ self
95
+ end
96
+
97
+ def array_child(index, slot)
98
+ cached_child(index, slot)
99
+ end
100
+
101
+ def hash_child(hash_bytes, slot)
102
+ cached_child(hash_bytes, slot)
103
+ end
104
+
105
+ def cached_child(index, slot)
106
+ @children.each_pair do |child, selector|
107
+ next unless selector == index && child.valid
108
+
109
+ begin
110
+ child.slot
111
+ rescue ExpiredViewError
112
+ next
113
+ end
114
+ return child
115
+ end
116
+ child = if transaction
117
+ transaction.cursor(slot: slot)
118
+ else
119
+ self.class.new(database: database, slot: slot, bound: bound)
120
+ end
121
+ child.instance_variable_set(:@parent, self)
122
+ child.instance_variable_set(:@selector, index)
123
+ @children[child] = index
124
+ child
125
+ end
126
+
127
+ def invalidate_children(index: nil, from: nil, except: nil)
128
+ @children.each_pair do |child, selector|
129
+ next if child.equal?(except)
130
+ next if index && selector != index
131
+ next if from && selector < from
132
+
133
+ child.instance_variable_set(:@valid, false)
134
+ end
135
+ end
136
+
137
+ # These states are internal transaction snapshots, including invalid views.
138
+ def checkpoint_state
139
+ [@slot, @valid]
140
+ end
141
+
142
+ def restore_checkpoint_state(state)
143
+ @slot, @valid = state
144
+ end
145
+
146
+ def read_bytes
147
+ database.read_bytes(slot, bound: bound)
148
+ end
149
+
150
+ protected
151
+
152
+ def collection_get(selector)
153
+ target = collection
154
+ if target.is_a?(ArrayList) || target.is_a?(LinkedArrayList)
155
+ target.get(integer_selector(selector))
156
+ else
157
+ target.get(string_selector(selector))
158
+ end
159
+ end
160
+
161
+ def collection_replace(selector, slot, owner: nil, collection: false, propagate: true)
162
+ target = self.collection
163
+ if target.is_a?(ArrayList) || target.is_a?(LinkedArrayList)
164
+ target.replace_child(integer_selector(selector), slot, owner: owner, collection: collection,
165
+ propagate: propagate)
166
+ else
167
+ target.replace_child(string_selector(selector), slot, owner: owner, collection: collection,
168
+ propagate: propagate)
169
+ end
170
+ end
171
+
172
+ def integer_selector(selector)
173
+ raise TypeError, "array index must be an Integer" unless selector.is_a?(Integer)
174
+
175
+ selector
176
+ end
177
+
178
+ def string_selector(selector)
179
+ raise TypeError, "collection key must be a String" unless selector.is_a?(String)
180
+
181
+ selector
182
+ end
183
+
184
+ attr_reader :parent, :selector, :valid
185
+
186
+ def collection
187
+ case slot.tag
188
+ when :array_list then ArrayList.new(self)
189
+ when :linked_array_list then LinkedArrayList.new(self)
190
+ when :sorted_map, :sorted_set then SortedMap.new(self)
191
+ when :hash_map, :hash_set, :counted_hash_map, :counted_hash_set then HashMap.from_cursor(self)
192
+ else raise InvalidValueError, "slot does not contain a supported collection"
193
+ end
194
+ end
195
+
196
+ def local_slot
197
+ @slot
198
+ end
199
+ end
200
+ end
201
+ end