Harden scripts and restore workflow
This commit is contained in:
233
scripts/restore.sh
Executable file
233
scripts/restore.sh
Executable file
@@ -0,0 +1,233 @@
|
||||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
ts() { date +"%Y-%m-%d %H:%M:%S"; }
|
||||
log() { echo "[$(ts)] [INFO] $*"; }
|
||||
warn(){ echo "[$(ts)] [WARN] $*" >&2; }
|
||||
err() { echo "[$(ts)] [ERROR] $*" >&2; }
|
||||
die() { err "$*"; exit 1; }
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage:
|
||||
./scripts/restore.sh <backup_file.bak> <target_db_name> [options]
|
||||
|
||||
Options:
|
||||
--replace Overwrite existing database.
|
||||
--compat <level> Set compatibility level after restore (e.g. 170).
|
||||
--recovery <model> Set recovery model after restore: SIMPLE|FULL|BULK_LOGGED
|
||||
Default: SIMPLE (recommended for local dev).
|
||||
--no-tuning Skip post-restore tuning (compat/recovery changes).
|
||||
|
||||
Examples:
|
||||
./scripts/restore.sh KundenDB.bak KundenDB
|
||||
./scripts/restore.sh KundenDB.bak KundenDB --replace
|
||||
./scripts/restore.sh KundenDB.bak KundenDB --compat 170 --recovery SIMPLE
|
||||
./scripts/restore.sh KundenDB.bak KundenDB --no-tuning
|
||||
|
||||
Notes:
|
||||
- Put the .bak into ./backup/ (host) so it appears in the container as:
|
||||
/var/opt/mssql/backup/<backup_file.bak>
|
||||
- Requires a running, healthy container named "mssql2025".
|
||||
EOF
|
||||
}
|
||||
|
||||
dump_logs() {
|
||||
warn "Container logs (last 250 lines):"
|
||||
docker logs --tail=250 mssql2025 2>/dev/null || true
|
||||
}
|
||||
|
||||
on_error() {
|
||||
local code=$?
|
||||
err "Restore failed (exit code=$code)."
|
||||
dump_logs
|
||||
exit "$code"
|
||||
}
|
||||
trap on_error ERR
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
# -------- Preflight --------
|
||||
command -v docker >/dev/null 2>&1 || die "docker not found in PATH."
|
||||
docker info >/dev/null 2>&1 || die "docker daemon not reachable."
|
||||
|
||||
[[ -f .env ]] || die "Missing .env. Create it first: cp .env.example .env"
|
||||
set -a
|
||||
# shellcheck disable=SC1091
|
||||
source .env
|
||||
set +a
|
||||
[[ -n "${MSSQL_SA_PASSWORD:-}" ]] || die "MSSQL_SA_PASSWORD is empty in .env"
|
||||
|
||||
# -------- Args --------
|
||||
[[ $# -ge 2 ]] || { usage; exit 1; }
|
||||
|
||||
bak_file="$1"
|
||||
target_db="$2"
|
||||
shift 2
|
||||
|
||||
replace="0"
|
||||
compat_level=""
|
||||
recovery_model="SIMPLE"
|
||||
do_tuning="1"
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--replace)
|
||||
replace="1"; shift ;;
|
||||
--compat)
|
||||
[[ $# -ge 2 ]] || die "--compat requires a value (e.g. 170)"
|
||||
compat_level="$2"
|
||||
shift 2 ;;
|
||||
--recovery)
|
||||
[[ $# -ge 2 ]] || die "--recovery requires SIMPLE|FULL|BULK_LOGGED"
|
||||
recovery_model="$2"
|
||||
shift 2 ;;
|
||||
--no-tuning)
|
||||
do_tuning="0"; shift ;;
|
||||
-h|--help)
|
||||
usage; exit 0 ;;
|
||||
*)
|
||||
die "Unknown argument: $1 (use --help)" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Validate recovery model
|
||||
case "$recovery_model" in
|
||||
SIMPLE|FULL|BULK_LOGGED) ;;
|
||||
*)
|
||||
die "Invalid --recovery value: '$recovery_model'. Use SIMPLE|FULL|BULK_LOGGED." ;;
|
||||
esac
|
||||
|
||||
# Validate compat level if provided (basic numeric check)
|
||||
if [[ -n "$compat_level" ]]; then
|
||||
[[ "$compat_level" =~ ^[0-9]+$ ]] || die "Invalid --compat value: '$compat_level' (must be numeric, e.g. 170)."
|
||||
fi
|
||||
|
||||
# Container must be running and healthy
|
||||
if ! docker ps --format '{{.Names}}' | grep -qx 'mssql2025'; then
|
||||
die "Container mssql2025 is not running. Start it first: ./scripts/start.sh"
|
||||
fi
|
||||
health="$(docker inspect -f '{{.State.Health.Status}}' mssql2025 2>/dev/null || true)"
|
||||
[[ "$health" == "healthy" ]] || die "Container is not healthy (health=$health). Run ./scripts/start.sh"
|
||||
|
||||
# Backup must exist on host
|
||||
host_bak_path="./backup/$bak_file"
|
||||
[[ -f "$host_bak_path" ]] || die "Backup file not found: $host_bak_path"
|
||||
|
||||
container_bak_path="/var/opt/mssql/backup/$bak_file"
|
||||
|
||||
log "Restore requested:"
|
||||
log " backup : $host_bak_path (container: $container_bak_path)"
|
||||
log " target : $target_db"
|
||||
log " replace : $replace"
|
||||
log " tuning : $do_tuning (recovery=$recovery_model${compat_level:+, compat=$compat_level})"
|
||||
|
||||
# -------- Get logical file names from backup --------
|
||||
log "Reading FILELISTONLY..."
|
||||
filelist=$(
|
||||
docker exec -i mssql2025 /opt/mssql-tools18/bin/sqlcmd \
|
||||
-S localhost -U sa -P "$MSSQL_SA_PASSWORD" \
|
||||
-W -s"|" -h -1 \
|
||||
-Q "SET NOCOUNT ON; RESTORE FILELISTONLY FROM DISK = N'$container_bak_path';" \
|
||||
< /dev/null \
|
||||
| tr -d '\r'
|
||||
)
|
||||
|
||||
[[ -n "$filelist" ]] || die "RESTORE FILELISTONLY returned no output. Is the .bak readable?"
|
||||
|
||||
data_logical="$(echo "$filelist" | awk -F'|' '$3 ~ /^D$/ {print $1; exit}')"
|
||||
log_logical="$(echo "$filelist" | awk -F'|' '$3 ~ /^L$/ {print $1; exit}')"
|
||||
|
||||
[[ -n "$data_logical" ]] || die "Could not detect data logical name (Type=D) from FILELISTONLY."
|
||||
[[ -n "$log_logical" ]] || die "Could not detect log logical name (Type=L) from FILELISTONLY."
|
||||
|
||||
log "Detected logical names:"
|
||||
log " DATA: $data_logical"
|
||||
log " LOG : $log_logical"
|
||||
|
||||
# Destination files in container
|
||||
dest_mdf="/var/opt/mssql/data/${target_db}.mdf"
|
||||
dest_ldf="/var/opt/mssql/data/${target_db}_log.ldf"
|
||||
|
||||
# -------- Build RESTORE DATABASE command --------
|
||||
with_opts="MOVE N'$data_logical' TO N'$dest_mdf',
|
||||
MOVE N'$log_logical' TO N'$dest_ldf',
|
||||
STATS = 5"
|
||||
if [[ "$replace" == "1" ]]; then
|
||||
with_opts="$with_opts,
|
||||
REPLACE"
|
||||
fi
|
||||
|
||||
restore_sql=$(
|
||||
cat <<SQL
|
||||
RESTORE DATABASE [$target_db]
|
||||
FROM DISK = N'$container_bak_path'
|
||||
WITH
|
||||
$with_opts;
|
||||
SQL
|
||||
)
|
||||
|
||||
log "Starting restore (large DBs can take a while)..."
|
||||
docker exec -i mssql2025 /opt/mssql-tools18/bin/sqlcmd \
|
||||
-S localhost -U sa -P "$MSSQL_SA_PASSWORD" \
|
||||
-b -Q "$restore_sql" < /dev/null
|
||||
|
||||
log "Restore finished ✅"
|
||||
|
||||
# -------- Post-restore info --------
|
||||
log "Post-restore database info:"
|
||||
info_sql=$(
|
||||
cat <<SQL
|
||||
SET NOCOUNT ON;
|
||||
SELECT
|
||||
name,
|
||||
state_desc,
|
||||
recovery_model_desc,
|
||||
compatibility_level
|
||||
FROM sys.databases
|
||||
WHERE name = N'$target_db';
|
||||
SQL
|
||||
)
|
||||
|
||||
docker exec -i mssql2025 /opt/mssql-tools18/bin/sqlcmd \
|
||||
-S localhost -U sa -P "$MSSQL_SA_PASSWORD" \
|
||||
-W -h -1 -Q "$info_sql" < /dev/null \
|
||||
| tr -d '\r' \
|
||||
| while read -r line; do
|
||||
[[ -n "$line" ]] && log " $line"
|
||||
done
|
||||
|
||||
# -------- Optional tuning --------
|
||||
if [[ "$do_tuning" == "1" ]]; then
|
||||
# Set recovery model (default SIMPLE)
|
||||
log "Applying recovery model: $recovery_model"
|
||||
tune_sql="ALTER DATABASE [$target_db] SET RECOVERY $recovery_model WITH NO_WAIT;"
|
||||
docker exec -i mssql2025 /opt/mssql-tools18/bin/sqlcmd \
|
||||
-S localhost -U sa -P "$MSSQL_SA_PASSWORD" \
|
||||
-b -Q "$tune_sql" < /dev/null
|
||||
|
||||
# Set compatibility level if requested
|
||||
if [[ -n "$compat_level" ]]; then
|
||||
log "Applying compatibility level: $compat_level"
|
||||
compat_sql="ALTER DATABASE [$target_db] SET COMPATIBILITY_LEVEL = $compat_level;"
|
||||
docker exec -i mssql2025 /opt/mssql-tools18/bin/sqlcmd \
|
||||
-S localhost -U sa -P "$MSSQL_SA_PASSWORD" \
|
||||
-b -Q "$compat_sql" < /dev/null
|
||||
fi
|
||||
|
||||
log "Tuning applied ✅"
|
||||
else
|
||||
warn "Post-restore tuning skipped (--no-tuning)."
|
||||
fi
|
||||
|
||||
# -------- Final info --------
|
||||
log "Final database info:"
|
||||
docker exec -i mssql2025 /opt/mssql-tools18/bin/sqlcmd \
|
||||
-S localhost -U sa -P "$MSSQL_SA_PASSWORD" \
|
||||
-W -h -1 -Q "$info_sql" < /dev/null \
|
||||
| tr -d '\r' \
|
||||
| while read -r line; do
|
||||
[[ -n "$line" ]] && log " $line"
|
||||
done
|
||||
|
||||
log "Done."
|
||||
53
sql/playground.sql
Normal file
53
sql/playground.sql
Normal file
@@ -0,0 +1,53 @@
|
||||
/* ------------------------------------------------------------
|
||||
sql/playground.sql
|
||||
Local playground for SQL Server 2025 in Docker
|
||||
------------------------------------------------------------ */
|
||||
|
||||
-- Basic server info
|
||||
SELECT
|
||||
@@SERVERNAME AS ServerName,
|
||||
@@VERSION AS SqlServerVersion,
|
||||
SYSDATETIME() AS ServerTimeLocal,
|
||||
SYSUTCDATETIME() AS ServerTimeUtc;
|
||||
|
||||
-- Switch to your dev database (created by seed/migrations)
|
||||
IF DB_ID(N'DevDb') IS NULL
|
||||
BEGIN
|
||||
PRINT 'Database DevDb does not exist yet. Run: ./scripts/seed.sh and ./scripts/migrate.sh';
|
||||
RETURN;
|
||||
END
|
||||
GO
|
||||
|
||||
USE DevDb;
|
||||
GO
|
||||
|
||||
SELECT DB_NAME() AS CurrentDatabase;
|
||||
GO
|
||||
|
||||
-- Check if example table exists and query it
|
||||
IF OBJECT_ID(N'dbo.Customer', N'U') IS NULL
|
||||
BEGIN
|
||||
PRINT 'Table dbo.Customer does not exist yet. Run: ./scripts/migrate.sh';
|
||||
END
|
||||
ELSE
|
||||
BEGIN
|
||||
PRINT 'dbo.Customer exists. Running sample queries...';
|
||||
|
||||
SELECT TOP (10) *
|
||||
FROM dbo.Customer
|
||||
ORDER BY CustomerId DESC;
|
||||
|
||||
SELECT COUNT(*) AS CustomerCount
|
||||
FROM dbo.Customer;
|
||||
END
|
||||
GO
|
||||
|
||||
/* ------------------------------------------------------------
|
||||
Scratch area (write your own queries below)
|
||||
------------------------------------------------------------ */
|
||||
|
||||
-- Example:
|
||||
-- SELECT TOP (100) *
|
||||
-- FROM dbo.Customer
|
||||
-- ORDER BY CustomerId;
|
||||
|
||||
3
sql/test.sql
Normal file
3
sql/test.sql
Normal file
@@ -0,0 +1,3 @@
|
||||
SELECT @@VERSION AS Version;
|
||||
SELECT DB_NAME() AS CurrentDb;
|
||||
SELECT GETDATE() AS CurrentDateTime;
|
||||
Reference in New Issue
Block a user