tigerbeetle-node 0.11.12 → 0.11.13

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 (45) hide show
  1. package/README.md +212 -196
  2. package/dist/.client.node.sha256 +1 -1
  3. package/package.json +3 -2
  4. package/src/node.zig +1 -0
  5. package/src/tigerbeetle/scripts/benchmark.bat +9 -2
  6. package/src/tigerbeetle/scripts/benchmark.sh +1 -1
  7. package/src/tigerbeetle/scripts/fail_on_diff.sh +9 -0
  8. package/src/tigerbeetle/scripts/fuzz_loop_hash_log.sh +12 -0
  9. package/src/tigerbeetle/scripts/scripts/benchmark.bat +9 -2
  10. package/src/tigerbeetle/scripts/scripts/benchmark.sh +1 -1
  11. package/src/tigerbeetle/scripts/scripts/fail_on_diff.sh +9 -0
  12. package/src/tigerbeetle/scripts/scripts/fuzz_loop_hash_log.sh +12 -0
  13. package/src/tigerbeetle/src/benchmark.zig +253 -231
  14. package/src/tigerbeetle/src/config.zig +2 -3
  15. package/src/tigerbeetle/src/constants.zig +2 -10
  16. package/src/tigerbeetle/src/io/linux.zig +15 -6
  17. package/src/tigerbeetle/src/lsm/forest.zig +1 -0
  18. package/src/tigerbeetle/src/lsm/forest_fuzz.zig +63 -14
  19. package/src/tigerbeetle/src/lsm/groove.zig +134 -70
  20. package/src/tigerbeetle/src/lsm/level_iterator.zig +2 -2
  21. package/src/tigerbeetle/src/lsm/manifest_level.zig +1 -0
  22. package/src/tigerbeetle/src/lsm/posted_groove.zig +7 -4
  23. package/src/tigerbeetle/src/lsm/segmented_array.zig +1 -0
  24. package/src/tigerbeetle/src/lsm/table.zig +29 -51
  25. package/src/tigerbeetle/src/lsm/table_immutable.zig +6 -17
  26. package/src/tigerbeetle/src/lsm/table_iterator.zig +2 -2
  27. package/src/tigerbeetle/src/lsm/table_mutable.zig +9 -26
  28. package/src/tigerbeetle/src/lsm/test.zig +1 -0
  29. package/src/tigerbeetle/src/lsm/tree.zig +2 -26
  30. package/src/tigerbeetle/src/lsm/tree_fuzz.zig +7 -2
  31. package/src/tigerbeetle/src/message_bus.zig +1 -0
  32. package/src/tigerbeetle/src/simulator.zig +14 -3
  33. package/src/tigerbeetle/src/state_machine/auditor.zig +1 -0
  34. package/src/tigerbeetle/src/state_machine.zig +402 -184
  35. package/src/tigerbeetle/src/stdx.zig +9 -0
  36. package/src/tigerbeetle/src/testing/cluster.zig +1 -0
  37. package/src/tigerbeetle/src/testing/packet_simulator.zig +19 -9
  38. package/src/tigerbeetle/src/testing/state_machine.zig +1 -0
  39. package/src/tigerbeetle/src/unit_tests.zig +20 -22
  40. package/src/tigerbeetle/src/vsr/README.md +1 -1
  41. package/src/tigerbeetle/src/vsr/client.zig +4 -4
  42. package/src/tigerbeetle/src/vsr/clock.zig +2 -0
  43. package/src/tigerbeetle/src/vsr/journal.zig +2 -0
  44. package/src/tigerbeetle/src/vsr/replica.zig +481 -246
  45. package/src/tigerbeetle/src/vsr.zig +104 -31
@@ -100,6 +100,17 @@ pub const IO = struct {
100
100
  try self.flush_submissions(wait_nr, timeouts, etime);
101
101
  // We can now just peek for any CQEs without waiting and without another syscall:
102
102
  try self.flush_completions(0, timeouts, etime);
103
+
104
+ // The SQE array is empty from flush_submissions(). Fill it up with unqueued completions.
105
+ // This runs before `self.completed` is flushed below to prevent new IO from reserving SQE
106
+ // slots and potentially starving those in `self.unqueued`.
107
+ // Loop over a copy to avoid an infinite loop of `enqueue()` re-adding to `self.unqueued`.
108
+ {
109
+ var copy = self.unqueued;
110
+ self.unqueued = .{};
111
+ while (copy.pop()) |completion| self.enqueue(completion);
112
+ }
113
+
103
114
  // Run completions only after all completions have been flushed:
104
115
  // Loop on a copy of the linked list, having reset the list first, so that any synchronous
105
116
  // append on running a completion is executed only the next time round the event loop,
@@ -109,12 +120,10 @@ pub const IO = struct {
109
120
  self.completed = .{};
110
121
  while (copy.pop()) |completion| completion.complete();
111
122
  }
112
- // Again, loop on a copy of the list to avoid an infinite loop:
113
- {
114
- var copy = self.unqueued;
115
- self.unqueued = .{};
116
- while (copy.pop()) |completion| self.enqueue(completion);
117
- }
123
+
124
+ // At this point, unqueued could have completions either by 1) those who didn't get an SQE
125
+ // during the popping of unqueued or 2) completion.complete() which start new IO. These
126
+ // unqueued completions will get priority to acquiring SQEs on the next flush().
118
127
  }
119
128
 
120
129
  fn flush_completions(self: *IO, wait_nr: u32, timeouts: *usize, etime: *bool) !void {
@@ -67,6 +67,7 @@ pub fn ForestType(comptime Storage: type, comptime groove_config: anytype) type
67
67
  open,
68
68
  };
69
69
 
70
+ pub const groove_config = groove_config;
70
71
  pub const GroovesOptions = _GroovesOptions;
71
72
 
72
73
  join_op: ?JoinOp = null,
@@ -16,6 +16,7 @@ const Account = @import("../tigerbeetle.zig").Account;
16
16
  const Storage = @import("../testing/storage.zig").Storage;
17
17
  const StateMachine = @import("../state_machine.zig").StateMachineType(Storage, .{
18
18
  .message_body_size_max = constants.message_body_size_max,
19
+ .lsm_batch_multiple = constants.lsm_batch_multiple,
19
20
  });
20
21
 
21
22
  const GridType = @import("grid.zig").GridType;
@@ -52,8 +53,13 @@ const Environment = struct {
52
53
  .cache_entries_posted = cache_entries_max,
53
54
  });
54
55
 
55
- // Each account put can generate a put and a tombstone in each index.
56
- const puts_since_compact_max = @divTrunc(forest_options.accounts.tree_options_object.commit_entries_max, 2);
56
+ // We must call compact after every 'batch'.
57
+ // Every `lsm_batch_multiple` batches may put/remove `value_count_max` values per index.
58
+ // Every `FuzzOp.put_account` issues one remove and one put per index.
59
+ const puts_since_compact_max = @divTrunc(
60
+ Forest.groove_config.accounts_mutable.ObjectTree.Table.value_count_max,
61
+ 2 * constants.lsm_batch_multiple,
62
+ );
57
63
 
58
64
  const compacts_per_checkpoint = std.math.divCeil(
59
65
  usize,
@@ -194,23 +200,66 @@ const Environment = struct {
194
200
  }
195
201
 
196
202
  fn prefetch_account(env: *Environment, id: u128) void {
197
- const groove = &env.forest.grooves.accounts;
198
- const Groove = @TypeOf(groove.*);
203
+ const groove_immutable = &env.forest.grooves.accounts_immutable;
204
+ const groove_mutable = &env.forest.grooves.accounts_mutable;
205
+
206
+ const GrooveImmutable = @TypeOf(groove_immutable.*);
207
+ const GrooveMutable = @TypeOf(groove_mutable.*);
199
208
  const Getter = struct {
209
+ _id: u128,
210
+ _groove_mutable: *GrooveMutable,
211
+ _groove_immutable: *GrooveImmutable,
212
+
200
213
  finished: bool = false,
201
- prefetch_context: Groove.PrefetchContext = undefined,
202
- fn prefetch_callback(prefetch_context: *Groove.PrefetchContext) void {
203
- const getter = @fieldParentPtr(@This(), "prefetch_context", prefetch_context);
214
+ prefetch_context_mutable: GrooveMutable.PrefetchContext = undefined,
215
+ prefetch_context_immutable: GrooveImmutable.PrefetchContext = undefined,
216
+
217
+ fn prefetch_start(getter: *@This()) void {
218
+ const groove = getter._groove_immutable;
219
+ groove.prefetch_setup(null);
220
+ groove.prefetch_enqueue(getter._id);
221
+ groove.prefetch(@This().prefetch_callback_immuttable, &getter.prefetch_context_immutable);
222
+ }
223
+
224
+ fn prefetch_callback_immuttable(prefetch_context: *GrooveImmutable.PrefetchContext) void {
225
+ const getter = @fieldParentPtr(@This(), "prefetch_context_immutable", prefetch_context);
226
+ const groove = getter._groove_mutable;
227
+ groove.prefetch_setup(null);
228
+
229
+ if (getter._groove_immutable.get(getter._id)) |immut| {
230
+ groove.prefetch_enqueue(immut.timestamp);
231
+ }
232
+
233
+ groove.prefetch(@This().prefetch_callback_mutable, &getter.prefetch_context_mutable);
234
+ }
235
+
236
+ fn prefetch_callback_mutable(prefetch_context: *GrooveMutable.PrefetchContext) void {
237
+ const getter = @fieldParentPtr(@This(), "prefetch_context_mutable", prefetch_context);
238
+ assert(!getter.finished);
204
239
  getter.finished = true;
205
240
  }
206
241
  };
207
- var getter = Getter{};
208
- groove.prefetch_setup(null);
209
- groove.prefetch_enqueue(id);
210
- groove.prefetch(Getter.prefetch_callback, &getter.prefetch_context);
242
+
243
+ var getter = Getter{
244
+ ._id = id,
245
+ ._groove_mutable = groove_mutable,
246
+ ._groove_immutable = groove_immutable,
247
+ };
248
+ getter.prefetch_start();
211
249
  while (!getter.finished) env.storage.tick();
212
250
  }
213
251
 
252
+ fn put_account(env: *Environment, a: *const Account) void {
253
+ env.forest.grooves.accounts_immutable.put(&StateMachine.AccountImmutable.from_account(a));
254
+ env.forest.grooves.accounts_mutable.put(&StateMachine.AccountMutable.from_account(a));
255
+ }
256
+
257
+ fn get_account(env: *Environment, id: u128) ?Account {
258
+ const immut = env.forest.grooves.accounts_immutable.get(id) orelse return null;
259
+ const mut = env.forest.grooves.accounts_mutable.get(immut.timestamp).?;
260
+ return StateMachine.into_account(immut, mut);
261
+ }
262
+
214
263
  fn apply(env: *Environment, fuzz_ops: []const FuzzOp) !void {
215
264
  // The forest should behave like a simple key-value data-structure.
216
265
  // We'll compare it to a hash map.
@@ -238,13 +287,13 @@ const Environment = struct {
238
287
  .put_account => |account| {
239
288
  // The forest requires prefetch before put.
240
289
  env.prefetch_account(account.id);
241
- env.forest.grooves.accounts.put(&account);
290
+ env.put_account(&account);
242
291
  try model.put(account.id, account);
243
292
  },
244
293
  .get_account => |id| {
245
294
  // Get account from lsm.
246
295
  env.prefetch_account(id);
247
- const lsm_account = env.forest.grooves.accounts.get(id);
296
+ const lsm_account = env.get_account(id);
248
297
 
249
298
  // Compare result to model.
250
299
  const model_account = model.get(id);
@@ -254,7 +303,7 @@ const Environment = struct {
254
303
  assert(std.mem.eql(
255
304
  u8,
256
305
  std.mem.asBytes(&model_account.?),
257
- std.mem.asBytes(lsm_account.?),
306
+ std.mem.asBytes(&lsm_account.?),
258
307
  ));
259
308
  }
260
309
  },
@@ -16,8 +16,6 @@ const snapshot_latest = @import("tree.zig").snapshot_latest;
16
16
  const compaction_snapshot_for_op = @import("tree.zig").compaction_snapshot_for_op;
17
17
 
18
18
  fn ObjectTreeHelpers(comptime Object: type) type {
19
- assert(@hasField(Object, "id"));
20
- assert(std.meta.fieldInfo(Object, .id).field_type == u128);
21
19
  assert(@hasField(Object, "timestamp"));
22
20
  assert(std.meta.fieldInfo(Object, .timestamp).field_type == u64);
23
21
 
@@ -121,6 +119,7 @@ fn IndexTreeType(
121
119
  comptime Storage: type,
122
120
  comptime Field: type,
123
121
  comptime tree_name: [:0]const u8,
122
+ comptime value_count_max: usize,
124
123
  ) type {
125
124
  const Key = CompositeKey(IndexCompositeKeyType(Field));
126
125
  const Table = TableType(
@@ -131,6 +130,7 @@ fn IndexTreeType(
131
130
  Key.sentinel_key,
132
131
  Key.tombstone,
133
132
  Key.tombstone_from_key,
133
+ value_count_max,
134
134
  .secondary_index,
135
135
  );
136
136
 
@@ -147,6 +147,10 @@ pub fn GrooveType(
147
147
  comptime Object: type,
148
148
  /// An anonymous struct instance which contains the following:
149
149
  ///
150
+ /// - value_count_max: { .field = usize }:
151
+ /// An anonymous struct which contains, for each field of `Object`,
152
+ /// the maximum number of values per table for the corresponding index tree.
153
+ ///
150
154
  /// - ignored: [][]const u8:
151
155
  /// An array of fields on the Object type that should not be given index trees
152
156
  ///
@@ -157,8 +161,9 @@ pub fn GrooveType(
157
161
  ) type {
158
162
  @setEvalBranchQuota(64000);
159
163
 
160
- assert(@hasField(Object, "id"));
161
- assert(std.meta.fieldInfo(Object, .id).field_type == u128);
164
+ const has_id = @hasField(Object, "id");
165
+ if (has_id) assert(std.meta.fieldInfo(Object, .id).field_type == u128);
166
+
162
167
  assert(@hasField(Object, "timestamp"));
163
168
  assert(std.meta.fieldInfo(Object, .timestamp).field_type == u64);
164
169
 
@@ -179,7 +184,12 @@ pub fn GrooveType(
179
184
 
180
185
  if (!ignored) {
181
186
  const tree_name = @typeName(Object) ++ "." ++ field.name;
182
- const IndexTree = IndexTreeType(Storage, field.field_type, tree_name);
187
+ const IndexTree = IndexTreeType(
188
+ Storage,
189
+ field.field_type,
190
+ tree_name,
191
+ @field(groove_options.value_count_max, field.name),
192
+ );
183
193
  index_fields = index_fields ++ [_]std.builtin.TypeInfo.StructField{
184
194
  .{
185
195
  .name = field.name,
@@ -219,7 +229,12 @@ pub fn GrooveType(
219
229
  // Create an IndexTree for the DerivedType:
220
230
  const tree_name = @typeName(Object) ++ "." ++ field.name;
221
231
  const DerivedType = @typeInfo(derive_return_type).Optional.child;
222
- const IndexTree = IndexTreeType(Storage, DerivedType, tree_name);
232
+ const IndexTree = IndexTreeType(
233
+ Storage,
234
+ DerivedType,
235
+ tree_name,
236
+ @field(groove_options.value_count_max, field.name),
237
+ );
223
238
 
224
239
  index_fields = index_fields ++ &.{
225
240
  .{
@@ -246,7 +261,7 @@ pub fn GrooveType(
246
261
  };
247
262
  }
248
263
 
249
- const ObjectTree = blk: {
264
+ const _ObjectTree = blk: {
250
265
  const Table = TableType(
251
266
  u64, // key = timestamp
252
267
  Object,
@@ -255,6 +270,7 @@ pub fn GrooveType(
255
270
  ObjectTreeHelpers(Object).sentinel_key,
256
271
  ObjectTreeHelpers(Object).tombstone,
257
272
  ObjectTreeHelpers(Object).tombstone_from_key,
273
+ groove_options.value_count_max.timestamp,
258
274
  .general,
259
275
  );
260
276
 
@@ -262,7 +278,7 @@ pub fn GrooveType(
262
278
  break :blk TreeType(Table, Storage, tree_name);
263
279
  };
264
280
 
265
- const IdTree = blk: {
281
+ const _IdTree = if (!has_id) void else blk: {
266
282
  const Table = TableType(
267
283
  u128,
268
284
  IdTreeValue,
@@ -271,6 +287,7 @@ pub fn GrooveType(
271
287
  IdTreeValue.sentinel_key,
272
288
  IdTreeValue.tombstone,
273
289
  IdTreeValue.tombstone_from_key,
290
+ groove_options.value_count_max.id,
274
291
  .general,
275
292
  );
276
293
 
@@ -278,7 +295,7 @@ pub fn GrooveType(
278
295
  break :blk TreeType(Table, Storage, tree_name);
279
296
  };
280
297
 
281
- const IndexTrees = @Type(.{
298
+ const _IndexTrees = @Type(.{
282
299
  .Struct = .{
283
300
  .layout = .Auto,
284
301
  .fields = index_fields,
@@ -296,22 +313,26 @@ pub fn GrooveType(
296
313
  });
297
314
 
298
315
  // Verify no hash collisions between all the trees:
299
- comptime var hashes: []const u128 = &.{ObjectTree.hash};
316
+ comptime var hashes: []const u128 = &.{_ObjectTree.hash};
300
317
 
301
- inline for (std.meta.fields(IndexTrees)) |field| {
302
- const IndexTree = @TypeOf(@field(@as(IndexTrees, undefined), field.name));
318
+ if (has_id) {
319
+ const hash: []const u128 = &.{_IdTree.hash};
320
+ assert(std.mem.indexOf(u128, hashes, hash) == null);
321
+ hashes = hashes ++ hash;
322
+ }
323
+ inline for (std.meta.fields(_IndexTrees)) |field| {
324
+ const IndexTree = @TypeOf(@field(@as(_IndexTrees, undefined), field.name));
303
325
  const hash: []const u128 = &.{IndexTree.hash};
304
-
305
326
  assert(std.mem.indexOf(u128, hashes, hash) == null);
306
327
  hashes = hashes ++ hash;
307
328
  }
308
329
 
309
330
  // Verify groove index count:
310
- const indexes_count_actual = std.meta.fields(IndexTrees).len;
331
+ const indexes_count_actual = std.meta.fields(_IndexTrees).len;
311
332
  const indexes_count_expect = std.meta.fields(Object).len -
312
333
  groove_options.ignored.len -
313
- // The id/timestamp field is implicitly ignored since it's the primary key for ObjectTree:
314
- 2 +
334
+ // The id/timestamp fields are implicitly ignored since it's the primary key for ObjectTree:
335
+ (1 + @boolToInt(has_id)) +
315
336
  std.meta.fields(@TypeOf(groove_options.derived)).len;
316
337
 
317
338
  assert(indexes_count_actual == indexes_count_expect);
@@ -372,6 +393,10 @@ pub fn GrooveType(
372
393
  return struct {
373
394
  const Groove = @This();
374
395
 
396
+ pub const ObjectTree = _ObjectTree;
397
+ pub const IdTree = _IdTree;
398
+ pub const IndexTrees = _IndexTrees;
399
+
375
400
  const Grid = GridType(Storage);
376
401
 
377
402
  const Callback = fn (*Groove) void;
@@ -381,24 +406,26 @@ pub fn GrooveType(
381
406
  open,
382
407
  };
383
408
 
384
- const PrefetchIDs = std.AutoHashMapUnmanaged(u128, void);
409
+ const primary_field = if (has_id) "id" else "timestamp";
410
+ const PrimaryKey = @TypeOf(@field(@as(Object, undefined), primary_field));
411
+ const PrefetchIDs = std.AutoHashMapUnmanaged(PrimaryKey, void);
385
412
 
386
413
  const PrefetchObjectsContext = struct {
387
414
  pub fn hash(_: PrefetchObjectsContext, object: Object) u64 {
388
- return std.hash.Wyhash.hash(0, mem.asBytes(&object.id));
415
+ return std.hash.Wyhash.hash(0, mem.asBytes(&@field(object, primary_field)));
389
416
  }
390
417
 
391
418
  pub fn eql(_: PrefetchObjectsContext, a: Object, b: Object) bool {
392
- return a.id == b.id;
419
+ return @field(a, primary_field) == @field(b, primary_field);
393
420
  }
394
421
  };
395
422
  const PrefetchObjectsAdapter = struct {
396
- pub fn hash(_: PrefetchObjectsAdapter, id: u128) u64 {
397
- return std.hash.Wyhash.hash(0, mem.asBytes(&id));
423
+ pub fn hash(_: PrefetchObjectsAdapter, key: PrimaryKey) u64 {
424
+ return std.hash.Wyhash.hash(0, mem.asBytes(&key));
398
425
  }
399
426
 
400
- pub fn eql(_: PrefetchObjectsAdapter, a_id: u128, b_object: Object) bool {
401
- return a_id == b_object.id;
427
+ pub fn eql(_: PrefetchObjectsAdapter, a_key: PrimaryKey, b_object: Object) bool {
428
+ return a_key == @field(b_object, primary_field);
402
429
  }
403
430
  };
404
431
  const PrefetchObjects = std.HashMapUnmanaged(Object, void, PrefetchObjectsContext, 70);
@@ -431,7 +458,7 @@ pub fn GrooveType(
431
458
  prefetch_entries_max: u32,
432
459
 
433
460
  tree_options_object: ObjectTree.Options,
434
- tree_options_id: IdTree.Options,
461
+ tree_options_id: if (has_id) IdTree.Options else void,
435
462
  tree_options_index: IndexTreeOptions,
436
463
  };
437
464
 
@@ -450,13 +477,13 @@ pub fn GrooveType(
450
477
  );
451
478
  errdefer object_tree.deinit(allocator);
452
479
 
453
- var id_tree = try IdTree.init(
480
+ var id_tree = if (!has_id) {} else (try IdTree.init(
454
481
  allocator,
455
482
  node_pool,
456
483
  grid,
457
484
  options.tree_options_id,
458
- );
459
- errdefer id_tree.deinit(allocator);
485
+ ));
486
+ errdefer if (has_id) id_tree.deinit(allocator);
460
487
 
461
488
  var index_trees_initialized: usize = 0;
462
489
  var index_trees: IndexTrees = undefined;
@@ -507,7 +534,7 @@ pub fn GrooveType(
507
534
  }
508
535
 
509
536
  groove.objects.deinit(allocator);
510
- groove.ids.deinit(allocator);
537
+ if (has_id) groove.ids.deinit(allocator);
511
538
 
512
539
  groove.prefetch_ids.deinit(allocator);
513
540
  groove.prefetch_objects.deinit(allocator);
@@ -515,8 +542,8 @@ pub fn GrooveType(
515
542
  groove.* = undefined;
516
543
  }
517
544
 
518
- pub fn get(groove: *const Groove, id: u128) ?*const Object {
519
- return groove.prefetch_objects.getKeyPtrAdapted(id, PrefetchObjectsAdapter{});
545
+ pub fn get(groove: *const Groove, key: PrimaryKey) ?*const Object {
546
+ return groove.prefetch_objects.getKeyPtrAdapted(key, PrefetchObjectsAdapter{});
520
547
  }
521
548
 
522
549
  /// Must be called directly before the state machine begins queuing ids for prefetch.
@@ -526,7 +553,7 @@ pub fn GrooveType(
526
553
  // output tables until the compaction is complete. (Until then, the output tables may
527
554
  // be in the manifest but not yet on disk).
528
555
  const snapshot_max = groove.objects.lookup_snapshot_max;
529
- assert(snapshot_max == groove.ids.lookup_snapshot_max);
556
+ assert(!has_id or snapshot_max == groove.ids.lookup_snapshot_max);
530
557
 
531
558
  const snapshot_target = snapshot orelse snapshot_max;
532
559
  assert(snapshot_target <= snapshot_max);
@@ -546,8 +573,13 @@ pub fn GrooveType(
546
573
  /// This must be called by the state machine for every key to be prefetched.
547
574
  /// We tolerate duplicate IDs enqueued by the state machine.
548
575
  /// For example, if all unique operations require the same two dependencies.
549
- pub fn prefetch_enqueue(groove: *Groove, id: u128) void {
550
- if (groove.ids.lookup_from_memory(groove.prefetch_snapshot.?, id)) |id_tree_value| {
576
+ pub fn prefetch_enqueue(groove: *Groove, key: PrimaryKey) void {
577
+ if (!has_id) {
578
+ groove.prefetch_ids.putAssumeCapacity(key, {});
579
+ return;
580
+ }
581
+
582
+ if (groove.ids.lookup_from_memory(groove.prefetch_snapshot.?, key)) |id_tree_value| {
551
583
  if (id_tree_value.tombstone()) {
552
584
  // Do nothing; an explicit ID tombstone indicates that the object was deleted.
553
585
  } else {
@@ -556,16 +588,16 @@ pub fn GrooveType(
556
588
  id_tree_value.timestamp,
557
589
  )) |object| {
558
590
  assert(!ObjectTreeHelpers(Object).tombstone(object));
559
- assert(object.id == id);
591
+ assert(object.id == key);
560
592
  groove.prefetch_objects.putAssumeCapacity(object.*, {});
561
593
  } else {
562
594
  // The id was in the IdTree's value cache, but not in the ObjectTree's
563
595
  // value cache.
564
- groove.prefetch_ids.putAssumeCapacity(id, {});
596
+ groove.prefetch_ids.putAssumeCapacity(key, {});
565
597
  }
566
598
  }
567
599
  } else {
568
- groove.prefetch_ids.putAssumeCapacity(id, {});
600
+ groove.prefetch_ids.putAssumeCapacity(key, {});
569
601
  }
570
602
  }
571
603
 
@@ -642,7 +674,7 @@ pub fn GrooveType(
642
674
  // TODO(ifreund): use a union for these to save memory, likely an extern union
643
675
  // so that we can safetly @ptrCast() until @fieldParentPtr() is implemented
644
676
  // for unions. See: https://github.com/ziglang/zig/issues/6611
645
- lookup_id: IdTree.LookupContext = undefined,
677
+ lookup_id: if (has_id) IdTree.LookupContext else void = undefined,
646
678
  lookup_object: ObjectTree.LookupContext = undefined,
647
679
 
648
680
  fn lookup_start_next(worker: *PrefetchWorker) void {
@@ -651,6 +683,11 @@ pub fn GrooveType(
651
683
  return;
652
684
  };
653
685
 
686
+ if (!has_id) {
687
+ worker.lookup_with_timestamp(id.*);
688
+ return;
689
+ }
690
+
654
691
  if (worker.context.groove.ids.lookup_from_memory(
655
692
  worker.context.snapshot,
656
693
  id.*,
@@ -699,32 +736,34 @@ pub fn GrooveType(
699
736
  );
700
737
  }
701
738
 
702
- if (id_tree_value.tombstone()) {
703
- worker.lookup_start_next();
739
+ if (!id_tree_value.tombstone()) {
740
+ worker.lookup_with_timestamp(id_tree_value.timestamp);
704
741
  return;
705
742
  }
743
+ }
706
744
 
707
- if (worker.context.groove.objects.lookup_from_memory(
708
- worker.context.snapshot,
709
- id_tree_value.timestamp,
710
- )) |object| {
711
- // The object is not a tombstone; the ID and Object trees are in sync.
712
- assert(!ObjectTreeHelpers(Object).tombstone(object));
745
+ worker.lookup_start_next();
746
+ }
713
747
 
714
- worker.context.groove.prefetch_objects.putAssumeCapacityNoClobber(object.*, {});
715
- worker.lookup_start_next();
716
- return;
717
- }
748
+ fn lookup_with_timestamp(worker: *PrefetchWorker, timestamp: u64) void {
749
+ if (worker.context.groove.objects.lookup_from_memory(
750
+ worker.context.snapshot,
751
+ timestamp,
752
+ )) |object| {
753
+ // The object is not a tombstone; the ID (if any) and Object trees are in sync.
754
+ assert(!ObjectTreeHelpers(Object).tombstone(object));
718
755
 
719
- worker.context.groove.objects.lookup_from_levels(
720
- lookup_object_callback,
721
- &worker.lookup_object,
722
- worker.context.snapshot,
723
- id_tree_value.timestamp,
724
- );
725
- } else {
756
+ worker.context.groove.prefetch_objects.putAssumeCapacityNoClobber(object.*, {});
726
757
  worker.lookup_start_next();
758
+ return;
727
759
  }
760
+
761
+ worker.context.groove.objects.lookup_from_levels(
762
+ lookup_object_callback,
763
+ &worker.lookup_object,
764
+ worker.context.snapshot,
765
+ timestamp,
766
+ );
728
767
  }
729
768
 
730
769
  fn lookup_object_callback(
@@ -733,7 +772,7 @@ pub fn GrooveType(
733
772
  ) void {
734
773
  const worker = @fieldParentPtr(PrefetchWorker, "lookup_object", completion);
735
774
 
736
- // The result must be non-null as we keep the ID and Object trees in sync.
775
+ // The result must be non-null as we keep the ID (if any) and Object trees in sync.
737
776
  const object = result.?;
738
777
  assert(!ObjectTreeHelpers(Object).tombstone(object));
739
778
 
@@ -743,14 +782,21 @@ pub fn GrooveType(
743
782
  };
744
783
 
745
784
  pub fn put_no_clobber(groove: *Groove, object: *const Object) void {
746
- const gop = groove.prefetch_objects.getOrPutAssumeCapacityAdapted(object.id, PrefetchObjectsAdapter{});
785
+ const gop = groove.prefetch_objects.getOrPutAssumeCapacityAdapted(
786
+ @field(object, primary_field),
787
+ PrefetchObjectsAdapter{},
788
+ );
747
789
  assert(!gop.found_existing);
748
790
  groove.insert(object);
749
791
  gop.key_ptr.* = object.*;
750
792
  }
751
793
 
752
794
  pub fn put(groove: *Groove, object: *const Object) void {
753
- const gop = groove.prefetch_objects.getOrPutAssumeCapacityAdapted(object.id, PrefetchObjectsAdapter{});
795
+ const gop = groove.prefetch_objects.getOrPutAssumeCapacityAdapted(
796
+ @field(object, primary_field),
797
+ PrefetchObjectsAdapter{},
798
+ );
799
+
754
800
  if (gop.found_existing) {
755
801
  groove.update(gop.key_ptr, object);
756
802
  } else {
@@ -762,7 +808,7 @@ pub fn GrooveType(
762
808
  /// Insert the value into the objects tree and its fields into the index trees.
763
809
  fn insert(groove: *Groove, object: *const Object) void {
764
810
  groove.objects.put(object);
765
- groove.ids.put(&IdTreeValue{ .id = object.id, .timestamp = object.timestamp });
811
+ if (has_id) groove.ids.put(&IdTreeValue{ .id = object.id, .timestamp = object.timestamp });
766
812
 
767
813
  inline for (std.meta.fields(IndexTrees)) |field| {
768
814
  const Helper = IndexTreeFieldHelperType(field.name);
@@ -776,7 +822,7 @@ pub fn GrooveType(
776
822
 
777
823
  /// Update the object and index trees by diff'ing the old and new values.
778
824
  fn update(groove: *Groove, old: *const Object, new: *const Object) void {
779
- assert(old.id == new.id);
825
+ assert(@field(old, primary_field) == @field(new, primary_field));
780
826
  assert(old.timestamp == new.timestamp);
781
827
 
782
828
  // Update the object tree entry if any of the fields (even ignored) are different.
@@ -806,12 +852,14 @@ pub fn GrooveType(
806
852
  }
807
853
  }
808
854
 
809
- /// Asserts that the object with the given ID exists.
810
- pub fn remove(groove: *Groove, id: u128) void {
811
- const object = groove.prefetch_objects.getKeyPtrAdapted(id, PrefetchObjectsAdapter{}).?;
855
+ /// Asserts that the object with the given PrimaryKey exists.
856
+ pub fn remove(groove: *Groove, key: PrimaryKey) void {
857
+ const object = groove.prefetch_objects.getKeyPtrAdapted(key, PrefetchObjectsAdapter{}).?;
812
858
 
813
859
  groove.objects.remove(object);
814
- groove.ids.remove(&IdTreeValue{ .id = object.id, .timestamp = object.timestamp });
860
+ if (has_id) {
861
+ groove.ids.remove(&IdTreeValue{ .id = object.id, .timestamp = object.timestamp });
862
+ }
815
863
 
816
864
  inline for (std.meta.fields(IndexTrees)) |field| {
817
865
  const Helper = IndexTreeFieldHelperType(field.name);
@@ -824,11 +872,14 @@ pub fn GrooveType(
824
872
 
825
873
  // TODO(zig) Replace this with a call to removeByPtr() after upgrading to 0.10.
826
874
  // removeByPtr() replaces an unnecessary lookup here with some pointer arithmetic.
827
- assert(groove.prefetch_objects.removeAdapted(object.id, PrefetchObjectsAdapter{}));
875
+ assert(groove.prefetch_objects.removeAdapted(
876
+ @field(object, primary_field),
877
+ PrefetchObjectsAdapter{},
878
+ ));
828
879
  }
829
880
 
830
881
  /// Maximum number of pending sync callbacks (ObjectTree + IdTree + IndexTrees).
831
- const join_pending_max = 2 + std.meta.fields(IndexTrees).len;
882
+ const join_pending_max = 1 + @boolToInt(has_id) + std.meta.fields(IndexTrees).len;
832
883
 
833
884
  fn JoinType(comptime join_op: JoinOp) type {
834
885
  return struct {
@@ -897,7 +948,7 @@ pub fn GrooveType(
897
948
  const Join = JoinType(.open);
898
949
  Join.start(groove, callback);
899
950
 
900
- groove.ids.open(Join.tree_callback(.ids));
951
+ if (has_id) groove.ids.open(Join.tree_callback(.ids));
901
952
  groove.objects.open(Join.tree_callback(.objects));
902
953
 
903
954
  inline for (std.meta.fields(IndexTrees)) |field| {
@@ -912,7 +963,7 @@ pub fn GrooveType(
912
963
  Join.start(groove, callback);
913
964
 
914
965
  // Compact the ObjectTree and IdTree
915
- groove.ids.compact(Join.tree_callback(.ids), op);
966
+ if (has_id) groove.ids.compact(Join.tree_callback(.ids), op);
916
967
  groove.objects.compact(Join.tree_callback(.objects), op);
917
968
 
918
969
  // Compact the IndexTrees.
@@ -928,7 +979,7 @@ pub fn GrooveType(
928
979
  Join.start(groove, callback);
929
980
 
930
981
  // Checkpoint the IdTree and ObjectTree.
931
- groove.ids.checkpoint(Join.tree_callback(.ids));
982
+ if (has_id) groove.ids.checkpoint(Join.tree_callback(.ids));
932
983
  groove.objects.checkpoint(Join.tree_callback(.objects));
933
984
 
934
985
  // Checkpoint the IndexTrees.
@@ -948,6 +999,19 @@ test "Groove" {
948
999
  Storage,
949
1000
  Transfer,
950
1001
  .{
1002
+ // Doesn't matter for this test.
1003
+ .value_count_max = .{
1004
+ .timestamp = 1,
1005
+ .id = 1,
1006
+ .debit_account_id = 1,
1007
+ .credit_account_id = 1,
1008
+ .user_data = 1,
1009
+ .pending_id = 1,
1010
+ .timeout = 1,
1011
+ .ledger = 1,
1012
+ .code = 1,
1013
+ .amount = 1,
1014
+ },
951
1015
  .ignored = [_][]const u8{ "reserved", "user_data", "flags" },
952
1016
  .derived = .{},
953
1017
  },
@@ -38,7 +38,7 @@ pub fn LevelIteratorType(comptime Table: type, comptime Storage: type) type {
38
38
  table_iterator: TableIterator,
39
39
  };
40
40
 
41
- const ValuesRingBuffer = RingBuffer(Value, Table.data.value_count_max, .pointer);
41
+ const ValuesRingBuffer = RingBuffer(Value, Table.data.block_value_count_max, .pointer);
42
42
  const TablesRingBuffer = RingBuffer(TableIteratorScope, 2, .array);
43
43
 
44
44
  grid: *Grid,
@@ -280,7 +280,7 @@ pub fn LevelIteratorType(comptime Table: type, comptime Storage: type) type {
280
280
 
281
281
  fn buffered_enough_values(it: LevelIterator) bool {
282
282
  return it.buffered_all_values() or
283
- it.buffered_value_count() >= Table.data.value_count_max;
283
+ it.buffered_value_count() >= Table.data.block_value_count_max;
284
284
  }
285
285
 
286
286
  /// Returns either: