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.
@@ -1,3 +1,4 @@
1
+ import postgres from 'postgres'
1
2
  import { sql } from '../query/index.js'
2
3
  import type { Manifold } from '../../core/pipework.js'
3
4
  import type { ResolvedDatabase } from '../../core/config/index.js'
@@ -6,6 +7,7 @@ import { OWNER_LABEL_MIGRATE } from '../db/owner-labels.js'
6
7
  import { checkMigrations, type MigrationCheckResult } from './check.js'
7
8
  import { readAppliedState } from './applied.js'
8
9
  import { applyMigrations, type MigrationNotice } from './apply.js'
10
+ import { repairInvalidIndexes, InvalidIndexRepairError, type InvalidIndex } from './invalid-indexes.js'
9
11
  import { provisionFrameworkTables, type FrameworkTablesResult } from './provision-framework.js'
10
12
 
11
13
  // Deliberately empty of hazard switches. Authorization for a hazardous
@@ -24,8 +26,16 @@ export interface MigrateResult {
24
26
  safetyCheck: MigrationCheckResult
25
27
  /** No-transaction files picked up half applied and finished. Issue #378. */
26
28
  resumed: ResumedMigration[]
27
- /** INVALID indexes a cut CREATE INDEX CONCURRENTLY left, dropped before the resumed statement reran. Issue #1003. */
29
+ /** INVALID indexes a cut CREATE INDEX CONCURRENTLY left, dropped before the statement that builds them reran. Issues #1003, #1005. */
28
30
  droppedInvalidIndexes: DroppedInvalidIndex[]
31
+ /**
32
+ * Every INVALID index this run met, with its case, what was done or must be
33
+ * done, and the outcome: (a) an owned index rebuilt, (b) a leftover of an owned
34
+ * index dropped, (c) anything else, untouched. Issue #1005.
35
+ */
36
+ invalidIndexes: InvalidIndex[]
37
+ /** Set when the catalog read for INVALID indexes failed, so none were checked. Issue #1005. */
38
+ invalidIndexReadError: string | null
29
39
  /** Notices and warnings raised by migration statements, in arrival order. Issue #970. */
30
40
  notices: MigrateNotice[]
31
41
  /** Trace/audit tables, columns and day partitions this run provisioned. */
@@ -54,6 +64,8 @@ export interface MigrateNotice extends MigrationNotice {
54
64
  statements: number
55
65
  }
56
66
 
67
+ export { InvalidIndexRepairError, type InvalidIndex }
68
+
57
69
  export class UnsafeMigrationError extends Error {
58
70
  override readonly name = 'UnsafeMigrationError'
59
71
  constructor(
@@ -76,7 +88,34 @@ export async function migrateAll(instance: Manifold, options: MigrateOptions = {
76
88
  return results
77
89
  }
78
90
 
91
+ /** One key per database: advisory locks are scoped to the database they are taken in. */
92
+ const MIGRATE_LOCK_KEY = `hashtextextended('pipework.migrate', 0)`
93
+
94
+ /**
95
+ * The connection that holds the run lock. It must live as long as the run: the
96
+ * default `max_lifetime` would close it mid-run and release the lock, so both
97
+ * timeouts are off, as in src/test/setup.ts.
98
+ */
99
+ export function openRunLock(url: string): ReturnType<typeof postgres> {
100
+ return postgres(url, { max: 1, idle_timeout: 0, max_lifetime: 0, onnotice: () => {} })
101
+ }
102
+
103
+ /**
104
+ * Runs one database's migrations and index repairs under a session advisory
105
+ * lock held for the whole run. A second run at once waits for it, then finds
106
+ * nothing to apply. The lock dies with its connection. Issue #1005.
107
+ */
79
108
  export async function migrateOne(instance: Manifold, dbConfig: ResolvedDatabase, options: MigrateOptions = {}): Promise<MigrateResult> {
109
+ const lock = openRunLock(dbConfig.url)
110
+ try {
111
+ await lock.unsafe(`SELECT pg_advisory_lock(${MIGRATE_LOCK_KEY})`)
112
+ return await migrateLocked(instance, dbConfig, options)
113
+ } finally {
114
+ await lock.end()
115
+ }
116
+ }
117
+
118
+ async function migrateLocked(instance: Manifold, dbConfig: ResolvedDatabase, options: MigrateOptions): Promise<MigrateResult> {
80
119
  const conn = instance.pool.getOrCreate(dbConfig, 'owner', OWNER_LABEL_MIGRATE)
81
120
 
82
121
  // Scope the safety gate to pending migrations only — hazards in already-applied
@@ -124,6 +163,15 @@ export async function migrateOne(instance: Manifold, dbConfig: ResolvedDatabase,
124
163
  },
125
164
  })
126
165
 
166
+ // After a run that finished without error only; a failing migration throws
167
+ // above and its rerun resumes and checks then. Throws when an owned index
168
+ // cannot be made valid.
169
+ const repaired = await repairInvalidIndexes({
170
+ migrationsFolder: dbConfig.migrations!,
171
+ migrationsTable: dbConfig.migrationTable,
172
+ connectionUrl: dbConfig.url,
173
+ })
174
+
127
175
  // Trace and audit storage rides the default database; their partitioned
128
176
  // tables are kept out of generated migrations (#313), so migrate provisions them.
129
177
  const frameworkTables = await provisionFrameworkTables(
@@ -146,6 +194,8 @@ export async function migrateOne(instance: Manifold, dbConfig: ResolvedDatabase,
146
194
  safetyCheck,
147
195
  resumed,
148
196
  droppedInvalidIndexes,
197
+ invalidIndexes: repaired.indexes,
198
+ invalidIndexReadError: repaired.readError,
149
199
  notices,
150
200
  frameworkTables,
151
201
  }