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,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../input_guard"
4
+
5
+ module XitDB
6
+ module Ruby
7
+ # Produces byte strings whose order matches the declared key type.
8
+ module OrderedKeys
9
+ TYPES = { string: String, integer: Integer, float: Float, time: Time }.freeze
10
+
11
+ def self.dump(value, kind:)
12
+ InputGuard.check!(value)
13
+ unless TYPES[kind].equal?(Object.instance_method(:class).bind_call(value))
14
+ raise UnsupportedValueError, "sorted key must match its declared key kind"
15
+ end
16
+
17
+ bytes = case value
18
+ when String
19
+ string_bytes(value)
20
+ when Integer then signed(value, unsigned(value.abs))
21
+ when Time then signed(value.to_r, rational(value.to_r.abs))
22
+ when Float
23
+ float_bytes(value)
24
+ else
25
+ raise UnsupportedValueError, "sorted key must match its declared key kind"
26
+ end
27
+ bytes.freeze
28
+ end
29
+
30
+ def self.string_bytes(value)
31
+ text = value.encode(Encoding::UTF_8)
32
+ raise UnsupportedEncodingError, "sorted string key must be valid UTF-8" unless text.valid_encoding?
33
+
34
+ text.b
35
+ rescue EncodingError => e
36
+ raise UnsupportedEncodingError, "sorted string key cannot convert to UTF-8", cause: e
37
+ end
38
+
39
+ def self.float_bytes(value)
40
+ raise UnsupportedValueError, "NaN keys are unsupported" if value.nan?
41
+
42
+ bits = [value.zero? ? 0.0 : value].pack("G").unpack1("Q>")
43
+ raise InvalidValueError, "invalid float key bits" unless bits.is_a?(Integer)
44
+
45
+ [bits[63] == 1 ? bits ^ Core::Format::UINT64_MAX : bits ^ (1 << 63)].pack("Q>")
46
+ end
47
+
48
+ def self.unsigned(number)
49
+ hex = number.to_s(16)
50
+ hex = "0#{hex}" if hex.length.odd?
51
+ magnitude = number.zero? ? "".b : [hex].pack("H*")
52
+ raise RangeError, "ordered integer exceeds physical range" if magnitude.bytesize > Core::Format::UINT64_MAX
53
+
54
+ [magnitude.bytesize].pack("Q>") + magnitude
55
+ end
56
+
57
+ def self.complement(bytes)
58
+ bytes.bytes.map { |byte| byte ^ 255 }.pack("C*")
59
+ end
60
+
61
+ def self.signed(number, body)
62
+ return "\x01".b if number.zero?
63
+
64
+ number.negative? ? "\x00".b + complement(body) : "\x02".b + body
65
+ end
66
+
67
+ def self.rational(number)
68
+ numerator = number.numerator
69
+ denominator = number.denominator
70
+ output = +"".b
71
+ inverted = false
72
+ loop do
73
+ quotient, remainder = numerator.divmod(denominator)
74
+ part = unsigned(quotient) + (remainder.zero? ? "\x00".b : "\x01".b)
75
+ output << (inverted ? complement(part) : part)
76
+ break if remainder.zero?
77
+
78
+ numerator = denominator
79
+ denominator = remainder
80
+ inverted = !inverted
81
+ end
82
+ output
83
+ end
84
+ private_class_method :unsigned, :complement, :signed, :rational, :string_bytes, :float_bytes
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,296 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "date"
4
+ require "bigdecimal"
5
+ require_relative "../core"
6
+ require_relative "../input_guard"
7
+ require_relative "wire"
8
+ require_relative "key_encoding"
9
+
10
+ module XitDB
11
+ module Ruby
12
+ # Retains a scalar payload when its application codec is unavailable.
13
+ class EncodedValue
14
+ attr_reader :codec_id, :format_tag, :bytes
15
+
16
+ def initialize(codec_id:, bytes:, format_tag: "rb")
17
+ value = Core::ByteValue.new(bytes, format_tag: format_tag)
18
+ @codec_id = codec_id.dup.freeze
19
+ @format_tag = value.format_tag
20
+ @bytes = value.bytes
21
+ freeze
22
+ end
23
+ end
24
+
25
+ # Serializes builtin Ruby scalar values and registered codec payloads.
26
+ module Scalars
27
+ TYPES = [NilClass, FalseClass, TrueClass, Integer, Float, String, Symbol, Time, Date, DateTime,
28
+ BigDecimal, Rational, EncodedValue, Core::ByteValue].freeze
29
+ private_constant :TYPES
30
+
31
+ def self.encode(value, core:, registry: nil)
32
+ InputGuard.check!(value)
33
+ if (codec = registry&.for_value(value))
34
+ payload = "\x01\x0a".b + Wire.t(codec.id) + Wire.b(codec.dump(value))
35
+ return core.bytes(payload, format_tag: "rb")
36
+ end
37
+ unless builtin_type?(Object.instance_method(:class).bind_call(value))
38
+ raise UnsupportedValueError, "unsupported Ruby scalar class"
39
+ end
40
+
41
+ value = checked_builtin(value)
42
+ builtin_slot(value, core)
43
+ end
44
+
45
+ def self.checked_builtin(value)
46
+ case value
47
+ when Object
48
+ checked_object_builtin(value)
49
+ else
50
+ raise UnsupportedValueError, "unsupported Ruby scalar class"
51
+ end
52
+ end
53
+
54
+ def self.checked_object_builtin(value)
55
+ return value if value.is_a?(DateTime)
56
+ return value if value.is_a?(Date)
57
+ return value if value.is_a?(BigDecimal)
58
+
59
+ case value
60
+ when Time, Rational, EncodedValue, Core::ByteValue
61
+ value
62
+ else
63
+ KeyEncoding.checked_key(value)
64
+ end
65
+ end
66
+
67
+ def self.builtin_slot(value, core)
68
+ case value
69
+ when nil then Core::Slot.null
70
+ when Integer
71
+ return Core::Slot.int(value) if (-(1 << 63)...(1 << 63)).cover?(value)
72
+ return Core::Slot.uint(value) if (0...(1 << 64)).cover?(value)
73
+
74
+ core.bytes("\x01\x04".b + Wire.z(value), format_tag: "rb")
75
+ when Float then Core::Slot.float(value)
76
+ when EncodedValue, Core::ByteValue then core.bytes(value.bytes, format_tag: value.format_tag)
77
+ else core.bytes("\x01".b + builtin_payload(value), format_tag: "rb")
78
+ end
79
+ end
80
+
81
+ def self.builtin_payload(value)
82
+ case value
83
+ when String then "\x01".b + Wire.t(value.encoding.name) + Wire.b(value)
84
+ when Symbol
85
+ name = value.to_s
86
+ "\x02".b + Wire.t(name.encoding.name) + Wire.b(name)
87
+ when false then "\x03\x00".b
88
+ when true then "\x03\x01".b
89
+ when Time then time_bytes(value)
90
+ when DateTime then "\x07".b + Wire.q(value.ajd) + Wire.q(value.offset) + Wire.f64(value.start)
91
+ when Date then "\x06".b + Wire.z(value.jd) + Wire.f64(value.start)
92
+ when Rational then "\x09".b + Wire.q(value)
93
+ else decimal_payload(value)
94
+ end
95
+ end
96
+
97
+ def self.decimal_payload(value)
98
+ unless value.is_a?(BigDecimal)
99
+ raise UnsupportedValueError, "unsupported Ruby scalar: #{Object.instance_method(:class).bind_call(value)}"
100
+ end
101
+
102
+ "\x08".b + decimal_bytes(value)
103
+ end
104
+
105
+ def self.time_bytes(value)
106
+ offset = value.utc_offset
107
+ raise UnsupportedValueError, "Ruby profile Time offsets require whole seconds" unless offset == offset.to_i
108
+
109
+ "\x05".b + Wire.q(value.to_r) + Wire.z(offset.to_i) + (value.utc? ? "\x01".b : "\x00".b)
110
+ end
111
+
112
+ def self.builtin_type?(type)
113
+ TYPES.include?(type)
114
+ end
115
+
116
+ def self.decode(slot, core:, bound: nil, registry: nil, immutable: true)
117
+ value = case slot.tag
118
+ when :none then nil
119
+ when :int then slot.value >= (1 << 63) ? slot.value - (1 << 64) : slot.value
120
+ when :uint then slot.value
121
+ when :float then [slot.value].pack("Q>").unpack1("G")
122
+ when :bytes, :short_bytes
123
+ stored = core.read_bytes(slot, bound: bound)
124
+ case stored.format_tag
125
+ when nil then stored.bytes.dup
126
+ when "rb" then decode_payload(stored.bytes, registry: registry)
127
+ else stored
128
+ end
129
+ else
130
+ raise InvalidValueError, "expected Ruby scalar, got slot tag #{slot.tag}"
131
+ end
132
+ immutable ? Object.instance_method(:freeze).bind_call(value) : value
133
+ end
134
+
135
+ def self.decode_payload(bytes, registry: nil)
136
+ reader = Wire::Reader.new(bytes)
137
+ version = reader.byte
138
+ raise UnsupportedEncodingError, "unsupported Ruby payload version #{version}" unless version == 1
139
+
140
+ value = decode_type(reader, bytes, registry)
141
+ reader.finish!
142
+ value
143
+ rescue ArgumentError, RangeError => e
144
+ raise InvalidValueError, e.message
145
+ end
146
+
147
+ def self.decode_type(reader, bytes, registry)
148
+ type = reader.byte
149
+ case type
150
+ when 1 then read_string(reader)
151
+ when 2 then read_symbol(reader)
152
+ when 3 then read_boolean?(reader)
153
+ when 4 then reader.z
154
+ when 5 then read_time(reader)
155
+ when 6 then Date.jd(reader.z, read_start(reader))
156
+ when 7 then read_datetime(reader)
157
+ when 8 then read_decimal(reader)
158
+ else decode_extended_type(type, reader, bytes, registry)
159
+ end
160
+ end
161
+
162
+ def self.decode_extended_type(type, reader, bytes, registry)
163
+ case type
164
+ when 9 then reader.q
165
+ when 10 then read_codec(reader, bytes, registry)
166
+ else raise UnsupportedEncodingError, "unsupported Ruby builtin type #{type}"
167
+ end
168
+ end
169
+
170
+ def self.read_symbol(reader)
171
+ name = read_string(reader)
172
+ raise InvalidValueError, "invalid Symbol bytes" unless name.valid_encoding?
173
+
174
+ name.to_sym
175
+ rescue EncodingError, ArgumentError => e
176
+ raise InvalidValueError, e.message
177
+ end
178
+
179
+ def self.read_boolean?(reader)
180
+ flag = reader.byte
181
+ raise InvalidValueError, "invalid Boolean flag" unless flag <= 1
182
+
183
+ flag == 1
184
+ end
185
+
186
+ def self.read_codec(reader, bytes, registry)
187
+ codec_id = reader.t
188
+ payload = reader.b
189
+ reader.finish!
190
+ codec = registry&.for_id(codec_id)
191
+ codec ? codec.load(payload) : EncodedValue.new(codec_id: codec_id, bytes: bytes)
192
+ end
193
+
194
+ def self.decimal_bytes(value)
195
+ return "\x03".b if value.nan?
196
+ return value.positive? ? "\x01".b : "\x02".b if value.infinite?
197
+
198
+ # @type var parts: [Integer, String, Integer, Integer]
199
+ parts = value.split
200
+ sign = parts[0]
201
+ digits = parts[1]
202
+ base = parts[2]
203
+ exponent = parts[3]
204
+ raise InvalidValueError, "decimal base must be ten" unless base == 10
205
+
206
+ "\x00".b + (sign.negative? ? "\x01".b : "\x00".b) + Wire.t(digits) + Wire.z(exponent)
207
+ end
208
+
209
+ def self.read_time(reader)
210
+ instant = reader.q
211
+ offset = reader.z
212
+ utc = reader.byte
213
+ unless utc <= 1 && offset.abs < 86_400 && (utc.zero? || offset.zero?)
214
+ raise InvalidValueError, "invalid Time offset or UTC flag"
215
+ end
216
+
217
+ time = Time.at(instant)
218
+ utc == 1 ? time.utc : time.getlocal(offset)
219
+ end
220
+
221
+ def self.read_start(reader)
222
+ start = reader.f64
223
+ raise InvalidValueError, "invalid calendar start" unless start.infinite? || (2_298_874..2_426_355).cover?(start)
224
+
225
+ start
226
+ end
227
+
228
+ def self.read_datetime(reader)
229
+ instant = reader.q
230
+ offset = reader.q
231
+ start = read_start(reader)
232
+ raise InvalidValueError, "invalid DateTime offset" unless offset.abs <= 1 && (offset * 86_400).denominator == 1
233
+
234
+ day, fraction = (instant + Rational(1, 2) + offset).divmod(1)
235
+ # @type var date: DateTime
236
+ date = DateTime.jd(day.to_i, 0, 0, 0, offset, start)
237
+ date + fraction
238
+ end
239
+
240
+ def self.read_decimal(reader)
241
+ case reader.byte
242
+ when 0 then read_finite_decimal(reader)
243
+ when 1 then build_decimal("Infinity")
244
+ when 2 then build_decimal("-Infinity")
245
+ when 3 then build_decimal("NaN")
246
+ else raise InvalidValueError, "invalid decimal kind"
247
+ end
248
+ end
249
+
250
+ def self.read_finite_decimal(reader)
251
+ sign = reader.byte
252
+ digits = reader.t
253
+ exponent = reader.z
254
+ unless sign <= 1 && /\A(?:0|[1-9](?:[0-9]*[1-9])?)\z/.match?(digits) && (digits != "0" || exponent.zero?)
255
+ raise InvalidValueError, "invalid finite decimal fields"
256
+ end
257
+
258
+ text = "#{"-" if sign == 1}0.#{digits}e#{exponent}"
259
+ value = build_decimal(text)
260
+ # @type var fields: [Integer, String, Integer, Integer]
261
+ fields = value.split
262
+ if fields != [sign == 1 ? -1 : 1, digits, 10, exponent]
263
+ raise InvalidValueError, "decimal is outside the supported exponent range"
264
+ end
265
+
266
+ value
267
+ end
268
+
269
+ def self.build_decimal(text)
270
+ BigDecimal.save_exception_mode do
271
+ BigDecimal.mode(BigDecimal::EXCEPTION_ALL, false)
272
+ BigDecimal(text)
273
+ end
274
+ end
275
+
276
+ def self.read_string(reader)
277
+ name = reader.t
278
+ bytes = reader.b
279
+ begin
280
+ encoding = Encoding.find(name)
281
+ rescue ArgumentError => e
282
+ raise UnsupportedEncodingError, e.message
283
+ end
284
+ raise UnsupportedEncodingError, "unknown encoding #{name}" unless encoding
285
+
286
+ bytes.force_encoding(encoding)
287
+ end
288
+ private_class_method :decode_payload, :read_string, :decimal_bytes, :read_time, :read_start,
289
+ :read_datetime, :read_decimal, :builtin_slot, :builtin_payload, :time_bytes,
290
+ :checked_builtin, :decimal_payload,
291
+ :checked_object_builtin, :decode_type, :decode_extended_type, :read_symbol,
292
+ :read_boolean?,
293
+ :read_codec, :read_finite_decimal, :build_decimal
294
+ end
295
+ end
296
+ end
@@ -0,0 +1,146 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../errors"
4
+
5
+ module XitDB
6
+ module Ruby
7
+ # Encodes and validates fields in Ruby profile payloads.
8
+ module Wire
9
+ def self.u(value)
10
+ unless value.is_a?(Integer) && value >= 0
11
+ raise InvalidValueError, "unsigned field requires a nonnegative Integer"
12
+ end
13
+
14
+ bytes = +"".b
15
+ loop do
16
+ byte = value & 0x7f
17
+ value >>= 7
18
+ bytes << (value.zero? ? byte : byte | 0x80)
19
+ break if value.zero?
20
+ end
21
+ bytes
22
+ end
23
+
24
+ def self.b(bytes)
25
+ u(bytes.bytesize) + bytes.b
26
+ end
27
+
28
+ def self.t(text)
29
+ utf8 = text.encode(Encoding::UTF_8)
30
+ raise InvalidValueError, "text field requires valid UTF-8" unless utf8.valid_encoding?
31
+
32
+ b(utf8)
33
+ rescue EncodingError => e
34
+ raise InvalidValueError, e.message
35
+ end
36
+
37
+ def self.z(value)
38
+ raise InvalidValueError, "signed field requires an Integer" unless value.is_a?(Integer)
39
+
40
+ hex = value.abs.to_s(16)
41
+ hex = "0#{hex}" if hex.length.odd?
42
+ magnitude = value.zero? ? "".b : [hex].pack("H*")
43
+ (value.negative? ? "\x01".b : "\x00".b) + b(magnitude)
44
+ end
45
+
46
+ def self.q(value)
47
+ raise InvalidValueError, "rational field requires a Rational" unless value.is_a?(Rational)
48
+
49
+ z(value.numerator) + u(value.denominator)
50
+ end
51
+
52
+ def self.f64(value)
53
+ [value].pack("G")
54
+ end
55
+
56
+ # Reads canonical fields and rejects truncated or trailing bytes.
57
+ class Reader
58
+ def initialize(bytes)
59
+ @bytes = bytes.b
60
+ @position = 0
61
+ end
62
+
63
+ def byte
64
+ value = take(1).getbyte(0)
65
+ raise InvalidValueError, "truncated payload field" unless value
66
+
67
+ value
68
+ end
69
+
70
+ def u
71
+ value = 0
72
+ shift = 0
73
+ loop do
74
+ part = byte
75
+ value |= (part & 0x7f) << shift
76
+ if part < 0x80
77
+ raise InvalidValueError, "nonminimal unsigned field" if shift.positive? && part.zero?
78
+
79
+ return value
80
+ end
81
+ shift += 7
82
+ end
83
+ end
84
+
85
+ def b
86
+ take(u)
87
+ end
88
+
89
+ def t
90
+ text = b.force_encoding(Encoding::UTF_8)
91
+ raise InvalidValueError, "invalid UTF-8 text field" unless text.valid_encoding?
92
+
93
+ text
94
+ end
95
+
96
+ def z
97
+ sign = byte
98
+ raise InvalidValueError, "invalid integer sign" unless sign <= 1
99
+
100
+ magnitude = b
101
+ if magnitude.getbyte(0)&.zero? || (sign == 1 && magnitude.empty?)
102
+ raise InvalidValueError, "nonminimal integer magnitude"
103
+ end
104
+
105
+ value = magnitude.empty? ? 0 : magnitude.unpack1("H*").to_s.to_i(16)
106
+ sign.zero? ? value : -value
107
+ end
108
+
109
+ def q
110
+ numerator = z
111
+ denominator = u
112
+ unless denominator.positive? && numerator.gcd(denominator) == 1
113
+ raise InvalidValueError, "rational must have a positive reduced denominator"
114
+ end
115
+
116
+ Rational(numerator, denominator)
117
+ end
118
+
119
+ def f64
120
+ value = take(8).unpack1("G")
121
+ raise InvalidValueError, "invalid float payload" unless value.is_a?(Float)
122
+
123
+ value
124
+ end
125
+
126
+ def finish!
127
+ raise InvalidValueError, "trailing payload bytes" unless @position == @bytes.bytesize
128
+
129
+ self
130
+ end
131
+
132
+ private
133
+
134
+ def take(size)
135
+ raise InvalidValueError, "truncated payload field" if size > @bytes.bytesize - @position
136
+
137
+ result = @bytes.byteslice(@position, size)
138
+ raise InvalidValueError, "truncated payload field" unless result
139
+
140
+ @position += size
141
+ result
142
+ end
143
+ end
144
+ end
145
+ end
146
+ end
data/lib/xitdb/ruby.rb ADDED
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../xitdb"
4
+ require_relative "ruby/scalars"
5
+ require_relative "ruby/key_encoding"
6
+ require_relative "ruby/containers"
7
+ require_relative "profiles/ruby"
8
+
9
+ module XitDB
10
+ # Opens databases with the Ruby value profile.
11
+ module Ruby
12
+ def self.open(path, readonly: false, lock_timeout: 5.0, codecs: [])
13
+ profile = Profiles::Ruby.new(codecs: codecs)
14
+ store, created = Database.send(:open_store, path, readonly: readonly)
15
+ opened = false
16
+ # @type var database: Database
17
+ begin
18
+ core = Core::Database.new(store: store, initialize_empty: created, lock_timeout: lock_timeout,
19
+ hash_id: "rb01", hash_size: 32)
20
+ database = Database.new(core: core, profile: profile, readonly: readonly)
21
+ opened = true
22
+ ensure
23
+ store.send(:close_unreturned) unless opened
24
+ end
25
+ return database unless block_given?
26
+
27
+ begin
28
+ yield database
29
+ ensure
30
+ database.close
31
+ end
32
+ end
33
+
34
+ def self.memory(codecs: [])
35
+ profile = Profiles::Ruby.new(codecs: codecs)
36
+ database = Database.new(core: Core::Database.memory(hash_id: "rb01", hash_size: 32), profile: profile)
37
+ return database unless block_given?
38
+
39
+ begin
40
+ yield database
41
+ ensure
42
+ database.close
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module XitDB
4
+ # A captured root map with an optional committed revision.
5
+ class Snapshot < Views::Map
6
+ attr_reader :revision
7
+
8
+ def initialize(context:, revision:)
9
+ @revision = revision
10
+ super(context: context, path: [], kind: :map)
11
+ end
12
+ end
13
+ end