feat(\db-tool\): introduce unified db-tool package with postgres harness and tests (T0-T8)

This commit is contained in:
2026-04-30 09:06:44 +00:00
parent 395bddee94
commit b5dcbf08a1
27 changed files with 2417 additions and 1 deletions

View File

@@ -0,0 +1,41 @@
: "${OLD_NAMESPACE:=}"
nl=$(printf '\nx')
nl=${nl%x}
___pop_namespace() {
v=${OLD_NAMESPACE%%"$nl"*}
case $OLD_NAMESPACE in
*"$nl"*)
OLD_NAMESPACE=${OLD_NAMESPACE#*"$nl"}
;;
*)
OLD_NAMESPACE=
;;
esac
printf '%s\n' "$v"
}
___peek_namespace() {
printf '%s\n' "${OLD_NAMESPACE%%"$nl"*}"
}
___push_namespace() {
if [ -n "$OLD_NAMESPACE" ]; then
OLD_NAMESPACE=$1"$nl$OLD_NAMESPACE"
else
OLD_NAMESPACE=$1
fi
}
change_namespace() {
___push_namespace "$HECTIC_NAMESPACE"
export HECTIC_NAMESPACE="$1"
}
restore_namespace() {
HECTIC_NAMESPACE=$(___pop_namespace)
export HECTIC_NAMESPACE
}

View File

@@ -12,4 +12,13 @@ in {
colors = hectic.writeDash "colors.sh" ''
${builtins.readFile ./colors.sh}
'';
change_namespace = hectic.writeDash "change_namespace.sh" ''
${builtins.readFile ./change_namespace.sh}
'';
quote = hectic.writeDash "quote.sh" ''
${builtins.readFile ./quote.sh}
'';
pager_or_cat = hectic.writeDash "pager_or_cat.sh" ''
${builtins.readFile ./pager_or_cat.sh}
'';
}

View File

@@ -0,0 +1,8 @@
pager_or_cat_init() {
# Pipe to pager only if stdout is a terminal, otherwise output directly
if [ -t 1 ]; then
PAGER_OR_CAT="${PAGER:-less}"
else
PAGER_OR_CAT=cat
fi
}

View File

@@ -0,0 +1 @@
quote() { printf "'%s'" "$(printf %s "$1" | sed "s/'/'\\\\''/g")"; }

99
package/db-tool/README.md Normal file
View File

@@ -0,0 +1,99 @@
# db-tool
PostgreSQL development database management tool. Drop-in replacement for per-project database.sh / postgres-init.sh / postgres-cleanup.sh scripts. Provides database, postgres-init, and postgres-cleanup binaries.
## Provided Binaries
| Binary | Description |
| --- | --- |
| `database` | Main script for managing migrations, deployments, and logs. |
| `postgres-init` | Ephemeral PostgreSQL cluster initialization and startup. |
| `postgres-cleanup` | Graceful shutdown and cleanup of the PostgreSQL cluster. |
## Required Environment Variables
These variables must be set for `db-tool` to function.
| Variable | Description |
| --- | --- |
| `LOCAL_DIR` | Absolute path to the project root directory. |
| `DB_URL` | Full PostgreSQL connection string (e.g., `postgresql://user@localhost/dbname?host=$PG_WORKING_DIR`). |
| `PG_WORKING_DIR` | Directory where the PostgreSQL cluster data and sockets are stored. |
## Optional Environment Variables
| Variable | Default Value | Description |
| --- | --- | --- |
| `DATABASE_DIR` | `${LOCAL_DIR}/db` | Root directory for database-related files. |
| `MIGRATION_DIR` | `${DATABASE_DIR}/migration` | Directory containing SQL migration files. |
| `DATABASE_SOURCE` | `${DATABASE_DIR}/src` | Directory containing source SQL files for hydration. |
| `PG_URL_VAR` | `PGURL` | The name of the environment variable where the computed PG URL will be exported. |
| `PG_LOG_PATH` | (unset) | Path to redirect PostgreSQL server logs. |
| `PATCH_LOG` | (stdout) | Path to log the output of database patches. |
| `HYDRATE_LOG` | (stdout) | Path to log the output of database hydration. |
## pull_staging Contract
The `pull_staging` subcommand allows importing data from a remote staging environment into the local `test-data.sql` file. This functionality requires four specific environment variables to be defined:
1. `STAGING_SSH_HOST`: The SSH destination for the staging server.
2. `STAGING_DB_URL`: The PostgreSQL connection string for the remote staging database.
3. `STAGING_DUMP_TABLES`: A space-separated list of tables to include in the data dump.
4. `STAGING_DUMP_FLAGS`: Additional flags to pass to `pg_dump` (e.g., `--column-inserts`).
If any of these variables are missing when `pull_staging` is invoked, the tool will exit with code 3 and print the name of the missing variable to stderr.
## Subcommands
- `deploy`: Execute the full deployment flow (hydrate + patch). Supports `--cleanup` to teardown after success.
- `log`: Inspect database logs. Supports `list` and index-based selection.
- `test`: Execute database tests located in `${DATABASE_DIR}/test/test.sql`.
- `check`: Run a deployment validation in an isolated, temporary PostgreSQL cluster.
- `cleanup`: Stop the local database cluster and remove the `PG_WORKING_DIR`.
- `pull_staging`: Import data from the staging environment based on the env contract.
- `init`: Wrapper around `postgres-init` to start the cluster.
- `migrator`: Directly invoke the migration tool with the correct environment context.
## shellHook Example
To use `db-tool` in a Nix development shell, add the following to your `flake.nix` or `shell.nix`:
```nix
{
# ...
devShells.default = pkgs.mkShell {
packages = [
pkgs.hectic.db-tool
pkgs.hectic.postgres-init
pkgs.hectic.postgres-cleanup
];
shellHook = ''
export LOCAL_DIR="$PWD"
export DATABASE_DIR="$LOCAL_DIR/db"
export MIGRATION_DIR="$DATABASE_DIR/migration"
export DATABASE_SOURCE="$DATABASE_DIR/src"
export PG_WORKING_DIR="$LOCAL_DIR/focus/postgresql"
export DB_URL="postgresql://user@localhost/dbname?host=$PG_WORKING_DIR&port=5432"
# for other non-db scripts (deploy.sh, task.sh, etc.):
export HECTIC_LIB="${pkgs.hectic.helpers.posix-shell.log}"
# Initialize and start the ephemeral database cluster
. ${pkgs.hectic.postgres-init}/bin/postgres-init
'';
};
}
```
## Exit Codes
| Code | Meaning |
| --- | --- |
| 1 | Generic error. |
| 2 | Ambiguous arguments or state. |
| 3 | Missing required argument or environment variable. |
| 5 | Provided table does not exist. |
| 9 | Argument or command not found. |
| 13 | Program bug or unexpected system state. |
| 127 | Command not found (missing dependency). |

1539
package/db-tool/database.sh Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,62 @@
{ dash, hectic, postgresql_17, neovim, openssh, coreutils, gawk, lib }:
let
shell = "${dash}/bin/dash";
database = hectic.writeShellApplication {
inherit shell;
bashOptions = [
"errexit"
"nounset"
];
# SC2209: false positive — PAGER_OR_CAT=cat stores the string "cat" intentionally
excludeShellChecks = [ "SC2209" ];
name = "database";
runtimeInputs = [ hectic.migrator hectic.parse-uri postgresql_17 neovim openssh coreutils gawk ];
text = ''
${builtins.readFile hectic.helpers.posix-shell.log}
${builtins.readFile hectic.helpers.posix-shell.change_namespace}
${builtins.readFile hectic.helpers.posix-shell.quote}
${builtins.readFile hectic.helpers.posix-shell.pager_or_cat}
${builtins.readFile ./database.sh}
'';
meta = {
description = "PostgreSQL development database management";
mainProgram = "database";
};
};
postgresInit = hectic.writeShellApplication {
inherit shell;
bashOptions = [ ];
name = "postgres-init";
runtimeInputs = [ postgresql_17 coreutils ];
text = builtins.readFile ./postgres-init.sh;
meta = {
description = "Initialize local PostgreSQL instance";
mainProgram = "postgres-init";
};
};
postgresCleanup = hectic.writeShellApplication {
inherit shell;
bashOptions = [ ];
name = "postgres-cleanup";
runtimeInputs = [ postgresql_17 coreutils ];
text = builtins.readFile ./postgres-cleanup.sh;
meta = {
description = "Clean up local PostgreSQL instance";
mainProgram = "postgres-cleanup";
};
};
in
{
"db-tool" = database;
"postgres-init" = postgresInit;
"postgres-cleanup" = postgresCleanup;
}

View File

@@ -0,0 +1,14 @@
#!/bin/dash
postgres_cleanup_main() {
if [ -z "${PG_WORKING_DIR:-}" ] && [ -z "${LOCAL_DIR:-}" ]; then return 0; fi
: "${PG_WORKING_DIR:=$LOCAL_DIR/focus/postgresql}"
if [ -f "${PG_WORKING_DIR}/data/postmaster.pid" ]; then
pg_ctl -D "${PG_WORKING_DIR}/data" -m fast -w stop || :
fi
return 0
}
if [ "$(basename "$0")" = 'postgres-cleanup' ]; then
postgres_cleanup_main "$@"
fi

View File

@@ -0,0 +1,49 @@
#!/bin/dash
postgres_init_main() {
if [ -z "${PG_WORKING_DIR:-}" ] && [ -z "${LOCAL_DIR:-}" ]; then
printf '%s\n' 'postgres-init: PG_WORKING_DIR or LOCAL_DIR is required' >&2
return 1
fi
: "${PG_WORKING_DIR:=$LOCAL_DIR/focus/postgresql}"
: "${PG_PORT:=5432}"
: "${PG_DATABASE:=testdb}"
: "${PG_DISABLE_LOGGING:=0}"
[ "${PG_SHARED_PRELOAD_LIBRARIES+x}" ] || PG_SHARED_PRELOAD_LIBRARIES='pg_cron'
: "${PG_URL_VAR:=PGURL}"
mkdir -p "$PG_WORKING_DIR" || return 1
wd="$PG_WORKING_DIR"; data="$wd/data"; sockdir="$wd/sock"; db="$PG_DATABASE"
pg_ctl -D "$data" -m fast -w stop >/dev/null 2>&1 || :
mkdir -p "$sockdir" || return 1
if [ "${PG_REUSE+x}" ] && [ -f "$data/PG_VERSION" ]; then PG_REUSE=1; else PG_REUSE=0; fi
if [ "$PG_REUSE" -eq 0 ]; then
rm -rf "$data" "$sockdir" || return 1
mkdir -p "$sockdir" || return 1
initdb -D "$data" --no-locale -E UTF8 || return 1
{ printf '%s\n' "listen_addresses = ''"; [ "$PG_DISABLE_LOGGING" -eq 0 ] && { printf '%s\n' 'logging_collector = on'; printf '%s\n' "log_directory = 'log'"; }; [ -n "$PG_SHARED_PRELOAD_LIBRARIES" ] && { printf '%s\n' "shared_preload_libraries = '$PG_SHARED_PRELOAD_LIBRARIES'"; printf '%s\n' "cron.database_name = '$db'"; printf '%s\n' "cron.host = '$sockdir'"; }; :; } >> "$data/postgresql.conf" || return 1
sed -i "1ilocal all all trust" "$data/pg_hba.conf" || return 1
fi
sed -i '/^[[:space:]]*port[[:space:]]*=/d' "$data/postgresql.conf" || return 1
sed -i '/^[[:space:]]*unix_socket_directories[[:space:]]*=/d' "$data/postgresql.conf" || return 1
{ printf '%s\n' "port = $PG_PORT"; printf '%s\n' "unix_socket_directories = '$sockdir'"; } >> "$data/postgresql.conf" || return 1
pg_ctl -D "$data" -o "-F" -w start || return 2
user="$(id -un)" || return 1
if [ "$PG_REUSE" -eq 0 ]; then createdb -h "$sockdir" -U "$user" "$db" || return 1; fi
psql -h "$sockdir" -p "$PG_PORT" -d "$db" -v ON_ERROR_STOP=1 -c 'select 1;' || return 1
export POSTGRESQL_HOST="$sockdir" POSTGRESQL_PORT="$PG_PORT" POSTGRESQL_USER="$user" POSTGRESQL_DATABASE="$db"
_pg_url="postgresql://${POSTGRESQL_USER}@/${POSTGRESQL_DATABASE}?host=${POSTGRESQL_HOST}&port=${POSTGRESQL_PORT}"
case $PG_URL_VAR in ''|*[!ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_]* ) printf '%s\n' 'postgres-init: invalid PG_URL_VAR' >&2; return 1 ;; esac
export "${PG_URL_VAR}=${_pg_url}" || return 1
return 0
}
if [ "$(basename "$0")" = 'postgres-init' ]; then
postgres_init_main "$@"
fi

View File

@@ -107,6 +107,7 @@
};
nativeBuildInputs = with pkgs; [pkg-config curl];
};
dbToolPkgs = pkgs.callPackage ./db-tool {};
in {
py3-datetime = pkgs.callPackage ./py3-datetime.nix {};
py3-marzban = pkgs.callPackage ./py3-marzban.nix { inherit self; };
@@ -140,6 +141,10 @@ in {
shellplot = pkgs.callPackage ./shellplot {};
onlinepubs2man = pkgs.callPackage ./onlinepubs2man {};
migrator = pkgs.callPackage ./migrator {};
"parse-uri" = pkgs.callPackage ./parse-uri {};
"db-tool" = dbToolPkgs."db-tool";
"postgres-init" = dbToolPkgs."postgres-init";
"postgres-cleanup" = dbToolPkgs."postgres-cleanup";
nbt2json = pkgs.callPackage ./nbt2json {};
hemar-parser = pkgs.callPackage ./hemar/parser {};
AstroTuxLauncher = pkgs.callPackage ./AstroTuxLauncher.nix {};

View File

@@ -0,0 +1,29 @@
{ stdenv, gcc, libpq, lib, bash }:
stdenv.mkDerivation {
pname = "parse-uri";
version = "1.0";
src = ./.;
doCheck = false;
nativeBuildInputs = [ gcc ];
buildInputs = [ libpq ];
INCLUDES = "-I${libpq.dev}/include";
LDFLAGS = "-L${libpq.out}/lib -lpq";
buildPhase = ''
${bash}/bin/sh ./make.sh build
'';
installPhase = ''
mkdir -p $out/bin
cp target/parse-uri $out/bin/
'';
meta = {
description = "parse-uri";
license = lib.licenses.mit;
};
}

32
package/parse-uri/main.c Normal file
View File

@@ -0,0 +1,32 @@
#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
#include <libpq-fe.h>
int main(int argc, char *argv[]) {
if (argc != 2) {
printf("only 1 argument allow, please provide uri\n");
exit(1);
}
const char *conninfo = argv[1];
char *errmsg = NULL;
PQconninfoOption *options = PQconninfoParse(conninfo, &errmsg);
if (!options) {
printf("Parse failed: %s\n", errmsg);
return 1;
}
for (PQconninfoOption *opt = options; opt->keyword != NULL; opt++) {
char upper[128];
size_t i = 0;
for (; opt->keyword[i] != '\0' && i < sizeof(upper)-1; i++)
upper[i] = toupper((unsigned char)opt->keyword[i]);
upper[i] = '\0';
printf("URI_%s=%s\n", upper, opt->val ? opt->val : "");
}
PQconninfoFree(options);
return 0;
}

41
package/parse-uri/make.sh Normal file
View File

@@ -0,0 +1,41 @@
#!/bin/sh
# Usage: make.sh [build|check] [--norun] [--debug] [--color]
PACKAGE_NAME="parse-uri"
check_dependencies() {
for dep in cc; do
if ! command -v "$dep" >/dev/null 2>&1; then
echo "Error: Required dependency '$dep' not found." >&2
exit 1
fi
done
}
check_dependencies
# Default flags
OPTFLAGS="-O2"
CFLAGS="-Wall -Wextra -Werror -pedantic"
STD_FLAGS="-std=c99"
MODE="${1:-build}"
shift
build() {
mkdir -p target
echo "# Build $PACKAGE_NAME"
# shellcheck disable=SC2086
cc $CFLAGS $OPTFLAGS $STD_FLAGS main.c -o "target/$PACKAGE_NAME" $LDFLAGS $INCLUDES
}
case "$MODE" in
build)
build
;;
check)
echo "No tests to run"
;;
*)
exit 1
;;
esac

View File

@@ -0,0 +1,82 @@
{ inputs, self, pkgs, system, ... }: let
lib = inputs.nixpkgs.lib;
# turn anything under test directory into a derivation that exposes $out/run.sh
mkTestDrv = folder: name: type:
if type == "directory" then
pkgs.runCommand "test-${name}" {} ''
if ! [ -f ${"${folder}/${name}/run.sh"} ]; then
echo no run.sh in test/${name}
exit 1
fi
mkdir -p "$out"
cp -r ${"${folder}/${name}"}/* "$out/"
chmod +x "$out/run.sh"
''
else if lib.hasSuffix ".sh" name then
pkgs.runCommand "test-${lib.removeSuffix ".sh" name}" {} ''
mkdir -p "$out"
install -Dm755 ${"${folder}/${name}"} "$out/run.sh"
''
else
null;
testDir = folder: builtins.readDir folder;
# attrset: testName -> drv with run.sh
testDrvs = folder:
lib.mapAttrs' (n: v:
lib.nameValuePair (lib.removeSuffix ".sh" n) v
) (lib.filterAttrs (_: v: v != null)
(lib.mapAttrs (n: t: mkTestDrv folder n t) (testDir folder)));
database = self.packages.${system}."db-tool";
postgresInit = self.packages.${system}."postgres-init";
postgresCleanup = self.packages.${system}."postgres-cleanup";
# Non-postgres tests: .sh files at ./test/ (excluding postgresql/ subdir)
nonPgTestDrvs =
lib.mapAttrs' (n: v: lib.nameValuePair (lib.removeSuffix ".sh" n) v)
(lib.filterAttrs (_: v: v != null)
(lib.mapAttrs (n: t: mkTestDrv ./test n t)
(lib.filterAttrs (n: _: n != "postgresql") (testDir ./test))));
# Postgres tests: subdirs at ./test/postgresql/
pgTestDrvs = testDrvs ./test/postgresql;
mkNonPgTest = testName: testDrv: pkgs.runCommand "db-tool-${testName}"
{
nativeBuildInputs = [ pkgs.coreutils pkgs.gnugrep pkgs.gnused ];
buildInputs = [ database postgresInit postgresCleanup pkgs.postgresql_17 pkgs.dash ];
} ''
${builtins.readFile self.legacyPackages.${system}.helpers.posix-shell.log}
test=${testDrv}
export HECTIC_LOG=trace
set -eu
# shellcheck disable=SC1090
. "$test/run.sh"
mkdir -p "$out"
'';
mkPgTest = testName: testDrv: pkgs.runCommand "db-tool-${testName}"
{
nativeBuildInputs = [ pkgs.coreutils pkgs.gnugrep pkgs.gnused ];
buildInputs = [ database postgresInit postgresCleanup pkgs.postgresql_17 pkgs.dash pkgs.netcat-openbsd ];
} ''
${builtins.readFile self.legacyPackages.${system}.helpers.posix-shell.log}
test=${testDrv}
export HECTIC_LOG=trace
set -eu
${builtins.readFile ./postgresql/_lib.sh}
# shellcheck disable=SC1090
. "$test/run.sh"
mkdir -p "$out"
'';
in
(lib.mapAttrs (name: drv: mkNonPgTest name drv) nonPgTestDrvs) //
(lib.mapAttrs (name: drv: mkPgTest name drv) pgTestDrvs)

View File

@@ -0,0 +1,147 @@
# shellcheck shell=dash
# Shared PostgreSQL harness for db-tool tests.
pg_harness__start_busy_socket() {
pg_harness_socket_dir="$1"
PG_HARNESS_BUSY_SOCKET_PATH="$pg_harness_socket_dir/.s.PGSQL.5432"
export PG_HARNESS_BUSY_SOCKET_PATH
rm -f "$PG_HARNESS_BUSY_SOCKET_PATH"
nc -l -U "$PG_HARNESS_BUSY_SOCKET_PATH" >/dev/null 2>&1 &
PG_HARNESS_BUSY_SOCKET_PID=$!
export PG_HARNESS_BUSY_SOCKET_PID
}
pg_harness_start() {
pg_harness_tmp_root="${TMPDIR:-/tmp}"
if [ "${PG_HARNESS_PGDATA_OVERRIDE+x}" ]; then
pgdata_dir="$PG_HARNESS_PGDATA_OVERRIDE"
else
pgdata_dir=$(mktemp -d "$pg_harness_tmp_root/pgdata_XXXXXX")
fi
PGDATA="$pgdata_dir"
export PGDATA
trap 'pg_harness_stop' EXIT INT TERM
if ! initdb -D "$pgdata_dir" --no-locale --encoding=UTF8 -U postgres >/dev/null 2>&1; then
pg_harness_stop
return 1
fi
if [ "${PG_HARNESS_INJECT_BUSY_SOCKET+x}" ] && ! [ "${PG_HARNESS_BUSY_SOCKET_PID+x}" ]; then
pg_harness__start_busy_socket "$pgdata_dir"
fi
if ! pg_ctl start -D "$pgdata_dir" -o "-k $pgdata_dir -h ''" >/dev/null 2>&1; then
pg_harness_stop
return 1
fi
PGHOST="$pgdata_dir"
PGPORT=""
PGUSER="postgres"
PGDATABASE="postgres"
export PGHOST PGPORT PGUSER PGDATABASE
POSTGRESQL_HOST="$PGHOST"
POSTGRESQL_PORT="$PGPORT"
POSTGRESQL_USER="$PGUSER"
POSTGRESQL_DATABASE="$PGDATABASE"
PGURL="postgresql://postgres@localhost/postgres?host=$pgdata_dir"
export POSTGRESQL_HOST POSTGRESQL_PORT POSTGRESQL_USER POSTGRESQL_DATABASE PGURL
pg_harness_ready=
pg_harness_attempt=0
while [ "$pg_harness_attempt" -lt 10 ]; do
if pg_isready -h "$pgdata_dir" >/dev/null 2>&1; then
pg_harness_ready=1
break
fi
sleep 0.5
pg_harness_attempt=$((pg_harness_attempt + 1))
done
if ! [ "$pg_harness_ready" = 1 ]; then
pg_harness_stop
return 1
fi
}
pg_harness_stop() {
trap - EXIT INT TERM
if [ "${PG_HARNESS_BUSY_SOCKET_PID+x}" ]; then
kill "$PG_HARNESS_BUSY_SOCKET_PID" >/dev/null 2>&1 || true
wait "$PG_HARNESS_BUSY_SOCKET_PID" >/dev/null 2>&1 || true
unset PG_HARNESS_BUSY_SOCKET_PID
fi
if [ "${PG_HARNESS_BUSY_SOCKET_PATH+x}" ] && [ -n "$PG_HARNESS_BUSY_SOCKET_PATH" ]; then
rm -f "$PG_HARNESS_BUSY_SOCKET_PATH"
fi
if [ "${PGDATA+x}" ] && [ -n "$PGDATA" ]; then
pg_ctl stop -D "$PGDATA" -m fast >/dev/null 2>&1 || true
rm -rf "$PGDATA"
fi
if [ "${PG_HARNESS_PGDATA_OVERRIDE+x}" ] && [ -n "$PG_HARNESS_PGDATA_OVERRIDE" ]; then
if ! [ "${PGDATA+x}" ] || [ "$PG_HARNESS_PGDATA_OVERRIDE" != "$PGDATA" ]; then
rm -rf "$PG_HARNESS_PGDATA_OVERRIDE"
fi
fi
if [ "${PG_HARNESS_CORRUPT_PATH+x}" ] && [ -n "$PG_HARNESS_CORRUPT_PATH" ]; then
rm -f "$PG_HARNESS_CORRUPT_PATH"
fi
unset PG_HARNESS_BUSY_SOCKET_PATH
unset PG_HARNESS_CORRUPT_PATH
unset PG_HARNESS_INJECT_BUSY_SOCKET
unset PG_HARNESS_PGDATA_OVERRIDE
unset PGHOST PGPORT PGUSER PGDATABASE PGDATA
unset POSTGRESQL_HOST POSTGRESQL_PORT POSTGRESQL_USER POSTGRESQL_DATABASE PGURL
}
pg_harness_start_corrupt_dir() {
pg_harness_tmp_root="${TMPDIR:-/tmp}"
PG_HARNESS_PGDATA_OVERRIDE=$(mktemp "$pg_harness_tmp_root/pgdata_corrupt_XXXXXX")
: > "$PG_HARNESS_PGDATA_OVERRIDE"
PG_HARNESS_CORRUPT_PATH="$PG_HARNESS_PGDATA_OVERRIDE"
export PG_HARNESS_PGDATA_OVERRIDE PG_HARNESS_CORRUPT_PATH
}
pg_harness_busy_socket() {
pg_harness_tmp_root="${TMPDIR:-/tmp}"
PG_HARNESS_INJECT_BUSY_SOCKET=1
export PG_HARNESS_INJECT_BUSY_SOCKET
if [ "${PGDATA+x}" ] && [ -d "$PGDATA" ]; then
pg_harness_socket_dir="$PGDATA"
else
if [ "${PG_HARNESS_PGDATA_OVERRIDE+x}" ]; then
pg_harness_socket_dir="$PG_HARNESS_PGDATA_OVERRIDE"
else
pg_harness_socket_dir=$(mktemp -d "$pg_harness_tmp_root/pgdata_busy_XXXXXX")
PG_HARNESS_PGDATA_OVERRIDE="$pg_harness_socket_dir"
export PG_HARNESS_PGDATA_OVERRIDE
fi
fi
if [ "${PGDATA+x}" ] && [ -d "$PGDATA" ] && ! [ "${PG_HARNESS_BUSY_SOCKET_PID+x}" ]; then
pg_harness__start_busy_socket "$pg_harness_socket_dir"
fi
}
pg_harness_kill_postmaster() {
if ! [ "${PGDATA+x}" ] || ! [ -f "$PGDATA/postmaster.pid" ]; then
return 1
fi
IFS= read -r pg_harness_postmaster_pid < "$PGDATA/postmaster.pid" || return 1
kill -KILL "$pg_harness_postmaster_pid"
}

View File

@@ -0,0 +1,19 @@
# shellcheck shell=dash
HECTIC_NAMESPACE=test-db-tool-help
log notice "test case: database --help exits 0"
if ! database --help > /tmp/help-out.txt 2>&1; then
log error "test failed: database --help exited non-zero"
exit 1
fi
for tok in deploy pull_staging cleanup check log init migrator; do
if ! grep -qF "$tok" /tmp/help-out.txt; then
log error "test failed: --help output missing token: $tok"
cat /tmp/help-out.txt >&2
exit 1
fi
done
log notice "test passed"

View File

@@ -0,0 +1,19 @@
# shellcheck shell=dash
HECTIC_NAMESPACE=test-db-tool-missing-dir
export PGURL=""
unset LOCAL_DIR DATABASE_DIR DB_URL 2>/dev/null || true
log notice "test case: database deploy fails without LOCAL_DIR"
set +e
database deploy 2>/tmp/missing-err.txt
code=$?
set -e
if [ "$code" = 0 ]; then
log error "test failed: database deploy exited 0 without LOCAL_DIR"
exit 1
fi
log notice "test passed: exited $code"

View File

@@ -0,0 +1,19 @@
# shellcheck shell=dash
HECTIC_NAMESPACE=test-db-tool-deploy-basic
pg_harness_start
LOCAL_DIR=$(mktemp -d)
export LOCAL_DIR
mkdir -p "$LOCAL_DIR/devshell"
printf '#!/bin/dash\nexit 0\n' > "$LOCAL_DIR/devshell/postgres-init.sh"
chmod +x "$LOCAL_DIR/devshell/postgres-init.sh"
log notice "test case: database deploy --no-hydrate --no-patch exits 0"
if ! database deploy --no-hydrate --no-patch; then
log error "database deploy failed"
exit 1
fi
log notice "test passed"

View File

@@ -0,0 +1,20 @@
# shellcheck shell=dash
HECTIC_NAMESPACE=test-db-tool-deploy-cleanup-flag
pg_harness_start
LOCAL_DIR=$(mktemp -d)
export LOCAL_DIR
mkdir -p "$LOCAL_DIR/devshell"
printf '#!/bin/dash\nexit 0\n' > "$LOCAL_DIR/devshell/postgres-init.sh"
printf '#!/bin/dash\nexit 0\n' > "$LOCAL_DIR/devshell/postgres-cleanup.sh"
chmod +x "$LOCAL_DIR/devshell/postgres-init.sh" "$LOCAL_DIR/devshell/postgres-cleanup.sh"
log notice "test case: database deploy --no-hydrate --no-patch --cleanup exits 0"
if ! database deploy --no-hydrate --no-patch --cleanup; then
log error "database deploy --cleanup failed"
exit 1
fi
log notice "test passed"

View File

@@ -0,0 +1,36 @@
# shellcheck shell=dash
HECTIC_NAMESPACE=test-db-tool-init-cleanup-roundtrip
PG_WORKING_DIR=$(mktemp -d)
export PG_WORKING_DIR PG_DATABASE=testdb PG_PORT=5432 PG_SHARED_PRELOAD_LIBRARIES=''
cleanup() {
postgres-cleanup
rm -rf "$PG_WORKING_DIR"
}
trap 'cleanup' EXIT INT TERM
log notice "test case: postgres-init starts cluster"
if ! postgres-init; then
log error "postgres-init failed"
exit 1
fi
pgurl="postgresql://$(id -un)@/testdb?host=${PG_WORKING_DIR}/sock&port=5432"
log notice "verifying connection"
if ! psql "$pgurl" -c 'SELECT 1;' >/dev/null 2>&1; then
log error "connection failed after postgres-init"
exit 1
fi
log notice "test case: postgres-cleanup stops cluster"
postgres-cleanup
log notice "verifying cluster stopped"
if pg_isready -h "${PG_WORKING_DIR}/sock" -p 5432 >/dev/null 2>&1; then
log error "postgres still running after cleanup"
exit 1
fi
log notice "test passed"

View File

@@ -0,0 +1,18 @@
# shellcheck shell=dash
HECTIC_NAMESPACE=test-db-tool-log-subcommand
PG_WORKING_DIR=$(mktemp -d)
LOCAL_DIR=$(mktemp -d)
export PG_WORKING_DIR LOCAL_DIR PGURL='postgresql://localhost/db'
mkdir -p "$PG_WORKING_DIR/data/log"
trap 'rm -rf "$PG_WORKING_DIR" "$LOCAL_DIR"' EXIT INT TERM
log notice "test case: database log list exits 0 with empty log dir"
if ! database log list; then
log error "database log list failed"
exit 1
fi
log notice "test passed"

View File

@@ -0,0 +1,19 @@
# shellcheck shell=dash
HECTIC_NAMESPACE=test-db-tool-postgres-cleanup-stale-pidfile
PG_WORKING_DIR=$(mktemp -d)
export PG_WORKING_DIR
mkdir -p "$PG_WORKING_DIR/data"
printf '99999999\n' > "$PG_WORKING_DIR/data/postmaster.pid"
trap 'rm -rf "$PG_WORKING_DIR"' EXIT INT TERM
log notice "test case: postgres-cleanup exits 0 with stale pidfile"
if ! postgres-cleanup; then
log error "postgres-cleanup failed with stale pidfile"
exit 1
fi
log notice "test passed"

View File

@@ -0,0 +1,42 @@
# shellcheck shell=dash
HECTIC_NAMESPACE=test-db-tool-postgres-init-busy-socket
PG_WORKING_DIR=$(mktemp -d)
export PG_WORKING_DIR PG_DATABASE=testdb PG_PORT=5432 PG_SHARED_PRELOAD_LIBRARIES=''
trap 'pg_harness_stop; rm -rf "$PG_WORKING_DIR"' EXIT INT TERM
log notice "setup: creating initial postgres cluster"
if ! postgres-init; then
log error "setup failed: initial postgres-init failed"
exit 1
fi
log notice "setup: stopping postgres to free socket"
postgres-cleanup
log notice "setup: occupying socket with netcat"
pg_harness__start_busy_socket "$PG_WORKING_DIR/sock"
i=0
while [ "$i" -lt 50 ] && ! [ -S "$PG_HARNESS_BUSY_SOCKET_PATH" ]; do
sleep 0.1
i=$((i + 1))
done
[ -S "$PG_HARNESS_BUSY_SOCKET_PATH" ] || { log error "busy socket not ready"; exit 1; }
printf '%d\n' "$PG_HARNESS_BUSY_SOCKET_PID" > "${PG_HARNESS_BUSY_SOCKET_PATH}.lock"
log notice "test case: postgres-init fails when socket is pre-occupied"
PG_REUSE=1
export PG_REUSE
set +e
postgres-init
code=$?
set -e
if [ "$code" = 0 ]; then
log error "test failed: postgres-init exited 0 with busy socket"
exit 1
fi
log notice "test passed: exited $code"

View File

@@ -0,0 +1,23 @@
# shellcheck shell=dash
HECTIC_NAMESPACE=test-db-tool-postgres-init-corrupt-pgdata
pg_harness_start_corrupt_dir
export PG_WORKING_DIR="$PG_HARNESS_PGDATA_OVERRIDE"
export PG_SHARED_PRELOAD_LIBRARIES=''
trap 'pg_harness_stop' EXIT INT TERM
log notice "test case: postgres-init fails when PG_WORKING_DIR is a regular file"
set +e
postgres-init
code=$?
set -e
if [ "$code" = 0 ]; then
log error "test failed: postgres-init exited 0 with corrupt dir"
exit 1
fi
log notice "test passed: exited $code"

View File

@@ -0,0 +1,26 @@
# shellcheck shell=dash
HECTIC_NAMESPACE=test-db-tool-pull-staging
export PGURL=""
unset STAGING_SSH_HOST STAGING_DB_URL STAGING_USER STAGING_HOST 2>/dev/null || true
log notice "test case: database pull_staging exits 3 without STAGING_SSH_HOST"
set +e
database pull_staging 2>/build/staging-err.txt
code=$?
set -e
if [ "$code" != 3 ]; then
log error "test failed: expected exit 3, got $code"
exit 1
fi
if ! grep -q 'STAGING_SSH_HOST' /build/staging-err.txt; then
log error "test failed: stderr does not mention STAGING_SSH_HOST"
exit 1
fi
log notice "test passed"
log notice "test passed"

View File

@@ -0,0 +1,16 @@
# shellcheck shell=dash
HECTIC_NAMESPACE=test-db-tool-unknown
log notice "test case: database nonsense exits non-zero (expected 1)"
set +e
database nonsense 2>/dev/null
code=$?
set -e
if [ "$code" = 0 ]; then
log error "test failed: database nonsense exited 0 (should be non-zero)"
exit 1
fi
log notice "test passed: exited $code"

View File

@@ -1,4 +1,5 @@
{ system, inputs, self, pkgs }:
(import ./migrator { inherit system inputs self pkgs; }) //
(import ./hemar { inherit system inputs self pkgs; }) //
(import (./. + "/sentinèlla") { inherit system inputs self pkgs; })
(import (./. + "/sentinèlla") { inherit system inputs self pkgs; }) //
(import ./db-tool { inherit system inputs self pkgs; })