@dxos/index-core 0.8.4-main.fcfe5033a5 → 0.9.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 (40) hide show
  1. package/LICENSE +102 -5
  2. package/README.md +1 -1
  3. package/dist/lib/neutral/index.mjs +190 -101
  4. package/dist/lib/neutral/index.mjs.map +4 -4
  5. package/dist/lib/neutral/meta.json +1 -1
  6. package/dist/types/src/index-engine.d.ts +31 -17
  7. package/dist/types/src/index-engine.d.ts.map +1 -1
  8. package/dist/types/src/index-tracker.d.ts +3 -3
  9. package/dist/types/src/index.d.ts +3 -3
  10. package/dist/types/src/index.d.ts.map +1 -1
  11. package/dist/types/src/indexes/entity-meta-index.d.ts +113 -0
  12. package/dist/types/src/indexes/entity-meta-index.d.ts.map +1 -0
  13. package/dist/types/src/indexes/entity-meta-index.test.d.ts +2 -0
  14. package/dist/types/src/indexes/entity-meta-index.test.d.ts.map +1 -0
  15. package/dist/types/src/indexes/fts-index.d.ts +7 -6
  16. package/dist/types/src/indexes/fts-index.d.ts.map +1 -1
  17. package/dist/types/src/indexes/index.d.ts +1 -1
  18. package/dist/types/src/indexes/interface.d.ts +15 -4
  19. package/dist/types/src/indexes/interface.d.ts.map +1 -1
  20. package/dist/types/src/indexes/reverse-ref-index.d.ts +3 -2
  21. package/dist/types/src/indexes/reverse-ref-index.d.ts.map +1 -1
  22. package/dist/types/src/utils.d.ts +3 -3
  23. package/dist/types/tsconfig.tsbuildinfo +1 -1
  24. package/package.json +13 -18
  25. package/src/index-engine.test.ts +138 -16
  26. package/src/index-engine.ts +123 -58
  27. package/src/index.ts +9 -3
  28. package/src/indexes/{object-meta-index.test.ts → entity-meta-index.test.ts} +114 -53
  29. package/src/indexes/{object-meta-index.ts → entity-meta-index.ts} +140 -71
  30. package/src/indexes/fts-index.test.ts +188 -34
  31. package/src/indexes/fts-index.ts +32 -15
  32. package/src/indexes/index.ts +1 -1
  33. package/src/indexes/interface.ts +16 -4
  34. package/src/indexes/reverse-ref-index.test.ts +57 -43
  35. package/src/indexes/reverse-ref-index.ts +22 -14
  36. package/src/utils.ts +5 -5
  37. package/dist/types/src/indexes/object-meta-index.d.ts +0 -88
  38. package/dist/types/src/indexes/object-meta-index.d.ts.map +0 -1
  39. package/dist/types/src/indexes/object-meta-index.test.d.ts +0 -2
  40. package/dist/types/src/indexes/object-meta-index.test.d.ts.map +0 -1
@@ -10,14 +10,14 @@ import * as Effect from 'effect/Effect';
10
10
  import * as Layer from 'effect/Layer';
11
11
 
12
12
  import { ATTR_TYPE } from '@dxos/echo/internal';
13
- import { DXN, ObjectId, SpaceId } from '@dxos/keys';
13
+ import { DXN, EntityId, SpaceId } from '@dxos/keys';
14
14
 
15
+ import { EntityMetaIndex } from './entity-meta-index';
15
16
  import { FtsIndex } from './fts-index';
16
17
  import type { IndexerObject } from './interface';
17
- import { ObjectMetaIndex } from './object-meta-index';
18
18
 
19
- const TYPE_PERSON = DXN.parse('dxn:type:com.example.type.person:0.1.0').toString();
20
- const TYPE_DEFAULT = DXN.parse('dxn:type:test.com/type/Type:0.1.0').toString();
19
+ const TYPE_PERSON = DXN.make('com.example.type.person', '0.1.0');
20
+ const TYPE_DEFAULT = DXN.make('com.example.type.Type', '0.1.0');
21
21
 
22
22
  const TestLayer = Layer.merge(
23
23
  SqliteClient.layer({
@@ -46,7 +46,7 @@ describe('FtsIndex', () => {
46
46
  'should insert snapshots and query them via MATCH',
47
47
  Effect.fnUntraced(function* () {
48
48
  const index = new FtsIndex();
49
- const metaIndex = new ObjectMetaIndex();
49
+ const metaIndex = new EntityMetaIndex();
50
50
  yield* index.migrate();
51
51
  yield* metaIndex.migrate();
52
52
 
@@ -55,11 +55,13 @@ describe('FtsIndex', () => {
55
55
  {
56
56
  spaceId,
57
57
  queueId: null,
58
+ queueNamespace: null,
58
59
  documentId: 'doc-1',
59
60
  recordId: null,
61
+ createdAt: null,
60
62
  updatedAt: Date.now(),
61
63
  data: {
62
- id: ObjectId.random(),
64
+ id: EntityId.random(),
63
65
  [ATTR_TYPE]: TYPE_PERSON,
64
66
  title: 'Hello Effect',
65
67
  body: 'This is a message about Effect and SQL.',
@@ -89,23 +91,25 @@ describe('FtsIndex', () => {
89
91
  'should upsert objects on update',
90
92
  Effect.fnUntraced(function* () {
91
93
  const index = new FtsIndex();
92
- const metaIndex = new ObjectMetaIndex();
94
+ const metaIndex = new EntityMetaIndex();
93
95
  yield* index.migrate();
94
96
  yield* metaIndex.migrate();
95
97
 
96
98
  const spaceId = SpaceId.random();
97
- const objectId = ObjectId.random();
99
+ const objectId = EntityId.random();
98
100
 
99
101
  // Initial insert.
100
102
  const obj1: IndexerObject = {
101
103
  spaceId,
102
104
  queueId: null,
105
+ queueNamespace: null,
103
106
  documentId: 'doc-1',
104
107
  recordId: null,
108
+ createdAt: null,
105
109
  updatedAt: Date.now(),
106
110
  data: {
107
111
  id: objectId,
108
- [ATTR_TYPE]: DXN.parse('dxn:type:com.example.type.person:0.1.0').toString(),
112
+ [ATTR_TYPE]: TYPE_PERSON,
109
113
  title: 'Original Title',
110
114
  },
111
115
  };
@@ -120,8 +124,10 @@ describe('FtsIndex', () => {
120
124
  const obj2: IndexerObject = {
121
125
  spaceId,
122
126
  queueId: null,
127
+ queueNamespace: null,
123
128
  documentId: 'doc-1',
124
129
  recordId: null,
130
+ createdAt: null,
125
131
  updatedAt: Date.now(),
126
132
  data: {
127
133
  id: objectId,
@@ -149,7 +155,7 @@ describe('FtsIndex', () => {
149
155
  'should handle non-sequential recordIds',
150
156
  Effect.fnUntraced(function* () {
151
157
  const index = new FtsIndex();
152
- const metaIndex = new ObjectMetaIndex();
158
+ const metaIndex = new EntityMetaIndex();
153
159
  yield* index.migrate();
154
160
  yield* metaIndex.migrate();
155
161
 
@@ -158,11 +164,13 @@ describe('FtsIndex', () => {
158
164
  {
159
165
  spaceId,
160
166
  queueId: null,
167
+ queueNamespace: null,
161
168
  documentId: 'doc-100',
162
169
  recordId: null,
170
+ createdAt: null,
163
171
  updatedAt: Date.now(),
164
172
  data: {
165
- id: ObjectId.random(),
173
+ id: EntityId.random(),
166
174
  [ATTR_TYPE]: TYPE_PERSON,
167
175
  title: 'Alpha Document',
168
176
  },
@@ -170,11 +178,13 @@ describe('FtsIndex', () => {
170
178
  {
171
179
  spaceId,
172
180
  queueId: null,
181
+ queueNamespace: null,
173
182
  documentId: 'doc-200',
174
183
  recordId: null,
184
+ createdAt: null,
175
185
  updatedAt: Date.now(),
176
186
  data: {
177
- id: ObjectId.random(),
187
+ id: EntityId.random(),
178
188
  [ATTR_TYPE]: TYPE_PERSON,
179
189
  title: 'Beta Document',
180
190
  },
@@ -182,11 +192,13 @@ describe('FtsIndex', () => {
182
192
  {
183
193
  spaceId,
184
194
  queueId: null,
195
+ queueNamespace: null,
185
196
  documentId: 'doc-1000',
186
197
  recordId: null,
198
+ createdAt: null,
187
199
  updatedAt: Date.now(),
188
200
  data: {
189
- id: ObjectId.random(),
201
+ id: EntityId.random(),
190
202
  [ATTR_TYPE]: TYPE_PERSON,
191
203
  title: 'Gamma Document',
192
204
  },
@@ -221,7 +233,7 @@ describe('FtsIndex', () => {
221
233
  'should query from one space only',
222
234
  Effect.fnUntraced(function* () {
223
235
  const index = new FtsIndex();
224
- const metaIndex = new ObjectMetaIndex();
236
+ const metaIndex = new EntityMetaIndex();
225
237
  yield* index.migrate();
226
238
  yield* metaIndex.migrate();
227
239
 
@@ -231,11 +243,13 @@ describe('FtsIndex', () => {
231
243
  const obj1: IndexerObject = {
232
244
  spaceId: space1,
233
245
  queueId: null,
246
+ queueNamespace: null,
234
247
  documentId: 'doc-s1',
235
248
  recordId: null,
249
+ createdAt: null,
236
250
  updatedAt: Date.now(),
237
251
  data: {
238
- id: ObjectId.random(),
252
+ id: EntityId.random(),
239
253
  [ATTR_TYPE]: TYPE_PERSON,
240
254
  title: 'Space One Content',
241
255
  },
@@ -244,11 +258,13 @@ describe('FtsIndex', () => {
244
258
  const obj2: IndexerObject = {
245
259
  spaceId: space2,
246
260
  queueId: null,
261
+ queueNamespace: null,
247
262
  documentId: 'doc-s2',
248
263
  recordId: null,
264
+ createdAt: null,
249
265
  updatedAt: Date.now(),
250
266
  data: {
251
- id: ObjectId.random(),
267
+ id: EntityId.random(),
252
268
  [ATTR_TYPE]: TYPE_PERSON,
253
269
  title: 'Space Two Content',
254
270
  },
@@ -294,7 +310,7 @@ describe('FtsIndex', () => {
294
310
  'partial word matches',
295
311
  Effect.fnUntraced(function* () {
296
312
  const index = new FtsIndex();
297
- const metaIndex = new ObjectMetaIndex();
313
+ const metaIndex = new EntityMetaIndex();
298
314
  yield* index.migrate();
299
315
  yield* metaIndex.migrate();
300
316
 
@@ -303,11 +319,13 @@ describe('FtsIndex', () => {
303
319
  {
304
320
  spaceId,
305
321
  queueId: null,
322
+ queueNamespace: null,
306
323
  documentId: 'doc-1',
307
324
  recordId: null,
325
+ createdAt: null,
308
326
  updatedAt: Date.now(),
309
327
  data: {
310
- id: ObjectId.random(),
328
+ id: EntityId.random(),
311
329
  [ATTR_TYPE]: TYPE_PERSON,
312
330
  title: 'Programming in TypeScript',
313
331
  body: 'Learn about functional programming patterns.',
@@ -316,11 +334,13 @@ describe('FtsIndex', () => {
316
334
  {
317
335
  spaceId,
318
336
  queueId: null,
337
+ queueNamespace: null,
319
338
  documentId: 'doc-2',
320
339
  recordId: null,
340
+ createdAt: null,
321
341
  updatedAt: Date.now(),
322
342
  data: {
323
- id: ObjectId.random(),
343
+ id: EntityId.random(),
324
344
  [ATTR_TYPE]: TYPE_PERSON,
325
345
  title: 'Database Design',
326
346
  body: 'Understanding program architecture.',
@@ -375,22 +395,24 @@ describe('FtsIndex', () => {
375
395
  'should query from specific queues',
376
396
  Effect.fnUntraced(function* () {
377
397
  const index = new FtsIndex();
378
- const metaIndex = new ObjectMetaIndex();
398
+ const metaIndex = new EntityMetaIndex();
379
399
  yield* index.migrate();
380
400
  yield* metaIndex.migrate();
381
401
 
382
402
  const spaceId = SpaceId.random();
383
- const queue1 = ObjectId.random();
384
- const queue2 = ObjectId.random();
403
+ const queue1 = EntityId.random();
404
+ const queue2 = EntityId.random();
385
405
 
386
406
  const spaceObj: IndexerObject = {
387
407
  spaceId,
388
408
  queueId: null,
409
+ queueNamespace: null,
389
410
  documentId: 'doc-space',
390
411
  recordId: null,
412
+ createdAt: null,
391
413
  updatedAt: Date.now(),
392
414
  data: {
393
- id: ObjectId.random(),
415
+ id: EntityId.random(),
394
416
  [ATTR_TYPE]: TYPE_PERSON,
395
417
  title: 'Space Content',
396
418
  },
@@ -399,11 +421,13 @@ describe('FtsIndex', () => {
399
421
  const queue1Obj: IndexerObject = {
400
422
  spaceId,
401
423
  queueId: queue1,
424
+ queueNamespace: 'data',
402
425
  documentId: null,
403
426
  recordId: null,
427
+ createdAt: null,
404
428
  updatedAt: Date.now(),
405
429
  data: {
406
- id: ObjectId.random(),
430
+ id: EntityId.random(),
407
431
  [ATTR_TYPE]: TYPE_PERSON,
408
432
  title: 'Queue One Content',
409
433
  },
@@ -412,11 +436,13 @@ describe('FtsIndex', () => {
412
436
  const queue2Obj: IndexerObject = {
413
437
  spaceId,
414
438
  queueId: queue2,
439
+ queueNamespace: 'data',
415
440
  documentId: null,
416
441
  recordId: null,
442
+ createdAt: null,
417
443
  updatedAt: Date.now(),
418
444
  data: {
419
- id: ObjectId.random(),
445
+ id: EntityId.random(),
420
446
  [ATTR_TYPE]: TYPE_PERSON,
421
447
  title: 'Queue Two Content',
422
448
  },
@@ -451,21 +477,23 @@ describe('FtsIndex', () => {
451
477
  'should query with includeAllQueues',
452
478
  Effect.fnUntraced(function* () {
453
479
  const index = new FtsIndex();
454
- const metaIndex = new ObjectMetaIndex();
480
+ const metaIndex = new EntityMetaIndex();
455
481
  yield* index.migrate();
456
482
  yield* metaIndex.migrate();
457
483
 
458
484
  const spaceId = SpaceId.random();
459
- const queueId = ObjectId.random();
485
+ const queueId = EntityId.random();
460
486
 
461
487
  const spaceObj: IndexerObject = {
462
488
  spaceId,
463
489
  queueId: null,
490
+ queueNamespace: null,
464
491
  documentId: 'doc-space',
465
492
  recordId: null,
493
+ createdAt: null,
466
494
  updatedAt: Date.now(),
467
495
  data: {
468
- id: ObjectId.random(),
496
+ id: EntityId.random(),
469
497
  [ATTR_TYPE]: TYPE_PERSON,
470
498
  title: 'Space Content',
471
499
  },
@@ -474,11 +502,13 @@ describe('FtsIndex', () => {
474
502
  const queueObj: IndexerObject = {
475
503
  spaceId,
476
504
  queueId,
505
+ queueNamespace: 'data',
477
506
  documentId: null,
478
507
  recordId: null,
508
+ createdAt: null,
479
509
  updatedAt: Date.now(),
480
510
  data: {
481
- id: ObjectId.random(),
511
+ id: EntityId.random(),
482
512
  [ATTR_TYPE]: TYPE_PERSON,
483
513
  title: 'Queue Content',
484
514
  },
@@ -513,22 +543,24 @@ describe('FtsIndex', () => {
513
543
  'should OR space and queue constraints',
514
544
  Effect.fnUntraced(function* () {
515
545
  const index = new FtsIndex();
516
- const metaIndex = new ObjectMetaIndex();
546
+ const metaIndex = new EntityMetaIndex();
517
547
  yield* index.migrate();
518
548
  yield* metaIndex.migrate();
519
549
 
520
550
  const space1 = SpaceId.random();
521
551
  const space2 = SpaceId.random();
522
- const queueInSpace2 = ObjectId.random();
552
+ const queueInSpace2 = EntityId.random();
523
553
 
524
554
  const space1Obj: IndexerObject = {
525
555
  spaceId: space1,
526
556
  queueId: null,
557
+ queueNamespace: null,
527
558
  documentId: 'doc-s1',
528
559
  recordId: null,
560
+ createdAt: null,
529
561
  updatedAt: Date.now(),
530
562
  data: {
531
- id: ObjectId.random(),
563
+ id: EntityId.random(),
532
564
  [ATTR_TYPE]: TYPE_PERSON,
533
565
  title: 'Space One Content',
534
566
  },
@@ -537,11 +569,13 @@ describe('FtsIndex', () => {
537
569
  const space2Obj: IndexerObject = {
538
570
  spaceId: space2,
539
571
  queueId: null,
572
+ queueNamespace: null,
540
573
  documentId: 'doc-s2',
541
574
  recordId: null,
575
+ createdAt: null,
542
576
  updatedAt: Date.now(),
543
577
  data: {
544
- id: ObjectId.random(),
578
+ id: EntityId.random(),
545
579
  [ATTR_TYPE]: TYPE_PERSON,
546
580
  title: 'Space Two Content',
547
581
  },
@@ -550,11 +584,13 @@ describe('FtsIndex', () => {
550
584
  const queueObj: IndexerObject = {
551
585
  spaceId: space2,
552
586
  queueId: queueInSpace2,
587
+ queueNamespace: 'data',
553
588
  documentId: null,
554
589
  recordId: null,
590
+ createdAt: null,
555
591
  updatedAt: Date.now(),
556
592
  data: {
557
- id: ObjectId.random(),
593
+ id: EntityId.random(),
558
594
  [ATTR_TYPE]: TYPE_PERSON,
559
595
  title: 'Queue Content',
560
596
  },
@@ -579,4 +615,122 @@ describe('FtsIndex', () => {
579
615
  expect(objectIds).not.toContain(space2Obj.data.id);
580
616
  }, Effect.provide(TestLayer)),
581
617
  );
618
+
619
+ describe('querySnapshotsJSON', () => {
620
+ it.effect(
621
+ 'returns snapshots for all present recordIds',
622
+ Effect.fnUntraced(function* () {
623
+ const index = new FtsIndex();
624
+ const metaIndex = new EntityMetaIndex();
625
+ yield* index.migrate();
626
+ yield* metaIndex.migrate();
627
+
628
+ const spaceId = SpaceId.random();
629
+ const objects: IndexerObject[] = [
630
+ {
631
+ spaceId,
632
+ queueId: EntityId.random(),
633
+ queueNamespace: 'data',
634
+ documentId: null,
635
+ recordId: null,
636
+ createdAt: null,
637
+ updatedAt: Date.now(),
638
+ data: { id: EntityId.random(), [ATTR_TYPE]: TYPE_PERSON, value: 'alpha' },
639
+ },
640
+ {
641
+ spaceId,
642
+ queueId: EntityId.random(),
643
+ queueNamespace: 'data',
644
+ documentId: null,
645
+ recordId: null,
646
+ createdAt: null,
647
+ updatedAt: Date.now(),
648
+ data: { id: EntityId.random(), [ATTR_TYPE]: TYPE_PERSON, value: 'beta' },
649
+ },
650
+ ];
651
+
652
+ yield* metaIndex.update(objects);
653
+ yield* metaIndex.lookupRecordIds(objects);
654
+ yield* index.update(objects);
655
+
656
+ const recordIds = objects.map((o) => o.recordId!);
657
+ const snapshots = yield* index.querySnapshotsJSON(recordIds);
658
+
659
+ expect(snapshots).toHaveLength(2);
660
+ const snapshotMap = new Map(snapshots.map((s) => [s.recordId, s.snapshot]));
661
+ expect((snapshotMap.get(objects[0].recordId!) as any).value).toBe('alpha');
662
+ expect((snapshotMap.get(objects[1].recordId!) as any).value).toBe('beta');
663
+ }, Effect.provide(TestLayer)),
664
+ );
665
+
666
+ it.effect(
667
+ 'omits stale recordIds not present in FTS index',
668
+ Effect.fnUntraced(function* () {
669
+ const index = new FtsIndex();
670
+ const metaIndex = new EntityMetaIndex();
671
+ yield* index.migrate();
672
+ yield* metaIndex.migrate();
673
+
674
+ const spaceId = SpaceId.random();
675
+ const object: IndexerObject = {
676
+ spaceId,
677
+ queueId: EntityId.random(),
678
+ queueNamespace: 'data',
679
+ documentId: null,
680
+ recordId: null,
681
+ createdAt: null,
682
+ updatedAt: Date.now(),
683
+ data: { id: EntityId.random(), [ATTR_TYPE]: TYPE_PERSON, value: 'present' },
684
+ };
685
+
686
+ yield* metaIndex.update([object]);
687
+ yield* metaIndex.lookupRecordIds([object]);
688
+ yield* index.update([object]);
689
+
690
+ // Query with the real id plus a stale/non-existent id.
691
+ const staleId = 99999;
692
+ const snapshots = yield* index.querySnapshotsJSON([object.recordId!, staleId]);
693
+
694
+ expect(snapshots).toHaveLength(1);
695
+ expect(snapshots[0].recordId).toBe(object.recordId!);
696
+ expect((snapshots[0].snapshot as any).value).toBe('present');
697
+ }, Effect.provide(TestLayer)),
698
+ );
699
+
700
+ it.effect(
701
+ 'handles more than 999 recordIds without exceeding SQLite variable limit',
702
+ Effect.fnUntraced(function* () {
703
+ const index = new FtsIndex();
704
+ const metaIndex = new EntityMetaIndex();
705
+ yield* index.migrate();
706
+ yield* metaIndex.migrate();
707
+
708
+ const spaceId = SpaceId.random();
709
+ const count = 1100;
710
+ const objects: IndexerObject[] = Array.from({ length: count }, (_, i) => ({
711
+ spaceId,
712
+ queueId: EntityId.random(),
713
+ queueNamespace: 'data',
714
+ documentId: null,
715
+ recordId: null,
716
+ createdAt: null,
717
+ updatedAt: Date.now(),
718
+ data: { id: EntityId.random(), [ATTR_TYPE]: TYPE_PERSON, index: i },
719
+ }));
720
+
721
+ yield* metaIndex.update(objects);
722
+ yield* metaIndex.lookupRecordIds(objects);
723
+ yield* index.update(objects);
724
+
725
+ const recordIds = objects.map((o) => o.recordId!);
726
+ const snapshots = yield* index.querySnapshotsJSON(recordIds);
727
+
728
+ expect(snapshots).toHaveLength(count);
729
+ const returnedIds = new Set(snapshots.map((s) => s.recordId));
730
+ for (const id of recordIds) {
731
+ expect(returnedIds.has(id)).toBe(true);
732
+ }
733
+ }, Effect.provide(TestLayer)),
734
+ );
735
+ });
582
736
  });
@@ -8,10 +8,14 @@ import type * as Statement from '@effect/sql/Statement';
8
8
  import * as Effect from 'effect/Effect';
9
9
 
10
10
  import type { Obj } from '@dxos/echo';
11
- import type { ObjectId, SpaceId } from '@dxos/keys';
11
+ import type { EntityId, SpaceId } from '@dxos/keys';
12
12
 
13
+ import type { EntityMeta } from './entity-meta-index';
13
14
  import type { Index, IndexerObject } from './interface';
14
- import type { ObjectMeta } from './object-meta-index';
15
+
16
+ // SQLite bound-variable limit (SQLITE_LIMIT_VARIABLE_NUMBER) is 999 in most builds.
17
+ // Use 500 as a safe chunk size for IN (...) clauses.
18
+ const SQL_CHUNK_SIZE = 500;
15
19
 
16
20
  /**
17
21
  * The space and queue constrains are combined together using a logical OR.
@@ -35,13 +39,13 @@ export interface FtsQuery {
35
39
  /**
36
40
  * Queue IDs to search within.
37
41
  */
38
- queueIds: readonly ObjectId[] | null;
42
+ queueIds: readonly EntityId[] | null;
39
43
  }
40
44
 
41
45
  /**
42
46
  * Result of FTS query including the indexed snapshot data.
43
47
  */
44
- export interface FtsResult extends ObjectMeta {
48
+ export interface FtsResult extends EntityMeta {
45
49
  /**
46
50
  * The indexed snapshot data (JSON string).
47
51
  * Used to load queue objects without going through document loading.
@@ -52,7 +56,7 @@ export interface FtsResult extends ObjectMeta {
52
56
  /**
53
57
  * Result of FTS query with rank.
54
58
  */
55
- export interface FtsQueryResult extends ObjectMeta {
59
+ export interface FtsQueryResult extends EntityMeta {
56
60
  /**
57
61
  * Relevance rank from FTS5.
58
62
  * Higher values indicate better matches.
@@ -157,7 +161,7 @@ export class FtsIndex implements Index {
157
161
  // BM25 returns negative values, negate to get higher = better match.
158
162
  // Order by rank descending so best matches come first.
159
163
  // Note: bm25() requires the actual table name, not an alias.
160
- const rows = yield* sql<ObjectMeta & { rank: number }>`
164
+ const rows = yield* sql<EntityMeta & { rank: number }>`
161
165
  SELECT m.*, -bm25(ftsIndex) AS rank
162
166
  FROM ftsIndex AS f
163
167
  JOIN objectMeta AS m ON f.rowid = m.recordId
@@ -167,7 +171,7 @@ export class FtsIndex implements Index {
167
171
  return rows;
168
172
  } else {
169
173
  // LIKE fallback - no ranking available, default to 1.
170
- const rows = yield* sql<ObjectMeta>`
174
+ const rows = yield* sql<EntityMeta>`
171
175
  SELECT m.*
172
176
  FROM ftsIndex AS f
173
177
  JOIN objectMeta AS m ON f.rowid = m.recordId
@@ -181,6 +185,7 @@ export class FtsIndex implements Index {
181
185
  /**
182
186
  * Query snapshots by recordIds.
183
187
  * Returns the parsed JSON snapshots for queue objects.
188
+ * RecordIds not present in the FTS index are silently omitted from the result.
184
189
  */
185
190
  querySnapshotsJSON(
186
191
  recordIds: number[],
@@ -190,14 +195,26 @@ export class FtsIndex implements Index {
190
195
  return [];
191
196
  }
192
197
  const sql = yield* SqlClient.SqlClient;
193
- const results = yield* sql<{
194
- rowid: number;
195
- snapshot: string;
196
- }>`SELECT rowid, snapshot FROM ftsIndex WHERE rowid IN ${sql.in(recordIds)}`;
197
- return results.map((r) => ({
198
- recordId: r.rowid,
199
- snapshot: JSON.parse(r.snapshot),
200
- }));
198
+
199
+ // Chunk to avoid SQLite bound-variable limit (SQLITE_LIMIT_VARIABLE_NUMBER,
200
+ // typically 999 in wasm builds). 500 gives a safe margin.
201
+ const chunks: number[][] = [];
202
+ for (let i = 0; i < recordIds.length; i += SQL_CHUNK_SIZE) {
203
+ chunks.push(recordIds.slice(i, i + SQL_CHUNK_SIZE));
204
+ }
205
+
206
+ const allResults: { recordId: number; snapshot: Obj.JSON }[] = [];
207
+ for (const chunk of chunks) {
208
+ const rows = yield* sql<{
209
+ rowid: number;
210
+ snapshot: string;
211
+ }>`SELECT rowid, snapshot FROM ftsIndex WHERE rowid IN ${sql.in(chunk)}`;
212
+ for (const r of rows) {
213
+ allResults.push({ recordId: r.rowid, snapshot: JSON.parse(r.snapshot) });
214
+ }
215
+ }
216
+
217
+ return allResults;
201
218
  });
202
219
  }
203
220
 
@@ -3,6 +3,6 @@
3
3
  //
4
4
 
5
5
  export * from './fts-index';
6
- export * from './object-meta-index';
6
+ export * from './entity-meta-index';
7
7
  export * from './reverse-ref-index';
8
8
  export * from './interface';
@@ -7,7 +7,7 @@ import type * as SqlError from '@effect/sql/SqlError';
7
7
  import type * as Effect from 'effect/Effect';
8
8
 
9
9
  import type { Obj } from '@dxos/echo';
10
- import type { ObjectId, SpaceId } from '@dxos/keys';
10
+ import type { EntityId, SpaceId } from '@dxos/keys';
11
11
 
12
12
  /**
13
13
  * Data describing objects returned from sources to the indexer.
@@ -18,7 +18,12 @@ export interface IndexerObject {
18
18
  * Queue id if object is from the queue.
19
19
  * If null, `documentId` must be set.
20
20
  */
21
- queueId: ObjectId | null;
21
+ queueId: EntityId | null;
22
+ /**
23
+ * Queue subspace namespace (e.g. 'data', 'trace') the object lives in.
24
+ * Set together with `queueId`; null for non-queue objects.
25
+ */
26
+ queueNamespace: string | null;
22
27
  /**
23
28
  * Document id if object is from the automerge document.
24
29
  * If null, `queueId` must be set.
@@ -27,8 +32,8 @@ export interface IndexerObject {
27
32
 
28
33
  /**
29
34
  * Record id from the objectMeta index.
30
- * `Null` before the object is stored in the ObjectMetaIndex.
31
- * Enriched by the IndexEngine after the object is stored in the ObjectMetaIndex.
35
+ * `Null` before the object is stored in the EntityMetaIndex.
36
+ * Enriched by the IndexEngine after the object is stored in the EntityMetaIndex.
32
37
  */
33
38
  recordId: number | null;
34
39
 
@@ -37,6 +42,13 @@ export interface IndexerObject {
37
42
  */
38
43
  data: Obj.JSON;
39
44
 
45
+ /**
46
+ * Unix ms timestamp when this object was first created.
47
+ * Sourced from system.createdAt in the automerge document; null for legacy objects
48
+ * created before this field was introduced.
49
+ */
50
+ createdAt: number | null;
51
+
40
52
  /**
41
53
  * Timestamp of the last update of the object.
42
54
  */