Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 42 additions & 3 deletions bin/local-dev/main.sh
Original file line number Diff line number Diff line change
Expand Up @@ -1398,6 +1398,33 @@ parse_changelog_changesets() {
' "$changelog"
}

# True when every error psql reported is a complaint that the object it tried
# to create is already there — i.e. the changeSet's effect is already in the
# schema and replaying it was a no-op.
#
# Why this is needed: on a fresh volume postgres runs sql/texera_ddl.sql itself
# (compose mounts ../../sql to /docker-entrypoint-initdb.d), and that DDL is
# kept in sync with sql/updates/*, so the changeSets we replay right afterwards
# re-create objects that already exist. The ones incidentally written with
# IF NOT EXISTS pass; the rest aborted `up` before the build ever started
# (#7064) — e.g. 28.sql's dataset_owner_uid_name_key, which texera_ddl.sql
# already creates as the auto-generated name for UNIQUE (owner_uid, name).
#
# Deliberately narrow. Only `already exists` counts:
# • `duplicate key` is a data conflict, not an applied schema change
# • no ERROR line at all means we cannot see why psql failed, and a failure
# we can't explain is never assumed harmless
# Everything else — syntax errors, missing relations, permissions — keeps
# failing loudly, so a genuinely incomplete schema still stops the build
# instead of reaching jOOQ codegen.
_sql_errors_all_already_exist() {
local f="${1:-}" errs=""
[[ -f "$f" ]] || return 1
errs=$(grep 'ERROR:' "$f" 2>/dev/null) || return 1
[[ -n "$errs" ]] || return 1
! printf '%s\n' "$errs" | grep -qv 'already exists'
}

# Reconcile sql/updates/* with the live DB so jOOQ codegen (which reads the
# database at sbt-compile time) sees the schema the checked-out code expects.
# The repo's official runner is liquibase (sql/docker-compose.yml — manual,
Expand Down Expand Up @@ -1465,12 +1492,24 @@ infra_apply_sql_updates() {
return 1
fi
tui_step "postgres: applying $path (changeSet $id)"
# Keep psql's stderr: it holds the one line that explains an abort,
# and _sql_errors_all_already_exist needs it to tell "this changeSet
# is already in the schema" apart from a real failure.
local psql_err="$LOG_DIR/psql-changeset-$id.err"
if ! sed 's/^\\c.*$//' "$sql_file" \
| docker exec -i "$pg" psql -U texera -d texera_db \
-v ON_ERROR_STOP=1 -f - >/dev/null 2>&1; then
tui_err "postgres: $path failed -- inspect with: docker exec -i texera-postgres psql -U texera -d texera_db < $path"
return 1
-v ON_ERROR_STOP=1 -f - >/dev/null 2>"$psql_err"; then
if _sql_errors_all_already_exist "$psql_err"; then
# Fall through to the INSERT below so the changeSet is
# recorded as applied and later runs stop retrying it.
tui_skip "postgres: $path already in schema (recording changeSet $id)"
else
tui_err "postgres: $path failed -- inspect with: docker exec -i texera-postgres psql -U texera -d texera_db < $path"
sed 's/^/ /' "$psql_err" >&2
return 1
fi
fi
rm -f "$psql_err"
fi
docker exec "$pg" psql -U texera -d texera_db -qc "
INSERT INTO public.databasechangelog
Expand Down
91 changes: 91 additions & 0 deletions bin/local-dev/tests/test_local_dev_sh.sh
Original file line number Diff line number Diff line change
Expand Up @@ -593,5 +593,96 @@ else
_fail "changelog references missing files:$missing_files"
fi

# 29) A changeSet the DB already satisfies must not abort `up`. On a fresh
# volume postgres' own /docker-entrypoint-initdb.d runs sql/texera_ddl.sql,
# which is kept in sync with sql/updates/*, so replaying those changeSets
# re-creates objects that are already there; the ones not written with
# IF NOT EXISTS used to kill `up` before the build ever started (#7064).
# `_sql_errors_all_already_exist` is the gate that tells that case apart
# from a real failure, so it's tested in both directions: it must say yes
# only for "already exists", and no for every other psql error, for a
# duplicate-key data conflict, and for input where it can't see any error
# at all (never assume harmless).
tolerate_fn=$(awk '/^_sql_errors_all_already_exist\(\)/{f=1} f{print} f&&/^}/{exit}' "$MAIN_SH")
if [[ -z "$tolerate_fn" ]]; then
_fail "_sql_errors_all_already_exist helper missing"
else
_tol_dir=$(mktemp -d 2>/dev/null || mktemp -d -t ldtol)
_tol_check() { # $1=label $2=expected rc $3=stderr contents
printf '%s' "$3" > "$_tol_dir/err"
local rc=0
( eval "$tolerate_fn"; _sql_errors_all_already_exist "$_tol_dir/err" ) || rc=$?
if (( rc == $2 )); then
_pass "already-applied detector: $1"
else
_fail "already-applied detector: $1" "expected rc=$2, got rc=$rc"
fi
}
# Positive: the real #7064 failure, and the other spellings postgres uses
# for an object that is already present.
_tol_check "relation already exists (the #7064 failure)" 0 \
'psql:<stdin>:44: ERROR: relation "dataset_owner_uid_name_key" already exists'
_tol_check "several already-exists errors, nothing else" 0 \
'psql:<stdin>:9: ERROR: column "x" of relation "dataset" already exists
psql:<stdin>:12: ERROR: constraint "y" for relation "dataset" already exists
psql:<stdin>:15: ERROR: type "z" already exists'
_tol_check "already-exists around harmless NOTICE/ROLLBACK chatter" 0 \
'NOTICE: Renamed 0 duplicate dataset name(s)
psql:<stdin>:44: ERROR: relation "dataset_owner_uid_name_key" already exists
ROLLBACK'
_tol_check "non-ASCII identifier already exists" 0 \
'psql:<stdin>:3: ERROR: relation "数据集_名称_key" already exists'
# Negative: anything we can't positively identify as already-applied has to
# keep failing loudly, or a genuinely broken schema reaches the sbt build.
_tol_check "syntax error" 1 \
'psql:<stdin>:7: ERROR: syntax error at or near "ALTERR"'
_tol_check "missing relation" 1 \
'psql:<stdin>:7: ERROR: relation "dataset" does not exist'
_tol_check "one already-exists mixed with one real error" 1 \
'psql:<stdin>:44: ERROR: relation "dataset_owner_uid_name_key" already exists
psql:<stdin>:51: ERROR: syntax error at or near "COMMITT"'
_tol_check "duplicate key is a data conflict, not an applied change" 1 \
'psql:<stdin>:44: ERROR: duplicate key value violates unique constraint "dataset_pkey"'
_tol_check "empty stderr" 1 ''
_tol_check "no ERROR line at all" 1 \
'NOTICE: table "dataset" does not exist, skipping'
# Edge: nothing to read. Guard against the helper "succeeding" on a path
# that was never written, which would swallow every failure.
rm -f "$_tol_dir/err"
rc=0
( eval "$tolerate_fn"; _sql_errors_all_already_exist "$_tol_dir/err" ) || rc=$?
if (( rc == 1 )); then
_pass "already-applied detector: missing stderr file"
else
_fail "already-applied detector: missing stderr file" "expected rc=1, got rc=$rc"
fi
rc=0
( eval "$tolerate_fn"; _sql_errors_all_already_exist ) || rc=$?
if (( rc == 1 )); then
_pass "already-applied detector: no argument"
else
_fail "already-applied detector: no argument" "expected rc=1, got rc=$rc"
fi
rm -rf "$_tol_dir"
fi

# 30) Wiring for #29: the replay loop must consult the detector instead of
# aborting on the first psql failure, and must stop discarding psql's
# stderr — the old `2>&1` to /dev/null meant the one line that explains the
# abort ("relation ... already exists") never reached the operator, who was
# told to re-run the file by hand to find out why.
updates_body=$(awk '/^infra_apply_sql_updates\(\)/{f=1} f{print} f&&/^}/{exit}' "$MAIN_SH")
if [[ "$updates_body" == *"_sql_errors_all_already_exist"* ]]; then
_pass "infra_apply_sql_updates consults the already-applied detector"
else
_fail "infra_apply_sql_updates aborts without checking for already-applied changeSets"
fi
if [[ "$updates_body" == *"ON_ERROR_STOP"* ]] \
&& ! printf '%s' "$updates_body" | grep -qE '\-f -[[:space:]]*>/dev/null[[:space:]]*2>&1'; then
_pass "infra_apply_sql_updates keeps psql stderr for diagnosis"
else
_fail "infra_apply_sql_updates still throws psql stderr away"
fi

printf "\n%d passed, %d failed\n" "$PASS" "$FAIL"
(( FAIL == 0 ))
Loading