@remit/drizzle-service 0.0.35 → 0.0.37
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 +3 -10
- package/src/db.ts +6 -6
- package/src/error.ts +2 -5
- package/src/index.ts +0 -1
- package/src/repair/thread-message-category-contract.ts +2 -4
- package/src/repair/thread-message-category.sqlite.test.ts +1 -2
- package/src/repair/thread-message-category.ts +15 -25
- package/src/repos/cascade-delete.ts +8 -22
- package/src/repos/category-fixture.ts +2 -19
- package/src/repos/filter-anchor.ts +4 -4
- package/src/repos/filter.ts +3 -3
- package/src/repos/i4-account-config.ts +2 -2
- package/src/repos/i4-account-export-request.ts +2 -2
- package/src/repos/i4-account-setting.ts +2 -2
- package/src/repos/i4-account.ts +2 -2
- package/src/repos/i4-mailbox-lock.ts +2 -2
- package/src/repos/i4-mailbox-special-use.ts +2 -2
- package/src/repos/i4-mailbox.ts +2 -2
- package/src/repos/i4-message-flag-push.test.ts +1 -1
- package/src/repos/i4-message-flag-push.ts +3 -3
- package/src/repos/i4-message-placement-move.test.ts +1 -1
- package/src/repos/i4-message-placement-move.ts +4 -3
- package/src/repos/i4-organize-job-request.ts +2 -2
- package/src/repos/i4-outbox-message.ts +2 -2
- package/src/repos/label.ts +2 -2
- package/src/repos/message-flag.ts +2 -2
- package/src/repos/message-label.ts +2 -2
- package/src/repos/message.ts +8 -10
- package/src/repos/quarantine.ts +2 -2
- package/src/repos/test-helpers.ts +11 -213
- package/src/repos/thread-message.sqlite.test.ts +35 -10
- package/src/repos/thread-message.test.ts +45 -105
- package/src/repos/thread-message.ts +11 -25
- package/src/repos/thread-search-predicates.ts +13 -41
- package/src/repos/unit-of-work.ts +1 -1
- package/src/schema/i4-account-config.ts +1 -1
- package/src/schema/i4-account-export-request.ts +1 -1
- package/src/schema/i4-account-setting.ts +1 -1
- package/src/schema/i4-address.ts +1 -1
- package/src/schema/i4-mailbox-lock.ts +1 -1
- package/src/schema/i4-mailbox.ts +1 -1
- package/src/schema/i4-message-flag-push.ts +1 -1
- package/src/schema/i4-message-placement-move.ts +1 -1
- package/src/schema/i4-organize-job-request.ts +1 -1
- package/src/schema/i4-outbox-message.ts +1 -1
- package/src/schema/message-data.ts +1 -1
- package/src/schema/outbox.ts +13 -56
- package/src/schema/quarantine.ts +1 -1
- package/src/schema/thread-message.ts +1 -1
- package/src/schema-full-sqlite.ts +8 -6
- package/src/schema.ts +4 -4
- package/src/sqlite-client.ts +4 -5
- package/src/test-db-sqlite.ts +4 -9
- package/src/test-db.ts +11 -66
- package/src/tx.ts +6 -12
- package/drizzle.config.ts +0 -15
- package/src/dialect.ts +0 -13
- package/src/repair/thread-message-category.test.ts +0 -229
- package/src/repos/thread-message-category.test.ts +0 -156
- package/src/schema/active-entities.ts +0 -19
- package/src/schema-full.ts +0 -16
|
@@ -1,229 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { after, before, describe, test } from "node:test";
|
|
3
|
-
import { createTestDb } from "../test-db.js";
|
|
4
|
-
import {
|
|
5
|
-
type RepairSqlClient,
|
|
6
|
-
repairStatement,
|
|
7
|
-
repairThreadMessageCategory,
|
|
8
|
-
} from "./thread-message-category.js";
|
|
9
|
-
import { describeRepairContract } from "./thread-message-category-contract.js";
|
|
10
|
-
|
|
11
|
-
const { pool, close } = await createTestDb();
|
|
12
|
-
|
|
13
|
-
after(async () => {
|
|
14
|
-
await close();
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
const client: RepairSqlClient = {
|
|
18
|
-
dialect: "postgres",
|
|
19
|
-
all: async (sql) => (await pool.query(sql)).rows,
|
|
20
|
-
run: async (sql) => (await pool.query(sql)).rowCount ?? 0,
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
const truncate = async (): Promise<void> => {
|
|
24
|
-
await pool.query("DELETE FROM thread_message");
|
|
25
|
-
await pool.query("DELETE FROM message");
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
describeRepairContract("postgres", async () => ({
|
|
29
|
-
client,
|
|
30
|
-
reset: truncate,
|
|
31
|
-
close: async () => undefined,
|
|
32
|
-
}));
|
|
33
|
-
|
|
34
|
-
// Postgres is where a mid-sync run can go wrong, and the two interleavings have
|
|
35
|
-
// opposite outcomes — so each is driven with two real connections rather than
|
|
36
|
-
// argued about. Under READ COMMITTED an UPDATE that meets a row a concurrent
|
|
37
|
-
// transaction just changed re-evaluates its WHERE clause against the new version
|
|
38
|
-
// of that row, but keeps reading other tables at its original snapshot.
|
|
39
|
-
//
|
|
40
|
-
// Both cases need a *re*-classification to be observable at all: `message` moving
|
|
41
|
-
// from one decided category to another. `backfillClassification` cannot do that
|
|
42
|
-
// (it returns early once the category is decided), but the primary path is
|
|
43
|
-
// re-enterable — `if (message.bodyStorageKey && !force)`, where `force` is the
|
|
44
|
-
// read-miss re-arm cue — and a forced re-fetch decides differently across a
|
|
45
|
-
// release that changed the classifier. So the uncovered case below is
|
|
46
|
-
// effectively unreachable rather than impossible. The seeds are synthetic
|
|
47
|
-
// because reproducing it otherwise would mean changing the classifier.
|
|
48
|
-
describe("thread_message.category repair — a writer racing the statement", () => {
|
|
49
|
-
const insertMessage = async (
|
|
50
|
-
messageId: string,
|
|
51
|
-
category: string,
|
|
52
|
-
): Promise<void> => {
|
|
53
|
-
await pool.query(
|
|
54
|
-
`INSERT INTO message (
|
|
55
|
-
message_id, mailbox_id, uid, sequence_number, rfc822_size, internal_date,
|
|
56
|
-
envelope_id, root_body_part_id, category, created_at, updated_at
|
|
57
|
-
) VALUES ($1, 'mbx-inbox', 1, 1, 100, 0, 'env-1', 'part-1', $2, 0, 0)`,
|
|
58
|
-
[messageId, category],
|
|
59
|
-
);
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
const insertRow = async (
|
|
63
|
-
messageId: string,
|
|
64
|
-
category: string,
|
|
65
|
-
): Promise<void> => {
|
|
66
|
-
await pool.query(
|
|
67
|
-
`INSERT INTO thread_message (
|
|
68
|
-
thread_message_id, thread_id, message_id, account_config_id, mailbox_id,
|
|
69
|
-
uid, reference_order, internal_date, sent_date, is_read, has_attachment,
|
|
70
|
-
has_stars, is_deleted, category, created_at, updated_at
|
|
71
|
-
) VALUES ($1, 'thr-1', $1, 'acct-1', 'mbx-inbox', 1, 0, 0, 0,
|
|
72
|
-
false, false, false, false, $2, 0, 1000)`,
|
|
73
|
-
[messageId, category],
|
|
74
|
-
);
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
// A body-sync denormalize, faithful to the shipped order (#326): the row
|
|
78
|
-
// first, the message second, both stamped with the wall clock at write time.
|
|
79
|
-
const reclassify = async (
|
|
80
|
-
connection: {
|
|
81
|
-
query: (sql: string, values?: unknown[]) => Promise<unknown>;
|
|
82
|
-
},
|
|
83
|
-
messageId: string,
|
|
84
|
-
category: string,
|
|
85
|
-
): Promise<void> => {
|
|
86
|
-
await connection.query(
|
|
87
|
-
"UPDATE thread_message SET category = $2, updated_at = $3 WHERE thread_message_id = $1",
|
|
88
|
-
[messageId, category, Date.now()],
|
|
89
|
-
);
|
|
90
|
-
await connection.query(
|
|
91
|
-
"UPDATE message SET category = $2, updated_at = $3 WHERE message_id = $1",
|
|
92
|
-
[messageId, category, Date.now()],
|
|
93
|
-
);
|
|
94
|
-
};
|
|
95
|
-
|
|
96
|
-
const categories = async (): Promise<Record<string, string>> => {
|
|
97
|
-
const { rows } = await pool.query<{
|
|
98
|
-
thread_message_id: string;
|
|
99
|
-
category: string;
|
|
100
|
-
}>("SELECT thread_message_id, category FROM thread_message");
|
|
101
|
-
return Object.fromEntries(
|
|
102
|
-
rows.map((row) => [row.thread_message_id, row.category]),
|
|
103
|
-
);
|
|
104
|
-
};
|
|
105
|
-
|
|
106
|
-
before(truncate);
|
|
107
|
-
|
|
108
|
-
// The case the guard covers: the writer's transaction begins after the
|
|
109
|
-
// statement, so its stamp is beyond the statement's clock and the re-check
|
|
110
|
-
// against the committed row excludes it. The repair is held on an unrelated
|
|
111
|
-
// row lock for the duration, which is what puts the writer inside the
|
|
112
|
-
// statement's window without touching its stamp.
|
|
113
|
-
test("a writer that begins after the statement keeps its value", async () => {
|
|
114
|
-
await truncate();
|
|
115
|
-
await insertMessage("aaa-blocker", "newsletter");
|
|
116
|
-
await insertRow("aaa-blocker", "uncategorized");
|
|
117
|
-
await insertMessage("bbb-raced", "newsletter");
|
|
118
|
-
await insertRow("bbb-raced", "uncategorized");
|
|
119
|
-
|
|
120
|
-
const holder = await pool.connect();
|
|
121
|
-
const writer = await pool.connect();
|
|
122
|
-
try {
|
|
123
|
-
await holder.query("BEGIN");
|
|
124
|
-
await holder.query(
|
|
125
|
-
"UPDATE thread_message SET updated_at = 1001 WHERE thread_message_id = 'aaa-blocker'",
|
|
126
|
-
);
|
|
127
|
-
|
|
128
|
-
const repairing = repairThreadMessageCategory(client);
|
|
129
|
-
// The repair reaches the blocker row — inserted first, so first in the
|
|
130
|
-
// sequential scan — and waits there. The sleep is on the holder's own
|
|
131
|
-
// connection, so nothing polls.
|
|
132
|
-
await holder.query("SELECT pg_sleep(0.5)");
|
|
133
|
-
|
|
134
|
-
await writer.query("BEGIN");
|
|
135
|
-
await reclassify(writer, "bbb-raced", "marketing");
|
|
136
|
-
await writer.query("COMMIT");
|
|
137
|
-
|
|
138
|
-
await holder.query("COMMIT");
|
|
139
|
-
|
|
140
|
-
// Only the blocker. A 2 here means the repair finished before the writer
|
|
141
|
-
// began, so the interleaving this test is named for did not happen.
|
|
142
|
-
assert.equal((await repairing).rowsWritten, 1);
|
|
143
|
-
} finally {
|
|
144
|
-
holder.release();
|
|
145
|
-
writer.release();
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
assert.deepEqual(await categories(), {
|
|
149
|
-
"aaa-blocker": "newsletter",
|
|
150
|
-
"bbb-raced": "marketing",
|
|
151
|
-
});
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
// The live mid-sync case, and it is safe: a writer whose transaction began
|
|
155
|
-
// before the statement, doing what body-sync actually does — filling in the
|
|
156
|
-
// copy of a category the message already holds. The repair blocks on the row,
|
|
157
|
-
// re-checks against the committed version, finds it now agrees, and writes
|
|
158
|
-
// nothing. This is the interleaving D16's safety argument rests on.
|
|
159
|
-
test("a concurrent denormalize of the same row leaves it correct", async () => {
|
|
160
|
-
await truncate();
|
|
161
|
-
await insertMessage("ddd-catchup", "newsletter");
|
|
162
|
-
await insertRow("ddd-catchup", "uncategorized");
|
|
163
|
-
|
|
164
|
-
const writer = await pool.connect();
|
|
165
|
-
try {
|
|
166
|
-
await writer.query("BEGIN");
|
|
167
|
-
await writer.query(
|
|
168
|
-
"UPDATE thread_message SET category = 'newsletter', updated_at = $1 WHERE thread_message_id = 'ddd-catchup'",
|
|
169
|
-
[Date.now()],
|
|
170
|
-
);
|
|
171
|
-
|
|
172
|
-
const repairing = repairThreadMessageCategory(client);
|
|
173
|
-
await writer.query("SELECT pg_sleep(0.5)");
|
|
174
|
-
await writer.query("COMMIT");
|
|
175
|
-
|
|
176
|
-
assert.equal((await repairing).rowsWritten, 0);
|
|
177
|
-
} finally {
|
|
178
|
-
writer.release();
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
assert.deepEqual(await categories(), { "ddd-catchup": "newsletter" });
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
// The known limit, pinned so it cannot silently widen. Reaching it needs four
|
|
185
|
-
// things at once: an unrepaired `behind` row, a writer whose transaction began
|
|
186
|
-
// before the statement, that writer's row write committing before the statement
|
|
187
|
-
// while its message write commits after, and that writer moving
|
|
188
|
-
// message.category from one decided value to a different one. Only a forced
|
|
189
|
-
// re-fetch across a classifier change does the fourth, which makes this
|
|
190
|
-
// effectively unreachable rather than impossible; the seed is synthetic and the
|
|
191
|
-
// assertion records the loss rather than endorsing it. If this test ever fails,
|
|
192
|
-
// the guard got stronger and D16 and the module header should say so.
|
|
193
|
-
test("a writer that began before the statement is outside the guard", async () => {
|
|
194
|
-
await truncate();
|
|
195
|
-
await insertMessage("ccc-reclass", "newsletter");
|
|
196
|
-
await insertRow("ccc-reclass", "uncategorized");
|
|
197
|
-
|
|
198
|
-
const writer = await pool.connect();
|
|
199
|
-
try {
|
|
200
|
-
await writer.query("BEGIN");
|
|
201
|
-
await reclassify(writer, "ccc-reclass", "marketing");
|
|
202
|
-
|
|
203
|
-
const repairing = repairThreadMessageCategory(client);
|
|
204
|
-
await writer.query("SELECT pg_sleep(0.5)");
|
|
205
|
-
await writer.query("COMMIT");
|
|
206
|
-
|
|
207
|
-
assert.equal((await repairing).rowsWritten, 1);
|
|
208
|
-
} finally {
|
|
209
|
-
writer.release();
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
assert.deepEqual(await categories(), { "ccc-reclass": "newsletter" });
|
|
213
|
-
const { rows } = await pool.query<{ category: string }>(
|
|
214
|
-
"SELECT category FROM message WHERE message_id = 'ccc-reclass'",
|
|
215
|
-
);
|
|
216
|
-
assert.deepEqual(rows, [{ category: "marketing" }]);
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
test("only the clock expression differs between the dialects", () => {
|
|
220
|
-
const postgres = repairStatement("postgres");
|
|
221
|
-
const sqlite = repairStatement("sqlite");
|
|
222
|
-
assert.ok(postgres.includes("EXTRACT(EPOCH FROM now())"));
|
|
223
|
-
assert.ok(sqlite.includes("unixepoch('subsec')"));
|
|
224
|
-
assert.equal(
|
|
225
|
-
postgres.replace(/CAST\(EXTRACT.*$/, ""),
|
|
226
|
-
sqlite.replace(/CAST\(unixepoch.*$/, ""),
|
|
227
|
-
);
|
|
228
|
-
});
|
|
229
|
-
});
|
|
@@ -1,156 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { after, before, describe, test } from "node:test";
|
|
3
|
-
import { drizzle } from "drizzle-orm/node-postgres";
|
|
4
|
-
import type pg from "pg";
|
|
5
|
-
import type { Db } from "../db.js";
|
|
6
|
-
import { threadMessageTable } from "../schema/thread-message.js";
|
|
7
|
-
import * as schema from "../schema.js";
|
|
8
|
-
import { createTestDb } from "../test-db.js";
|
|
9
|
-
import { categoryFixtureRows, TRIMMED_TOTALS } from "./category-fixture.js";
|
|
10
|
-
import { DrizzleThreadMessageRepository } from "./thread-message.js";
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* The Postgres half of the category-predicate guard (#304). The behavioural
|
|
14
|
-
* coverage is in thread-message.sqlite.test.ts against the full measured shape;
|
|
15
|
-
* what this file exists for is the query plan, which is dialect-specific:
|
|
16
|
-
* `EXPLAIN QUERY PLAN` is SQLite syntax and Postgres needs its own `EXPLAIN`.
|
|
17
|
-
*
|
|
18
|
-
* It runs against the embedded Postgres harness, which pushes the generated
|
|
19
|
-
* drizzle schema, so `tm_by_mailbox_category_date` is present here for the same
|
|
20
|
-
* reason it is present in the sqlite harness: the index is declared in TypeSpec
|
|
21
|
-
* and emitted into the schema both dialects derive from. That is what makes the
|
|
22
|
-
* assertion a real guard rather than a restatement of the predicate — remove the
|
|
23
|
-
* index from the schema and it fails.
|
|
24
|
-
*
|
|
25
|
-
* The fixture is the measured shape with its common categories trimmed
|
|
26
|
-
* (TRIMMED_TOTALS): the rare tail the assertions use is untouched, and the
|
|
27
|
-
* planner needs only enough rows to prefer an index over a sequential scan. It
|
|
28
|
-
* also needs statistics before it will, so the fixture is ANALYZEd; a production
|
|
29
|
-
* instance gets that from autovacuum.
|
|
30
|
-
*/
|
|
31
|
-
|
|
32
|
-
const ACCOUNT = "acct-category-pg";
|
|
33
|
-
const MAILBOX = "mbx-category-pg";
|
|
34
|
-
|
|
35
|
-
describe("thread-message category predicate (postgres)", () => {
|
|
36
|
-
let pool: pg.Pool;
|
|
37
|
-
let close: () => Promise<void>;
|
|
38
|
-
let repo: DrizzleThreadMessageRepository;
|
|
39
|
-
const logged: Array<{ text: string; params: unknown[] }> = [];
|
|
40
|
-
|
|
41
|
-
before(async () => {
|
|
42
|
-
({ pool, close } = await createTestDb());
|
|
43
|
-
|
|
44
|
-
const db = drizzle(pool, {
|
|
45
|
-
schema,
|
|
46
|
-
logger: {
|
|
47
|
-
logQuery(text: string, params: unknown[]) {
|
|
48
|
-
logged.push({ text, params });
|
|
49
|
-
},
|
|
50
|
-
},
|
|
51
|
-
}) as unknown as Db<Record<string, unknown>>;
|
|
52
|
-
repo = new DrizzleThreadMessageRepository(db);
|
|
53
|
-
|
|
54
|
-
const rows = categoryFixtureRows({
|
|
55
|
-
totals: TRIMMED_TOTALS,
|
|
56
|
-
accountConfigId: ACCOUNT,
|
|
57
|
-
mailboxId: MAILBOX,
|
|
58
|
-
});
|
|
59
|
-
const insertDb = drizzle(pool, { schema });
|
|
60
|
-
for (let i = 0; i < rows.length; i += 500) {
|
|
61
|
-
await insertDb.insert(threadMessageTable).values(rows.slice(i, i + 500));
|
|
62
|
-
}
|
|
63
|
-
await pool.query("ANALYZE thread_message");
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
after(async () => {
|
|
67
|
-
await close();
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
test("a filtered page is a full page of matches, however far back they sit", async () => {
|
|
71
|
-
const social = await repo.searchByMailboxWindow(
|
|
72
|
-
ACCOUNT,
|
|
73
|
-
MAILBOX,
|
|
74
|
-
{ category: ["social"] },
|
|
75
|
-
{ limit: 50, order: "desc", excludeDeleted: true },
|
|
76
|
-
);
|
|
77
|
-
assert.equal(social.items.length, 50);
|
|
78
|
-
assert.ok(social.items.every((item) => item.category === "social"));
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
test("a continuation token walks the whole match set without repeats or gaps", async () => {
|
|
82
|
-
const seen: string[] = [];
|
|
83
|
-
let continuationToken: string | undefined;
|
|
84
|
-
do {
|
|
85
|
-
const page = await repo.searchByMailboxWindow(
|
|
86
|
-
ACCOUNT,
|
|
87
|
-
MAILBOX,
|
|
88
|
-
{ category: ["social"] },
|
|
89
|
-
{
|
|
90
|
-
limit: 25,
|
|
91
|
-
order: "desc",
|
|
92
|
-
excludeDeleted: true,
|
|
93
|
-
continuationToken,
|
|
94
|
-
},
|
|
95
|
-
);
|
|
96
|
-
seen.push(...page.items.map((item) => item.threadMessageId));
|
|
97
|
-
continuationToken = page.continuationToken;
|
|
98
|
-
} while (continuationToken);
|
|
99
|
-
assert.equal(seen.length, TRIMMED_TOTALS.social);
|
|
100
|
-
assert.equal(new Set(seen).size, TRIMMED_TOTALS.social);
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
describe("query plan", () => {
|
|
104
|
-
// Each assertion names the category column as well as the index: both
|
|
105
|
-
// engines can reach this index on its (account_config_id, mailbox_id)
|
|
106
|
-
// prefix alone, so an index-only assertion would still pass with the
|
|
107
|
-
// predicate removed.
|
|
108
|
-
const assertServedByIndex = async (
|
|
109
|
-
run: () => Promise<unknown>,
|
|
110
|
-
): Promise<void> => {
|
|
111
|
-
logged.length = 0;
|
|
112
|
-
await run();
|
|
113
|
-
const selects = logged.filter((entry) => /^\s*select/i.test(entry.text));
|
|
114
|
-
assert.ok(selects.length > 0, "the repo issued a select");
|
|
115
|
-
const plan: string[] = [];
|
|
116
|
-
for (const entry of selects) {
|
|
117
|
-
const explained = await pool.query<{ "QUERY PLAN": string }>({
|
|
118
|
-
text: `EXPLAIN ${entry.text}`,
|
|
119
|
-
values: entry.params,
|
|
120
|
-
});
|
|
121
|
-
plan.push(...explained.rows.map((row) => row["QUERY PLAN"]));
|
|
122
|
-
}
|
|
123
|
-
const text = plan.join("\n");
|
|
124
|
-
assert.ok(
|
|
125
|
-
text.includes("tm_by_mailbox_category_date"),
|
|
126
|
-
`no plan node used the index:\n${text}`,
|
|
127
|
-
);
|
|
128
|
-
assert.ok(
|
|
129
|
-
/Index Cond:[^\n]*category/.test(text),
|
|
130
|
-
`the index was used but not on category:\n${text}`,
|
|
131
|
-
);
|
|
132
|
-
};
|
|
133
|
-
|
|
134
|
-
test("the filtered window is served by tm_by_mailbox_category_date", async () => {
|
|
135
|
-
await assertServedByIndex(() =>
|
|
136
|
-
repo.searchByMailboxWindow(
|
|
137
|
-
ACCOUNT,
|
|
138
|
-
MAILBOX,
|
|
139
|
-
{ category: ["social"] },
|
|
140
|
-
{ limit: 50, order: "desc", excludeDeleted: true },
|
|
141
|
-
),
|
|
142
|
-
);
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
test("the filtered count is served by tm_by_mailbox_category_date", async () => {
|
|
146
|
-
await assertServedByIndex(() =>
|
|
147
|
-
repo.countByMailbox(
|
|
148
|
-
ACCOUNT,
|
|
149
|
-
MAILBOX,
|
|
150
|
-
{ category: ["social"] },
|
|
151
|
-
{ excludeDeleted: true },
|
|
152
|
-
),
|
|
153
|
-
);
|
|
154
|
-
});
|
|
155
|
-
});
|
|
156
|
-
});
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
// The dialect-selected entity table set (RFC 036 D1). Both generated packages
|
|
2
|
-
// export the same table symbols under the same names — they differ only in the
|
|
3
|
-
// column builders (`pgTable`/`jsonb`/`timestamp` vs `sqliteTable`/`text(json)`/
|
|
4
|
-
// `integer`). A process runs one dialect (see ../dialect.ts), so the active set
|
|
5
|
-
// is chosen once here and re-exported through the schema facades the repos
|
|
6
|
-
// import from.
|
|
7
|
-
//
|
|
8
|
-
// The cast to the Postgres module type is the single "typing loosens at the
|
|
9
|
-
// injection boundary" point RFC 036 D1 names: the repos are written once
|
|
10
|
-
// against the Postgres-typed shape, and at runtime the SQLite tables carry
|
|
11
|
-
// their own dialect so the queries generate the correct SQL. The backend
|
|
12
|
-
// already crosses this boundary with a cast on the db handle.
|
|
13
|
-
import * as pgEntities from "@remit/drizzle-pg-schema";
|
|
14
|
-
import * as sqliteEntities from "@remit/drizzle-sqlite-schema";
|
|
15
|
-
import { isSqlite } from "../dialect.js";
|
|
16
|
-
|
|
17
|
-
export const entities: typeof pgEntities = isSqlite()
|
|
18
|
-
? (sqliteEntities as unknown as typeof pgEntities)
|
|
19
|
-
: pgEntities;
|
package/src/schema-full.ts
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
// Complete Postgres drizzle schema for committed-migration GENERATION only —
|
|
2
|
-
// consumed by packages/migrate/drizzle.entities.config.ts and the drift guard
|
|
3
|
-
// (npm-scripts/check-vps-migrations.mjs), never by `pushSchema`.
|
|
4
|
-
//
|
|
5
|
-
// It pulls the generated entity package in wholesale, so a new TypeSpec entity
|
|
6
|
-
// flows into the committed migration with nothing to hand-maintain — the
|
|
7
|
-
// omission that let eight tables drift out of the deployed schema. The only
|
|
8
|
-
// addition is the `outbox` infra table, which has no entity. This file imports
|
|
9
|
-
// the raw pg outbox directly (not the dialect-selected `outboxTable`), so
|
|
10
|
-
// migration generation never depends on the runtime `DATA_BACKEND`.
|
|
11
|
-
//
|
|
12
|
-
// schema.ts stays the app/dev surface (single `*Table` alias per table, what
|
|
13
|
-
// the repos and `pushSchema` need); this file exposes canonical names, so it
|
|
14
|
-
// must not be fed to `pushSchema` alongside schema.ts (duplicate index names).
|
|
15
|
-
export * from "@remit/drizzle-pg-schema";
|
|
16
|
-
export { pgOutboxTable as outboxTable } from "./schema/outbox.js";
|