postgres: a withdrawn consumer keeps its database; retiring renames, never drops (hq issue 241)
This commit is contained in:
@@ -145,14 +145,43 @@ export class PostgresClient {
|
||||
}
|
||||
}
|
||||
|
||||
/** Drop a database and its owning role, idempotently, after evicting live connections. */
|
||||
async dropDatabaseAndRole(database: string, role: string): Promise<void> {
|
||||
/**
|
||||
* Withdraw a consumer without destroying anything (novox/hq issue 241): its login can no longer log
|
||||
* in and its open connections are ended, and its database stays exactly as it was, under its own
|
||||
* name. A consumer that comes back is given the same database — create sets LOGIN again — which is
|
||||
* what a provider must do when "no longer asked for" turns out to be a moment's misreading.
|
||||
*/
|
||||
async lockRole(role: string): Promise<void> {
|
||||
const roles = await this.query("SELECT 1 FROM pg_roles WHERE rolname = " + literal(role));
|
||||
if (roles.rows.length === 0) return;
|
||||
await this.query(`ALTER ROLE ${ident(role)} NOLOGIN`);
|
||||
await this.query(
|
||||
"SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE usename = " +
|
||||
literal(role) + " AND pid <> pg_backend_pid()",
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Take a database out of service on purpose: rename it aside to `<name>_deleted_<date>` and lock
|
||||
* its owner. Never a drop — the data stays on the server under the new name until a person
|
||||
* removes it by hand. Returns the name it now has.
|
||||
*/
|
||||
async retireDatabase(database: string, now: Date = new Date()): Promise<string> {
|
||||
const found = await this.query(
|
||||
"SELECT pg_get_userbyid(datdba) AS owner FROM pg_database WHERE datname = " + literal(database),
|
||||
);
|
||||
if (found.rows.length === 0) throw new Error(`no database named ${database}`);
|
||||
const owner = String((found.rows[0] as Record<string, unknown>).owner ?? "");
|
||||
const aside = retiredName(database, now);
|
||||
const taken = await this.query("SELECT 1 FROM pg_database WHERE datname = " + literal(aside));
|
||||
if (taken.rows.length > 0) throw new Error(`${aside} already exists; retire it by hand first`);
|
||||
if (owner && owner !== "postgres") await this.query(`ALTER ROLE ${ident(owner)} NOLOGIN`);
|
||||
await this.query(
|
||||
"SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = " +
|
||||
literal(database) + " AND pid <> pg_backend_pid()",
|
||||
);
|
||||
await this.query(`DROP DATABASE IF EXISTS ${ident(database)}`);
|
||||
await this.query(`DROP ROLE IF EXISTS ${ident(role)}`);
|
||||
await this.query(`ALTER DATABASE ${ident(database)} RENAME TO ${ident(aside)}`);
|
||||
return aside;
|
||||
}
|
||||
|
||||
/** List the non-template databases, with size, for the postgres_list_databases tool. */
|
||||
@@ -301,3 +330,10 @@ function parseCsv(text: string): string[][] {
|
||||
endRecord();
|
||||
return records;
|
||||
}
|
||||
|
||||
/** The name a retired database is renamed to: `<name>_deleted_<yyyymmdd>`, within postgres's 63 bytes. */
|
||||
export function retiredName(database: string, now: Date = new Date()): string {
|
||||
const stamp = now.toISOString().slice(0, 10).replace(/-/g, "");
|
||||
const suffix = `_deleted_${stamp}`;
|
||||
return database.slice(0, 63 - suffix.length) + suffix;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user