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:
Danila Khodjaef
2026-07-07 15:26:35 +03:00
co-authored by Cursor
parent ac1ae5794f
commit 313666a4ab
3 changed files with 141 additions and 13 deletions
+100 -11
View File
@@ -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();