feat(\db-tool\): introduce unified db-tool package with postgres harness and tests (T0-T8)
This commit is contained in:
99
package/db-tool/README.md
Normal file
99
package/db-tool/README.md
Normal 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
1539
package/db-tool/database.sh
Normal file
File diff suppressed because it is too large
Load Diff
62
package/db-tool/default.nix
Normal file
62
package/db-tool/default.nix
Normal 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;
|
||||
}
|
||||
14
package/db-tool/postgres-cleanup.sh
Normal file
14
package/db-tool/postgres-cleanup.sh
Normal 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
|
||||
49
package/db-tool/postgres-init.sh
Normal file
49
package/db-tool/postgres-init.sh
Normal 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
|
||||
Reference in New Issue
Block a user