@remit/drizzle-service 0.0.103 → 0.0.105
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.
- package/package.json +4 -3
- package/src/bound-parameters.test.ts +57 -0
- package/src/bound-parameters.ts +49 -0
- package/src/databases.ts +10 -9
- package/src/db.ts +1 -0
- package/src/index.ts +4 -0
- package/src/repair/junk-only-address.sqlite.test.ts +9 -5
- package/src/repos/account.conformance.d1.test.ts +20 -0
- package/src/repos/account.conformance.sqlite.test.ts +9 -5
- package/src/repos/calendar-collection.conformance.d1.test.ts +20 -0
- package/src/repos/calendar-default.d1.test.ts +4 -0
- package/src/repos/calendar-default.sqlite.test.ts +3 -76
- package/src/repos/calendar-default.suite.ts +68 -0
- package/src/repos/calendar-event-index.conformance.d1.test.ts +20 -0
- package/src/repos/calendar-event-index.conformance.sqlite.test.ts +3 -1
- package/src/repos/calendar-event-index.ts +36 -21
- package/src/repos/calendar-feed-token.conformance.d1.test.ts +20 -0
- package/src/repos/calendar-object.conformance.d1.test.ts +20 -0
- package/src/repos/calendar-put.d1.test.ts +4 -0
- package/src/repos/calendar-put.sqlite.test.ts +3 -209
- package/src/repos/calendar-put.suite.ts +210 -0
- package/src/repos/calendar-store.ts +17 -10
- package/src/repos/cascade-delete.test.ts +9 -2
- package/src/repos/cascade-delete.ts +3 -2
- package/src/repos/envelope.d1.test.ts +66 -0
- package/src/repos/envelope.sqlite.test.ts +5 -1
- package/src/repos/envelope.test.ts +5 -1
- package/src/repos/envelope.ts +40 -31
- package/src/repos/filter.d1.test.ts +4 -0
- package/src/repos/filter.suite.ts +476 -0
- package/src/repos/filter.test.ts +3 -472
- package/src/repos/i4-account-config.d1.test.ts +4 -0
- package/src/repos/i4-account-config.suite.ts +131 -0
- package/src/repos/i4-account-config.test.ts +3 -127
- package/src/repos/i4-account-export-request.d1.test.ts +4 -0
- package/src/repos/i4-account-export-request.suite.ts +44 -0
- package/src/repos/i4-account-export-request.test.ts +3 -40
- package/src/repos/i4-account-setting.d1.test.ts +4 -0
- package/src/repos/i4-account-setting.suite.ts +100 -0
- package/src/repos/i4-account-setting.test.ts +3 -93
- package/src/repos/i4-account.test.ts +17 -10
- package/src/repos/i4-address-junk-move.sqlite.test.ts +9 -5
- package/src/repos/i4-mailbox-special-use.sqlite.test.ts +9 -5
- package/src/repos/i4-mailbox.sqlite.test.ts +17 -10
- package/src/repos/i4-mailbox.test.ts +17 -10
- package/src/repos/i4-outbox-attachment.test.ts +9 -5
- package/src/repos/label.conformance.d1.test.ts +20 -0
- package/src/repos/message.sqlite.test.ts +59 -1
- package/src/repos/message.test.ts +5 -1
- package/src/repos/message.ts +66 -0
- package/src/repos/sync-warning.ts +35 -22
- package/src/repos/thread-message-body-muted.sqlite.test.ts +32 -24
- package/src/repos/thread-message-field-terms.sqlite.test.ts +18 -10
- package/src/repos/thread-message.sqlite.test.ts +15 -9
- package/src/repos/thread-message.test.ts +54 -29
- package/src/repos/thread-message.ts +13 -9
- package/src/repos/thread-search-predicates.ts +36 -63
- package/src/schema/message-data.ts +3 -0
- package/src/schema-sets/mail.ts +1 -0
- package/src/search/thread-message-search.sqlite.test.ts +62 -14
- package/src/search/thread-message-search.ts +27 -21
- package/src/sqlite-repositories.ts +6 -3
- package/src/test-d1.ts +57 -0
- package/src/test-db-sqlite.ts +1 -1
- package/src/test-shipped-sqlite-schema.ts +8 -6
- package/src/test-stores.ts +43 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import { after, before, describe, test } from "node:test";
|
|
3
|
+
import { BETTER_SQLITE3_LIMITS } from "../bound-parameters.js";
|
|
3
4
|
import {
|
|
4
5
|
envelopeId as deriveEnvelopeId,
|
|
5
6
|
rootBodyPartId as deriveRootBodyPartId,
|
|
@@ -43,7 +44,10 @@ describe("DrizzleMessageRepository (sqlite)", () => {
|
|
|
43
44
|
|
|
44
45
|
before(async () => {
|
|
45
46
|
({ db, close } = await createSqliteTestDb(SCHEMA));
|
|
46
|
-
repo = createSqliteRepositories(
|
|
47
|
+
repo = createSqliteRepositories(
|
|
48
|
+
{ app: db, mail: db, search: db },
|
|
49
|
+
BETTER_SQLITE3_LIMITS,
|
|
50
|
+
).message;
|
|
47
51
|
});
|
|
48
52
|
|
|
49
53
|
after(async () => {
|
|
@@ -279,4 +283,58 @@ describe("DrizzleMessageRepository (sqlite)", () => {
|
|
|
279
283
|
await repo.delete(MESSAGE_ID);
|
|
280
284
|
await assert.rejects(() => repo.get(MESSAGE_ID));
|
|
281
285
|
});
|
|
286
|
+
|
|
287
|
+
test("listMissingSearchText names stored bodies without search text, after the cursor", async () => {
|
|
288
|
+
const id = (n: number): string => `00000000-0000-0000-3333-00000000000${n}`;
|
|
289
|
+
for (const n of [1, 2, 3, 4]) {
|
|
290
|
+
await repo.create({ ...BASE_INPUT, messageId: id(n), uid: 100 + n });
|
|
291
|
+
}
|
|
292
|
+
for (const n of [1, 2, 4]) {
|
|
293
|
+
await repo.update(id(n), { bodyStorageKey: `file:///bodies/${n}` });
|
|
294
|
+
}
|
|
295
|
+
await repo.setSearchText(id(2), "already indexed");
|
|
296
|
+
|
|
297
|
+
const ours = (rows: Array<{ messageId: string }>): string[] =>
|
|
298
|
+
rows
|
|
299
|
+
.map((row) => row.messageId)
|
|
300
|
+
.filter((messageId) => messageId.includes("-3333-"));
|
|
301
|
+
|
|
302
|
+
assert.deepEqual(ours(await repo.listMissingSearchText("", 100)), [
|
|
303
|
+
id(1),
|
|
304
|
+
id(4),
|
|
305
|
+
]);
|
|
306
|
+
assert.deepEqual(ours(await repo.listMissingSearchText(id(1), 100)), [
|
|
307
|
+
id(4),
|
|
308
|
+
]);
|
|
309
|
+
assert.deepEqual(
|
|
310
|
+
await repo
|
|
311
|
+
.listMissingSearchText(id(1), 100)
|
|
312
|
+
.then((rows) => rows.find((row) => row.messageId === id(4))),
|
|
313
|
+
{
|
|
314
|
+
messageId: id(4),
|
|
315
|
+
bodyStorageKey: "file:///bodies/4",
|
|
316
|
+
},
|
|
317
|
+
);
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
test("copySearchText gives a copy the source's search text", async () => {
|
|
321
|
+
const source = "00000000-0000-0000-4444-000000000001";
|
|
322
|
+
const copy = "00000000-0000-0000-4444-000000000002";
|
|
323
|
+
for (const [messageId, uid] of [
|
|
324
|
+
[source, 201],
|
|
325
|
+
[copy, 202],
|
|
326
|
+
] as const) {
|
|
327
|
+
await repo.create({ ...BASE_INPUT, messageId, uid });
|
|
328
|
+
await repo.update(messageId, { bodyStorageKey: "file:///bodies/shared" });
|
|
329
|
+
}
|
|
330
|
+
await repo.setSearchText(source, "the shared body");
|
|
331
|
+
|
|
332
|
+
await repo.copySearchText(source, copy);
|
|
333
|
+
|
|
334
|
+
const missing = await repo.listMissingSearchText("", 100);
|
|
335
|
+
assert.equal(
|
|
336
|
+
missing.some((row) => row.messageId === copy),
|
|
337
|
+
false,
|
|
338
|
+
);
|
|
339
|
+
});
|
|
282
340
|
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import { after, before, describe, test } from "node:test";
|
|
3
|
+
import { BETTER_SQLITE3_LIMITS } from "../bound-parameters.js";
|
|
3
4
|
import {
|
|
4
5
|
envelopeId as deriveEnvelopeId,
|
|
5
6
|
rootBodyPartId as deriveRootBodyPartId,
|
|
@@ -37,7 +38,10 @@ describe("DrizzleMessageRepository", () => {
|
|
|
37
38
|
|
|
38
39
|
before(async () => {
|
|
39
40
|
({ db, stop } = await createTestDb());
|
|
40
|
-
const repos = createSqliteRepositories(
|
|
41
|
+
const repos = createSqliteRepositories(
|
|
42
|
+
{ app: db, mail: db, search: db },
|
|
43
|
+
BETTER_SQLITE3_LIMITS,
|
|
44
|
+
);
|
|
41
45
|
messageRepo = repos.message;
|
|
42
46
|
envelopeRepo = repos.envelope;
|
|
43
47
|
});
|
package/src/repos/message.ts
CHANGED
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
envelopeTable,
|
|
40
40
|
messageFlagTable,
|
|
41
41
|
messageReferenceTable,
|
|
42
|
+
messageSearchTextTable,
|
|
42
43
|
messageTable,
|
|
43
44
|
rawMessageStorageTable,
|
|
44
45
|
} from "../schema/message-data.js";
|
|
@@ -153,6 +154,9 @@ export function deleteMessageSubtree(
|
|
|
153
154
|
.where(
|
|
154
155
|
and(inList(bodyStructureRefetchTable.messageId, messageIds), guard),
|
|
155
156
|
),
|
|
157
|
+
db
|
|
158
|
+
.delete(messageSearchTextTable)
|
|
159
|
+
.where(and(inList(messageSearchTextTable.messageId, messageIds), guard)),
|
|
156
160
|
db
|
|
157
161
|
.delete(envelopeTable)
|
|
158
162
|
.where(and(inList(envelopeTable.messageId, messageIds), guard)),
|
|
@@ -444,6 +448,68 @@ export class DrizzleMessageRepository implements IMessageRepository {
|
|
|
444
448
|
return this.get(messageId);
|
|
445
449
|
}
|
|
446
450
|
|
|
451
|
+
async setSearchText(messageId: string, text: string): Promise<void> {
|
|
452
|
+
const now = Date.now();
|
|
453
|
+
await this.db
|
|
454
|
+
.insert(messageSearchTextTable)
|
|
455
|
+
.values({ messageId, text, createdAt: now, updatedAt: now })
|
|
456
|
+
.onConflictDoUpdate({
|
|
457
|
+
target: messageSearchTextTable.messageId,
|
|
458
|
+
set: { text, updatedAt: now },
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
async copySearchText(
|
|
463
|
+
sourceMessageId: string,
|
|
464
|
+
targetMessageId: string,
|
|
465
|
+
): Promise<void> {
|
|
466
|
+
const [source] = await this.db
|
|
467
|
+
.select({ text: messageSearchTextTable.text })
|
|
468
|
+
.from(messageSearchTextTable)
|
|
469
|
+
.where(eq(messageSearchTextTable.messageId, sourceMessageId));
|
|
470
|
+
if (!source) return;
|
|
471
|
+
await this.setSearchText(targetMessageId, source.text);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
async syncSearch(messageIds: string[]): Promise<void> {
|
|
475
|
+
if (messageIds.length === 0) return;
|
|
476
|
+
const listings = await this.db
|
|
477
|
+
.select({ threadMessageId: threadMessageTable.threadMessageId })
|
|
478
|
+
.from(threadMessageTable)
|
|
479
|
+
.where(inList(threadMessageTable.messageId, messageIds));
|
|
480
|
+
await this.search.sync(listings.map((row) => row.threadMessageId));
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
async listMissingSearchText(
|
|
484
|
+
after: string,
|
|
485
|
+
limit: number,
|
|
486
|
+
): ReturnType<IMessageRepository["listMissingSearchText"]> {
|
|
487
|
+
const rows = await this.db
|
|
488
|
+
.select({
|
|
489
|
+
messageId: messageTable.messageId,
|
|
490
|
+
bodyStorageKey: messageTable.bodyStorageKey,
|
|
491
|
+
})
|
|
492
|
+
.from(messageTable)
|
|
493
|
+
.leftJoin(
|
|
494
|
+
messageSearchTextTable,
|
|
495
|
+
eq(messageSearchTextTable.messageId, messageTable.messageId),
|
|
496
|
+
)
|
|
497
|
+
.where(
|
|
498
|
+
and(
|
|
499
|
+
gt(messageTable.messageId, after),
|
|
500
|
+
isNotNull(messageTable.bodyStorageKey),
|
|
501
|
+
isNull(messageSearchTextTable.messageId),
|
|
502
|
+
),
|
|
503
|
+
)
|
|
504
|
+
.orderBy(asc(messageTable.messageId))
|
|
505
|
+
.limit(limit);
|
|
506
|
+
return rows.flatMap((row) =>
|
|
507
|
+
row.bodyStorageKey
|
|
508
|
+
? [{ messageId: row.messageId, bodyStorageKey: row.bodyStorageKey }]
|
|
509
|
+
: [],
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
|
|
447
513
|
async delete(messageId: string): Promise<void> {
|
|
448
514
|
await this.deleteMany([messageId]);
|
|
449
515
|
}
|
|
@@ -6,14 +6,24 @@ import type {
|
|
|
6
6
|
} from "@remit/data-ports";
|
|
7
7
|
import { deriveSyncWarningId } from "@remit/data-ports/id";
|
|
8
8
|
import { and, count, desc, eq, gte, sql } from "drizzle-orm";
|
|
9
|
-
import type {
|
|
9
|
+
import type { WriteBatch } from "../batch.js";
|
|
10
|
+
import { chunkRows } from "../bound-parameters.js";
|
|
11
|
+
import type { RepositoryDb, SqliteStore } from "../db.js";
|
|
10
12
|
import { syncWarningTable } from "../schema/sync-warning.js";
|
|
11
13
|
import { inList } from "./in-list.js";
|
|
12
14
|
|
|
13
15
|
type DB = RepositoryDb<Record<string, unknown>>;
|
|
14
16
|
|
|
15
17
|
export class SyncWarningRepo implements ISyncWarningRepository {
|
|
16
|
-
|
|
18
|
+
private readonly db: DB;
|
|
19
|
+
private readonly batch: WriteBatch;
|
|
20
|
+
private readonly maxBoundParameters: number;
|
|
21
|
+
|
|
22
|
+
constructor(store: SqliteStore<Record<string, unknown>>) {
|
|
23
|
+
this.db = store.db;
|
|
24
|
+
this.batch = store.batch;
|
|
25
|
+
this.maxBoundParameters = store.maxBoundParameters;
|
|
26
|
+
}
|
|
17
27
|
|
|
18
28
|
listByMailboxId = async (mailboxId: string): Promise<SyncWarningItem[]> =>
|
|
19
29
|
this.db
|
|
@@ -46,26 +56,29 @@ export class SyncWarningRepo implements ISyncWarningRepository {
|
|
|
46
56
|
recordFailures = async (keys: SyncWarningKey[]): Promise<void> => {
|
|
47
57
|
if (keys.length === 0) return;
|
|
48
58
|
const now = Date.now();
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
59
|
+
const rows = keys.map((key) => ({
|
|
60
|
+
syncWarningId: deriveSyncWarningId(
|
|
61
|
+
key.accountId,
|
|
62
|
+
key.mailboxId,
|
|
63
|
+
key.uidValidity,
|
|
64
|
+
key.uid,
|
|
65
|
+
key.stage,
|
|
66
|
+
),
|
|
67
|
+
...key,
|
|
68
|
+
attempts: 1,
|
|
69
|
+
createdAt: now,
|
|
70
|
+
}));
|
|
71
|
+
await this.batch(
|
|
72
|
+
chunkRows(syncWarningTable, rows, this.maxBoundParameters).map((slice) =>
|
|
73
|
+
this.db
|
|
74
|
+
.insert(syncWarningTable)
|
|
75
|
+
.values(slice)
|
|
76
|
+
.onConflictDoUpdate({
|
|
77
|
+
target: syncWarningTable.syncWarningId,
|
|
78
|
+
set: { attempts: sql`${syncWarningTable.attempts} + 1` },
|
|
79
|
+
}),
|
|
80
|
+
),
|
|
81
|
+
);
|
|
69
82
|
};
|
|
70
83
|
|
|
71
84
|
deleteMany = async (syncWarningIds: string[]): Promise<void> => {
|
|
@@ -10,17 +10,14 @@
|
|
|
10
10
|
import assert from "node:assert/strict";
|
|
11
11
|
import { after, before, describe, test } from "node:test";
|
|
12
12
|
import type { CreateThreadMessageInput } from "@remit/data-ports";
|
|
13
|
+
import { BETTER_SQLITE3_LIMITS } from "../bound-parameters.js";
|
|
13
14
|
import { addressTable } from "../schema/i4-address.js";
|
|
15
|
+
import { messageSearchTextTable } from "../schema/message-data.js";
|
|
14
16
|
import { threadMessageTable } from "../schema/thread-message.js";
|
|
15
17
|
import { createSqliteRepositories } from "../sqlite-repositories.js";
|
|
16
18
|
import { createSqliteTestDb } from "../test-db-sqlite.js";
|
|
17
|
-
import { shippedIndexDdl } from "../test-shipped-sqlite-schema.js";
|
|
18
19
|
import type { DrizzleThreadMessageRepository } from "./thread-message.js";
|
|
19
20
|
|
|
20
|
-
// The index the mail baseline creates for this predicate, read from the one
|
|
21
|
-
// committed source so the plan assertion below is about the real object.
|
|
22
|
-
const mutedIndexDdl = shippedIndexDdl("address_by_normalized_email");
|
|
23
|
-
|
|
24
21
|
const ACCOUNT = "acct-seam";
|
|
25
22
|
const MAILBOX = "mbx-seam";
|
|
26
23
|
const SCOPE = new Set([MAILBOX]);
|
|
@@ -57,15 +54,21 @@ describe("thread-message body and muted-sender predicates (sqlite)", () => {
|
|
|
57
54
|
|
|
58
55
|
before(async () => {
|
|
59
56
|
({ db, sqlite, close } = await createSqliteTestDb(
|
|
60
|
-
{
|
|
57
|
+
{
|
|
58
|
+
threadMessage: threadMessageTable,
|
|
59
|
+
address: addressTable,
|
|
60
|
+
messageSearchText: messageSearchTextTable,
|
|
61
|
+
},
|
|
61
62
|
{ searchIndex: true },
|
|
62
63
|
));
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
64
|
+
repo = createSqliteRepositories(
|
|
65
|
+
{
|
|
66
|
+
app: db,
|
|
67
|
+
mail: db,
|
|
68
|
+
search: db,
|
|
69
|
+
},
|
|
70
|
+
BETTER_SQLITE3_LIMITS,
|
|
71
|
+
).threadMessage;
|
|
69
72
|
});
|
|
70
73
|
|
|
71
74
|
after(async () => {
|
|
@@ -86,15 +89,20 @@ describe("thread-message body and muted-sender predicates (sqlite)", () => {
|
|
|
86
89
|
}),
|
|
87
90
|
);
|
|
88
91
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
92
|
+
const parcel = makeInput({
|
|
93
|
+
subject: "unrelated note",
|
|
94
|
+
fromEmail: "noise@example.com",
|
|
95
|
+
fromName: "Noise",
|
|
96
|
+
snippet: "Your parcel was left with the concierge",
|
|
97
|
+
sentDate: BASE_DATE,
|
|
98
|
+
});
|
|
99
|
+
await db.insert(messageSearchTextTable).values({
|
|
100
|
+
messageId: parcel.messageId,
|
|
101
|
+
text: "Your parcel was left with the concierge",
|
|
102
|
+
createdAt: 0,
|
|
103
|
+
updatedAt: 0,
|
|
104
|
+
});
|
|
105
|
+
await repo.create(parcel);
|
|
98
106
|
});
|
|
99
107
|
|
|
100
108
|
test("the term reaches the body text, not just subject and From", async () => {
|
|
@@ -123,16 +131,16 @@ describe("thread-message body and muted-sender predicates (sqlite)", () => {
|
|
|
123
131
|
assert.equal(result.items.length, 1);
|
|
124
132
|
});
|
|
125
133
|
|
|
126
|
-
test("a
|
|
134
|
+
test("a short word prefix reads the body too", async () => {
|
|
127
135
|
const result = await repo.searchByDate(
|
|
128
136
|
ACCOUNT,
|
|
129
|
-
{ query: "
|
|
137
|
+
{ query: "co" },
|
|
130
138
|
{ mailboxIds: SCOPE, limit: 50 },
|
|
131
139
|
);
|
|
132
140
|
|
|
133
141
|
assert.ok(
|
|
134
142
|
result.items.some((item) => item.snippet?.includes("concierge")),
|
|
135
|
-
"the
|
|
143
|
+
"the prefix matched the body preview",
|
|
136
144
|
);
|
|
137
145
|
});
|
|
138
146
|
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import { after, before, describe, test } from "node:test";
|
|
3
3
|
import type { CreateThreadMessageInput } from "@remit/data-ports";
|
|
4
|
+
import { BETTER_SQLITE3_LIMITS } from "../bound-parameters.js";
|
|
5
|
+
import { messageSearchTextTable } from "../schema/message-data.js";
|
|
4
6
|
import { threadMessageTable } from "../schema/thread-message.js";
|
|
5
7
|
import { createSqliteRepositories } from "../sqlite-repositories.js";
|
|
6
8
|
import { createSqliteTestDb } from "../test-db-sqlite.js";
|
|
@@ -51,15 +53,21 @@ describe("DrizzleThreadMessageRepository.listByFieldTerms (sqlite, #459)", () =>
|
|
|
51
53
|
|
|
52
54
|
before(async () => {
|
|
53
55
|
const created = await createSqliteTestDb(
|
|
54
|
-
{
|
|
56
|
+
{
|
|
57
|
+
threadMessage: threadMessageTable,
|
|
58
|
+
messageSearchText: messageSearchTextTable,
|
|
59
|
+
},
|
|
55
60
|
{ searchIndex: true },
|
|
56
61
|
);
|
|
57
62
|
close = created.close;
|
|
58
|
-
repo = createSqliteRepositories(
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
+
repo = createSqliteRepositories(
|
|
64
|
+
{
|
|
65
|
+
app: created.db,
|
|
66
|
+
mail: created.db,
|
|
67
|
+
search: created.db,
|
|
68
|
+
},
|
|
69
|
+
BETTER_SQLITE3_LIMITS,
|
|
70
|
+
).threadMessage;
|
|
63
71
|
|
|
64
72
|
await repo.create(
|
|
65
73
|
makeInput({
|
|
@@ -160,10 +168,10 @@ describe("DrizzleThreadMessageRepository.listByFieldTerms (sqlite, #459)", () =>
|
|
|
160
168
|
]);
|
|
161
169
|
});
|
|
162
170
|
|
|
163
|
-
//
|
|
164
|
-
//
|
|
165
|
-
//
|
|
166
|
-
//
|
|
171
|
+
// A field term is the folded LIKE, and sqlite's lower() folds ASCII only —
|
|
172
|
+
// `é` never matches a stored `CAFÉ`. Applying it anyway would drop a row
|
|
173
|
+
// the caller's own matcher accepts, which is #459 again for that clause
|
|
174
|
+
// shape, so a non-ASCII term narrows nothing.
|
|
167
175
|
test("drops a short accented term rather than missing the row it should match", async () => {
|
|
168
176
|
const result = await repo.listByFieldTerms(
|
|
169
177
|
ACCOUNT,
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import { after, before, describe, test } from "node:test";
|
|
3
3
|
import type { CreateThreadMessageInput } from "@remit/data-ports";
|
|
4
|
+
import { BETTER_SQLITE3_LIMITS } from "../bound-parameters.js";
|
|
5
|
+
import { messageSearchTextTable } from "../schema/message-data.js";
|
|
4
6
|
import { threadMessageTable } from "../schema/thread-message.js";
|
|
5
7
|
import { createSqliteRepositories } from "../sqlite-repositories.js";
|
|
6
8
|
import { createSqliteTestDb } from "../test-db-sqlite.js";
|
|
@@ -17,9 +19,9 @@ import {
|
|
|
17
19
|
} from "./thread-message.js";
|
|
18
20
|
|
|
19
21
|
// The thread-message repo on sqlite (RFC 036 D1): CRUD, keyset pagination, and
|
|
20
|
-
// text search — the FTS5
|
|
21
|
-
//
|
|
22
|
-
// and case-insensitive
|
|
22
|
+
// text search — the FTS5 unicode61 index (D4), matching words by prefix, and
|
|
23
|
+
// the folded-LIKE scan for terms with no word. The search contract is accent-
|
|
24
|
+
// and case-insensitive word-prefix match over subject and sender.
|
|
23
25
|
|
|
24
26
|
const ACCOUNT = "acct-thread";
|
|
25
27
|
const MAILBOX = "mbx-thread";
|
|
@@ -55,14 +57,18 @@ describe("DrizzleThreadMessageRepository (sqlite)", () => {
|
|
|
55
57
|
({ db, sqlite, close } = await createSqliteTestDb(
|
|
56
58
|
{
|
|
57
59
|
threadMessage: threadMessageTable,
|
|
60
|
+
messageSearchText: messageSearchTextTable,
|
|
58
61
|
},
|
|
59
62
|
{ searchIndex: true },
|
|
60
63
|
));
|
|
61
|
-
repo = createSqliteRepositories(
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
64
|
+
repo = createSqliteRepositories(
|
|
65
|
+
{
|
|
66
|
+
app: db,
|
|
67
|
+
mail: db,
|
|
68
|
+
search: db,
|
|
69
|
+
},
|
|
70
|
+
BETTER_SQLITE3_LIMITS,
|
|
71
|
+
).threadMessage;
|
|
66
72
|
});
|
|
67
73
|
|
|
68
74
|
after(async () => {
|
|
@@ -115,7 +121,7 @@ describe("DrizzleThreadMessageRepository (sqlite)", () => {
|
|
|
115
121
|
assert.ok(byName.items.some((i) => i.fromName === "Alice Kramer"));
|
|
116
122
|
});
|
|
117
123
|
|
|
118
|
-
test("FTS
|
|
124
|
+
test("FTS search folds diacritics both ways", async () => {
|
|
119
125
|
await repo.create(makeInput({ subject: "Réunion budget" }));
|
|
120
126
|
const folded = await repo.searchByMailbox(
|
|
121
127
|
ACCOUNT,
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import assert from "node:assert";
|
|
2
2
|
import { after, before, describe, test } from "node:test";
|
|
3
|
-
import type {
|
|
3
|
+
import type {
|
|
4
|
+
CreateThreadMessageInput,
|
|
5
|
+
IMessageRepository,
|
|
6
|
+
} from "@remit/data-ports";
|
|
4
7
|
import shortUuid from "short-uuid";
|
|
8
|
+
import { BETTER_SQLITE3_LIMITS } from "../bound-parameters.js";
|
|
9
|
+
import { messageSearchTextTable } from "../schema/message-data.js";
|
|
5
10
|
import { threadMessageTable } from "../schema/thread-message.js";
|
|
6
11
|
import { createSqliteRepositories } from "../sqlite-repositories.js";
|
|
7
12
|
import { createSqliteTestDb } from "../test-db-sqlite.js";
|
|
@@ -40,11 +45,14 @@ function makeInput(
|
|
|
40
45
|
};
|
|
41
46
|
}
|
|
42
47
|
|
|
43
|
-
// A real better-sqlite3 database with the FTS5
|
|
48
|
+
// A real better-sqlite3 database with the FTS5 objects the migrator
|
|
44
49
|
// installs, so the search predicates run their shipped path.
|
|
45
50
|
const setupDb = () =>
|
|
46
51
|
createSqliteTestDb(
|
|
47
|
-
{
|
|
52
|
+
{
|
|
53
|
+
threadMessage: threadMessageTable,
|
|
54
|
+
messageSearchText: messageSearchTextTable,
|
|
55
|
+
},
|
|
48
56
|
{ searchIndex: true },
|
|
49
57
|
);
|
|
50
58
|
|
|
@@ -87,11 +95,14 @@ describe("DrizzleThreadMessageRepository.searchByMailboxWindow / countByMailbox"
|
|
|
87
95
|
before(async () => {
|
|
88
96
|
const harness = await setupDb();
|
|
89
97
|
close = harness.close;
|
|
90
|
-
repo = createSqliteRepositories(
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
98
|
+
repo = createSqliteRepositories(
|
|
99
|
+
{
|
|
100
|
+
app: harness.db,
|
|
101
|
+
mail: harness.db,
|
|
102
|
+
search: harness.db,
|
|
103
|
+
},
|
|
104
|
+
BETTER_SQLITE3_LIMITS,
|
|
105
|
+
).threadMessage;
|
|
95
106
|
});
|
|
96
107
|
|
|
97
108
|
after(async () => {
|
|
@@ -194,7 +205,7 @@ describe("DrizzleThreadMessageRepository.searchByMailboxWindow / countByMailbox"
|
|
|
194
205
|
});
|
|
195
206
|
|
|
196
207
|
// ── Scenario 4 ────────────────────────────────────────────────────────────
|
|
197
|
-
// Matching runs over the WHOLE mailbox via the
|
|
208
|
+
// Matching runs over the WHOLE mailbox via the full-text index, so an old
|
|
198
209
|
// match well behind the recent rows is still found and paged (the DynamoDB
|
|
199
210
|
// recent-window bound does not apply here — the improvement over #443 on the
|
|
200
211
|
// DDB path).
|
|
@@ -386,17 +397,23 @@ describe("DrizzleThreadMessageRepository.searchByMailboxWindow / countByMailbox"
|
|
|
386
397
|
|
|
387
398
|
describe("DrizzleThreadMessageRepository.searchByDate", () => {
|
|
388
399
|
let repo: DrizzleThreadMessageRepository;
|
|
400
|
+
let messages: IMessageRepository;
|
|
389
401
|
let close: () => Promise<void>;
|
|
390
402
|
const cleanup: Array<() => Promise<void>> = [];
|
|
391
403
|
|
|
392
404
|
before(async () => {
|
|
393
405
|
const harness = await setupDb();
|
|
394
406
|
close = harness.close;
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
407
|
+
const repositories = createSqliteRepositories(
|
|
408
|
+
{
|
|
409
|
+
app: harness.db,
|
|
410
|
+
mail: harness.db,
|
|
411
|
+
search: harness.db,
|
|
412
|
+
},
|
|
413
|
+
BETTER_SQLITE3_LIMITS,
|
|
414
|
+
);
|
|
415
|
+
repo = repositories.threadMessage;
|
|
416
|
+
messages = repositories.message;
|
|
400
417
|
});
|
|
401
418
|
|
|
402
419
|
after(async () => {
|
|
@@ -412,9 +429,11 @@ describe("DrizzleThreadMessageRepository.searchByDate", () => {
|
|
|
412
429
|
rows: Array<Partial<CreateThreadMessageInput>>,
|
|
413
430
|
): Promise<void> {
|
|
414
431
|
for (const overrides of rows) {
|
|
415
|
-
const
|
|
416
|
-
|
|
417
|
-
|
|
432
|
+
const input = makeInput(accountConfigId, mailboxId, overrides);
|
|
433
|
+
if (overrides.snippet) {
|
|
434
|
+
await messages.setSearchText(input.messageId, overrides.snippet);
|
|
435
|
+
}
|
|
436
|
+
const created = await repo.create(input);
|
|
418
437
|
cleanup.push(() => repo.delete(accountConfigId, created.threadMessageId));
|
|
419
438
|
}
|
|
420
439
|
}
|
|
@@ -923,11 +942,14 @@ describe("DrizzleThreadMessageRepository — native text search", () => {
|
|
|
923
942
|
before(async () => {
|
|
924
943
|
const harness = await setupDb();
|
|
925
944
|
close = harness.close;
|
|
926
|
-
repo = createSqliteRepositories(
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
945
|
+
repo = createSqliteRepositories(
|
|
946
|
+
{
|
|
947
|
+
app: harness.db,
|
|
948
|
+
mail: harness.db,
|
|
949
|
+
search: harness.db,
|
|
950
|
+
},
|
|
951
|
+
BETTER_SQLITE3_LIMITS,
|
|
952
|
+
).threadMessage;
|
|
931
953
|
});
|
|
932
954
|
|
|
933
955
|
after(async () => {
|
|
@@ -1053,7 +1075,7 @@ describe("DrizzleThreadMessageRepository — native text search", () => {
|
|
|
1053
1075
|
assert.deepEqual(subjectsOf(res), ["shared token here"]);
|
|
1054
1076
|
});
|
|
1055
1077
|
|
|
1056
|
-
test("LIKE metacharacters in
|
|
1078
|
+
test("LIKE metacharacters in a query with no word match literally", async () => {
|
|
1057
1079
|
const acct = uuid();
|
|
1058
1080
|
const mbx = uuid();
|
|
1059
1081
|
await seed(acct, mbx, [
|
|
@@ -1061,7 +1083,7 @@ describe("DrizzleThreadMessageRepository — native text search", () => {
|
|
|
1061
1083
|
{ subject: "50 percent off" },
|
|
1062
1084
|
]);
|
|
1063
1085
|
|
|
1064
|
-
const res = await repo.searchByMailboxWindow(acct, mbx, { query: "
|
|
1086
|
+
const res = await repo.searchByMailboxWindow(acct, mbx, { query: "%" });
|
|
1065
1087
|
assert.deepEqual(subjectsOf(res), ["50% off today"]);
|
|
1066
1088
|
});
|
|
1067
1089
|
|
|
@@ -1099,11 +1121,14 @@ describe("DrizzleThreadMessageRepository — core CRUD", () => {
|
|
|
1099
1121
|
before(async () => {
|
|
1100
1122
|
const harness = await setupDb();
|
|
1101
1123
|
close = harness.close;
|
|
1102
|
-
repo = createSqliteRepositories(
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1124
|
+
repo = createSqliteRepositories(
|
|
1125
|
+
{
|
|
1126
|
+
app: harness.db,
|
|
1127
|
+
mail: harness.db,
|
|
1128
|
+
search: harness.db,
|
|
1129
|
+
},
|
|
1130
|
+
BETTER_SQLITE3_LIMITS,
|
|
1131
|
+
).threadMessage;
|
|
1107
1132
|
});
|
|
1108
1133
|
|
|
1109
1134
|
after(async () => {
|