@clickup/pg-microsharding
Version:
Microshards support for PostgreSQL
95 lines (91 loc) • 3.25 kB
text/typescript
import compact from "lodash/compact";
import partition from "lodash/partition";
import { pgDump } from "./names";
import type { Sql } from "./quote";
import { join, shellQuote } from "./quote";
import { runShell } from "./runShell";
// The downside of turning this on is that, if we run pg_dump over the migrated
// shard, then it will emit "ALTER TABLE ... ADD CONSTRAINT ... NOT VALID" (i.e.
// PG remembers that the FKs were not validated). This is not the end of the
// world, but it's not worth the ~5% overall speedup this option gives.
const SKIP_FK_VALIDATION = false;
/**
* Runs pg_dump, does some basic parsing and returns the result.
*/
export async function getDump({
fromDsn,
schema,
}: {
fromDsn: string;
schema: string;
}): Promise<{
preData: Sql;
primaryKeys: string[];
postData: Sql;
}> {
const preData = parseDump(
await runShell(
`${pgDump(fromDsn)} -n ${shellQuote(schema)} --section=pre-data`,
null,
`Reading pre-data DDL (except indexes) for ${schema} from ${fromDsn}`,
),
);
const postDataAll = parseDump(
await runShell(
`${pgDump(fromDsn)} -n ${shellQuote(schema)} --section=post-data`,
null,
`Reading post-data DDL (indexes, foreign keys) for ${schema} from ${fromDsn}`,
),
);
// For logical replication to work, each table on the subscription part must
// have an index that allows the logical worker to find the row it receives
// from the publication end. PostgreSQL 16+ can use regular indexes for that,
// but the earlier versions require the subscription end to have either a
// REPLICA IDENTITY or a PRIMARY KEY. Thus, we extract such indexes from the
// post-data section of the dump and create them before creating the
// subscription.
const [primaryKeys, postData] = partition(
postDataAll,
(entry) =>
entry.match(
/^ALTER TABLE .* ADD CONSTRAINT .* PRIMARY KEY \([^)]+\);$/s,
) ||
entry.match(
/^CREATE UNIQUE INDEX (.*?) ON .*\nALTER TABLE .*? REPLICA IDENTITY USING INDEX \1;$/s,
),
);
return {
preData: join(preData, "\n"),
primaryKeys,
postData: join(postData, "\n"),
};
}
/**
* The text generated by pg_dump consists of entries divided by 3 lines of
* "--"-comments. We split the dump into that entries, and only then analyze it
* in the further logic.
*/
function parseDump(dump: string): string[] {
const entries = compact(
dump
.split(/^--\n-- \w+[^\n]*\n--\n/m)
.map((entry) => entry.trim())
// Added in PG17+ and is not supported in earlier versions.
.map((entry) =>
entry.replace(/^SET transaction_timeout = [^\n]+/m, "-- $&"),
)
// If enabled, makes FKs creation instant: we don't need to check
// integrity at creation time, since the data on the source is already
// validated. (It will still check at tuples write time though.)
.map((entry) =>
SKIP_FK_VALIDATION
? entry.replace(
/^(ALTER TABLE ONLY [^\n]+\n +ADD CONSTRAINT [^\n]+ FOREIGN KEY [^\n]+ REFERENCES [^\n]+)(;)$/s,
"$1 NOT VALID$2",
)
: entry,
),
);
entries[0] = entries[0].replace(/\n\n+/gs, "\n");
return entries;
}