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,361 @@
1
+ # frozen_string_literal: true
2
+
3
+ module XitDB
4
+ module Core
5
+ # A byte-ordered map backed by a counted B-tree.
6
+ class SortedMap
7
+ include Enumerable
8
+
9
+ attr_reader :cursor
10
+
11
+ def initialize(cursor, set: false)
12
+ @cursor = cursor
13
+ tag = if cursor.slot.tag == :none
14
+ set ? :sorted_set : :sorted_map
15
+ else
16
+ cursor.slot.tag
17
+ end
18
+ raise InvalidValueError, "slot does not contain a sorted collection" unless %i[sorted_map
19
+ sorted_set].include?(tag)
20
+
21
+ @tree = Tree.new(cursor, tag: tag)
22
+ end
23
+
24
+ def size
25
+ @tree.size
26
+ end
27
+
28
+ def get(key_bytes)
29
+ entry = @tree.find(key_bytes)
30
+ @tree.record(entry).last if entry
31
+ end
32
+
33
+ def include?(key_bytes)
34
+ !@tree.find(key_bytes).nil?
35
+ end
36
+
37
+ def put(key_bytes, value:)
38
+ replace_child(key_bytes, value)
39
+ end
40
+
41
+ def add(key_bytes)
42
+ @tree.store(key_bytes, key_only: true)
43
+ self
44
+ end
45
+
46
+ def remove(key_bytes)
47
+ @tree.remove(key_bytes)
48
+ end
49
+
50
+ def rank(key_bytes)
51
+ @tree.locate(key_bytes).last
52
+ end
53
+
54
+ def at(index)
55
+ entry = @tree.at(index)
56
+ decode(entry) if entry
57
+ end
58
+
59
+ def each(&block)
60
+ return enum_for(:each) unless block
61
+
62
+ each_from_index(0, &block)
63
+ end
64
+
65
+ def each_from(key_bytes, &block)
66
+ return enum_for(:each_from, key_bytes) unless block
67
+
68
+ each_from_index(rank(key_bytes), &block)
69
+ end
70
+
71
+ def each_from_index(index)
72
+ return enum_for(:each_from_index, index) unless block_given?
73
+
74
+ @tree.each_from_index(index) { |entry| yield decode(entry) }
75
+ self
76
+ end
77
+
78
+ def cursor_at(key_bytes, create: false)
79
+ slot = get(key_bytes)
80
+ if !slot && create
81
+ add(key_bytes)
82
+ slot = get(key_bytes)
83
+ end
84
+ cursor.hash_child(key_bytes.b.freeze, slot) if slot
85
+ end
86
+
87
+ def replace_child(key_bytes, slot, **)
88
+ @tree.store(key_bytes, value: slot, **)
89
+ self
90
+ end
91
+
92
+ private
93
+
94
+ def decode(entry)
95
+ key, value = @tree.record(entry)
96
+ [cursor.database.read_bytes(key, bound: cursor.bound).bytes, value]
97
+ end
98
+
99
+ # Maintains sorted records and branch separators.
100
+ class Tree < BTree
101
+ BRANCH_SIZE = 418
102
+ # A mutable sorted node and its separators.
103
+ class Node
104
+ attr_accessor :kind, :slots, :counts, :separators
105
+
106
+ def initialize(kind, slots, counts, separators)
107
+ @kind = kind
108
+ @slots = slots
109
+ @counts = counts
110
+ @separators = separators
111
+ end
112
+
113
+ def size
114
+ kind.zero? ? slots.size : counts.sum
115
+ end
116
+ end
117
+
118
+ def record(entry)
119
+ return [entry, Slot.new] if %i[bytes short_bytes].include?(entry.tag)
120
+ raise CorruptionError, "invalid sorted entry" unless %i[kv_pair sorted_kv_pair].include?(entry.tag)
121
+
122
+ offset = entry.value + (entry.tag == :kv_pair ? cursor.database.header.hash_size : 0)
123
+ bytes = read(offset, 18)
124
+ key = Slot.parse(Format.slice(bytes, 0, 9))
125
+ value = Slot.parse(Format.slice(bytes, 9, 9))
126
+ unless %i[bytes short_bytes].include?(key.tag) && Slot::TAGS.include?(value.tag) && value.tag != :index
127
+ raise CorruptionError, "invalid sorted record slots"
128
+ end
129
+
130
+ [key, value]
131
+ end
132
+
133
+ def find(key)
134
+ _path, node, index, found, = locate(key)
135
+ node.slots[index] if found
136
+ end
137
+
138
+ def locate(key)
139
+ raise TypeError, "sorted key must be a String" unless key.is_a?(String)
140
+
141
+ key = key.b
142
+ count, pointer = header
143
+ # @type var path: sorted_path
144
+ path = []
145
+ # @type var seen: Hash[Integer?, bool]
146
+ seen = {}
147
+ rank = 0
148
+ loop do
149
+ raise CorruptionError, "cyclic sorted branch" if seen[pointer]
150
+
151
+ seen[pointer] = true
152
+ node = pointer ? read_node(pointer, count) : Node.new(0, [], [], [])
153
+ if node.kind.zero?
154
+ index = lower_bound(node.slots.size) { |i| key_bytes(record(node.slots[i]).first) >= key }
155
+ found = index < node.slots.size && key_bytes(record(node.slots[index]).first) == key
156
+ return [path, node, index, found, rank + index]
157
+ end
158
+ index = lower_bound(node.slots.size - 1) { |i| key_bytes(node.separators[i + 1]) > key }
159
+ raise CorruptionError, "missing sorted branch pointer" unless pointer
160
+
161
+ path << [pointer, node, index]
162
+ rank += node.counts.first(index).sum
163
+ pointer = node.slots[index].value
164
+ count = node.counts[index]
165
+ end
166
+ end
167
+
168
+ def store(key, value: nil, key_only: false, owner: nil, collection: false, propagate: true)
169
+ unless key_only
170
+ raise TypeError, "expected Slot" unless value
171
+
172
+ validate_slot(value)
173
+ end
174
+ mutate do |transaction|
175
+ count, pointer = header
176
+ path, node, index, found, = locate(key)
177
+ next if found && key_only
178
+ raise RangeError, "sorted size exceeds unsigned 64-bit range" if !found && count == Format::UINT64_MAX
179
+
180
+ store_leaf(transaction, key, node, index, found, key_only, value)
181
+ root = write_path(transaction, path, node, pointer_for(path, pointer))
182
+ publish(transaction, count + (found ? 0 : 1), root, propagate: propagate)
183
+ cursor.invalidate_children(index: key.b, except: owner) unless collection || key_only
184
+ end
185
+ self
186
+ end
187
+
188
+ def remove(key)
189
+ mutate do |transaction|
190
+ count, pointer = header
191
+ path, node, index, found, = locate(key)
192
+ next false unless found
193
+
194
+ node.slots.delete_at(index)
195
+ old_pointer = pointer_for(path, pointer)
196
+ path.reverse_each do |parent_pointer, parent, child_index|
197
+ if node.slots.empty?
198
+ parent.slots.delete_at(child_index)
199
+ parent.counts.delete_at(child_index)
200
+ parent.separators.delete_at(child_index)
201
+ parent.separators[0] = Slot.new unless parent.separators.empty?
202
+ else
203
+ parent.slots[child_index] = Slot.new(tag: :index, value: write_node(transaction, node, old_pointer))
204
+ parent.counts[child_index] = node.size
205
+ end
206
+ node = parent
207
+ old_pointer = parent_pointer
208
+ end
209
+ root_pointer = collapse_sorted_root(transaction, node, old_pointer, count - 1)
210
+ publish(transaction, count - 1, root_pointer)
211
+ cursor.invalidate_children(index: key.b)
212
+ true
213
+ end
214
+ end
215
+
216
+ private
217
+
218
+ def store_leaf(transaction, key, node, index, found, key_only, value)
219
+ key_slot = found ? record(node.slots[index]).first : cursor.database.bytes(key)
220
+ entry = if key_only
221
+ key_slot
222
+ else
223
+ raise TypeError, "expected Slot" unless value
224
+
225
+ Slot.new(tag: :sorted_kv_pair, value: transaction.append(key_slot.to_bytes + value.to_bytes))
226
+ end
227
+ found ? node.slots[index] = entry : node.slots.insert(index, entry)
228
+ end
229
+
230
+ def write_path(transaction, path, node, pointer)
231
+ result = split_node(transaction, node, pointer)
232
+ path.reverse_each do |parent_pointer, parent, child_index|
233
+ parent.slots[child_index, 1] = result.map { |ptr, _, _| Slot.new(tag: :index, value: ptr) }
234
+ parent.counts[child_index, 1] = result.map { |_, size, _| size }
235
+ parent.separators.insert(child_index + 1, split_separator(result)) if result.size == 2
236
+ result = split_node(transaction, parent, parent_pointer)
237
+ end
238
+ return result.first.first if result.size == 1
239
+
240
+ write_node(transaction, Node.new(1, result.map { |ptr, _, _| Slot.new(tag: :index, value: ptr) },
241
+ result.map { |_, size, _| size }, [Slot.new, split_separator(result)]))
242
+ end
243
+
244
+ def collapse_sorted_root(transaction, node, old_pointer, count)
245
+ unless node.kind == 1 && node.slots.size == 1
246
+ node = Node.new(0, [], [], []) if node.slots.empty?
247
+ return write_node(transaction, node, old_pointer)
248
+ end
249
+
250
+ pointer = node.slots.first.value
251
+ # @type var seen: Hash[Integer, bool]
252
+ seen = {}
253
+ loop do
254
+ raise CorruptionError, "cyclic sorted branch" if seen[pointer]
255
+
256
+ seen[pointer] = true
257
+ child = read_node(pointer, count)
258
+ return pointer unless child.kind == 1 && child.slots.size == 1
259
+
260
+ pointer = child.slots.first.value
261
+ end
262
+ end
263
+
264
+ def split_separator(result)
265
+ result.last.last || raise(CorruptionError, "missing sorted split separator")
266
+ end
267
+
268
+ def lower_bound(size)
269
+ left = 0
270
+ right = size
271
+ while left < right
272
+ middle = (left + right) / 2
273
+ yield(middle) ? right = middle : left = middle + 1
274
+ end
275
+ left
276
+ end
277
+
278
+ def key_bytes(slot)
279
+ raise CorruptionError, "invalid sorted key" unless %i[bytes short_bytes].include?(slot.tag)
280
+
281
+ cursor.database.read_bytes(slot, bound: cursor.bound).bytes
282
+ end
283
+
284
+ def read_node(pointer, expected, bound: cursor.bound)
285
+ kind, num = Format.pair(read(pointer, 2, bound: bound), "CC")
286
+ validate_header(kind, num)
287
+
288
+ bytes = read(pointer, kind.zero? ? LEAF_SIZE : BRANCH_SIZE, bound: bound)
289
+ slots = Array.new(num) { |i| Slot.parse(Format.slice(bytes, 2 + (i * 9), 9)) }
290
+ # @type var separators: Array[Slot]
291
+ separators = kind.zero? ? [] : Array.new(num) { |i| Slot.parse(Format.slice(bytes, LEAF_SIZE + (i * 9), 9)) }
292
+ # @type var counts: Array[Integer]
293
+ counts = kind.zero? ? [] : Format.integers(Format.slice(bytes, 290, 128), "Q>*").first(num)
294
+ node = Node.new(kind, slots, counts, separators)
295
+ raise CorruptionError, "invalid sorted node" unless valid_node?(node) && node.size == expected
296
+
297
+ node
298
+ end
299
+
300
+ def validate_header(kind, count)
301
+ return if [0, 1].include?(kind) && count <= CAPACITY && (kind.zero? || count.positive?)
302
+
303
+ raise CorruptionError, "invalid sorted node header"
304
+ end
305
+
306
+ def valid_node?(node)
307
+ if node.kind.zero?
308
+ tags = %i[bytes short_bytes kv_pair sorted_kv_pair]
309
+ node.slots.all? { |slot| tags.include?(slot.tag) }
310
+ else
311
+ tags = %i[bytes short_bytes]
312
+ node.slots.all? { |slot| slot.tag == :index } && node.separators.drop(1).all? do |slot|
313
+ tags.include?(slot.tag)
314
+ end
315
+ end
316
+ end
317
+
318
+ def write_node(transaction, node, old_pointer = nil)
319
+ bytes = [node.kind, node.slots.size].pack("CC")
320
+ bytes << padded_slots(node.slots)
321
+ unless node.kind.zero?
322
+ raise CorruptionError, "invalid sorted branch" unless node.is_a?(Node)
323
+
324
+ bytes << padded_slots(node.separators)
325
+ bytes << (node.counts + ([0] * (CAPACITY - node.counts.size))).pack("Q>*")
326
+ end
327
+ if old_pointer && old_pointer >= transaction.mutable_start
328
+ transaction.write_at(old_pointer, bytes)
329
+ old_pointer
330
+ else
331
+ transaction.append(bytes)
332
+ end
333
+ end
334
+
335
+ def padded_slots(slots)
336
+ slots.map(&:to_bytes).join + ("\0".b * ((CAPACITY - slots.size) * 9))
337
+ end
338
+
339
+ def split_node(transaction, node, old_pointer)
340
+ return [[write_node(transaction, node, old_pointer), node.size, nil]] if node.slots.size <= CAPACITY
341
+
342
+ separator = node.kind.zero? ? record(node.slots[SPLIT_COUNT]).first : node.separators[SPLIT_COUNT]
343
+ left = Node.new(node.kind, node.slots.first(SPLIT_COUNT), node.counts.first(SPLIT_COUNT),
344
+ node.separators.first(SPLIT_COUNT))
345
+ right = Node.new(node.kind, node.slots.drop(SPLIT_COUNT), node.counts.drop(SPLIT_COUNT),
346
+ node.separators.drop(SPLIT_COUNT))
347
+ [[write_node(transaction, left, old_pointer), left.size, nil],
348
+ [write_node(transaction, right), right.size, separator]]
349
+ end
350
+ end
351
+ private_constant :Tree
352
+ end
353
+
354
+ # A byte-ordered collection of keys.
355
+ class SortedSet < SortedMap
356
+ def initialize(cursor)
357
+ super(cursor, set: true)
358
+ end
359
+ end
360
+ end
361
+ end
@@ -0,0 +1,159 @@
1
+ # frozen_string_literal: true
2
+
3
+ module XitDB
4
+ module Core
5
+ # Owns writable cursors and their rollback checkpoints.
6
+ class Transaction
7
+ Checkpoint = Struct.new(:owner, :slots, :length, :cancelled, keyword_init: true) # rubocop:disable Lint/StructNewOverride -- Length is the saved store boundary.
8
+ private_constant :Checkpoint
9
+
10
+ attr_reader :root, :mutable_start
11
+
12
+ def initialize(database, store, slot)
13
+ @database = database
14
+ @store = store
15
+ @owner = Thread.current
16
+ @active = true
17
+ @cancelled = false
18
+ @cursors = ObjectSpace::WeakMap.new
19
+ @checkpoints = []
20
+ @mutable_start = store.length
21
+ @root = cursor(slot: slot)
22
+ end
23
+
24
+ def check_active!
25
+ @database.check_open!
26
+ raise ExpiredViewError, "transaction has ended" unless @active
27
+ raise TransactionStateError, "transaction belongs to another thread" unless @owner == Thread.current
28
+ end
29
+
30
+ def check_logical_mutation!
31
+ check_active!
32
+ raise TransactionStateError, "a logical mutation is already active" if @logical_mutation
33
+ end
34
+
35
+ def with_logical_mutation
36
+ check_logical_mutation!
37
+ @logical_mutation = true
38
+ begin
39
+ yield
40
+ ensure
41
+ @logical_mutation = false
42
+ end
43
+ end
44
+
45
+ def cursor(slot:)
46
+ check_active!
47
+ validate_slot(slot)
48
+ cursor = Cursor.new(database: @database, slot: slot, bound: length, transaction: self)
49
+ @cursors[cursor] = true
50
+ cursor
51
+ end
52
+
53
+ def check_cursor!(cursor)
54
+ check_active!
55
+ raise ExpiredViewError, "cursor was discarded by restore" unless @cursors.key?(cursor)
56
+ end
57
+
58
+ def check_replacement!(cursor, value)
59
+ check_cursor!(cursor)
60
+ validate_slot(value)
61
+ end
62
+
63
+ def length
64
+ check_active!
65
+ @store.length
66
+ end
67
+
68
+ def append(bytes)
69
+ check_active!
70
+ @store.append(bytes)
71
+ end
72
+
73
+ def read_at(offset, size)
74
+ check_active!
75
+ @database.read_at(offset, size, bound: length)
76
+ end
77
+
78
+ # Collections must copy blocks below this boundary before changing them.
79
+ def write_at(offset, bytes)
80
+ check_active!
81
+ raise TransactionStateError, "write requires copy-on-write" if offset < @mutable_start
82
+ raise RangeError, "write exceeds allocated data" if offset + bytes.bytesize > length
83
+
84
+ @store.write_at(offset, bytes)
85
+ end
86
+
87
+ def freeze
88
+ check_active!
89
+ @mutable_start = length
90
+ end
91
+
92
+ def checkpoint
93
+ check_active!
94
+ # @type var slots: Hash[Cursor, cursor_state]
95
+ slots = {}
96
+ @cursors.each_key { |cursor| slots[cursor] = cursor.checkpoint_state }
97
+ checkpoint = Checkpoint.new(owner: self, slots: slots, length: freeze, cancelled: @cancelled).freeze
98
+ @checkpoints << checkpoint
99
+ checkpoint
100
+ end
101
+
102
+ def restore(checkpoint)
103
+ check_active!
104
+ index = @checkpoints.index { |item| item.equal?(checkpoint) }
105
+ unless index && checkpoint.owner.equal?(self) && checkpoint.length <= length
106
+ raise TransactionStateError, "checkpoint does not belong to this transaction state"
107
+ end
108
+
109
+ @store.truncate(checkpoint.length)
110
+ @cursors = ObjectSpace::WeakMap.new
111
+ checkpoint.slots.each do |cursor, state|
112
+ @cursors[cursor] = true
113
+ cursor.restore_checkpoint_state(state)
114
+ end
115
+ @mutable_start = checkpoint.length
116
+ @cancelled = checkpoint.cancelled
117
+ (@checkpoints.slice!(index..) || []).each { |item| item.slots.clear }
118
+ nil
119
+ end
120
+
121
+ # Internal operations release rollback state after a successful mutation.
122
+ def release_checkpoint(checkpoint)
123
+ check_active!
124
+ unless checkpoint.is_a?(Checkpoint) && checkpoint.owner.equal?(self)
125
+ raise TransactionStateError, "checkpoint belongs to another transaction"
126
+ end
127
+
128
+ @checkpoints.delete_if { |item| item.equal?(checkpoint) }
129
+ checkpoint.slots.clear
130
+ nil
131
+ end
132
+
133
+ def cancel_publication
134
+ check_active!
135
+ @cancelled = true
136
+ end
137
+
138
+ def cancelled?
139
+ check_active!
140
+ @cancelled
141
+ end
142
+
143
+ def expire!
144
+ @active = false
145
+ @checkpoints.each { |checkpoint| checkpoint.slots.clear }
146
+ @checkpoints.clear
147
+ @cursors = ObjectSpace::WeakMap.new
148
+ @root = nil
149
+ end
150
+
151
+ private
152
+
153
+ def validate_slot(slot)
154
+ raise TypeError, "expected Slot" unless slot.is_a?(Slot)
155
+ raise CorruptionError, "unknown slot tag" unless Slot::TAGS.include?(slot.tag)
156
+ end
157
+ end
158
+ end
159
+ end
data/lib/xitdb/core.rb ADDED
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "errors"
4
+ require_relative "core/format"
5
+ require_relative "core/hasher"
6
+ require_relative "core/crc64"
7
+ require_relative "core/memory_store"
8
+ require_relative "core/file_store"
9
+ require_relative "core/bytes"
10
+ require_relative "core/cursor"
11
+ require_relative "core/transaction"
12
+ require_relative "core/database"
13
+ require_relative "core/array_list"
14
+ require_relative "core/b_tree"
15
+ require_relative "core/linked_array_list"
16
+ require_relative "core/hash_map"
17
+ require_relative "core/sorted_map"