test: add podman/docker dbtest tool for postgres, mssql and mysql
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
# dbtest: container test databases
|
||||
|
||||
Podman (preferred) or Docker. One tool for postgres, mssql, mysql.
|
||||
|
||||
## Commands
|
||||
|
||||
| Command | Action |
|
||||
|---|---|
|
||||
| `./tests/dbtest/dbtest.sh up <db\|all>` | Start, wait for ready, run init |
|
||||
| `down <db\|all>` | Stop and remove |
|
||||
| `restart <db>` | Fresh container |
|
||||
| `status <db\|all>` | State and connection string |
|
||||
| `env <db\|all>` | Print `export` line for the test env var |
|
||||
| `test <db> [pkgs]` | up, `go test`, down |
|
||||
| `shell <db>` | Interactive client |
|
||||
| `exec <db> <file>` | Run SQL file |
|
||||
| `logs <db>` | Container logs |
|
||||
|
||||
## Databases
|
||||
|
||||
| db | Port | Env var | Init | Default test pkgs |
|
||||
|---|---|---|---|---|
|
||||
| postgres | 5439 | `RELSPEC_TEST_PG_CONN` | `tests/postgres/init.sql` | readers/pgsql, writers/pgsql, writers/sqlexec |
|
||||
| mssql | 1439 | `RELSPEC_TEST_MSSQL_CONN` | `test_data/mssql/test_schema.sql` (creates `RelSpecTest`) | readers/mssql, writers/mssql |
|
||||
| mysql | 3309 | `RELSPEC_TEST_MYSQL_CONN` | `tests/dbtest/init/mysql.sql` | none (no Go driver/reader yet) |
|
||||
|
||||
## Env
|
||||
|
||||
| Var | Effect |
|
||||
|---|---|
|
||||
| `DBTEST_RUNTIME` | Force `podman` or `docker` |
|
||||
| `DBTEST_TIMEOUT` | Ready wait, seconds (default 120) |
|
||||
| `DBTEST_KEEP=1` | Keep container after `test` |
|
||||
| `DBTEST_GOFLAGS` | Extra `go test` flags |
|
||||
|
||||
## Add a database
|
||||
|
||||
1. Add `dbs/<name>.sh` defining: `DB_NAME DB_IMAGE DB_CONTAINER DB_PORT DB_INTERNAL_PORT DB_ENV DB_INIT_MOUNT DB_CONN_VAR DB_CONN DB_DEFAULT_PKGS` and functions `db_ready db_post_init db_shell db_exec_file`.
|
||||
2. Add the name to `ALL_DBS` in `dbtest.sh`.
|
||||
|
||||
## Notes
|
||||
|
||||
- Containers are named `relspec-test-<db>`; existing `tests/postgres/*.sh` and `make docker-*` use the same postgres name/port 5439 and remain independent.
|
||||
- mssql needs ~2GB RAM and takes longer to become ready.
|
||||
- Tests skip when the env var is unset.
|
||||
@@ -0,0 +1,17 @@
|
||||
# SQL Server container definition (sourced by dbtest.sh)
|
||||
DB_NAME=mssql
|
||||
DB_IMAGE=mcr.microsoft.com/mssql/server:2022-latest
|
||||
DB_CONTAINER=relspec-test-mssql
|
||||
DB_PORT=1439
|
||||
DB_INTERNAL_PORT=1433
|
||||
DB_ENV=(-e ACCEPT_EULA=Y -e "MSSQL_SA_PASSWORD=StrongPassword123!" -e MSSQL_PID=Express)
|
||||
DB_INIT_MOUNT="$ROOT/test_data/mssql/test_schema.sql:/init/test_schema.sql"
|
||||
DB_CONN_VAR=RELSPEC_TEST_MSSQL_CONN
|
||||
DB_CONN="sqlserver://sa:StrongPassword123!@localhost:1439?database=RelSpecTest"
|
||||
DB_DEFAULT_PKGS="./pkg/readers/mssql/ ./pkg/writers/mssql/"
|
||||
|
||||
_sqlcmd() { rt exec -i "$DB_CONTAINER" /opt/mssql-tools18/bin/sqlcmd -C -S localhost -U sa -P 'StrongPassword123!' "$@"; }
|
||||
db_ready() { _sqlcmd -Q "SELECT 1" >/dev/null 2>&1; }
|
||||
db_post_init() { _sqlcmd -b -i /init/test_schema.sql >/dev/null; }
|
||||
db_shell() { rt exec -it "$DB_CONTAINER" /opt/mssql-tools18/bin/sqlcmd -C -S localhost -U sa -P 'StrongPassword123!' -d RelSpecTest "$@"; }
|
||||
db_exec_file() { _sqlcmd -b -d RelSpecTest < "$1"; }
|
||||
@@ -0,0 +1,16 @@
|
||||
# MySQL container definition (sourced by dbtest.sh)
|
||||
DB_NAME=mysql
|
||||
DB_IMAGE=docker.io/library/mysql:8.4
|
||||
DB_CONTAINER=relspec-test-mysql
|
||||
DB_PORT=3309
|
||||
DB_INTERNAL_PORT=3306
|
||||
DB_ENV=(-e MYSQL_ROOT_PASSWORD=relspec_root_password -e MYSQL_DATABASE=relspec_test -e MYSQL_USER=relspec -e MYSQL_PASSWORD=relspec_test_password)
|
||||
DB_INIT_MOUNT="$ROOT/tests/dbtest/init/mysql.sql:/docker-entrypoint-initdb.d/init.sql"
|
||||
DB_CONN_VAR=RELSPEC_TEST_MYSQL_CONN
|
||||
DB_CONN="relspec:relspec_test_password@tcp(localhost:3309)/relspec_test"
|
||||
DB_DEFAULT_PKGS=""
|
||||
|
||||
db_ready() { rt exec "$DB_CONTAINER" mysqladmin ping -h 127.0.0.1 -urelspec -prelspec_test_password --silent >/dev/null 2>&1; }
|
||||
db_post_init() { :; }
|
||||
db_shell() { rt exec -it "$DB_CONTAINER" mysql -urelspec -prelspec_test_password relspec_test "$@"; }
|
||||
db_exec_file() { rt exec -i "$DB_CONTAINER" mysql -urelspec -prelspec_test_password relspec_test < "$1"; }
|
||||
@@ -0,0 +1,16 @@
|
||||
# Postgres container definition (sourced by dbtest.sh)
|
||||
DB_NAME=postgres
|
||||
DB_IMAGE=docker.io/library/postgres:16-alpine
|
||||
DB_CONTAINER=relspec-test-postgres
|
||||
DB_PORT=5439
|
||||
DB_INTERNAL_PORT=5432
|
||||
DB_ENV=(-e POSTGRES_USER=relspec -e POSTGRES_PASSWORD=relspec_test_password -e POSTGRES_DB=relspec_test)
|
||||
DB_INIT_MOUNT="$ROOT/tests/postgres/init.sql:/docker-entrypoint-initdb.d/init.sql"
|
||||
DB_CONN_VAR=RELSPEC_TEST_PG_CONN
|
||||
DB_CONN="postgres://relspec:relspec_test_password@localhost:5439/relspec_test"
|
||||
DB_DEFAULT_PKGS="./pkg/readers/pgsql/ ./pkg/writers/pgsql/ ./pkg/writers/sqlexec/"
|
||||
|
||||
db_ready() { rt exec "$DB_CONTAINER" pg_isready -U relspec -d relspec_test >/dev/null 2>&1; }
|
||||
db_post_init() { :; }
|
||||
db_shell() { rt exec -it "$DB_CONTAINER" psql -U relspec -d relspec_test "$@"; }
|
||||
db_exec_file() { rt exec -i "$DB_CONTAINER" psql -v ON_ERROR_STOP=1 -U relspec -d relspec_test < "$1"; }
|
||||
Executable
+130
@@ -0,0 +1,130 @@
|
||||
#!/usr/bin/env bash
|
||||
# Reusable podman/docker test database tool for postgres, mssql and mysql.
|
||||
# Usage: dbtest.sh <command> <db> [args]
|
||||
set -euo pipefail
|
||||
|
||||
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
ROOT="$(cd "$HERE/../.." && pwd)"
|
||||
TIMEOUT="${DBTEST_TIMEOUT:-120}"
|
||||
ALL_DBS=(postgres mssql mysql)
|
||||
|
||||
usage() {
|
||||
cat <<USAGE
|
||||
dbtest.sh <command> <db|all> [args]
|
||||
|
||||
commands:
|
||||
up <db> start container, wait until ready, run init
|
||||
down <db> stop and remove container
|
||||
restart <db> down + up
|
||||
status <db> running state and connection string
|
||||
env <db> print 'export VAR=conn' (use: eval "\$(dbtest.sh env postgres)")
|
||||
logs <db> container logs
|
||||
shell <db> interactive client
|
||||
exec <db> <file> run a SQL file against the database
|
||||
test <db> [pkgs] up, run go tests with conn env set, down (keep with DBTEST_KEEP=1)
|
||||
list supported databases
|
||||
|
||||
dbs: ${ALL_DBS[*]}
|
||||
env: DBTEST_RUNTIME=podman|docker DBTEST_TIMEOUT=secs DBTEST_KEEP=1 DBTEST_GOFLAGS=...
|
||||
USAGE
|
||||
}
|
||||
|
||||
die() { echo "error: $*" >&2; exit 1; }
|
||||
log() { echo "[dbtest] $*" >&2; }
|
||||
|
||||
detect_runtime() {
|
||||
if [ -n "${DBTEST_RUNTIME:-}" ]; then echo "$DBTEST_RUNTIME"; return; fi
|
||||
if command -v podman >/dev/null 2>&1; then echo podman
|
||||
elif command -v docker >/dev/null 2>&1; then echo docker
|
||||
else die "neither podman nor docker is installed"; fi
|
||||
}
|
||||
RUNTIME="$(detect_runtime)"
|
||||
rt() { "$RUNTIME" "$@"; }
|
||||
|
||||
load_db() {
|
||||
local f="$HERE/dbs/${1:-}.sh"
|
||||
[ -f "$f" ] || die "unknown db '${1:-}' (supported: ${ALL_DBS[*]})"
|
||||
# shellcheck disable=SC1090
|
||||
source "$f"
|
||||
}
|
||||
|
||||
is_running() { [ "$(rt inspect -f '{{.State.Running}}' "$DB_CONTAINER" 2>/dev/null || true)" = "true" ]; }
|
||||
|
||||
cmd_up() {
|
||||
if is_running; then
|
||||
log "$DB_NAME already running"
|
||||
else
|
||||
rt rm -f "$DB_CONTAINER" >/dev/null 2>&1 || true
|
||||
log "starting $DB_NAME ($DB_IMAGE) on port $DB_PORT using $RUNTIME"
|
||||
rt run -d --name "$DB_CONTAINER" "${DB_ENV[@]}" \
|
||||
-p "$DB_PORT:$DB_INTERNAL_PORT" \
|
||||
-v "$DB_INIT_MOUNT:ro,Z" "$DB_IMAGE" >/dev/null
|
||||
fi
|
||||
log "waiting for $DB_NAME (max ${TIMEOUT}s)"
|
||||
local i=0
|
||||
until db_ready; do
|
||||
i=$((i + 1))
|
||||
if [ "$i" -ge "$TIMEOUT" ]; then
|
||||
rt logs --tail 50 "$DB_CONTAINER" >&2 || true
|
||||
die "$DB_NAME did not become ready"
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
db_post_init
|
||||
log "$DB_NAME ready: $DB_CONN_VAR=$DB_CONN"
|
||||
}
|
||||
|
||||
cmd_down() {
|
||||
rt rm -f "$DB_CONTAINER" >/dev/null 2>&1 || true
|
||||
log "$DB_NAME removed"
|
||||
}
|
||||
|
||||
cmd_status() {
|
||||
if is_running; then echo "$DB_NAME: running ($DB_CONTAINER, port $DB_PORT)"; else echo "$DB_NAME: stopped"; fi
|
||||
echo "$DB_CONN_VAR=$DB_CONN"
|
||||
}
|
||||
|
||||
cmd_test() {
|
||||
local pkgs="${*:-$DB_DEFAULT_PKGS}"
|
||||
[ -n "$pkgs" ] || die "no test packages for $DB_NAME; pass packages as arguments"
|
||||
cmd_up
|
||||
[ -n "${DBTEST_KEEP:-}" ] || trap cmd_down EXIT
|
||||
export "$DB_CONN_VAR=$DB_CONN"
|
||||
cd "$ROOT"
|
||||
# shellcheck disable=SC2086
|
||||
go test -count=1 ${DBTEST_GOFLAGS:-} $pkgs
|
||||
}
|
||||
|
||||
cmd="${1:-}"
|
||||
case "$cmd" in
|
||||
""|-h|--help|help) usage; exit 0 ;;
|
||||
list) printf '%s\n' "${ALL_DBS[@]}"; exit 0 ;;
|
||||
esac
|
||||
shift
|
||||
target="${1:-}"
|
||||
[ -n "$target" ] || die "missing <db>"
|
||||
shift || true
|
||||
|
||||
run_one() {
|
||||
load_db "$1"
|
||||
shift
|
||||
case "$cmd" in
|
||||
up) cmd_up ;;
|
||||
down) cmd_down ;;
|
||||
restart) cmd_down; cmd_up ;;
|
||||
status) cmd_status ;;
|
||||
env) echo "export $DB_CONN_VAR='$DB_CONN'" ;;
|
||||
logs) rt logs "$DB_CONTAINER" ;;
|
||||
shell) db_shell "$@" ;;
|
||||
exec) [ -f "${1:-}" ] || die "usage: exec <db> <file>"; db_exec_file "$1" ;;
|
||||
test) cmd_test "$@" ;;
|
||||
*) usage; die "unknown command '$cmd'" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
if [ "$target" = "all" ]; then
|
||||
case "$cmd" in up|down|restart|status|env) ;; *) die "'all' is only valid for up/down/restart/status/env" ;; esac
|
||||
for d in "${ALL_DBS[@]}"; do (run_one "$d"); done
|
||||
else
|
||||
run_one "$target" "$@"
|
||||
fi
|
||||
@@ -0,0 +1,21 @@
|
||||
-- Minimal MySQL fixture for relspec container tests.
|
||||
-- Extend when a MySQL reader/writer is added.
|
||||
CREATE TABLE users (
|
||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
email VARCHAR(255) NOT NULL,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY uq_users_email (email)
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
CREATE TABLE posts (
|
||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id BIGINT UNSIGNED NOT NULL,
|
||||
title VARCHAR(255) NOT NULL,
|
||||
body TEXT NULL,
|
||||
KEY idx_posts_user (user_id),
|
||||
CONSTRAINT fk_posts_user FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
CREATE VIEW v_user_posts AS
|
||||
SELECT u.id AS user_id, u.email, p.id AS post_id, p.title
|
||||
FROM users u JOIN posts p ON p.user_id = u.id;
|
||||
Reference in New Issue
Block a user