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