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,271 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module XitDB
|
|
4
|
+
module Views
|
|
5
|
+
# Distinguishes omitted arguments from stored nil values.
|
|
6
|
+
class MissingArgument
|
|
7
|
+
def initialize
|
|
8
|
+
freeze
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Tracks a collection location and its owning snapshot or transaction.
|
|
13
|
+
class Base
|
|
14
|
+
include Enumerable
|
|
15
|
+
|
|
16
|
+
LONG_MAX = (1 << (([0].pack("l!").bytesize * 8) - 1)) - 1
|
|
17
|
+
private_constant :LONG_MAX
|
|
18
|
+
|
|
19
|
+
attr_reader :context, :path, :kind
|
|
20
|
+
|
|
21
|
+
def initialize(context:, path:, kind:)
|
|
22
|
+
@context = context
|
|
23
|
+
@path = path.map { |selector| selector.is_a?(String) ? selector.dup.freeze : selector }.freeze
|
|
24
|
+
@kind = kind
|
|
25
|
+
@location = context.observe(@path)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def check!
|
|
29
|
+
context.check!
|
|
30
|
+
return unless @location&.expired
|
|
31
|
+
|
|
32
|
+
raise ExpiredViewError, "view ancestor was replaced"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def slot
|
|
36
|
+
context.read do
|
|
37
|
+
check!
|
|
38
|
+
context.resolve(path)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def adapter
|
|
43
|
+
context.collection(slot, path: path)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def size
|
|
47
|
+
context.read { adapter.size }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def empty?
|
|
51
|
+
size.zero?
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def each
|
|
55
|
+
return enum_for(:each) unless block_given?
|
|
56
|
+
|
|
57
|
+
each_entries(-> { adapter.each_entry }) do |*entry|
|
|
58
|
+
if %i[map sorted_map].include?(kind)
|
|
59
|
+
yield entry[0], entry[1]
|
|
60
|
+
else
|
|
61
|
+
yield entry[0]
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def slot_for_assignment(**)
|
|
67
|
+
slot
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def to_ruby
|
|
71
|
+
context.read { ValueWalker.materialize(self) }
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def ==(other)
|
|
75
|
+
context.read do
|
|
76
|
+
case other
|
|
77
|
+
when Base then other = other.to_ruby
|
|
78
|
+
end
|
|
79
|
+
to_ruby == other
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def inspect
|
|
84
|
+
location = path.first(4).map { |selector| selector.is_a?(String) ? selector.byteslice(0, 32) : selector }
|
|
85
|
+
suffix = path.size > 4 ? "..." : ""
|
|
86
|
+
"#<#{self.class} profile=#{context.profile.class.name} path=#{location.inspect}#{suffix}>"
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def open_bytes(key)
|
|
90
|
+
raise ArgumentError, "open_bytes requires a block" unless block_given?
|
|
91
|
+
|
|
92
|
+
# @type var reader: Streams::Reader?
|
|
93
|
+
reader = nil
|
|
94
|
+
registration = nil
|
|
95
|
+
reader = context.read do
|
|
96
|
+
selector = stream_selector(key)
|
|
97
|
+
if %i[array linked_array].include?(kind) && !integer_argument(selector).between?(0, size - 1)
|
|
98
|
+
raise KeyError, "byte value not found"
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
registration = context.register_stream(entry_path(selector), write: false)
|
|
102
|
+
stored = adapter.lookup(selector)
|
|
103
|
+
raise KeyError, "byte value not found" unless stored
|
|
104
|
+
|
|
105
|
+
Streams::Reader.new(context, stored)
|
|
106
|
+
end
|
|
107
|
+
yield reader
|
|
108
|
+
ensure
|
|
109
|
+
reader&.expire!
|
|
110
|
+
context.release_stream(registration) if registration
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def write_bytes(key)
|
|
114
|
+
raise ArgumentError, "write_bytes requires a block" unless block_given?
|
|
115
|
+
|
|
116
|
+
check!
|
|
117
|
+
context.writable!
|
|
118
|
+
selector = stream_selector(key)
|
|
119
|
+
if %i[array linked_array].include?(kind)
|
|
120
|
+
selector = integer_argument(selector)
|
|
121
|
+
raise IndexError, "array index outside list" if selector.negative?
|
|
122
|
+
|
|
123
|
+
validate_growth(selector + 1)
|
|
124
|
+
end
|
|
125
|
+
context.check_stream_mutation!(path, **stream_mutation(selector))
|
|
126
|
+
Tempfile.create("xitdb-bytes", binmode: true) do |spool|
|
|
127
|
+
writer = Streams::Writer.new(context, spool)
|
|
128
|
+
registration = context.register_stream(entry_path(selector), write: true)
|
|
129
|
+
begin
|
|
130
|
+
result = yield writer
|
|
131
|
+
ensure
|
|
132
|
+
writer.expire!
|
|
133
|
+
context.release_stream(registration)
|
|
134
|
+
end
|
|
135
|
+
present = !adapter.lookup(selector).nil?
|
|
136
|
+
edit(**stream_mutation(selector)) do |collection|
|
|
137
|
+
spool.rewind
|
|
138
|
+
stored = Core::Bytes.copy(spool, size: spool.size, database: context.database.core,
|
|
139
|
+
transaction: context.writable_transaction)
|
|
140
|
+
if %i[array linked_array].include?(kind)
|
|
141
|
+
position = integer_argument(selector)
|
|
142
|
+
collection.assign(collection.size, Core::Slot.null) while collection.size < position
|
|
143
|
+
end
|
|
144
|
+
collection.assign(selector, stored)
|
|
145
|
+
end
|
|
146
|
+
context.replace_location(entry_path(selector))
|
|
147
|
+
context.change_membership(path) unless present
|
|
148
|
+
result
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
protected
|
|
153
|
+
|
|
154
|
+
def clear_entries?
|
|
155
|
+
collection = adapter
|
|
156
|
+
needed = if defined?(Ruby::Containers) && collection.is_a?(Ruby::Containers)
|
|
157
|
+
collection.clear_needed?
|
|
158
|
+
else
|
|
159
|
+
!collection.empty?
|
|
160
|
+
end
|
|
161
|
+
return false unless needed
|
|
162
|
+
|
|
163
|
+
edit do |current|
|
|
164
|
+
if defined?(Ruby::Containers) && current.is_a?(Ruby::Containers)
|
|
165
|
+
current.clear
|
|
166
|
+
else
|
|
167
|
+
current.each_entry { |selector, _| current.delete(selector) }
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
true
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def edit(**options)
|
|
174
|
+
context.mutate(path, **options) do |cursor|
|
|
175
|
+
collection = context.collection(cursor.slot, path: path)
|
|
176
|
+
yield collection
|
|
177
|
+
cursor.replace(collection.slot)
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def stream_selector(key)
|
|
182
|
+
check!
|
|
183
|
+
case kind
|
|
184
|
+
when :map, :sorted_map
|
|
185
|
+
collection = adapter
|
|
186
|
+
if defined?(Ruby::Containers) && collection.is_a?(Ruby::Containers)
|
|
187
|
+
collection.selector_identity(key)
|
|
188
|
+
else
|
|
189
|
+
context.profile.key_bytes(key)
|
|
190
|
+
end
|
|
191
|
+
case key
|
|
192
|
+
when String, Time then key.dup.freeze
|
|
193
|
+
else key
|
|
194
|
+
end
|
|
195
|
+
when :array, :linked_array then normalize_index(key)
|
|
196
|
+
else raise TypeError, "byte streams require a map or array view"
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def stream_mutation(selector)
|
|
201
|
+
if %i[array linked_array].include?(kind)
|
|
202
|
+
position = integer_argument(selector)
|
|
203
|
+
{ positions: [size, position].min..position }
|
|
204
|
+
else
|
|
205
|
+
{ targets: [entry_path(selector)] }
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def integer_argument(value)
|
|
210
|
+
result = ::Integer.try_convert(value) || raise(TypeError, "no implicit conversion into Integer")
|
|
211
|
+
raise RangeError, "integer too big to convert into long" unless result.between?(-LONG_MAX - 1, LONG_MAX)
|
|
212
|
+
|
|
213
|
+
result
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def captured_context(readonly: false)
|
|
217
|
+
stored = slot
|
|
218
|
+
context.transaction&.freeze
|
|
219
|
+
Context.new(database: context.database, root: stored, bound: context.read_bound,
|
|
220
|
+
transaction: context.transaction, readonly: readonly, reference_proof: context.reference_proof)
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def entry_path(selector)
|
|
224
|
+
path + [selector]
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def each_entries(entries, selectors_only: false)
|
|
228
|
+
# @type var iterator: Enumerator[entry, adapter]?
|
|
229
|
+
iterator = nil
|
|
230
|
+
generation = nil
|
|
231
|
+
exhausted = false
|
|
232
|
+
while true # rubocop:disable Style/InfiniteLoop -- Consumer StopIteration must propagate.
|
|
233
|
+
values = context.read do
|
|
234
|
+
check!
|
|
235
|
+
unless iterator
|
|
236
|
+
iterator = entries.call
|
|
237
|
+
generation = @location&.membership
|
|
238
|
+
end
|
|
239
|
+
if @location&.membership != generation
|
|
240
|
+
raise ConcurrentModificationError, "collection membership changed during iteration"
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
begin
|
|
244
|
+
active_iterator = iterator || raise(ConcurrentModificationError, "missing collection iterator")
|
|
245
|
+
selector, stored = active_iterator.next
|
|
246
|
+
rescue StopIteration
|
|
247
|
+
exhausted = true
|
|
248
|
+
next
|
|
249
|
+
end
|
|
250
|
+
decode_entry(selector, stored, selectors_only: selectors_only == true)
|
|
251
|
+
end
|
|
252
|
+
break if exhausted
|
|
253
|
+
|
|
254
|
+
yield(*values)
|
|
255
|
+
end
|
|
256
|
+
self
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def decode_entry(selector, stored, selectors_only:)
|
|
260
|
+
return [selector] if selectors_only || %i[set sorted_set].include?(kind)
|
|
261
|
+
|
|
262
|
+
# Transaction entries resolve again so replacement values stay live.
|
|
263
|
+
if context.transaction
|
|
264
|
+
stored = adapter.lookup(selector) || raise(ExpiredViewError, "view location no longer exists")
|
|
265
|
+
end
|
|
266
|
+
value = context.profile.decode(stored, context: context, path: entry_path(selector))
|
|
267
|
+
%i[map sorted_map].include?(kind) ? [selector, value] : [value]
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module XitDB
|
|
4
|
+
module Views
|
|
5
|
+
# Provides array operations backed by the linked positional tree.
|
|
6
|
+
class LinkedArray < Array
|
|
7
|
+
def split(index)
|
|
8
|
+
context.read do
|
|
9
|
+
index = integer_argument(index)
|
|
10
|
+
index += size if index.negative?
|
|
11
|
+
raise IndexError, "array split outside list" unless index.between?(0, size)
|
|
12
|
+
|
|
13
|
+
[self[0, index], self[index, size - index]]
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module XitDB
|
|
4
|
+
module Views
|
|
5
|
+
# Provides key-based reads and writes for persistent maps.
|
|
6
|
+
class Map < Base
|
|
7
|
+
UNSET = MissingArgument.new.freeze
|
|
8
|
+
private_constant :UNSET
|
|
9
|
+
|
|
10
|
+
def [](key)
|
|
11
|
+
context.read do
|
|
12
|
+
stored = adapter.lookup(key)
|
|
13
|
+
context.profile.decode(stored, context: context, path: path + [key]) if stored
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def fetch(key, default = UNSET)
|
|
18
|
+
found, value = context.read do
|
|
19
|
+
present = key?(key)
|
|
20
|
+
[present, present ? self[key] : nil]
|
|
21
|
+
end
|
|
22
|
+
return value if found
|
|
23
|
+
return yield key if block_given?
|
|
24
|
+
|
|
25
|
+
case default
|
|
26
|
+
when MissingArgument then nil
|
|
27
|
+
else return default
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
raise KeyError, "key not found: #{key.inspect}"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def dig(*keys)
|
|
34
|
+
context.read { dig_value(keys) }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def dig_value(keys)
|
|
38
|
+
raise ArgumentError, "at least one key is required" if keys.empty?
|
|
39
|
+
|
|
40
|
+
# @type var value: top
|
|
41
|
+
value = self
|
|
42
|
+
keys.each_with_index do |key, index|
|
|
43
|
+
case value
|
|
44
|
+
when nil then return nil
|
|
45
|
+
when Map then value = value[key]
|
|
46
|
+
when Array then value = value[integer_argument(key)]
|
|
47
|
+
when Object
|
|
48
|
+
raise TypeError, "#{value.class} does not have #dig method" unless value.respond_to?(:dig)
|
|
49
|
+
|
|
50
|
+
return Object.instance_method(:public_method).bind_call(value, :dig).call(*keys.drop(index))
|
|
51
|
+
else
|
|
52
|
+
raise TypeError, "value does not have #dig method"
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
value
|
|
56
|
+
end
|
|
57
|
+
private :dig_value
|
|
58
|
+
|
|
59
|
+
def key?(key)
|
|
60
|
+
context.read { !adapter.lookup(key).nil? }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def each_key(&block)
|
|
64
|
+
return enum_for(:each_key) unless block
|
|
65
|
+
|
|
66
|
+
each_entries(-> { adapter.each_entry }, selectors_only: true, &block)
|
|
67
|
+
self
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def each_value
|
|
71
|
+
return enum_for(:each_value) unless block_given?
|
|
72
|
+
|
|
73
|
+
each { |_key, value| yield value }
|
|
74
|
+
self
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def keys
|
|
78
|
+
context.read { each_key.to_a }
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def values
|
|
82
|
+
context.read { each_value.to_a }
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def []=(key, value)
|
|
86
|
+
check!
|
|
87
|
+
context.writable!
|
|
88
|
+
present = key?(key)
|
|
89
|
+
context.mutate(path, targets: [path + [key]]) do |cursor|
|
|
90
|
+
encoded = context.profile.encode(value, context: context, path: path + [key])
|
|
91
|
+
collection = context.collection(cursor.slot, path: path)
|
|
92
|
+
collection.assign(key, encoded)
|
|
93
|
+
cursor.replace(collection.slot)
|
|
94
|
+
end
|
|
95
|
+
context.replace_location(path + [key])
|
|
96
|
+
context.change_membership(path) unless present
|
|
97
|
+
value
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def delete(key)
|
|
101
|
+
check!
|
|
102
|
+
context.writable!
|
|
103
|
+
return block_given? ? yield(key) : nil unless key?(key)
|
|
104
|
+
|
|
105
|
+
context.check_stream_mutation!(path, targets: [path + [key]])
|
|
106
|
+
|
|
107
|
+
value = nil
|
|
108
|
+
context.mutate(path, targets: [path + [key]]) do |cursor|
|
|
109
|
+
# Capture before deletion, with loaders inside the mutation checkpoint.
|
|
110
|
+
stored = adapter.lookup(key) || raise(KeyError, "key no longer exists")
|
|
111
|
+
context.writable_transaction.freeze
|
|
112
|
+
captured = Context.new(database: context.database, root: stored, bound: context.read_bound,
|
|
113
|
+
transaction: context.transaction)
|
|
114
|
+
value = context.profile.decode(stored, context: captured, path: [])
|
|
115
|
+
collection = context.collection(cursor.slot, path: path)
|
|
116
|
+
collection.delete(key)
|
|
117
|
+
cursor.replace(collection.slot)
|
|
118
|
+
end
|
|
119
|
+
context.replace_location(path + [key])
|
|
120
|
+
context.change_membership(path)
|
|
121
|
+
value
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def clear
|
|
125
|
+
check!
|
|
126
|
+
context.writable!
|
|
127
|
+
return self unless clear_entries?
|
|
128
|
+
|
|
129
|
+
context.invalidate_children(path)
|
|
130
|
+
context.change_membership(path)
|
|
131
|
+
self
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def merge!(hash)
|
|
135
|
+
check!
|
|
136
|
+
context.writable!
|
|
137
|
+
raise TypeError, "merge! requires a Hash" unless Object.instance_method(:instance_of?).bind_call(hash, Hash)
|
|
138
|
+
|
|
139
|
+
# @type var replaced: ::Array[selector]
|
|
140
|
+
replaced = []
|
|
141
|
+
added = false
|
|
142
|
+
targets = Hash.instance_method(:keys).bind_call(hash).map { |key| path + [key] }
|
|
143
|
+
context.mutate(path, targets: targets) do |cursor|
|
|
144
|
+
encoded = context.profile.encode(hash, context: context, path: path)
|
|
145
|
+
input = context.collection(encoded, path: path)
|
|
146
|
+
next if input.empty?
|
|
147
|
+
|
|
148
|
+
collection = context.collection(cursor.slot, path: path)
|
|
149
|
+
input.each_entry do |selector, stored|
|
|
150
|
+
added = true unless collection.lookup(selector)
|
|
151
|
+
collection.assign(selector, stored)
|
|
152
|
+
replaced << selector
|
|
153
|
+
end
|
|
154
|
+
cursor.replace(collection.slot)
|
|
155
|
+
end
|
|
156
|
+
replaced.each { |selector| context.replace_location(path + [selector]) }
|
|
157
|
+
context.change_membership(path) if added
|
|
158
|
+
self
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module XitDB
|
|
4
|
+
module Views
|
|
5
|
+
# Provides membership reads and writes for persistent sets.
|
|
6
|
+
class Set < Base
|
|
7
|
+
def include?(value)
|
|
8
|
+
context.read { !adapter.lookup(value).nil? }
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def add(value)
|
|
12
|
+
check!
|
|
13
|
+
context.writable!
|
|
14
|
+
return self if include?(value)
|
|
15
|
+
|
|
16
|
+
edit { |collection| collection.assign(value, Core::Slot.new) }
|
|
17
|
+
context.change_membership(path)
|
|
18
|
+
self
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def delete(value)
|
|
22
|
+
check!
|
|
23
|
+
context.writable!
|
|
24
|
+
return self unless include?(value)
|
|
25
|
+
|
|
26
|
+
edit { |collection| collection.delete(value) }
|
|
27
|
+
context.change_membership(path)
|
|
28
|
+
self
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def clear
|
|
32
|
+
check!
|
|
33
|
+
context.writable!
|
|
34
|
+
return self unless clear_entries?
|
|
35
|
+
|
|
36
|
+
context.change_membership(path)
|
|
37
|
+
self
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module XitDB
|
|
4
|
+
module Views
|
|
5
|
+
# Adds ordered iteration and pagination to sorted collection views.
|
|
6
|
+
module Sorted
|
|
7
|
+
def rank(key)
|
|
8
|
+
context.read { adapter.rank(key) }
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def from(key)
|
|
12
|
+
return enum_for(:from, key) unless block_given?
|
|
13
|
+
|
|
14
|
+
each_entries(-> { adapter.each_from(key) }) { |*entry| yield(*entry) }
|
|
15
|
+
self
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def from_index(index)
|
|
19
|
+
return enum_for(:from_index, index) unless block_given?
|
|
20
|
+
|
|
21
|
+
each_entries(-> { adapter.each_from_index(integer_argument(index)) }) { |*entry| yield(*entry) }
|
|
22
|
+
self
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def page(offset:, limit:)
|
|
26
|
+
check!
|
|
27
|
+
offset = integer_argument(offset)
|
|
28
|
+
limit = integer_argument(limit)
|
|
29
|
+
raise ArgumentError, "negative page limit" if limit.negative?
|
|
30
|
+
return enum_for(:page, offset: offset, limit: limit) unless block_given?
|
|
31
|
+
return self if limit.zero?
|
|
32
|
+
|
|
33
|
+
remaining = limit
|
|
34
|
+
from_index(offset) do |*entry|
|
|
35
|
+
yield(*entry)
|
|
36
|
+
remaining -= 1
|
|
37
|
+
break if remaining.zero?
|
|
38
|
+
end
|
|
39
|
+
self
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# A map whose keys have a stable storage order.
|
|
44
|
+
class SortedMap < Map
|
|
45
|
+
include Sorted
|
|
46
|
+
|
|
47
|
+
def merge!(hash)
|
|
48
|
+
return super unless defined?(Profiles::Ruby) && context.profile.is_a?(Profiles::Ruby)
|
|
49
|
+
|
|
50
|
+
check!
|
|
51
|
+
context.writable!
|
|
52
|
+
raise TypeError, "merge! requires a Hash" unless Object.instance_method(:instance_of?).bind_call(hash, Hash)
|
|
53
|
+
|
|
54
|
+
# @type var replaced: ::Array[selector]
|
|
55
|
+
replaced = []
|
|
56
|
+
added = false
|
|
57
|
+
targets = Hash.instance_method(:keys).bind_call(hash).map { |key| path + [key] }
|
|
58
|
+
edit(targets: targets) do |collection|
|
|
59
|
+
context.profile.input_kind(hash, path: path)
|
|
60
|
+
hash.each_pair do |key, value|
|
|
61
|
+
added = true unless collection.lookup(key)
|
|
62
|
+
stored = context.profile.encode(value, context: context, path: path + [key])
|
|
63
|
+
collection.assign(key, stored)
|
|
64
|
+
replaced << key
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
replaced.each { |key| context.replace_location(path + [key]) }
|
|
68
|
+
context.change_membership(path) if added
|
|
69
|
+
self
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
class SortedSet < Set
|
|
74
|
+
include Sorted
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
data/lib/xitdb.rb
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "xitdb/version"
|
|
4
|
+
require_relative "xitdb/errors"
|
|
5
|
+
require_relative "xitdb/core"
|
|
6
|
+
require_relative "xitdb/declarations"
|
|
7
|
+
require_relative "xitdb/profiles/portable"
|
|
8
|
+
require_relative "xitdb/context"
|
|
9
|
+
require_relative "xitdb/value_walker"
|
|
10
|
+
require_relative "xitdb/streams"
|
|
11
|
+
require_relative "xitdb/views/base"
|
|
12
|
+
require_relative "xitdb/views/map"
|
|
13
|
+
require_relative "xitdb/views/array"
|
|
14
|
+
require_relative "xitdb/views/linked_array"
|
|
15
|
+
require_relative "xitdb/views/set"
|
|
16
|
+
require_relative "xitdb/views/sorted"
|
|
17
|
+
require_relative "xitdb/revision"
|
|
18
|
+
require_relative "xitdb/snapshot"
|
|
19
|
+
require_relative "xitdb/history"
|
|
20
|
+
require_relative "xitdb/database"
|
data/sig/base.rbs
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
module XitDB
|
|
2
|
+
VERSION: String
|
|
3
|
+
|
|
4
|
+
type diagnostic_value = nil | bool | Integer | Float | Symbol | String | Array[diagnostic_value] | Hash[diagnostic_value, diagnostic_value]
|
|
5
|
+
|
|
6
|
+
class Error < StandardError
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
class FormatVersionError < Error
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
class CorruptionError < Error
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
class ProfileMismatchError < Error
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
class TransactionStateError < Error
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
class ClosedError < Error
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
class ExpiredViewError < Error
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
class ForeignRevisionError < Error
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
class ReadOnlyError < Error
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
class ConcurrentModificationError < Error
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
class BusyError < Error
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
class CodecError < Error
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
class CodecConfigurationError < CodecError
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
class ValueError < Error
|
|
46
|
+
@path: diagnostic_value
|
|
47
|
+
attr_reader path: diagnostic_value
|
|
48
|
+
def initialize: (?String? message, ?path: top) -> void
|
|
49
|
+
private
|
|
50
|
+
def copy_path: (top value) -> diagnostic_value
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
class InvalidValueError < ValueError
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
class UnsupportedValueError < ValueError
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
class UnsupportedEncodingError < ValueError
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
class CyclicValueError < ValueError
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
class ForeignValueError < ValueError
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
class KeyCollisionError < ValueError
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
class CommitError < Error
|
|
72
|
+
@outcome: :not_committed | :committed | :unknown
|
|
73
|
+
attr_reader outcome: :not_committed | :committed | :unknown
|
|
74
|
+
def initialize: (?String? message, outcome: :not_committed | :committed | :unknown) -> void
|
|
75
|
+
end
|
|
76
|
+
end
|