@remit/drizzle-service 1.0.1 → 2.0.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remit/drizzle-service",
3
- "version": "1.0.1",
3
+ "version": "2.0.0",
4
4
  "description": "Drizzle ORM service over SQLite",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
package/src/databases.ts CHANGED
@@ -4,11 +4,13 @@ import type { Db, SqliteStore } from "./db.js";
4
4
  import { messageDataSchema } from "./schema/message-data.js";
5
5
  import { createSqliteDatabase } from "./sqlite-client.js";
6
6
 
7
- // The three databases a self-host instance runs on (#1420). The app database
8
- // holds what lives in the client: accounts, config, rules, settings and auth.
9
- // The tenant mail database holds what lives on the IMAP server, plus the local
10
- // rows that refer to it. The tenant search database holds the full-text index,
11
- // rebuilt from the mail database, so it never commits together with it.
7
+ // The three databases an instance runs on (#1420), each on the file its
8
+ // SQLITE_*_DB_PATH names; a self-host points all three at one remit.db. The app
9
+ // database holds what lives in the client: accounts, config, rules, settings
10
+ // and auth. The tenant mail database holds what lives on the IMAP server, plus
11
+ // the local rows that refer to it. The tenant search database holds the
12
+ // full-text index, rebuilt from the mail database, so it never commits together
13
+ // with it.
12
14
  export interface SqliteDatabases {
13
15
  app: Db<Record<string, unknown>>;
14
16
  mail: Db<Record<string, unknown>>;
@@ -68,15 +68,18 @@ const journalEntries = (set: string): unknown[] => {
68
68
  };
69
69
 
70
70
  /**
71
- * The `<file>:<table>` pairs `read_schema_version` in the wrapper sums on the
72
- * live databases.
71
+ * The `<database>:<table>` pairs `read_schema_version` in the wrapper sums on
72
+ * the live databases, each database read from the file it is configured on.
73
73
  */
74
74
  const wrapperCountedTables = (): string[] => {
75
75
  const match = read("deploy/vps/remit").match(
76
- /for p in ((?:\w+\.db:__drizzle_migrations_\w+ ?)+); do/,
76
+ /for p in ((?:\$_sv_\w+:__drizzle_migrations_\w+ ?)+); do/,
77
77
  );
78
78
  assert.ok(match, "the wrapper no longer sums any __drizzle_migrations table");
79
- return match[1].trim().split(/\s+/);
79
+ return match[1]
80
+ .trim()
81
+ .split(/\s+/)
82
+ .map((pair) => pair.replace(/^\$_sv_/, ""));
80
83
  };
81
84
 
82
85
  describe("schema version accounting", () => {
@@ -147,7 +150,7 @@ describe("schema version accounting", () => {
147
150
  test("the wrapper counts exactly the tables the migrator writes", () => {
148
151
  assert.deepEqual(
149
152
  wrapperCountedTables().sort(),
150
- sets.map(({ database, table }) => `${database}.db:${table}`).sort(),
153
+ sets.map(({ database, table }) => `${database}:${table}`).sort(),
151
154
  );
152
155
  });
153
156
 
@@ -161,6 +161,29 @@ describe("the thread-message search index in its own database", () => {
161
161
  assert.deepEqual(await found("receipt"), [created.threadMessageId]);
162
162
  });
163
163
 
164
+ test("rebuilds while a live writer indexes a message the rebuild has yet to reach", async () => {
165
+ const created = await repo.create(input({ subject: "Raced reminder" }));
166
+ const store = syncStore(search.db, BETTER_SQLITE3_LIMITS);
167
+ let raced = false;
168
+ const migrator = new SearchIndex(mail.db, {
169
+ ...store,
170
+ batch: async (items) => {
171
+ if (!raced) {
172
+ raced = true;
173
+ await index.sync([created.threadMessageId]);
174
+ }
175
+ return store.batch(items);
176
+ },
177
+ });
178
+
179
+ await migrator.rebuild();
180
+ await migrator.rebuild();
181
+
182
+ assert.equal(raced, true);
183
+ assert.equal(await index.needsRebuild(), false);
184
+ assert.deepEqual(await found("reminder"), [created.threadMessageId]);
185
+ });
186
+
164
187
  test("matches the body text past the snippet, through a rewrite and a rebuild", async () => {
165
188
  const created = await repo.create(
166
189
  input({ subject: "Long letter", snippet: "Dear reader, a preview" }),
@@ -267,13 +267,24 @@ export class SearchIndex {
267
267
  .limit(DOCUMENTS_PER_REBUILD_PAGE);
268
268
  const last = rows.at(-1);
269
269
  if (!last) return indexed;
270
+ // A live writer may index one of these messages after the delete
271
+ // above, so each page replaces its documents the way sync does.
270
272
  await this.batch(
271
- chunkRows(threadMessageSearchTable, rows, this.maxBoundParameters).map(
272
- (slice) =>
273
- this.search
274
- .insert(threadMessageSearchTable)
275
- .values(slice.map(toDocument)),
276
- ),
273
+ chunkRows(
274
+ threadMessageSearchTable,
275
+ rows,
276
+ this.maxBoundParameters,
277
+ ).flatMap((slice) => [
278
+ this.search.delete(threadMessageSearchTable).where(
279
+ inArray(
280
+ threadMessageSearchTable.rowid,
281
+ slice.map((row) => searchRowid(row.threadMessageId)),
282
+ ),
283
+ ),
284
+ this.search
285
+ .insert(threadMessageSearchTable)
286
+ .values(slice.map(toDocument)),
287
+ ]),
277
288
  );
278
289
  indexed += rows.length;
279
290
  after = last.threadMessageId;