@@ -32,7 +32,11 @@ pub fn migration_job_name(replica_name: &str) -> String {
3232/// SCHEMAS (comma-separated list)
3333/// MIGRATION_CALLBACK_URL
3434static MIGRATION_SCRIPT : & str = r#"#!/bin/bash
35- set -o pipefail
35+ # pipefail is intentionally NOT set: psql's per-statement failures don't
36+ # propagate into the script's exit code (see ON_ERROR_STOP discussion
37+ # below), and pg_dump can fail mid-stream after producing partial output
38+ # — we still want psql to apply whatever it did receive, then exit
39+ # normally so the replica can come up.
3640
3741# Parse comma-separated schema list
3842IFS=',' read -ra SCHEMA_ARRAY <<< "$SCHEMAS"
@@ -58,6 +62,17 @@ for schema in "${SCHEMA_ARRAY[@]}"; do
5862 echo "Migrating schema: $schema"
5963done
6064
65+ # Capture psql's stderr for visibility on partial failures.
66+ PSQL_STDERR=$(mktemp)
67+
68+ # ON_ERROR_STOP is deliberately NOT set: persistent_schemas like dbt
69+ # contain views derived from upstream tables, and across upstream schema
70+ # changes (renamed columns, dropped tables) some view DDL in the old
71+ # replica's schema becomes invalid against the new restore's source
72+ # tables. Failing the whole migration on the first such error blocks the
73+ # replica from coming up at all. Tolerance trades schema completeness
74+ # for replica availability — clients can regenerate the broken views
75+ # afterward, but the replica must be reachable.
6176PGPASSWORD="$SOURCE_PASSWORD" pg_dump \
6277 -h "$SOURCE_HOST" -p 5432 -U "$SOURCE_USER" -d "$SOURCE_DB" \
6378 "${SCHEMA_ARGS[@]}" \
@@ -66,21 +81,30 @@ PGPASSWORD="$SOURCE_PASSWORD" pg_dump \
6681 --verbose \
6782| PGPASSWORD="$TARGET_PASSWORD" psql \
6883 -h "$TARGET_HOST" -p 5432 -U "$TARGET_USER" -d "$TARGET_DB" \
69- -v ON_ERROR_STOP=1 -- quiet
84+ -- quiet 2> >(tee "$PSQL_STDERR" >&2)
7085
71- EXIT_CODE=$?
86+ PSQL_EXIT=$?
87+ PSQL_ERROR_COUNT=$(grep -c '^ERROR:' "$PSQL_STDERR" 2>/dev/null || echo 0)
88+ PSQL_ERROR_COUNT=${PSQL_ERROR_COUNT:-0}
89+ rm -f "$PSQL_STDERR"
7290
73- if [ $EXIT_CODE -eq 0 ]; then
74- echo ""
91+ echo ""
92+ if [ "$PSQL_EXIT" -ne 0 ]; then
93+ echo "=== psql exited non-zero ($PSQL_EXIT); proceeding so the replica can come up ===" >&2
94+ fi
95+
96+ if [ "$PSQL_ERROR_COUNT" -gt 0 ]; then
97+ echo "=== Schema migration tolerated $PSQL_ERROR_COUNT statement error(s); some objects may need regenerating ===" >&2
98+ report_result "partial: $PSQL_ERROR_COUNT statement error(s)"
99+ else
75100 echo "=== Schema migration completed successfully ==="
76101 report_result 'success'
77- else
78- echo ""
79- echo "=== Schema migration failed with exit code $EXIT_CODE ===" >&2
80- report_result "Migration failed with exit code $EXIT_CODE"
81102fi
82103
83- exit $EXIT_CODE
104+ # Always exit 0: any non-fatal issues are reported via the callback
105+ # above. Treating partial migrations as Job failures puts the operator
106+ # into a retry loop that never converges (the same views keep failing).
107+ exit 0
84108"# ;
85109
86110/// Build the schema migration Job spec.
@@ -266,6 +290,29 @@ mod tests {
266290 }
267291 }
268292
293+ #[ test]
294+ fn migration_script_is_tolerant_to_statement_errors ( ) {
295+ // The migration script must NOT use `ON_ERROR_STOP=1`. Persistent
296+ // schemas (e.g. dbt) contain views derived from upstream tables;
297+ // when upstream schema migrations rename or drop those columns,
298+ // some view recreations fail. Aborting the entire migration on
299+ // the first such error blocks the replica from coming up, which
300+ // is a worse outcome than a partial migration that clients can
301+ // patch up afterwards.
302+ assert ! (
303+ !MIGRATION_SCRIPT . contains( "ON_ERROR_STOP=1" ) ,
304+ "migration script must not enable ON_ERROR_STOP=1 — statement errors should be tolerated so the replica can come up"
305+ ) ;
306+ assert ! (
307+ MIGRATION_SCRIPT . contains( "exit 0" ) ,
308+ "migration script must exit 0 on completion; non-fatal errors are reported via the callback body"
309+ ) ;
310+ assert ! (
311+ MIGRATION_SCRIPT . contains( "partial" ) ,
312+ "migration script must report partial migrations via the callback so the operator can surface them"
313+ ) ;
314+ }
315+
269316 #[ test]
270317 fn migration_job_name_format ( ) {
271318 assert_eq ! (
0 commit comments