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