mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 13:01:00 +00:00
perf(db): index every acting foreign key and drop prefix indexes
Twenty-one foreign keys with ON DELETE CASCADE or SET NULL had no index leading with their column, so deleting a parent scanned the child table once per row; medication_intake_events.medication_id was the one an account delete paid for. Seventeen small-table indexes and the measurements (user_id, type, measured_at) index repeated the leading columns of another index and only cost writes. Migration 0415 adds and drops the small-table ones in one DO block under a bounded lock wait; 0416 drops the measurements index concurrently, in a file of its own. An integration test holds the schema to both rules from the catalog, each with a counter-check, and compares the plans of the user-and-type query shapes on a seeded table with and without the dropped index. The lock-timeout guard now also requires a CONCURRENTLY statement to stand alone in its file.
This commit is contained in:
1 parent
41de0ca126
commit
03aec3ec12
6 files changed
+572
-21
No files matched your search
@@ -89,6 +89,56 @@ function violations(name: string, sql: string): string[] {
|
||||
return found;
|
||||
}
|
||||
|
||||
/**
|
||||
* `CREATE INDEX CONCURRENTLY` / `DROP INDEX CONCURRENTLY` refuse to run in a
|
||||
* transaction block, and under `migrate deploy` they do so whenever the file
|
||||
* holds any other statement (SQLSTATE 25001, seen against Prisma 7 and
|
||||
* Postgres 16). Such a statement therefore stands alone in its file.
|
||||
*/
|
||||
function concurrentViolations(name: string, sql: string): string[] {
|
||||
const statements = topLevelStatements(sql);
|
||||
const concurrent = statements.filter((statement) =>
|
||||
/\bCONCURRENTLY\b/i.test(statement),
|
||||
);
|
||||
if (concurrent.length === 0 || statements.length === 1) return [];
|
||||
return [
|
||||
`${name}: a CONCURRENTLY statement must be the only statement in its file`,
|
||||
];
|
||||
}
|
||||
|
||||
describe("concurrent index statements", () => {
|
||||
it("stand alone in their migration file", () => {
|
||||
const found = migrations().flatMap(({ name, sql }) =>
|
||||
concurrentViolations(name, sql),
|
||||
);
|
||||
expect(found, found.join("\n")).toEqual([]);
|
||||
});
|
||||
|
||||
it("sees the concurrent migrations there are", () => {
|
||||
const concurrent = migrations()
|
||||
.filter(({ sql }) =>
|
||||
/\bCONCURRENTLY\b/i.test(topLevelStatements(sql).join(";")),
|
||||
)
|
||||
.map(({ name }) => name);
|
||||
expect(concurrent).toContain("0416_drop_measurements_type_prefix_index");
|
||||
});
|
||||
|
||||
it("refuses a concurrent statement beside another one", () => {
|
||||
expect(
|
||||
concurrentViolations(
|
||||
"a",
|
||||
'DROP INDEX CONCURRENTLY IF EXISTS "x";\nDROP INDEX CONCURRENTLY IF EXISTS "y";',
|
||||
),
|
||||
).toHaveLength(1);
|
||||
expect(
|
||||
concurrentViolations(
|
||||
"b",
|
||||
'-- CONCURRENTLY in a comment\nDROP INDEX CONCURRENTLY IF EXISTS "x";',
|
||||
),
|
||||
).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("migration lock waits", () => {
|
||||
it("bounds lock waits where they take effect and marks only repeatable files", () => {
|
||||
const found = migrations().flatMap(({ name, sql }) =>
|
||||
@@ -97,11 +147,13 @@ describe("migration lock waits", () => {
|
||||
expect(found, found.join("\n")).toEqual([]);
|
||||
});
|
||||
|
||||
it("sees the one migration that bounds its wait today", () => {
|
||||
it("sees the migrations that bound their wait", () => {
|
||||
const marked = migrations()
|
||||
.filter(({ sql }) => RETRY_MARKER.test(sql))
|
||||
.map(({ name }) => name);
|
||||
expect(marked).toContain("0412_measurement_hot_updates");
|
||||
expect(marked).toContain("0414_drop_medication_compliance_rollups");
|
||||
expect(marked).toContain("0415_foreign_key_and_prefix_indexes");
|
||||
});
|
||||
|
||||
it("catches each shape it exists to refuse", () => {
|
||||
|
||||
@@ -19,8 +19,8 @@
|
||||
* - All-time scan rather than a rolling window. The
|
||||
* `personal_records_user_metric_idx` index makes
|
||||
* `ORDER BY value LIMIT 1` constant-time; reading the worst-case
|
||||
* ~30k measurement rows per metric is ~50 ms with the typed
|
||||
* `measurements_user_id_type_measured_at_idx` index.
|
||||
* ~30k measurement rows per metric is ~50 ms over the
|
||||
* `(user_id, type, measured_at, …)` unique index.
|
||||
* - Tie handling: an equal-value reading writes a row but the caller
|
||||
* suppresses the push notification (the user matched, not beat
|
||||
* their best). The notification suppression lives on the job
|
||||
|
||||
Reference in new issue
Block a user