#!/usr/bin/env bash # Merge one player's rows from a source SQLite DB into a target SQLite DB. # # Usage (sourced or executed): # ./scripts/merge-player-db.sh # # Replaces any existing target user with the same username. # Copies (when present in both DBs): User, InventoryItem, Transaction, # PaymentOrder, CaseKeyProgress, CatalogEntry, PrestigeLog, UserAchievement. # Skips multiplayer state (Jackpot*/Duel*). # Tolerates older backups missing newer tables/columns (e.g. PaymentOrder). # # Requires matching Item / Case / Achievement ids in both DBs (same catalog lineage). # Requires sqlite3. set -euo pipefail SRC_DB="${1:-}" DST_DB="${2:-}" USERNAME="${3:-}" if [[ -z "$SRC_DB" || -z "$DST_DB" || -z "$USERNAME" ]]; then echo "Usage: $0 " >&2 exit 1 fi if [[ ! -f "$SRC_DB" ]]; then echo "Error: source DB not found: $SRC_DB" >&2 exit 1 fi if [[ ! -f "$DST_DB" ]]; then echo "Error: target DB not found: $DST_DB" >&2 echo "Tip: run a full import once first, or start the app so the DB exists." >&2 exit 1 fi if ! command -v sqlite3 >/dev/null 2>&1; then echo "Error: sqlite3 is required for player-only import" >&2 exit 1 fi sql_quote() { local s="$1" s="${s//\'/\'\'}" printf "'%s'" "$s" } table_exists() { local db="$1" local table="$2" local n n="$(sqlite3 "$db" "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name=$(sql_quote "$table");")" [[ "$n" -gt 0 ]] } # Column names for a table (space-separated), excluding names passed after $2 table_cols() { local db="$1" local table="$2" shift 2 local -A skip=() local e col for e in "$@"; do skip["$e"]=1 done while IFS='|' read -r _ col _; do [[ -n "$col" && -z "${skip[$col]:-}" ]] && printf '%s\n' "$col" done < <(sqlite3 -separator '|' "$db" "PRAGMA table_info($(sql_quote "$table"));") } # Intersection of columns present in BOTH DBs, preserving destination order. common_cols() { local table="$1" shift local -A src_set=() local col while IFS= read -r col; do [[ -n "$col" ]] && src_set["$col"]=1 done < <(table_cols "$SRC_DB" "$table" "$@") local -a out=() while IFS= read -r col; do [[ -n "$col" && -n "${src_set[$col]:-}" ]] && out+=("$col") done < <(table_cols "$DST_DB" "$table" "$@") local IFS=', ' echo "${out[*]}" } count_rows() { local table="$1" local qtable="$1" [[ "$table" == "Transaction" ]] && qtable='"Transaction"' if table_exists "$SRC_DB" "$table"; then sqlite3 "$SRC_DB" "SELECT COUNT(*) FROM $qtable WHERE userId = $SRC_UID;" else echo "skip" fi } SRC_UID="$(sqlite3 "$SRC_DB" "SELECT id FROM User WHERE username = $(sql_quote "$USERNAME") LIMIT 1;")" if [[ -z "$SRC_UID" ]]; then echo "Error: no user with username '$USERNAME' in source DB" >&2 exit 2 fi echo "==> Merging player '$USERNAME' (source id=$SRC_UID)" echo " source: $SRC_DB" echo " target: $DST_DB" N_INV="$(count_rows InventoryItem)" N_TX="$(count_rows Transaction)" N_PAY="$(count_rows PaymentOrder)" N_KEY="$(count_rows CaseKeyProgress)" N_CAT="$(count_rows CatalogEntry)" N_PRE="$(count_rows PrestigeLog)" N_ACH="$(count_rows UserAchievement)" echo " rows: inventory=$N_INV tx=$N_TX payments=$N_PAY keys=$N_KEY catalog=$N_CAT prestige=$N_PRE achievements=$N_ACH" USER_COLS="$(common_cols User id)" if [[ -z "$USER_COLS" ]]; then echo "Error: no common User columns between source and target" >&2 exit 1 fi INV_COLS="$(common_cols InventoryItem id userId)" TX_COLS="$(common_cols Transaction id userId)" if [[ -z "$INV_COLS" || -z "$TX_COLS" ]]; then echo "Error: InventoryItem / Transaction schema mismatch" >&2 exit 1 fi PAY_COLS="" if table_exists "$SRC_DB" PaymentOrder && table_exists "$DST_DB" PaymentOrder; then PAY_COLS="$(common_cols PaymentOrder userId)" fi KEY_COLS="" table_exists "$SRC_DB" CaseKeyProgress && table_exists "$DST_DB" CaseKeyProgress && KEY_COLS="$(common_cols CaseKeyProgress id userId)" CAT_COLS="" table_exists "$SRC_DB" CatalogEntry && table_exists "$DST_DB" CatalogEntry && CAT_COLS="$(common_cols CatalogEntry id userId)" PRE_COLS="" table_exists "$SRC_DB" PrestigeLog && table_exists "$DST_DB" PrestigeLog && PRE_COLS="$(common_cols PrestigeLog id userId)" ACH_COLS="" table_exists "$SRC_DB" UserAchievement && table_exists "$DST_DB" UserAchievement && ACH_COLS="$(common_cols UserAchievement id userId)" SQL_EXTRA_DELETE="" SQL_EXTRA_INSERT="" if [[ -n "$PAY_COLS" ]]; then SQL_EXTRA_DELETE+=" DELETE FROM PaymentOrder WHERE id IN (SELECT id FROM src.PaymentOrder WHERE userId = $SRC_UID); " SQL_EXTRA_INSERT+=" INSERT INTO PaymentOrder ($PAY_COLS, userId) SELECT $PAY_COLS, (SELECT newId FROM _merge_uid) FROM src.PaymentOrder WHERE userId = $SRC_UID; " else echo " note: PaymentOrder absent in backup — skipped (osu top-up orders)" fi if [[ -n "$KEY_COLS" ]]; then SQL_EXTRA_INSERT+=" INSERT INTO CaseKeyProgress (userId, $KEY_COLS) SELECT (SELECT newId FROM _merge_uid), $KEY_COLS FROM src.CaseKeyProgress WHERE userId = $SRC_UID; " fi if [[ -n "$CAT_COLS" ]]; then SQL_EXTRA_INSERT+=" INSERT INTO CatalogEntry (userId, $CAT_COLS) SELECT (SELECT newId FROM _merge_uid), $CAT_COLS FROM src.CatalogEntry WHERE userId = $SRC_UID; " fi if [[ -n "$PRE_COLS" ]]; then SQL_EXTRA_INSERT+=" INSERT INTO PrestigeLog (userId, $PRE_COLS) SELECT (SELECT newId FROM _merge_uid), $PRE_COLS FROM src.PrestigeLog WHERE userId = $SRC_UID; " fi if [[ -n "$ACH_COLS" ]]; then SQL_EXTRA_INSERT+=" INSERT INTO UserAchievement (userId, $ACH_COLS) SELECT (SELECT newId FROM _merge_uid), $ACH_COLS FROM src.UserAchievement WHERE userId = $SRC_UID; " fi SQL_CLEANUP="" table_exists "$DST_DB" JackpotRound && SQL_CLEANUP+=" UPDATE JackpotRound SET winnerUserId = NULL WHERE winnerUserId IN (SELECT id FROM User WHERE username = $(sql_quote "$USERNAME")); " table_exists "$DST_DB" DuelRoom && SQL_CLEANUP+=" UPDATE DuelRoom SET winnerUserId = NULL WHERE winnerUserId IN (SELECT id FROM User WHERE username = $(sql_quote "$USERNAME")); DELETE FROM DuelRoom WHERE creatorId IN (SELECT id FROM User WHERE username = $(sql_quote "$USERNAME")); " table_exists "$DST_DB" JackpotDeposit && SQL_CLEANUP+=" DELETE FROM JackpotDeposit WHERE userId IN (SELECT id FROM User WHERE username = $(sql_quote "$USERNAME")); " table_exists "$DST_DB" DuelDeposit && SQL_CLEANUP+=" DELETE FROM DuelDeposit WHERE userId IN (SELECT id FROM User WHERE username = $(sql_quote "$USERNAME")); " sqlite3 "$DST_DB" < Player merged as target id=$NEW_UID" echo " (jackpot/duel deposits & rooms not imported)"