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