Make dev->prod deploy work for non-superuser role and fix image sync.
restore-db-data.js: fall back to multi-pass insert when the DB role cannot set session_replication_role, and parse multi-line INSERT values (bios) as whole statements. sync-images-to-prod.ps1: fix SMB root variable typo and fail fast with guidance when the share is not mapped. Document SMB mapping, non-superuser restore, and a troubleshooting table in the release runbook. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
ac1ae5794f
commit
313666a4ab
+100
-11
@@ -28,11 +28,46 @@ function parseArgs(argv) {
|
||||
};
|
||||
}
|
||||
|
||||
// A single-quoted SQL string is closed only when the running count of quote
|
||||
// characters is even (doubled '' escapes count as two, so they stay even).
|
||||
function quotesBalanced(text) {
|
||||
let count = 0;
|
||||
for (let i = 0; i < text.length; i += 1) {
|
||||
if (text[i] === "'") count += 1;
|
||||
}
|
||||
return count % 2 === 0;
|
||||
}
|
||||
|
||||
// Values (e.g. artist bios) can contain embedded newlines, so a statement may
|
||||
// span multiple physical lines. Accumulate lines until the quotes are balanced
|
||||
// and the statement ends with ';'.
|
||||
function extractInsertStatements(content) {
|
||||
return content
|
||||
.split(/\r?\n/)
|
||||
.map((line) => line.trim())
|
||||
.filter((line) => line.startsWith('INSERT INTO '));
|
||||
const lines = content.split(/\r?\n/);
|
||||
const statements = [];
|
||||
let current = null;
|
||||
|
||||
for (const line of lines) {
|
||||
if (current === null) {
|
||||
if (line.startsWith('INSERT INTO ')) {
|
||||
current = line;
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
current += `\n${line}`;
|
||||
}
|
||||
|
||||
if (quotesBalanced(current) && current.trimEnd().endsWith(';')) {
|
||||
statements.push(current);
|
||||
current = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (current !== null) {
|
||||
statements.push(current);
|
||||
}
|
||||
|
||||
return statements;
|
||||
}
|
||||
|
||||
async function truncatePublicTables(client) {
|
||||
@@ -79,19 +114,73 @@ async function main() {
|
||||
console.log(`Truncating public tables in "${dbName}"...`);
|
||||
await truncatePublicTables(client);
|
||||
|
||||
// Fast path: disable FK/trigger checks for a single-pass load. Requires
|
||||
// permission to set session_replication_role (superuser, or on PG 15+ a
|
||||
// "GRANT SET ON PARAMETER session_replication_role TO <role>"). If the role
|
||||
// lacks that privilege, fall back to a multi-pass insert that retries rows
|
||||
// whose foreign keys are not yet satisfied, so no superuser is needed.
|
||||
let replicaMode = false;
|
||||
try {
|
||||
await client.query('SET session_replication_role = replica');
|
||||
replicaMode = true;
|
||||
} catch (err) {
|
||||
if (err.code === '42501') {
|
||||
console.warn(
|
||||
' note: cannot set session_replication_role (role is not superuser); using multi-pass insert',
|
||||
);
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Restoring ${inserts.length} INSERT statements...`);
|
||||
await client.query('SET session_replication_role = replica');
|
||||
let restored = 0;
|
||||
try {
|
||||
for (const statement of inserts) {
|
||||
await client.query(statement);
|
||||
restored += 1;
|
||||
if (restored % 500 === 0) {
|
||||
console.log(` ${restored}/${inserts.length}`);
|
||||
if (replicaMode) {
|
||||
for (const statement of inserts) {
|
||||
await client.query(statement);
|
||||
restored += 1;
|
||||
if (restored % 500 === 0) {
|
||||
console.log(` ${restored}/${inserts.length}`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Each statement auto-commits on its own, so a foreign-key failure only
|
||||
// rejects that one row; retry it on a later pass once its parent exists.
|
||||
let remaining = inserts;
|
||||
let pass = 0;
|
||||
while (remaining.length > 0) {
|
||||
pass += 1;
|
||||
const retry = [];
|
||||
let progressed = 0;
|
||||
for (const statement of remaining) {
|
||||
try {
|
||||
await client.query(statement);
|
||||
restored += 1;
|
||||
progressed += 1;
|
||||
if (restored % 500 === 0) {
|
||||
console.log(` ${restored}/${inserts.length}`);
|
||||
}
|
||||
} catch (err) {
|
||||
if (err.code === '23503') {
|
||||
retry.push(statement);
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (progressed === 0) {
|
||||
throw new Error(
|
||||
`Restore stalled on pass ${pass}: ${retry.length} rows have unresolved foreign keys`,
|
||||
);
|
||||
}
|
||||
remaining = retry;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
await client.query('SET session_replication_role = DEFAULT');
|
||||
if (replicaMode) {
|
||||
await client.query('SET session_replication_role = DEFAULT');
|
||||
}
|
||||
}
|
||||
|
||||
await client.end();
|
||||
|
||||
Reference in New Issue
Block a user