pipework 0.293.0 → 0.294.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/CHANGELOG.md +4 -0
- package/REFERENCE.md +9 -0
- package/dist/cli/commands/migrate.d.ts.map +1 -1
- package/dist/cli/commands/migrate.js +17 -2
- package/dist/cli/commands/migrate.js.map +1 -1
- package/dist/data/migrate/apply.d.ts +8 -0
- package/dist/data/migrate/apply.d.ts.map +1 -1
- package/dist/data/migrate/apply.js +43 -10
- package/dist/data/migrate/apply.js.map +1 -1
- package/dist/data/migrate/index.d.ts +1 -1
- package/dist/data/migrate/index.d.ts.map +1 -1
- package/dist/data/migrate/index.js +1 -1
- package/dist/data/migrate/index.js.map +1 -1
- package/dist/data/migrate/invalid-indexes.d.ts +51 -0
- package/dist/data/migrate/invalid-indexes.d.ts.map +1 -0
- package/dist/data/migrate/invalid-indexes.js +219 -0
- package/dist/data/migrate/invalid-indexes.js.map +1 -0
- package/dist/data/migrate/run.d.ts +23 -1
- package/dist/data/migrate/run.d.ts.map +1 -1
- package/dist/data/migrate/run.js +38 -0
- package/dist/data/migrate/run.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/commands/migrate.ts +17 -2
- package/src/data/migrate/apply.ts +53 -9
- package/src/data/migrate/index.ts +1 -1
- package/src/data/migrate/invalid-indexes.ts +296 -0
- package/src/data/migrate/run.ts +51 -1
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import postgres from 'postgres';
|
|
1
2
|
import type { Manifold } from '../../core/pipework.js';
|
|
2
3
|
import type { ResolvedDatabase } from '../../core/config/index.js';
|
|
3
4
|
import { type MigrationCheckResult } from './check.js';
|
|
4
5
|
import { type MigrationNotice } from './apply.js';
|
|
6
|
+
import { InvalidIndexRepairError, type InvalidIndex } from './invalid-indexes.js';
|
|
5
7
|
import { type FrameworkTablesResult } from './provision-framework.js';
|
|
6
8
|
export interface MigrateOptions {
|
|
7
9
|
/** Called as each notice or warning a migration statement raises arrives. Issue #970. */
|
|
@@ -14,8 +16,16 @@ export interface MigrateResult {
|
|
|
14
16
|
safetyCheck: MigrationCheckResult;
|
|
15
17
|
/** No-transaction files picked up half applied and finished. Issue #378. */
|
|
16
18
|
resumed: ResumedMigration[];
|
|
17
|
-
/** INVALID indexes a cut CREATE INDEX CONCURRENTLY left, dropped before the
|
|
19
|
+
/** INVALID indexes a cut CREATE INDEX CONCURRENTLY left, dropped before the statement that builds them reran. Issues #1003, #1005. */
|
|
18
20
|
droppedInvalidIndexes: DroppedInvalidIndex[];
|
|
21
|
+
/**
|
|
22
|
+
* Every INVALID index this run met, with its case, what was done or must be
|
|
23
|
+
* done, and the outcome: (a) an owned index rebuilt, (b) a leftover of an owned
|
|
24
|
+
* index dropped, (c) anything else, untouched. Issue #1005.
|
|
25
|
+
*/
|
|
26
|
+
invalidIndexes: InvalidIndex[];
|
|
27
|
+
/** Set when the catalog read for INVALID indexes failed, so none were checked. Issue #1005. */
|
|
28
|
+
invalidIndexReadError: string | null;
|
|
19
29
|
/** Notices and warnings raised by migration statements, in arrival order. Issue #970. */
|
|
20
30
|
notices: MigrateNotice[];
|
|
21
31
|
/** Trace/audit tables, columns and day partitions this run provisioned. */
|
|
@@ -40,11 +50,23 @@ export interface MigrateNotice extends MigrationNotice {
|
|
|
40
50
|
statement: number;
|
|
41
51
|
statements: number;
|
|
42
52
|
}
|
|
53
|
+
export { InvalidIndexRepairError, type InvalidIndex };
|
|
43
54
|
export declare class UnsafeMigrationError extends Error {
|
|
44
55
|
readonly check: MigrationCheckResult;
|
|
45
56
|
readonly name = "UnsafeMigrationError";
|
|
46
57
|
constructor(check: MigrationCheckResult, message: string);
|
|
47
58
|
}
|
|
48
59
|
export declare function migrateAll(instance: Manifold, options?: MigrateOptions): Promise<MigrateResult[]>;
|
|
60
|
+
/**
|
|
61
|
+
* The connection that holds the run lock. It must live as long as the run: the
|
|
62
|
+
* default `max_lifetime` would close it mid-run and release the lock, so both
|
|
63
|
+
* timeouts are off, as in src/test/setup.ts.
|
|
64
|
+
*/
|
|
65
|
+
export declare function openRunLock(url: string): ReturnType<typeof postgres>;
|
|
66
|
+
/**
|
|
67
|
+
* Runs one database's migrations and index repairs under a session advisory
|
|
68
|
+
* lock held for the whole run. A second run at once waits for it, then finds
|
|
69
|
+
* nothing to apply. The lock dies with its connection. Issue #1005.
|
|
70
|
+
*/
|
|
49
71
|
export declare function migrateOne(instance: Manifold, dbConfig: ResolvedDatabase, options?: MigrateOptions): Promise<MigrateResult>;
|
|
50
72
|
//# sourceMappingURL=run.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../../src/data/migrate/run.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../../src/data/migrate/run.ts"],"names":[],"mappings":"AAAA,OAAO,QAAQ,MAAM,UAAU,CAAA;AAE/B,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAA;AACtD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAA;AAGlE,OAAO,EAAmB,KAAK,oBAAoB,EAAE,MAAM,YAAY,CAAA;AAEvE,OAAO,EAAmB,KAAK,eAAe,EAAE,MAAM,YAAY,CAAA;AAClE,OAAO,EAAwB,uBAAuB,EAAE,KAAK,YAAY,EAAE,MAAM,sBAAsB,CAAA;AACvG,OAAO,EAA4B,KAAK,qBAAqB,EAAE,MAAM,0BAA0B,CAAA;AAM/F,MAAM,WAAW,cAAc;IAC7B,yFAAyF;IACzF,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,aAAa,KAAK,IAAI,CAAA;CAC3C;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,MAAM,CAAA;IAChB,aAAa,EAAE,MAAM,CAAA;IACrB,UAAU,EAAE,MAAM,EAAE,CAAA;IACpB,WAAW,EAAE,oBAAoB,CAAA;IACjC,4EAA4E;IAC5E,OAAO,EAAE,gBAAgB,EAAE,CAAA;IAC3B,sIAAsI;IACtI,qBAAqB,EAAE,mBAAmB,EAAE,CAAA;IAC5C;;;;OAIG;IACH,cAAc,EAAE,YAAY,EAAE,CAAA;IAC9B,+FAA+F;IAC/F,qBAAqB,EAAE,MAAM,GAAG,IAAI,CAAA;IACpC,yFAAyF;IACzF,OAAO,EAAE,aAAa,EAAE,CAAA;IACxB,2EAA2E;IAC3E,eAAe,EAAE,qBAAqB,CAAA;CACvC;AAED,MAAM,WAAW,mBAAmB;IAClC,GAAG,EAAE,MAAM,CAAA;IACX,yCAAyC;IACzC,SAAS,EAAE,MAAM,CAAA;IACjB,gCAAgC;IAChC,KAAK,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,gBAAgB;IAC/B,GAAG,EAAE,MAAM,CAAA;IACX,gDAAgD;IAChD,aAAa,EAAE,MAAM,CAAA;IACrB,eAAe,EAAE,MAAM,CAAA;CACxB;AAED,MAAM,WAAW,aAAc,SAAQ,eAAe;IACpD,GAAG,EAAE,MAAM,CAAA;IACX,yCAAyC;IACzC,SAAS,EAAE,MAAM,CAAA;IACjB,UAAU,EAAE,MAAM,CAAA;CACnB;AAED,OAAO,EAAE,uBAAuB,EAAE,KAAK,YAAY,EAAE,CAAA;AAErD,qBAAa,oBAAqB,SAAQ,KAAK;aAG3B,KAAK,EAAE,oBAAoB;IAF7C,SAAkB,IAAI,0BAAyB;gBAE7B,KAAK,EAAE,oBAAoB,EAC3C,OAAO,EAAE,MAAM;CAIlB;AAED,wBAAsB,UAAU,CAAC,QAAQ,EAAE,QAAQ,EAAE,OAAO,GAAE,cAAmB,GAAG,OAAO,CAAC,aAAa,EAAE,CAAC,CAU3G;AAKD;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,UAAU,CAAC,OAAO,QAAQ,CAAC,CAEpE;AAED;;;;GAIG;AACH,wBAAsB,UAAU,CAAC,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,gBAAgB,EAAE,OAAO,GAAE,cAAmB,GAAG,OAAO,CAAC,aAAa,CAAC,CAQrI"}
|
package/dist/data/migrate/run.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
|
+
import postgres from 'postgres';
|
|
1
2
|
import { sql } from '../query/index.js';
|
|
2
3
|
import { quoteIdentifier } from '../db/identifiers.js';
|
|
3
4
|
import { OWNER_LABEL_MIGRATE } from '../db/owner-labels.js';
|
|
4
5
|
import { checkMigrations } from './check.js';
|
|
5
6
|
import { readAppliedState } from './applied.js';
|
|
6
7
|
import { applyMigrations } from './apply.js';
|
|
8
|
+
import { repairInvalidIndexes, InvalidIndexRepairError } from './invalid-indexes.js';
|
|
7
9
|
import { provisionFrameworkTables } from './provision-framework.js';
|
|
10
|
+
export { InvalidIndexRepairError };
|
|
8
11
|
export class UnsafeMigrationError extends Error {
|
|
9
12
|
check;
|
|
10
13
|
name = 'UnsafeMigrationError';
|
|
@@ -23,7 +26,32 @@ export async function migrateAll(instance, options = {}) {
|
|
|
23
26
|
}
|
|
24
27
|
return results;
|
|
25
28
|
}
|
|
29
|
+
/** One key per database: advisory locks are scoped to the database they are taken in. */
|
|
30
|
+
const MIGRATE_LOCK_KEY = `hashtextextended('pipework.migrate', 0)`;
|
|
31
|
+
/**
|
|
32
|
+
* The connection that holds the run lock. It must live as long as the run: the
|
|
33
|
+
* default `max_lifetime` would close it mid-run and release the lock, so both
|
|
34
|
+
* timeouts are off, as in src/test/setup.ts.
|
|
35
|
+
*/
|
|
36
|
+
export function openRunLock(url) {
|
|
37
|
+
return postgres(url, { max: 1, idle_timeout: 0, max_lifetime: 0, onnotice: () => { } });
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Runs one database's migrations and index repairs under a session advisory
|
|
41
|
+
* lock held for the whole run. A second run at once waits for it, then finds
|
|
42
|
+
* nothing to apply. The lock dies with its connection. Issue #1005.
|
|
43
|
+
*/
|
|
26
44
|
export async function migrateOne(instance, dbConfig, options = {}) {
|
|
45
|
+
const lock = openRunLock(dbConfig.url);
|
|
46
|
+
try {
|
|
47
|
+
await lock.unsafe(`SELECT pg_advisory_lock(${MIGRATE_LOCK_KEY})`);
|
|
48
|
+
return await migrateLocked(instance, dbConfig, options);
|
|
49
|
+
}
|
|
50
|
+
finally {
|
|
51
|
+
await lock.end();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
async function migrateLocked(instance, dbConfig, options) {
|
|
27
55
|
const conn = instance.pool.getOrCreate(dbConfig, 'owner', OWNER_LABEL_MIGRATE);
|
|
28
56
|
// Scope the safety gate to pending migrations only — hazards in already-applied
|
|
29
57
|
// migrations are history, not a decision the operator still gets to make.
|
|
@@ -62,6 +90,14 @@ export async function migrateOne(instance, dbConfig, options = {}) {
|
|
|
62
90
|
options.onNotice?.(full);
|
|
63
91
|
},
|
|
64
92
|
});
|
|
93
|
+
// After a run that finished without error only; a failing migration throws
|
|
94
|
+
// above and its rerun resumes and checks then. Throws when an owned index
|
|
95
|
+
// cannot be made valid.
|
|
96
|
+
const repaired = await repairInvalidIndexes({
|
|
97
|
+
migrationsFolder: dbConfig.migrations,
|
|
98
|
+
migrationsTable: dbConfig.migrationTable,
|
|
99
|
+
connectionUrl: dbConfig.url,
|
|
100
|
+
});
|
|
65
101
|
// Trace and audit storage rides the default database; their partitioned
|
|
66
102
|
// tables are kept out of generated migrations (#313), so migrate provisions them.
|
|
67
103
|
const frameworkTables = await provisionFrameworkTables(conn.drizzle, instance.config.raw, {
|
|
@@ -77,6 +113,8 @@ export async function migrateOne(instance, dbConfig, options = {}) {
|
|
|
77
113
|
safetyCheck,
|
|
78
114
|
resumed,
|
|
79
115
|
droppedInvalidIndexes,
|
|
116
|
+
invalidIndexes: repaired.indexes,
|
|
117
|
+
invalidIndexReadError: repaired.readError,
|
|
80
118
|
notices,
|
|
81
119
|
frameworkTables,
|
|
82
120
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"run.js","sourceRoot":"","sources":["../../../src/data/migrate/run.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,mBAAmB,CAAA;AAGvC,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AACtD,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAA;AAC3D,OAAO,EAAE,eAAe,EAA6B,MAAM,YAAY,CAAA;AACvE,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAA;AAC/C,OAAO,EAAE,eAAe,EAAwB,MAAM,YAAY,CAAA;AAClE,OAAO,EAAE,wBAAwB,EAA8B,MAAM,0BAA0B,CAAA;
|
|
1
|
+
{"version":3,"file":"run.js","sourceRoot":"","sources":["../../../src/data/migrate/run.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -121,10 +121,25 @@ function reportResumed(result: MigrateResult): void {
|
|
|
121
121
|
}
|
|
122
122
|
for (const d of result.droppedInvalidIndexes) {
|
|
123
123
|
warn(
|
|
124
|
-
`${result.database}:
|
|
125
|
-
`left by
|
|
124
|
+
`${result.database}: "${d.tag}" statement ${d.statement}: dropped INVALID index ${d.index} ` +
|
|
125
|
+
`left by a cut build before building it again.`,
|
|
126
126
|
)
|
|
127
127
|
}
|
|
128
|
+
for (const i of result.invalidIndexes) {
|
|
129
|
+
if (i.case === 'a' && i.outcome === 'repaired') {
|
|
130
|
+
warn(`${result.database}: rebuilt INVALID index ${i.index} with the statement from "${i.tag}".`)
|
|
131
|
+
} else if (i.case === 'b' && i.outcome === 'repaired') {
|
|
132
|
+
warn(`${result.database}: dropped INVALID leftover ${i.index}; ${i.reason}.`)
|
|
133
|
+
} else {
|
|
134
|
+
warn(
|
|
135
|
+
`${result.database}: INVALID index ${i.index} on ${i.table} was left alone: ${i.reason}. ` +
|
|
136
|
+
`Its owner can run: ${i.printed.join(' ; ')}`,
|
|
137
|
+
)
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (result.invalidIndexReadError !== null) {
|
|
141
|
+
warn(`${result.database}: could not check for INVALID indexes: ${result.invalidIndexReadError}`)
|
|
142
|
+
}
|
|
128
143
|
}
|
|
129
144
|
|
|
130
145
|
function reportFrameworkTables(result: MigrateResult): void {
|
|
@@ -200,15 +200,14 @@ export async function applyMigrations(options: ApplyOptions): Promise<number> {
|
|
|
200
200
|
if (from > 0) options.onResume?.(migration.tag, from + 1, migration.statements.length)
|
|
201
201
|
|
|
202
202
|
for (let i = from; i < migration.statements.length; i++) {
|
|
203
|
-
//
|
|
204
|
-
//
|
|
205
|
-
//
|
|
206
|
-
//
|
|
207
|
-
//
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
}
|
|
203
|
+
// Before every CREATE INDEX CONCURRENTLY, not only a resumed one: a
|
|
204
|
+
// cut build leaves the name held by an INVALID index, which the plain
|
|
205
|
+
// form fails on ("already exists") and IF NOT EXISTS silently keeps,
|
|
206
|
+
// and a cut in a file's first statement leaves no progress mark, so
|
|
207
|
+
// the rerun starts at 0. Only the statement's own index; a VALID
|
|
208
|
+
// index of that name is left alone. Issues #1003, #1005.
|
|
209
|
+
const dropped = await dropInvalidIndexOf(client, migration.statements[i]!)
|
|
210
|
+
if (dropped !== null) options.onDropInvalidIndex?.(migration.tag, i + 1, dropped)
|
|
212
211
|
await inStatement(migration.tag, i + 1, migration.statements.length, () => client.unsafe(migration.statements[i]!))
|
|
213
212
|
// Recorded AFTER the statement it describes: marking first would let
|
|
214
213
|
// a death between the mark and the statement skip work silently,
|
|
@@ -222,6 +221,27 @@ export async function applyMigrations(options: ApplyOptions): Promise<number> {
|
|
|
222
221
|
)
|
|
223
222
|
options.onStatement?.(migration.tag, i + 1, migration.statements.length, applied + 1, migrations.length)
|
|
224
223
|
}
|
|
224
|
+
// Never record a file applied while an index it built is INVALID.
|
|
225
|
+
// The progress mark goes back to that statement, so the rerun drops
|
|
226
|
+
// the index and builds it again. Issue #1005.
|
|
227
|
+
for (const [i, statement] of migration.statements.entries()) {
|
|
228
|
+
// Only builds the rerun can repair: `CREATE INDEX ... ON ONLY parent` is
|
|
229
|
+
// INVALID until every partition's index attaches, and no rerun fixes that.
|
|
230
|
+
const target = parseCreateIndexConcurrently(statement)
|
|
231
|
+
if (target === null) continue
|
|
232
|
+
if (await isInvalidIndex(client, target)) {
|
|
233
|
+
await client.unsafe(
|
|
234
|
+
`INSERT INTO ${progress} (tag, hash, statements_done) VALUES ($1, $2, $3)
|
|
235
|
+
ON CONFLICT (tag) DO UPDATE SET statements_done = EXCLUDED.statements_done, updated_at = now()`,
|
|
236
|
+
[migration.tag, migration.hash, i],
|
|
237
|
+
)
|
|
238
|
+
throw new Error(
|
|
239
|
+
`Migration "${migration.tag}" statement ${i + 1} built index ${target.index} on ${target.table}, ` +
|
|
240
|
+
`but the index is INVALID, so the migration is not recorded as applied. The next run drops it ` +
|
|
241
|
+
`and builds it again.`,
|
|
242
|
+
)
|
|
243
|
+
}
|
|
244
|
+
}
|
|
225
245
|
await client.unsafe(
|
|
226
246
|
`INSERT INTO ${table} ("hash", "created_at", "tag") VALUES ($1, $2, $3)`,
|
|
227
247
|
[migration.hash, migration.folderMillis, migration.tag],
|
|
@@ -344,6 +364,10 @@ export function resumePoint(
|
|
|
344
364
|
}
|
|
345
365
|
|
|
346
366
|
const IDENT = String.raw`"(?:[^"]|"")+"|[A-Za-z_][A-Za-z0-9_$]*`
|
|
367
|
+
const CREATE_INDEX_RE = new RegExp(
|
|
368
|
+
String.raw`^\s*CREATE\s+(?:UNIQUE\s+)?INDEX\s+(?:CONCURRENTLY\s+)?(?:IF\s+NOT\s+EXISTS\s+)?(${IDENT})\s+ON\s+(?:ONLY\s+)?((?:${IDENT})(?:\s*\.\s*(?:${IDENT}))?)`,
|
|
369
|
+
'i',
|
|
370
|
+
)
|
|
347
371
|
const CREATE_INDEX_CONCURRENTLY_RE = new RegExp(
|
|
348
372
|
String.raw`^\s*CREATE\s+(?:UNIQUE\s+)?INDEX\s+CONCURRENTLY\s+(?:IF\s+NOT\s+EXISTS\s+)?(${IDENT})\s+ON\s+(?:ONLY\s+)?((?:${IDENT})(?:\s*\.\s*(?:${IDENT}))?)`,
|
|
349
373
|
'i',
|
|
@@ -364,6 +388,26 @@ export function parseCreateIndexConcurrently(statement: string): { index: string
|
|
|
364
388
|
return { index: unquoteIdent(match[1]!), table: match[2]! }
|
|
365
389
|
}
|
|
366
390
|
|
|
391
|
+
/**
|
|
392
|
+
* Like `parseCreateIndexConcurrently`, for a CREATE [UNIQUE] INDEX with or
|
|
393
|
+
* without CONCURRENTLY. Null for any other statement or an unnamed index.
|
|
394
|
+
*/
|
|
395
|
+
export function parseCreateIndex(statement: string): { index: string; table: string } | null {
|
|
396
|
+
const match = CREATE_INDEX_RE.exec(stripComments(statement))
|
|
397
|
+
if (match === null) return null
|
|
398
|
+
return { index: unquoteIdent(match[1]!), table: match[2]! }
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/** True when an index of this name on this table exists and is INVALID. */
|
|
402
|
+
async function isInvalidIndex(client: Sql, target: { index: string; table: string }): Promise<boolean> {
|
|
403
|
+
const found = await client.unsafe<{ one: number }[]>(
|
|
404
|
+
`SELECT 1 AS one FROM pg_index i JOIN pg_class c ON c.oid = i.indexrelid
|
|
405
|
+
WHERE c.relname = $1 AND NOT i.indisvalid AND i.indrelid = to_regclass($2)`,
|
|
406
|
+
[target.index, target.table],
|
|
407
|
+
)
|
|
408
|
+
return found.length > 0
|
|
409
|
+
}
|
|
410
|
+
|
|
367
411
|
/**
|
|
368
412
|
* Drop the INVALID index a cut CREATE INDEX CONCURRENTLY left under the name
|
|
369
413
|
* this statement is about to create, on the statement's own table. Returns the
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { migrateAll, migrateOne, UnsafeMigrationError, type MigrateResult, type MigrateOptions } from './run.js'
|
|
1
|
+
export { migrateAll, migrateOne, UnsafeMigrationError, InvalidIndexRepairError, type InvalidIndex, type MigrateResult, type MigrateOptions } from './run.js'
|
|
2
2
|
export { checkMigrations, formatCheckResult, type MigrationCheckResult, type FileHazards, type MigrationHazard, type Severity } from './check.js'
|
|
3
3
|
export { hasPragma, PRAGMA_FOR, type PragmaKind } from './pragma.js'
|
|
4
4
|
export { readAppliedState, type AppliedState } from './applied.js'
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import postgres, { type Sql } from 'postgres'
|
|
2
|
+
import { stripComments } from './statements.js'
|
|
3
|
+
import { parseCreateIndex, readMigrations } from './apply.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* What migrate does with an INVALID index met in a database it migrated.
|
|
7
|
+
* Issue #1005.
|
|
8
|
+
*
|
|
9
|
+
* - `a`: an index a migration owns. Dropped and rebuilt with the migration's
|
|
10
|
+
* own CREATE statement.
|
|
11
|
+
* - `b`: a `_ccnew` / `_ccold` leftover of an owned index whose counterpart is
|
|
12
|
+
* VALID. Dropped.
|
|
13
|
+
* - `c`: anything else. Nothing is touched; the statements to run are printed.
|
|
14
|
+
*/
|
|
15
|
+
export interface InvalidIndex {
|
|
16
|
+
case: 'a' | 'b' | 'c'
|
|
17
|
+
/** Schema-qualified, quoted. */
|
|
18
|
+
index: string
|
|
19
|
+
/** Schema-qualified, quoted. */
|
|
20
|
+
table: string
|
|
21
|
+
/** What migrate did: rebuilt (a), dropped the leftover (b), or nothing (c). */
|
|
22
|
+
action: 'rebuilt' | 'dropped' | 'none'
|
|
23
|
+
outcome: 'repaired' | 'failed' | 'warned'
|
|
24
|
+
/** For (a): the tag of the migration whose CREATE was rerun. */
|
|
25
|
+
tag?: string
|
|
26
|
+
/** For (a): the statement rerun. */
|
|
27
|
+
statement?: string
|
|
28
|
+
/** For (c) and a failed (a): the statements its owner can run. */
|
|
29
|
+
printed: string[]
|
|
30
|
+
/** Why migrate left it alone, or what failed. */
|
|
31
|
+
reason?: string
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface InvalidIndexReport {
|
|
35
|
+
indexes: InvalidIndex[]
|
|
36
|
+
/** Set when the catalog read failed; nothing was checked. */
|
|
37
|
+
readError: string | null
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export class InvalidIndexRepairError extends Error {
|
|
41
|
+
override readonly name = 'InvalidIndexRepairError'
|
|
42
|
+
constructor(
|
|
43
|
+
public readonly indexes: InvalidIndex[],
|
|
44
|
+
message: string,
|
|
45
|
+
) {
|
|
46
|
+
super(message)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface RepairOptions {
|
|
51
|
+
migrationsFolder: string
|
|
52
|
+
migrationsTable: string
|
|
53
|
+
connectionUrl: string
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const LOCK_TIMEOUT = '30s'
|
|
57
|
+
const LEFTOVER_RE = /_cc(?:new|old)\d*$/
|
|
58
|
+
const IDENT = String.raw`"(?:[^"]|"")+"|[A-Za-z_][A-Za-z0-9_$]*`
|
|
59
|
+
const DROP_INDEX_RE = new RegExp(
|
|
60
|
+
String.raw`^\s*DROP\s+INDEX\s+(?:CONCURRENTLY\s+)?(?:IF\s+EXISTS\s+)?((?:(?:${IDENT})(?:\s*\.\s*(?:${IDENT}))?\s*,?\s*)+)`,
|
|
61
|
+
'i',
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
interface Row {
|
|
65
|
+
oid: string
|
|
66
|
+
tableOid: string
|
|
67
|
+
schema: string
|
|
68
|
+
name: string
|
|
69
|
+
tableSchema: string
|
|
70
|
+
table: string
|
|
71
|
+
def: string
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
interface Owner {
|
|
75
|
+
tag: string
|
|
76
|
+
statement: string
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function quoteIdent(name: string): string {
|
|
80
|
+
return `"${name.replace(/"/g, '""')}"`
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function unquoteIdent(token: string): string {
|
|
84
|
+
return token.startsWith('"') ? token.slice(1, -1).replace(/""/g, '"') : token.toLowerCase()
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** The (schema?, name) pairs a DROP INDEX statement names; empty for any other statement. */
|
|
88
|
+
function parseDropIndex(statement: string): { schema: string | null; name: string }[] {
|
|
89
|
+
const match = DROP_INDEX_RE.exec(stripComments(statement))
|
|
90
|
+
if (match === null) return []
|
|
91
|
+
const tokens = match[1]!.match(new RegExp(`(?:${IDENT})(?:\\s*\\.\\s*(?:${IDENT}))?`, 'g')) ?? []
|
|
92
|
+
return tokens.map((token) => {
|
|
93
|
+
const parts = token.match(new RegExp(IDENT, 'g'))!
|
|
94
|
+
return parts.length === 2
|
|
95
|
+
? { schema: unquoteIdent(parts[0]!), name: unquoteIdent(parts[1]!) }
|
|
96
|
+
: { schema: null, name: unquoteIdent(parts[0]!) }
|
|
97
|
+
})
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** An index definition with its own name removed, so a leftover compares equal to its counterpart. */
|
|
101
|
+
function nameless(def: string): string {
|
|
102
|
+
return def.replace(/^(CREATE (?:UNIQUE )?INDEX )(?:"(?:[^"]|"")+"|\S+)( ON )/, '$1$2')
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const INVALID_INDEXES = `
|
|
106
|
+
SELECT i.indexrelid::text AS oid, i.indrelid::text AS "tableOid", n.nspname AS schema, c.relname AS name,
|
|
107
|
+
tn.nspname AS "tableSchema", t.relname AS "table", pg_get_indexdef(i.indexrelid) AS def
|
|
108
|
+
FROM pg_index i
|
|
109
|
+
JOIN pg_class c ON c.oid = i.indexrelid
|
|
110
|
+
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
111
|
+
JOIN pg_class t ON t.oid = i.indrelid
|
|
112
|
+
JOIN pg_namespace tn ON tn.oid = t.relnamespace
|
|
113
|
+
WHERE NOT i.indisvalid
|
|
114
|
+
AND c.relkind = 'i'
|
|
115
|
+
AND n.nspname <> 'information_schema' AND n.nspname NOT LIKE 'pg\\_%'
|
|
116
|
+
AND NOT EXISTS (SELECT 1 FROM pg_depend d
|
|
117
|
+
WHERE d.classid = 'pg_class'::regclass AND d.objid = i.indexrelid AND d.deptype = 'e')
|
|
118
|
+
AND NOT EXISTS (SELECT 1 FROM pg_stat_progress_create_index p WHERE p.relid = i.indrelid)
|
|
119
|
+
ORDER BY n.nspname, c.relname`
|
|
120
|
+
|
|
121
|
+
async function building(client: Sql, tableOid: string): Promise<boolean> {
|
|
122
|
+
const rows = await client.unsafe(
|
|
123
|
+
`SELECT 1 FROM pg_stat_progress_create_index WHERE relid = $1::oid`,
|
|
124
|
+
[tableOid],
|
|
125
|
+
)
|
|
126
|
+
return rows.length > 0
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** The applied migrations' CREATE INDEX statements that still stand, keyed by table oid and index name. */
|
|
130
|
+
async function ownedIndexes(client: Sql, options: RepairOptions): Promise<Map<string, Owner>> {
|
|
131
|
+
const applied = new Set(
|
|
132
|
+
(await client.unsafe<{ tag: string }[]>(
|
|
133
|
+
`SELECT tag FROM ${quoteIdent(options.migrationsTable)} WHERE tag IS NOT NULL`,
|
|
134
|
+
)).map((r) => r.tag),
|
|
135
|
+
)
|
|
136
|
+
const owned = new Map<string, Owner & { schema: string }>()
|
|
137
|
+
for (const migration of readMigrations(options)) {
|
|
138
|
+
if (!applied.has(migration.tag)) continue
|
|
139
|
+
for (const statement of migration.statements) {
|
|
140
|
+
const create = parseCreateIndex(statement)
|
|
141
|
+
if (create !== null) {
|
|
142
|
+
const [table] = await client.unsafe<{ oid: string; schema: string }[]>(
|
|
143
|
+
`SELECT c.oid::text AS oid, n.nspname AS schema FROM pg_class c
|
|
144
|
+
JOIN pg_namespace n ON n.oid = c.relnamespace WHERE c.oid = to_regclass($1)`,
|
|
145
|
+
[create.table],
|
|
146
|
+
)
|
|
147
|
+
if (table !== undefined) owned.set(`${table.oid}:${create.index}`, { tag: migration.tag, statement, schema: table.schema })
|
|
148
|
+
continue
|
|
149
|
+
}
|
|
150
|
+
for (const dropped of parseDropIndex(statement)) {
|
|
151
|
+
for (const [key, owner] of owned) {
|
|
152
|
+
if (key.endsWith(`:${dropped.name}`) && (dropped.schema === null || dropped.schema === owner.schema)) {
|
|
153
|
+
owned.delete(key)
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return owned
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Repair the INVALID indexes the applied migrations made, and report the rest.
|
|
164
|
+
* Called once per database after `applyMigrations` finished without error,
|
|
165
|
+
* while the run's advisory lock is held. Throws `InvalidIndexRepairError` when
|
|
166
|
+
* an owned index cannot be made VALID. Issue #1005.
|
|
167
|
+
*/
|
|
168
|
+
export async function repairInvalidIndexes(options: RepairOptions): Promise<InvalidIndexReport> {
|
|
169
|
+
const client = postgres(options.connectionUrl, { max: 1, onnotice: () => {} })
|
|
170
|
+
try {
|
|
171
|
+
let rows: Row[]
|
|
172
|
+
let owned: Map<string, Owner>
|
|
173
|
+
try {
|
|
174
|
+
rows = await client.unsafe<Row[]>(INVALID_INDEXES)
|
|
175
|
+
owned = rows.length === 0 ? new Map() : await ownedIndexes(client, options)
|
|
176
|
+
} catch (err) {
|
|
177
|
+
return { indexes: [], readError: err instanceof Error ? err.message : String(err) }
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const qualified = (schema: string, name: string): string => `${quoteIdent(schema)}.${quoteIdent(name)}`
|
|
181
|
+
const results: InvalidIndex[] = []
|
|
182
|
+
const failures: string[] = []
|
|
183
|
+
const ownedKey = (r: Row): string => `${r.tableOid}:${r.name}`
|
|
184
|
+
const printed = (r: Row, rebuild: boolean): string[] => [
|
|
185
|
+
...(rebuild ? [`REINDEX INDEX CONCURRENTLY ${qualified(r.schema, r.name)}`] : []),
|
|
186
|
+
`DROP INDEX CONCURRENTLY ${qualified(r.schema, r.name)}`,
|
|
187
|
+
]
|
|
188
|
+
|
|
189
|
+
// (a) owned: drop and rerun the migration's own CREATE.
|
|
190
|
+
const leftovers: Row[] = []
|
|
191
|
+
for (const row of rows) {
|
|
192
|
+
const owner = owned.get(ownedKey(row))
|
|
193
|
+
if (owner === undefined) {
|
|
194
|
+
if (LEFTOVER_RE.test(row.name)) leftovers.push(row)
|
|
195
|
+
else {
|
|
196
|
+
results.push({
|
|
197
|
+
case: 'c', index: qualified(row.schema, row.name), table: qualified(row.tableSchema, row.table),
|
|
198
|
+
action: 'none', outcome: 'warned', printed: printed(row, true),
|
|
199
|
+
reason: 'no applied migration owns this index',
|
|
200
|
+
})
|
|
201
|
+
}
|
|
202
|
+
continue
|
|
203
|
+
}
|
|
204
|
+
// Another session may have started a build on the table since the read.
|
|
205
|
+
if (await building(client, row.tableOid)) continue
|
|
206
|
+
const entry: InvalidIndex = {
|
|
207
|
+
case: 'a', index: qualified(row.schema, row.name), table: qualified(row.tableSchema, row.table),
|
|
208
|
+
action: 'rebuilt', outcome: 'repaired', tag: owner.tag, statement: owner.statement, printed: [],
|
|
209
|
+
}
|
|
210
|
+
try {
|
|
211
|
+
await client.unsafe(`SET lock_timeout = '${LOCK_TIMEOUT}'`)
|
|
212
|
+
try {
|
|
213
|
+
await client.unsafe(`DROP INDEX CONCURRENTLY IF EXISTS ${entry.index}`)
|
|
214
|
+
} finally {
|
|
215
|
+
await client.unsafe('RESET lock_timeout')
|
|
216
|
+
}
|
|
217
|
+
await client.unsafe(owner.statement)
|
|
218
|
+
} catch (err) {
|
|
219
|
+
const detail = err instanceof Error ? err.message : String(err)
|
|
220
|
+
const extra = (err as { detail?: unknown }).detail
|
|
221
|
+
entry.outcome = 'failed'
|
|
222
|
+
entry.reason = typeof extra === 'string' ? `${detail} (${extra})` : detail
|
|
223
|
+
entry.printed = printed(row, true)
|
|
224
|
+
failures.push(`${entry.index} (rerunning "${owner.tag}"): ${entry.reason}`)
|
|
225
|
+
}
|
|
226
|
+
results.push(entry)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// (b) / (c) leftovers, classified against the table's indexes as they stand after (a).
|
|
230
|
+
const byTable = new Map<string, { name: string; schema: string; valid: boolean; def: string }[]>()
|
|
231
|
+
for (const row of leftovers) {
|
|
232
|
+
if (!byTable.has(row.tableOid)) {
|
|
233
|
+
byTable.set(row.tableOid, await client.unsafe(
|
|
234
|
+
`SELECT c.relname AS name, n.nspname AS schema, i.indisvalid AS valid, pg_get_indexdef(i.indexrelid) AS def
|
|
235
|
+
FROM pg_index i JOIN pg_class c ON c.oid = i.indexrelid
|
|
236
|
+
JOIN pg_namespace n ON n.oid = c.relnamespace WHERE i.indrelid = $1::oid`,
|
|
237
|
+
[row.tableOid],
|
|
238
|
+
))
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
for (const row of leftovers) {
|
|
242
|
+
const own = nameless(row.def)
|
|
243
|
+
const matches = byTable.get(row.tableOid)!.filter((o) => o.name !== row.name && nameless(o.def) === own)
|
|
244
|
+
const counterpart = matches.length === 1 ? matches[0]! : undefined
|
|
245
|
+
const base = { index: qualified(row.schema, row.name), table: qualified(row.tableSchema, row.table) }
|
|
246
|
+
if (counterpart === undefined) {
|
|
247
|
+
results.push({
|
|
248
|
+
case: 'c', ...base, action: 'none', outcome: 'warned', printed: printed(row, true),
|
|
249
|
+
reason: matches.length === 0
|
|
250
|
+
? 'a leftover of an index build or rebuild, and no index on the table matches it'
|
|
251
|
+
: 'a leftover of an index build or rebuild, and more than one index on the table matches it',
|
|
252
|
+
})
|
|
253
|
+
continue
|
|
254
|
+
}
|
|
255
|
+
const counterpartOwned = owned.has(`${row.tableOid}:${counterpart.name}`)
|
|
256
|
+
if (!counterpartOwned || !counterpart.valid) {
|
|
257
|
+
results.push({
|
|
258
|
+
case: 'c', ...base, action: 'none', outcome: 'warned', printed: printed(row, !counterpart.valid),
|
|
259
|
+
reason: counterpartOwned
|
|
260
|
+
? `a leftover of ${qualified(counterpart.schema, counterpart.name)}, which is not valid`
|
|
261
|
+
: `a leftover of ${qualified(counterpart.schema, counterpart.name)}, which no applied migration owns`,
|
|
262
|
+
})
|
|
263
|
+
continue
|
|
264
|
+
}
|
|
265
|
+
if (await building(client, row.tableOid)) continue
|
|
266
|
+
const entry: InvalidIndex = { case: 'b', ...base, action: 'dropped', outcome: 'repaired', printed: [],
|
|
267
|
+
reason: `a leftover of ${qualified(counterpart.schema, counterpart.name)}, which stays in use` }
|
|
268
|
+
try {
|
|
269
|
+
await client.unsafe(`SET lock_timeout = '${LOCK_TIMEOUT}'`)
|
|
270
|
+
try {
|
|
271
|
+
await client.unsafe(`DROP INDEX CONCURRENTLY IF EXISTS ${base.index}`)
|
|
272
|
+
} finally {
|
|
273
|
+
await client.unsafe('RESET lock_timeout')
|
|
274
|
+
}
|
|
275
|
+
} catch (err) {
|
|
276
|
+
entry.outcome = 'failed'
|
|
277
|
+
entry.action = 'none'
|
|
278
|
+
entry.reason = err instanceof Error ? err.message : String(err)
|
|
279
|
+
entry.printed = printed(row, false)
|
|
280
|
+
}
|
|
281
|
+
results.push(entry)
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (failures.length > 0) {
|
|
285
|
+
throw new InvalidIndexRepairError(
|
|
286
|
+
results,
|
|
287
|
+
`migrate could not make ${failures.length} INVALID ${failures.length === 1 ? 'index' : 'indexes'} ` +
|
|
288
|
+
`its migrations built valid:\n ${failures.join('\n ')}\n` +
|
|
289
|
+
`The next run drops each and builds it again; fix the cause first (for a UNIQUE index, the duplicate rows).`,
|
|
290
|
+
)
|
|
291
|
+
}
|
|
292
|
+
return { indexes: results, readError: null }
|
|
293
|
+
} finally {
|
|
294
|
+
await client.end()
|
|
295
|
+
}
|
|
296
|
+
}
|