Files
util.nix/package/db-tool/database.sh

1631 lines
45 KiB
Bash

# shellcheck shell=dash
# shellcheck disable=SC3043
: "${REMAINING_ARGS:=}"
: "${DEFAULT_BACKUP_PATH:=${LOCAL_DIR:-$PWD}/focus/postgresql-backup/}"
: "${SCRIPT_NAME:=$(basename "$0")}"
SCRIPT_NAME=${SCRIPT_NAME%%.sh}
: "${PATCH_SQL:=${PROFILE_DIR:-${LOCAL_DIR:-$PWD}}/test-data.sql}"
if [ "${PG_LOG_PATH+x}" ]; then
: "${PATCH_LOG:=${PG_LOG_PATH}/patch.stdout.log}"
: "${HYDRATE_LOG:=${PG_LOG_PATH}/hydrate.stdout.log}"
fi
: "${DATABASE_DIR:=${LOCAL_DIR:-$PWD}/db}"
: "${MIGRATION_DIR:=$DATABASE_DIR/migration}"
: "${DATABASE_SOURCE:=$DATABASE_DIR/src}"
: "${HYDRATE_ENTRYPOINT:=entrypoint.sql}"
HYDRATE_ENTRYPOINT="${DATABASE_SOURCE}/${HYDRATE_ENTRYPOINT}"
: "${ENVIRONMENT:=}"
pager_or_cat_init
: "${DB_URL:=${PGURL:-}}"
form_psql_args() {
local psql_args="$PGURL -v ON_ERROR_STOP=1"
#for var in ${VARIABLE_LIST:-}; do
# psql_args="$psql_args -v $var"
#done
printf '%s' "$psql_args"
}
psql_logged() {
local log_file="$1"
shift
if [ -n "$log_file" ]; then
psql "$@" > "$log_file"
else
psql "$@"
fi
}
db_exec() {
local sql="$1"
# shellcheck disable=SC2046
printf '%s' "$sql" | psql $(form_psql_args)
}
todo() {
log panic "TODO"
exit 1
}
log_pager() {
# shellcheck disable=SC2068
nvim -R \
-u NONE \
-c 'nnoremap q :qa!<CR>' \
-c 'runtime! plugin/*.vim' \
-c 'set conceallevel=3' \
$@ \
-
}
help_log() {
# shellcheck disable=SC2059
printf "$(cat <<EOF
${BGREEN}Usage:${NC} $SCRIPT_NAME log [list|<file>|<index>]
View PostgreSQL logs for current PG_WORKING_DIR.
${BGREEN}Modes:${NC}
${BCYAN}log$NC Interactive pager when attached to TTY,
non-interactive concat when piped/redirected
${BCYAN}log list$NC List available log files with numeric indexes
${BCYAN}log <index>$NC Open/print one file by numeric index from list
${BCYAN}log <file>$NC Open/print one file by exact name or unique substring
${BGREEN}Examples:${NC}
$SCRIPT_NAME log
$SCRIPT_NAME log list
$SCRIPT_NAME log 1
$SCRIPT_NAME log postgresql-2026-04-21
EOF
)" | "$PAGER_OR_CAT"
}
db_log_list_files() {
log_dir="$1"
for log_file in "$log_dir"/*; do
[ -f "$log_file" ] || continue
printf '%s\n' "$log_file"
done
}
db_log_print_list() {
log_dir="$1"
idx=1
found=0
for log_file in "$log_dir"/*; do
[ -f "$log_file" ] || continue
found=1
printf '%2s %s\n' "$idx" "$(basename "$log_file")"
idx=$((idx + 1))
done
if [ "$found" -eq 0 ]; then
log warn "no log files in $log_dir"
fi
}
db_log_resolve_one() {
log_dir="$1"
selector="$2"
if [ -f "$log_dir/$selector" ]; then
printf '%s\n' "$log_dir/$selector"
return 0
fi
if [ -n "$selector" ] && [ "$selector" -eq "$selector" ] 2>/dev/null; then
idx=1
for log_file in "$log_dir"/*; do
[ -f "$log_file" ] || continue
if [ "$idx" -eq "$selector" ]; then
printf '%s\n' "$log_file"
return 0
fi
idx=$((idx + 1))
done
return 1
fi
matches=
match_count=0
for log_file in "$log_dir"/*; do
[ -f "$log_file" ] || continue
case "$(basename "$log_file")" in
*"$selector"*)
match_count=$((match_count + 1))
matches="$matches$(printf '%s\n' "$log_file")"
;;
esac
done
if [ "$match_count" -eq 1 ]; then
printf '%s' "$matches"
return 0
fi
return 1
}
help() {
# shellcheck disable=SC2059
printf "$(cat <<EOF
${BRED}TLDR; For the most lazy$NC
${BCYAN}$SCRIPT_NAME deploy$NC ${BRED}Сделать заебись$NC: deploy local db + hydrate sources + patch test data
${BRED}Further useless infomation:$NC
${BGREEN}Usage:${NC} $SCRIPT_NAME [OPTIONS] <SUBCOMMAND> [OPTIONS]
PostgreSQL development database management
${BGREEN}Global Options:${NC}
$BCYAN-h$NC, $BCYAN--help$NC Show this help message
$BCYAN-i$NC, $BCYAN--inherits$NC Parent table for hectic.migration
Can be specified multiple times
$BCYAN-u$NC, $BCYAN--url$NC PostgreSQL connection string (overrides PGURL)
${BGREEN}Database Subcommands:${NC}
${BCYAN}deploy ${CYAN}[OPTIONS]$NC Initialize and deploy PostgreSQL database
Default action with full setup including hydrate and patch
Options:
$BCYAN--no-patch$NC Skip applying test-data.sql
$BCYAN--no-hydrate$NC Skip building from source files
${BCYAN}init${NC} ${CYAN}[OPTIONS]$NC Initialize PostgreSQL cluster without hydrate or patch
${BCYAN}restore${NC} ${CYAN}[PATH]$NC Restore database from backup
${BCYAN}backup${NC} Create database backup
Creates compressed backup at $BBLACK$DEFAULT_BACKUP_PATH$NC
${BCYAN}patch${NC} ${CYAN}[OPTIONS]$NC Patch file: $BBLACK$PATCH_SQL$NC
Apply patch to current database
Uses patch file if exists
Creates empty patch file if not found
Options:
$BCYAN--edit$NC, $BCYAN-e$NC Edit test-data.sql in \$EDITOR before applying
${BCYAN}hydrate${NC} ${CYAN}[OPTIONS]$NC Build database from source files
Runs $BBLACK$HYDRATE_ENTRYPOINT$NC
${BCYAN}migrator${NC} ${CYAN}[OPTIONS]$NC Run database migrations
Uses for production or pre production
${BCYAN}log${NC} ${CYAN}[ARG]$NC Show PostgreSQL logs
ARG: ${BBLACK}list$NC, numeric index, or filename/substring
No ARG: interactive on TTY, concat output when piped
${BCYAN}replay${NC} ${CYAN}[OPTIONS]$NC Restore database from point to point using SQL
Useful for development after minor schema changes
${BRED}WIP$NC
${BCYAN}pull_staging${NC} Pull data from staging DB into test-data.sql
Uses STAGING_* environment variables for connection and dump selection
${BCYAN}format${NC} Format database source files
${BRED}WIP$NC
${BCYAN}test${NC} Run SQL test suite (BEGIN/ROLLBACK, no side effects)
Executes ${BBLACK}${DATABASE_DIR}/test/test.sql$NC
${BCYAN}diff${NC} ${CYAN}[OPTIONS] [PATH]$NC Compare backup+migrations vs current sources
Shows schema differences between production and dev
Creates isolated instances for safe comparison
Subcommands:
${BCYAN}log$NC Show diff operation logs
${BCYAN}check${NC} ${CYAN}[OPTIONS]$NC Full deploy + cleanup in an isolated temporary cluster
Does not affect your running development database
Options:
$BCYAN--no-patch$NC Skip applying test-data.sql
$BCYAN--no-hydrate$NC Skip building from source files
$BCYAN-m$NC Mock external API calls
${BCYAN}cleanup${NC} Stop the running PostgreSQL cluster and remove its working directory
${BGREEN}Environment Variables:${NC}
${BBLACK}PGURL$NC PostgreSQL connection string (auto-detected)
${BBLACK}PROFILE_DIR$NC Profile directory (auto-detected)
${BBLACK}DATABASE_SOURCE$NC Database source files directory (default $BBLACK$DATABASE_SOURCE$NC)
${BGREEN}Examples:${NC}
${BBLACK}# Basic operations$NC
$SCRIPT_NAME deploy Deploy database with full setup
$SCRIPT_NAME deploy --no-patch Deploy without test data
$SCRIPT_NAME init Initialize PostgreSQL only
$SCRIPT_NAME restore Restore from default backup
$SCRIPT_NAME restore /path/to/backup Restore from specific path
$SCRIPT_NAME backup Create backup
$SCRIPT_NAME log Show database logs
$SCRIPT_NAME log list List available log files
$SCRIPT_NAME log 1 Show one log file by index
$SCRIPT_NAME log postgresql-2026-04-21 Show one log file by name pattern
$SCRIPT_NAME patch Apply test data
$SCRIPT_NAME patch --edit Edit test data and apply
${BBLACK}# Schema comparison$NC
$SCRIPT_NAME diff Compare backup vs sources
$SCRIPT_NAME diff -o diff.txt Save comparison to file
$SCRIPT_NAME diff --with-data Include data in comparison
${BBLACK}# Isolated validation$NC
$SCRIPT_NAME check Full deploy + cleanup in temp cluster
$SCRIPT_NAME check -P Check without test data
$SCRIPT_NAME check -H Check without hydrating schema
EOF
)" | "$PAGER_OR_CAT"
}
help_patch() {
# shellcheck disable=SC2059
printf "$(cat <<EOF
${BGREEN}Usage:${NC} $SCRIPT_NAME patch [OPTIONS]
Apply test-data.sql to database
${BGREEN}Options:
$BCYAN-e$NC, $BCYAN--edit$NC Edit test-data.sql in $BBLACK\$EDITOR$NC before applying
EOF
)" | "$PAGER_OR_CAT"
}
help_hydrate() {
# shellcheck disable=SC2059
printf "$(cat <<EOF
${BGREEN}Usage:${NC} $SCRIPT_NAME hydrate [OPTIONS]
Build database from source files
This command builds the database schema from source files by executing
the hydrate entrypoint SQL file. Run hectic secrets hook before
hydration for environment-specific configuration.
${BGREEN}Options:${NC}
$BCYAN-H$NC, $BCYAN--no-hook$NC Skip hectic secrets hook
Useful when secrets are not needed or already applied
$BCYAN-m$NC, $BCYAN--mock$NC Enable mock mode for external API calls
Replaces HTTP-calling functions with stubs/test data
Cron functions get hardcoded test data instead
$BCYAN-h$NC, $BCYAN--help$NC Show this help message
${BGREEN}Process:${NC}
1. Run hectic secrets hook (unless $BBLACK\`--no-hook\`$NC specified)
Executes $BBLACK${LOCAL_DIR}/lib/hook/postgres-secrets.sh$NC
2. Execute hydrate entrypoint SQL file
Runs $BBLACK$HYDRATE_ENTRYPOINT$NC
${BGREEN}Source Files:${NC}
DATABASE_SOURCE: $BBLACK${DATABASE_SOURCE}$NC
HYDRATE_ENTRYPOINT: $BBLACK${HYDRATE_ENTRYPOINT}$NC
${BGREEN}Environment Variables:${NC}
${BBLACK}PGURL$NC PostgreSQL connection string (required)
${BBLACK}DATABASE_SOURCE$NC Path to database source files directory
${BBLACK}HYDRATE_ENTRYPOINT$NC Path to entrypoint SQL file
${BBLACK}ENVIRONMENT$NC Environment name for secrets hook
${BBLACK}PG_LOG_PATH$NC Log directory for hydrate output
${BGREEN}Examples:${NC}
$SCRIPT_NAME hydrate Build database with secrets hook
$SCRIPT_NAME hydrate --no-hook Build without running secrets hook
$SCRIPT_NAME hydrate --mock Build with external APIs mocked
${BGREEN}Notes:${NC}
- Database must be running and accessible via ${BBLACK}PGURL$NC
- Hydration logs are written to $BBLACK${HYDRATE_LOG:-stdout}$NC
- This command is typically called by $BBLACK\`deploy\`$NC subcommand
EOF
)" | "$PAGER_OR_CAT"
}
help_backup() {
# shellcheck disable=SC2059
printf "$(cat <<EOF
${BGREEN}Usage:${NC} $SCRIPT_NAME backup [OPTIONS]
Create a compressed backup of the PostgreSQL server
This command creates a full physical backup of the PostgreSQL server
using $BBLACK\`pg_basebackup\`$NC. The backup is stored in a compressed
tar format at $BBLACK$DEFAULT_BACKUP_PATH$NC.
${BGREEN}Process:${NC}
1. Removes any existing backup at $BBLACK$DEFAULT_BACKUP_PATH$NC
2. Creates fresh backup directory
3. Runs $BBLACK\`pg_basebackup\`$NC with compression
4. Stores $BBLACK\`base.tar.gz\`$NC and $BBLACK\`pg_wal.tar.gz\`$NC files
${BGREEN}Connection Requirements:${NC}
Uses PostgreSQL connection from ${BBLACK}PGURL$NC or $BBLACK--url$NC environment variable
${BGREEN}Files Created:${NC}
- ${BBLACK}\`base.tar.gz\`$NC: Database cluster backup
- ${BBLACK}\`pg_wal.tar.gz\`$NC: Write-Ahead Log files (if present)
${BGREEN}Examples:${NC}
$SCRIPT_NAME backup Create backup at default location
${BGREEN}Notes:${NC}
- This is a physical backup, not logical dump
- Database must be running and accessible
- Backup can be restored with $BBLACK\`restore\`$NC subcommand
EOF
)" | "$PAGER_OR_CAT"
}
help_restore() {
# shellcheck disable=SC2059
printf "$(cat <<EOF
${BGREEN}Usage:${NC} $SCRIPT_NAME restore [OPTIONS] [path]
Restore PostgreSQL database from a physical backup
This command restores a PostgreSQL database from a backup created by
the $BBLACK\`backup\`$NC subcommand. It performs a complete restore including
data directory, WAL files, and configuration.
${BGREEN}Arguments:
${BCYAN}path$NC Backup directory path (optional)
If not specified defaults to $BBLACK$DEFAULT_BACKUP_PATH$NC
Should contain base.tar.gz and optionally pg_wal.tar.gz
${BGREEN}Options:${NC}
$BCYAN--archive$NC Extract from archive file first
Treat PATH as a compressed archive file to extract
$BCYAN-h$NC, $BCYAN--help$NC Show this help message
${BGREEN}Process:${NC}
1. If $BBLACK\`--archive\`$NC used: extract archive to temporary directory
2. Run $BBLACK\`postgres-cleanup\`$NC
to stop and clean existing database
3. Clear PG_WORKING_DIR directory completely
4. Extract $BBLACK\`base.tar.gz\`$NC to $BBLACK$PG_WORKING_DIR$NC directory
5. Extract $BBLACK\`pg_wal.tar.gz\`$NC to ${BBLACK}$PG_WORKING_DIR/pg_wal$NC if present
6. Remove recovery signals to ensure clean startup
7. Start PostgreSQL with existing configuration
${BGREEN}Backup Directory Requirements:${NC}
- $BBLACK\`base.tar.gz\`$NC: Main database cluster backup (required)
- $BBLACK\`pg_wal.tar.gz\`$NC: Write-Ahead Log files (optional)
${BGREEN}Environment Variables:${NC}
${BBLACK}PG_WORKING_DIR$NC PostgreSQL data directory (auto-detected)
${BGREEN}Examples:${NC}
$SCRIPT_NAME restore Restore from default backup
$SCRIPT_NAME restore /path/to/backup Restore from specific path
$SCRIPT_NAME restore --archive backup.tar.gz Extract and restore from archive
${BGREEN}Warnings:${NC}
- This will completely overwrite the existing database
- Database will be stopped during restore process
- All current data will be lost
- ${BRED}Only development usage for now$NC
EOF
)" | "$PAGER_OR_CAT"
}
help_diff() {
# shellcheck disable=SC2059
printf "$(cat <<EOF
${BGREEN}Usage:${NC} $SCRIPT_NAME diff [OPTIONS] [BACKUP_PATH]
$SCRIPT_NAME diff log
Compare two full PostgreSQL deployments and show the diff
This command helps identify schema differences between a production backup
and the current source code. It provisions two isolated PostgreSQL instances,
restores one from backup with migrations applied, hydrates the other from
current sources, then compares their schemas.
${BGREEN}Arguments:
${BCYAN}BACKUP_PATH${NC} Path to backup directory (optional)
If not specified defaults to $BBLACK$DEFAULT_BACKUP_PATH$NC
${BGREEN}Options:${NC}
$BCYAN--tables${NC} ${CYAN}<list>${NC} Comma-separated list of tables to diff
Example: --tables users,orders,products
$BCYAN-m${NC}, $BCYAN--mock${NC} Mock external API calls when hydrating DB2
Replaces HTTP-calling functions with stubs/test data
$BCYAN-h${NC}, $BCYAN--help${NC} Show this help message
${BGREEN}Subcommands:${NC}
${BCYAN}log${NC} Show PostgreSQL logs from diff operation
Displays logs from both DB1 and DB2 instances
${BGREEN}Process:${NC}
1. Create two isolated PostgreSQL instances (DB1 and DB2)
2. Restore DB1 from backup and apply all migrations
3. Hydrate DB2 from current source files
4. Dump both databases to SQL files
5. Compare dumps and present diff
6. Clean up temporary instances
${BGREEN}Environment Variables:${NC}
${BBLACK}DATABASE_SOURCE$NC Path to database source files
${BGREEN}Examples:${NC}
$SCRIPT_NAME diff Compare using default backup
$SCRIPT_NAME diff /path/to/backup Compare using specific backup
$SCRIPT_NAME diff --tables users,orders Compare specific tables
$SCRIPT_NAME diff -m Compare with external APIs mocked
$SCRIPT_NAME diff log Show logs from diff operation
${BGREEN}Notes:${NC}
- Does not affect your development database
- Requires backup created with $BBLACK\`backup\`$NC subcommand
EOF
)" | "$PAGER_OR_CAT"
}
help_init() {
# shellcheck disable=SC2059
printf "$(cat <<EOF
${BGREEN}Usage:${NC} $SCRIPT_NAME init [OPTIONS]
Initialize PostgreSQL cluster without running hydrate or patch.
${BGREEN}Options:${NC}
${BCYAN}--reuse${NC} Do not wipe database if it already exists
${BCYAN}-h${NC}, ${BCYAN}--help${NC} Show this help message
${BGREEN}Examples:${NC}
$SCRIPT_NAME init
$SCRIPT_NAME init --reuse
EOF
)" | "$PAGER_OR_CAT"
}
help_deploy() {
# shellcheck disable=SC2059
printf "$(cat <<EOF
${BGREEN}Usage:${NC} $SCRIPT_NAME deploy [OPTIONS]
Initialize and deploy a complete PostgreSQL database for development
This is the primary command for setting up a fresh database. It initializes
PostgreSQL, builds the database from source files, and applies test data.
${BGREEN}Deployment Process:${NC}
1. Initialize PostgreSQL server via $BBLACK\`postgres-init\`$NC
2. Run $BBLACK\`hydrate\`$NC to build database from source files (skips with $BBLACK\`--no-hydrate\`$NC)
- Includes hectic secrets hook by default
3. Run $BBLACK\`patch\`$NC to apply test-data.sql (skips with $BBLACK\`--no-patch\`$NC)
${BGREEN}Options:${NC}
$BCYAN--resue$NC Do not wipe databse if it exists
$BCYAN-P$NC, $BCYAN--no-patch$NC Skip applying test-data.sql
$BCYAN-H$NC, $BCYAN--no-hydrate$NC Skip building from source files
$BCYAN-m$NC, $BCYAN--mock$NC Mock external API calls (stubs + test data)
$BCYAN-h$NC, $BCYAN--help$NC Show this help message
${BGREEN}Source Files:${NC}
DATABASE_SOURCE: $BBLACK${DATABASE_SOURCE}$NC
- $BBLACK\`entrypoint.sql\`$NC: Main database schema definition
${BGREEN}Test Data:${NC}
$BBLACK$PROFILE_DIR/test-data.sql$NC:
Test data and sample records
- Created automatically if doesn't exist
- You can contain here development/sample data for testing
${BGREEN}Environment Variables:${NC}
${BBLACK}DATABASE_SOURCE$NC Path to database source files directory
${BBLACK}PROFILE_DIR$NC Profile directory containing test-data.sql
${BBLACK}PGDATA$NC PostgreSQL data directory (auto-detected)
${BBLACK}PG_LOG_PATH$NC PostgreSQL log directory
${BGREEN}Examples:${NC}
$SCRIPT_NAME deploy Full deployment with all steps
$SCRIPT_NAME deploy -P Deploy without test data
$SCRIPT_NAME deploy -H Deploy without rebuilding schema
$SCRIPT_NAME deploy -m Deploy with external APIs mocked
$SCRIPT_NAME deploy -P -H Just (re)setup PostgreSQL
$SCRIPT_NAME deploy --reuse Just (re)start postgreSQL,
Does not remove state
Includes $BBLACK\`--no-hydrate\`$NC & $BBLACK\`--no-patch\`$NC
${BGREEN}Notes:${NC}
- This will completely overwrite PostgreSQL sever (unless $BBLACK\`--reuse\`$NC )
- PostgreSQL server will be (re)started
- Database connection required via ${BBLACK}PGURL$NC
EOF
)" | "$PAGER_OR_CAT"
}
# shellcheck disable=SC2120
subcommand_pull_staging() {
change_namespace 'db pull_staging'
if [ -z "${STAGING_SSH_HOST-}" ]; then
printf 'STAGING_SSH_HOST is not set\n' >&2
exit 3
fi
if [ -z "${STAGING_DB_URL-}" ]; then
printf 'STAGING_DB_URL is not set\n' >&2
exit 3
fi
if [ -z "${STAGING_DUMP_TABLES-}" ]; then
printf 'STAGING_DUMP_TABLES is not set\n' >&2
exit 3
fi
if [ -z "${STAGING_DUMP_FLAGS-}" ]; then
printf 'STAGING_DUMP_FLAGS is not set\n' >&2
exit 3
fi
while [ $# -gt 0 ]; do
case $1 in
-h|--help)
help_pull_staging
exit 0
;;
--*|-*)
log error "pull_staging argument $1 does not exist"
exit 9
;;
*)
log error "pull_staging: unexpected argument $1"
exit 9
;;
esac
done
PG_DUMP_TABLE_ARGS=''
for tbl in $STAGING_DUMP_TABLES; do
PG_DUMP_TABLE_ARGS="$PG_DUMP_TABLE_ARGS -t $(quote "$tbl")"
done
log notice "pulling data from ${WHITE}${STAGING_SSH_HOST}${NC}"
remote_dump_command="pg_dump $(quote "$STAGING_DB_URL") $STAGING_DUMP_FLAGS$PG_DUMP_TABLE_ARGS 2>/dev/null"
# shellcheck disable=SC2029
STAGING_DUMP=$(ssh "$STAGING_SSH_HOST" "$remote_dump_command") || {
log error "failed to dump staging database via SSH"
exit 1
}
if [ -z "$STAGING_DUMP" ]; then
log error "staging dump returned empty output"
exit 1
fi
SEQ_RESETS=''
for tbl in $STAGING_DUMP_TABLES; do
escaped_tbl=$(printf '%s' "$tbl" | sed "s/'/''/g")
SEQ_RESETS="$SEQ_RESETS
SELECT setval(seq_name, COALESCE((SELECT MAX(\"id\") FROM $tbl), 1))
FROM (SELECT pg_get_serial_sequence('$escaped_tbl', 'id') AS seq_name) AS seq
WHERE seq_name IS NOT NULL;"
done
mkdir -p "$(dirname "$PATCH_SQL")"
touch "$PATCH_SQL"
cat > "$PATCH_SQL" <<EOSQL
-- Test data for local development
-- Pulled from staging ($STAGING_SSH_HOST) on $(date -u '+%Y-%m-%d %H:%M UTC')
-- Auto-generated by: $SCRIPT_NAME pull_staging
$STAGING_DUMP
-- Reset sequences to match imported data
$SEQ_RESETS
EOSQL
log notice "written to ${WHITE}${PATCH_SQL}${NC}"
log notice "tables dumped: ${WHITE}${STAGING_DUMP_TABLES}${NC}"
restore_namespace
}
help_pull_staging() {
# shellcheck disable=SC2059
printf "$(cat <<EOF
${BGREEN}Usage:${NC} $SCRIPT_NAME pull_staging
Pull data from staging database into test-data.sql (data only, no schema).
Reads the staging connection and dump configuration from environment variables,
SSHs to the staging server, runs pg_dump, and writes the result to
\$PATCH_SQL (${BBLACK}${PATCH_SQL}${NC}).
${BGREEN}Required environment variables:${NC}
${BBLACK}STAGING_SSH_HOST${NC} SSH host alias or hostname
${BBLACK}STAGING_DB_URL${NC} PostgreSQL connection string used by pg_dump
${BBLACK}STAGING_DUMP_TABLES${NC} Space-separated tables passed as -t selectors
${BBLACK}STAGING_DUMP_FLAGS${NC} Extra pg_dump flags
${BGREEN}Examples:${NC}
STAGING_SSH_HOST=staging \
STAGING_DB_URL=postgresql://postgres@localhost/app \
STAGING_DUMP_TABLES='public.users public.orders' \
STAGING_DUMP_FLAGS='--data-only --column-inserts --on-conflict-do-nothing --no-owner --no-privileges --no-comments' \
$SCRIPT_NAME pull_staging
EOF
)" | "$PAGER_OR_CAT"
}
help_test() {
# shellcheck disable=SC2059
printf "$(cat <<EOF
${BGREEN}Usage:${NC} $SCRIPT_NAME test
Run SQL test suite against the current database
Executes ${BBLACK}${DATABASE_DIR}/test/test.sql${NC} wrapped in
BEGIN/ROLLBACK transactions (no side effects).
${BGREEN}Requirements:${NC}
Database must be deployed and hydrated before running tests.
${BGREEN}Examples:${NC}
$SCRIPT_NAME test Run all tests
EOF
)" | "$PAGER_OR_CAT"
}
subcommand_test() {
change_namespace 'db test'
while [ $# -gt 0 ]; do
case $1 in
-h|--help)
help_test
exit 0
;;
--*|-*)
log error "test argument $1 does not exists"
exit 9
;;
*)
log error "test subcommand $1 does not exists"
exit 9
;;
esac
done
TEST_SQL="${DATABASE_DIR}/test/test.sql"
if [ ! -f "$TEST_SQL" ]; then
log error "test file not found: $TEST_SQL"
exit 1
fi
log notice "running tests from $WHITE$TEST_SQL$NC"
# shellcheck disable=SC2046
if psql $(form_psql_args) -f "$TEST_SQL"; then
log notice "all tests ${GREEN}passed"
else
log error "tests ${RED}failed"
exit 1
fi
restore_namespace
}
subcommand_format() {
todo
}
subcommand_backup() {
change_namespace 'db backup'
while [ $# -gt 0 ]; do
case $1 in
-h|--help)
help_backup
exit 0
;;
--*|-*)
log error "restore argument $1 does not exists"
exit 9
;;
*)
log error "restore subcommand $1 does not exists"
exit 9
;;
esac
done
rm -rf "${DEFAULT_BACKUP_PATH:?}"
mkdir "$DEFAULT_BACKUP_PATH"
env PGPASSWORD="${URI_PASSWORD}" pg_basebackup \
-h "${URI_HOST}" -p "${URI_PORT:?}" -U "${URI_USER:?}" \
-D "${DEFAULT_BACKUP_PATH:?}" -Ft -X stream -z -P
restore_namespace
}
# shellcheck disable=SC2120
subcommand_restore() {
change_namespace 'db restore'
: "${PG_WORKING_DIR:="$LOCAL_DIR/focus/postgresql"}"
# - to set postgresql server
# data & sock directory
while [ $# -gt 0 ]; do
case $1 in
-h|--help)
help_restore
exit 0
;;
--archive)
RESTORE_NEED_UNPACK=1
shift
;;
--*|-*)
log error "restore argument $1 does not exists"
exit 9
;;
*)
# NOTE: Only first argument unrecognized argument can be path
if [ ${RESTORE_BACKUP_PATH+x} ]; then
log error "restore subcommand $1 does not exists"
exit 9
else
RESTORE_BACKUP_PATH=$1
shift
fi
;;
esac
done
if [ ${RESTORE_NEED_UNPACK+x} ]; then
local backup_dir
backup_dir=$(mktemp -d)
mkdir "$backup_dir"
tar -xzf "${RESTORE_BACKUP_PATH+x}" -C "${backup_dir}"
RESTORE_BACKUP_PATH="$backup_dir"
trap 'rm -rf "$RESTORE_BACKUP_PATH"' EXIT INT HUP
fi
if ! [ ${RESTORE_BACKUP_PATH+x} ]; then
RESTORE_BACKUP_PATH="$DEFAULT_BACKUP_PATH"
fi
postgres-cleanup
local data="${PG_WORKING_DIR:?}/data"
rm -rf "${data:?}"/
mkdir -m 700 "${data}"
tar -xzf "${RESTORE_BACKUP_PATH:?}/base.tar.gz" -C "${data}"
if [ -f "${RESTORE_BACKUP_PATH:?}/pg_wal.tar.gz" ]; then
tar -xzf "${RESTORE_BACKUP_PATH:?}/pg_wal.tar.gz" -C "${data}/pg_wal"
fi
env PG_REUSE= postgres-init
rm -f "${data}/standby.signal" "${data}/recovery.signal"
restore_namespace
}
subcommand_log() {
change_namespace 'db log'
: "${PG_WORKING_DIR:="$LOCAL_DIR/focus/postgresql"}"
LOG_DIR="${PG_WORKING_DIR}/data/log"
if [ ! -d "$LOG_DIR" ]; then
log error "log directory not found: $LOG_DIR"
exit 1
fi
while [ $# -gt 0 ]; do
case $1 in
-h|--help)
help_log
exit 0
;;
--*|-*)
log error "log argument $1 does not exist"
exit 9
;;
*)
if [ "$1" = 'list' ]; then
db_log_print_list "$LOG_DIR"
restore_namespace
return 0
fi
resolved_file="$(db_log_resolve_one "$LOG_DIR" "$1" || true)"
if [ -z "$resolved_file" ]; then
log error "log file not found or ambiguous: $1"
log info "use '$SCRIPT_NAME log list' to inspect available files"
exit 1
fi
if [ -t 1 ]; then
log_pager "$resolved_file"
else
cat "$resolved_file"
fi
restore_namespace
return 0
;;
esac
done
if [ -t 1 ]; then
LOG_HELP="${LOCAL_DIR}/focus/database-log-help"
# shellcheck disable=SC2059
printf "$(cat <<'EOF'
press `q` - exit from logs
use `gt` `gT` to navigate through different log files
EOF
)" > "$LOG_HELP"
# shellcheck disable=SC2046
set -- $(db_log_list_files "$LOG_DIR")
if [ "$#" -eq 0 ]; then
log warn "no log files in $LOG_DIR"
restore_namespace
return 0
fi
if [ "$#" -gt 1 ]; then
log_pager -p "$LOG_HELP" "$@"
else
log_pager "$1"
fi
else
found=0
for log_file in "$LOG_DIR"/*; do
[ -f "$log_file" ] || continue
found=1
cat "$log_file"
done
if [ "$found" -eq 0 ]; then
log warn "no log files in $LOG_DIR"
fi
fi
restore_namespace
}
subcommand_replay() {
change_namespace 'db replay'
todo
}
# shellcheck disable=SC2120
subcommand_hydrate() {
change_namespace 'db hydrate'
while [ $# -gt 0 ]; do
case $1 in
-h|--help)
help_hydrate
exit 0
;;
-H|--no-hook)
HYDRATE_NO_HOOK=1
shift
;;
-m|--mock)
HYDRATE_USE_MOCK=1
shift
;;
--*|-*)
log error "hydrate argument $1 does not exist"
exit 9
;;
*)
log error "hydrate subcommand $1 does not exist"
exit 9
;;
esac
done
if [ ! "${HYDRATE_NO_HOOK+x}" ]; then
log info "hectic bundle hook"
# shellcheck disable=SC2059
printf "${BBLACK}"
dotenv_content=""
if [ -n "${HECTIC_DOTENV_FILE:-}" ] && [ -r "$HECTIC_DOTENV_FILE" ]; then
dotenv_content="$(cat "$HECTIC_DOTENV_FILE")"
elif [ -n "${ENVIRONMENT:-}" ] && [ -r "${LOCAL_DIR}/.env.${ENVIRONMENT}" ]; then
dotenv_content="$(cat "${LOCAL_DIR}/.env.${ENVIRONMENT}")"
fi
apply_hectic_bundle "$PGURL" "$dotenv_content"
# shellcheck disable=SC2059
printf "${NC}"
else
log info "skipping hectic bundle hook"
fi
local mock_arg=""
if [ "${HYDRATE_USE_MOCK+x}" ]; then
log info "mock mode enabled — external API calls will be stubbed"
mock_arg="-v USE_MOCK=1"
fi
log notice "hydrate database sources"
# shellcheck disable=SC2059
printf "${BBLACK}"
# shellcheck disable=SC2046
# shellcheck disable=SC2086
if psql_logged "${HYDRATE_LOG:-}" $(form_psql_args) $mock_arg \
-f "$HYDRATE_ENTRYPOINT"; then
log notice "hydrating succes"
else
log error "hydrate error, check $WHITE${HYDRATE_LOG:-stdout}$NC for more"
exit 1
fi
restore_namespace
}
subcommand_migrator() {
change_namespace 'db migrator'
db_url="${DB_URL:-$PGURL}"
env MIGRATION_DIR="$MIGRATION_DIR" DB_URL="$db_url" migrator "$@"
migrator_exit_code="$?"
restore_namespace
return "$migrator_exit_code"
}
# ___parse_deploy_flags -- shared option parser for deploy/check
# Sets: DEPLOY_NO_PATCH, DEPLOY_NO_HYDRATE, HYDRATE_USE_MOCK, DEPLOY_REUSE
# Returns remaining unparsed args via REMAINING_ARGS (caller resets first)
# Caller must supply a CALLER name for error messages (first positional arg).
___parse_deploy_flags() {
_caller="${1:?___parse_deploy_flags: caller name required}"; shift
while [ $# -gt 0 ]; do
case $1 in
-P|--no-patch)
DEPLOY_NO_PATCH=1
shift
;;
-H|--no-hydrate)
DEPLOY_NO_HYDRATE=1
shift
;;
-m|--mock)
HYDRATE_USE_MOCK=1
shift
;;
--reuse)
DEPLOY_NO_PATCH=1
DEPLOY_NO_HYDRATE=1
DEPLOY_REUSE=1
shift
;;
*)
log error "$_caller: unknown argument $1"
exit 9
;;
esac
done
}
# ___run_deploy_flow -- shared hydrate+patch execution for deploy/check
___run_deploy_flow() {
if [ ! "${DEPLOY_NO_HYDRATE+x}" ]; then
subcommand_hydrate
fi
if [ ! "${DEPLOY_NO_PATCH+x}" ]; then
subcommand_patch
fi
}
subcommand_init() {
: "${PG_WORKING_DIR:=${LOCAL_DIR}/focus/postgresql}"
# - to set postgresql server
# data & sock directory
change_namespace 'db init'
unset DEPLOY_REUSE
while [ $# -gt 0 ]; do
case $1 in
-h|--help)
help_init
exit 0
;;
--reuse)
DEPLOY_REUSE=1
shift
;;
--*|-*)
log error "init argument $1 does not exist"
exit 9
;;
*)
log error "init: unexpected argument $1"
exit 9
;;
esac
done
{
[ "${DEPLOY_REUSE+x}" ] && export PG_REUSE
postgres-init
}
restore_namespace
}
subcommand_deploy() {
: "${PG_WORKING_DIR:="$LOCAL_DIR/focus/postgresql"}"
# - to set postgresql server
# data & sock directory
change_namespace 'db deploy'
unset DEPLOY_NO_PATCH DEPLOY_NO_HYDRATE DEPLOY_REUSE DEPLOY_CLEANUP
_deploy_extra=
while [ $# -gt 0 ]; do
case $1 in
-h|--help)
help_deploy
exit 0
;;
-c|--cleanup)
DEPLOY_CLEANUP=1
shift
;;
*)
_deploy_extra="$_deploy_extra $(quote "$1")"
shift
;;
esac
done
[ "$_deploy_extra" = '' ] || eval "___parse_deploy_flags 'deploy' $_deploy_extra"
{
[ "${DEPLOY_REUSE+x}" ] && export PG_REUSE
postgres-init
}
___run_deploy_flow
if [ "${DEPLOY_CLEANUP+x}" ]; then
log info "cleanup: stopping postgresql"
postgres-cleanup
fi
restore_namespace
}
# shellcheck disable=SC2120
subcommand_patch() {
change_namespace 'db patch'
while [ $# -gt 0 ]; do
case $1 in
-e|--edit)
PATCH_EDIT=1
shift
;;
-h|--help)
help_patch
exit 0
;;
--*|-*)
log error "patch argument $1 does not exists"
exit 9
;;
*)
log error "patch subcommand $1 does not exists"
exit 9
;;
esac
done
mkdir -p "$(dirname "$PATCH_SQL")"
touch "$PATCH_SQL"
if [ "${PATCH_EDIT+x}" ]; then
log notice "opening $WHITE$PATCH_SQL$NC in \$EDITOR"
"${EDITOR:-vi}" "$PATCH_SQL"
exit 0
fi
log notice "retoring $WHITE$PATCH_SQL$NC in database"
# shellcheck disable=SC2059
printf "${BBLACK}"
# shellcheck disable=SC2046
if psql_logged "${PATCH_LOG:-}" $(form_psql_args) -f "$PATCH_SQL"; then
log notice "restoring succes"
else
log error "error, check $WHITE${PATCH_LOG:-stdout}$NC for more"
exit 1
fi
restore_namespace
}
___diff_dump_schema() {
local socket_dir="$1"
local port="$2"
local output_file="$3"
local tables="${4:-}"
log info "dumping schema to $WHITE$output_file$NC"
if [ -n "$tables" ]; then
# Dump specific tables with data
local table_args=""
old_IFS="$IFS"
IFS=','
for table in $tables; do
table_args="$table_args -t $table"
done
IFS="$old_IFS"
# shellcheck disable=SC2086
pg_dump -h "$socket_dir" -p "$port" testdb \
--schema-only --no-owner --no-privileges \
$table_args > "$output_file" 2>/dev/null
# shellcheck disable=SC2086
pg_dump -h "$socket_dir" -p "$port" testdb \
--data-only --no-owner --no-privileges \
$table_args >> "$output_file" 2>/dev/null
else
# Schema only
pg_dump -h "$socket_dir" -p "$port" testdb \
--schema-only --no-owner --no-privileges \
> "$output_file" 2>/dev/null
fi
}
___diff_immutable_tables() {
local socket_dir="$1"
local port="$2"
psql -h "$socket_dir" -p "$port" -d testdb -tAv ON_ERROR_STOP=1 -c "$(cat <<'SQL'
SELECT n.nspname || '.' || c.relname
FROM pg_inherits i
JOIN pg_class c ON c.oid = i.inhrelid
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE i.inhparent = 'hectic.immutable'::regclass
AND c.relkind = 'r'
ORDER BY 1
SQL
)" 2>/dev/null
}
___diff_immutable_data() {
local sock1="$1"
local port1="$2"
local sock2="$3"
local port2="$4"
local out_file="$5"
if ! psql -h "$sock1" -p "$port1" -d testdb -tAc \
"SELECT 1 FROM pg_class c JOIN pg_namespace n ON n.oid=c.relnamespace WHERE n.nspname='hectic' AND c.relname='immutable';" \
>/dev/null 2>&1
then
return 0
fi
local tables1 tables2 tables
tables1=$(___diff_immutable_tables "$sock1" "$port1" || true)
tables2=$(___diff_immutable_tables "$sock2" "$port2" || true)
tables=$(printf '%s\n%s\n' "$tables1" "$tables2" | sort -u | sed '/^$/d')
if [ -z "$tables" ]; then
return 0
fi
log notice "diffing data of tables inheriting hectic.immutable"
printf '\n--- IMMUTABLE TABLE DATA ---\n' >> "$out_file"
local data1 data2 differs=0
data1=$(mktemp)
data2=$(mktemp)
trap 'rm -f "$data1" "$data2"' EXIT INT HUP
for tbl in $tables; do
log info " $tbl"
: > "$data1"
: > "$data2"
pg_dump -h "$sock1" -p "$port1" testdb \
--data-only --no-owner --no-privileges --column-inserts -t "$tbl" \
> "$data1" 2>/dev/null || :
pg_dump -h "$sock2" -p "$port2" testdb \
--data-only --no-owner --no-privileges --column-inserts -t "$tbl" \
> "$data2" 2>/dev/null || :
{
printf '\n=== %s ===\n' "$tbl"
if diff --color=always -u "$data1" "$data2"; then
printf '(no differences)\n'
else
differs=1
fi
} >> "$out_file"
done
rm -f "$data1" "$data2"
trap - EXIT INT HUP
return $differs
}
help_check() {
# shellcheck disable=SC2059
printf "$(cat <<EOF
${BGREEN}Usage:${NC} $SCRIPT_NAME check [OPTIONS]
Run a full deploy in an isolated temporary PostgreSQL cluster, then clean up.
Does not affect your running development database.
${BGREEN}Process:${NC}
1. Create a temporary working directory under $BBLACK\$LOCAL_DIR/focus/postgresql-check-tmp$NC
2. Run postgres-init against that temporary cluster
3. Run hydrate (unless $BBLACK\`--no-hydrate\`$NC)
4. Run patch (unless $BBLACK\`--no-patch\`$NC)
5. Stop the temporary cluster via postgres-cleanup
6. Remove the temporary working directory
${BGREEN}Options:${NC}
$BCYAN-P$NC, $BCYAN--no-patch$NC Skip applying test-data.sql
$BCYAN-H$NC, $BCYAN--no-hydrate$NC Skip building from source files
$BCYAN-m$NC, $BCYAN--mock$NC Mock external API calls
$BCYAN-h$NC, $BCYAN--help$NC Show this help message
${BGREEN}Examples:${NC}
$SCRIPT_NAME check Full deploy + cleanup
$SCRIPT_NAME check -P Skip test data
$SCRIPT_NAME check -H Skip schema hydration
$SCRIPT_NAME check -m Mock external APIs
EOF
)" | "$PAGER_OR_CAT"
}
# ___stop_and_remove_working_dir -- stop PG cluster and remove its directory
# Expects: $1 = working dir path
___stop_and_remove_working_dir() {
_wd="${1:?___stop_and_remove_working_dir: working dir required}"
log info "stopping cluster at $WHITE$_wd$NC"
PG_WORKING_DIR="$_wd" \
postgres-cleanup
log info "removing $WHITE$_wd$NC"
rm -rf "$_wd"
}
subcommand_check() {
change_namespace 'db check'
unset DEPLOY_NO_PATCH DEPLOY_NO_HYDRATE HYDRATE_USE_MOCK DEPLOY_REUSE
_check_extra=
while [ $# -gt 0 ]; do
case $1 in
-h|--help)
help_check
exit 0
;;
*)
_check_extra="$_check_extra $(quote "$1")"
shift
;;
esac
done
[ "$_check_extra" = '' ] || eval "___parse_deploy_flags 'check' $_check_extra"
CHECK_WORKING_DIR="${LOCAL_DIR}/focus/postgresql-check-tmp"
log info "isolated cluster: $WHITE$CHECK_WORKING_DIR$NC"
rm -rf "$CHECK_WORKING_DIR"
mkdir -p "$CHECK_WORKING_DIR"
trap '___stop_and_remove_working_dir "$CHECK_WORKING_DIR"' EXIT INT HUP
log notice "check: initializing temporary PostgreSQL cluster"
PG_WORKING_DIR="$CHECK_WORKING_DIR" \
PG_DATABASE="${PG_DATABASE:-testdb}" \
PG_DISABLE_LOGGING=1 \
postgres-init
PGURL="postgresql://$(id -un)/${PG_DATABASE:-testdb}?host=${CHECK_WORKING_DIR}/sock&port=${PG_PORT:-5432}"
export PGURL
log info "check PGURL: $WHITE$PGURL$NC"
___run_deploy_flow
log notice "check: ${GREEN}success${NC} — deploy completed cleanly"
restore_namespace
}
subcommand_cleanup() {
change_namespace 'db cleanup'
: "${PG_WORKING_DIR:="$LOCAL_DIR/focus/postgresql"}"
while [ $# -gt 0 ]; do
case $1 in
-h|--help)
# shellcheck disable=SC2059
printf "$(cat <<EOF
${BGREEN}Usage:${NC} $SCRIPT_NAME cleanup
Stop the running PostgreSQL cluster and remove its working directory.
Defaults to $BBLACK$LOCAL_DIR/focus/postgresql$NC unless ${BBLACK}PG_WORKING_DIR$NC is set.
EOF
)" | "$PAGER_OR_CAT"
exit 0
;;
--*|-*)
log error "cleanup argument $1 does not exist"
exit 9
;;
*)
log error "cleanup: unexpected argument $1"
exit 9
;;
esac
done
___stop_and_remove_working_dir "$PG_WORKING_DIR"
restore_namespace
}
subcommand_diff() {
change_namespace 'db diff'
DIFF_TABLES="" # TODO: add cron table
DIFF_NO_CRON=0 # TODO: useless option
DIFF_BACKUP_PATH=""
while [ $# -gt 0 ]; do
case $1 in
-h|--help)
help_diff
exit 0
;;
--tables)
DIFF_TABLES="$2"
shift 2
;;
--no-cron)
DIFF_NO_CRON=1
shift
;;
-m|--mock)
HYDRATE_USE_MOCK=1
shift
;;
log)
DIFF_LOG=1
shift
;;
--*|-*)
log error "diff argument $1 does not exist"
exit 9
;;
*)
# NOTE: yes, RESTORE, not DIFF prefix
if [ -z "$RESTORE_BACKUP_PATH" ]; then
RESTORE_BACKUP_PATH="$1"
shift
else
log error "diff: unexpected argument $1"
exit 9
fi
;;
esac
done
DIFF_TMPDIR="${LOCAL_DIR}/focus/database-diff-operation"
DIFF_PGDATA1="$DIFF_TMPDIR/pgdata1"
DIFF_PGDATA2="$DIFF_TMPDIR/pgdata2"
# TODO: suka, logi drugie
DIFF_PGLOGFILE1="$DIFF_PGDATA1/logfile"
DIFF_PGLOGFILE2="$DIFF_PGDATA2/logfile"
DIFF_PGURL1="postgresql://localhost:5432/testdb?host=$DIFF_PGDATA1/sock"
DIFF_PGURL2="postgresql://localhost:5432/testdb?host=$DIFF_PGDATA2/sock"
if [ "${DIFF_LOG+x}" ]; then
log_pager -O "$DIFF_PGLOGFILE1" "$DIFF_PGLOGFILE2"
exit 0
fi
log info "create temporary directory for isolated instances"
mkdir -p "$DIFF_TMPDIR"
trap 'pg_ctl -D "$DIFF_TMPDIR/pgdata1/data" stop -m fast >/dev/null 2>&1 || true; pg_ctl -D "$DIFF_TMPDIR/pgdata2/data" stop -m fast >/dev/null 2>&1 || true;' EXIT INT HUP
mkdir -p "$DIFF_PGDATA1" "$DIFF_PGDATA2"
log info "DB1: $WHITE$DIFF_PGDATA1$NC"
log info "DB2: $WHITE$DIFF_PGDATA2$NC"
log notice "comparing backup vs sources"
log info "backup: $WHITE$DIFF_BACKUP_PATH$NC"
log info "sources: $WHITE$DATABASE_SOURCE$NC"
log notice "provisioning ${WHITE}DB1$NC (backup + migrations)"
PG_WORKING_DIR="$DIFF_PGDATA1" PG_LOG_PATH="$DIFF_PGDATA1" subcommand_restore
log info "applying migrations to ${WHITE}DB1$NC"
subcommand_migrator migrate up all \
--db-url \
"$DIFF_PGURL1" \
|| {
log warn "migrations failed or none to apply"
}
log notice "provisioning ${WHITE}DB2$NC (current sources)"
log info "initializing ${WHITE}DB2$NC with postgres-init"
PG_WORKING_DIR="$DIFF_PGDATA2" \
PG_DATABASE="testdb" \
PG_DISABLE_LOGGING=1 \
postgres-init || {
log error "failed to initialize ${WHITE}DB2$NC"
exit 1
}
log info "hydrating ${WHITE}DB2$NC from sources"
PGURL="$DIFF_PGURL2" \
subcommand_hydrate || {
log error "hydration failed"
exit 1
}
subcommand_migrator init \
--db-url \
"$DIFF_PGURL2" \
|| {
log error "init migration failed"
exit 1
}
log notice "dumping schemas"
DIFF_DUMP1="$DIFF_TMPDIR/target.sql"
DIFF_DUMP2="$DIFF_TMPDIR/source.sql"
___diff_dump_schema "$DIFF_PGDATA1/sock" "5432" "$DIFF_DUMP1" "$DIFF_TABLES"
___diff_dump_schema "$DIFF_PGDATA2/sock" "5432" "$DIFF_DUMP2" "$DIFF_TABLES"
# Optional: filter out cron tables
if [ "$DIFF_NO_CRON" = "1" ]; then
log info "filtering cron tables"
grep -v "cron\." "$DIFF_DUMP1" > "$DIFF_DUMP1.filtered" 2>/dev/null || true
grep -v "cron\." "$DIFF_DUMP2" > "$DIFF_DUMP2.filtered" 2>/dev/null || true
mv "$DIFF_DUMP1.filtered" "$DIFF_DUMP1"
mv "$DIFF_DUMP2.filtered" "$DIFF_DUMP2"
fi
log notice "generating diff"
if diff --color=always -u "$DIFF_DUMP1" "$DIFF_DUMP2" \
> "$DIFF_TMPDIR/diff"
then
log notice "no schema differences found"
schema_differs=0
else
log notice "schema differences found"
schema_differs=1
fi
___diff_immutable_data \
"$DIFF_PGDATA1/sock" "5432" \
"$DIFF_PGDATA2/sock" "5432" \
"$DIFF_TMPDIR/diff"
data_status=$?
if [ "$schema_differs" = 0 ] && [ "$data_status" = 0 ]; then
log notice "no differences found"
else
"$PAGER_OR_CAT" "$DIFF_TMPDIR/diff"
fi
restore_namespace
}
# ALLOWED_ACTIONS='backup,log'
if ! [ "${AS_LIBRARY+x}" ]; then
if [ $# -eq 0 ]; then
help
exit 1
fi
if [ "$1" = '-h' ] || [ "$1" = '--help' ] || [ "$1" = '-?' ] || [ "$1" = 'help' ] || [ "$1" = '?' ]; then
help
exit 0
fi
while [ $# -gt 0 ]; do
case $1 in
-u|--url)
PGURL=$2
DB_URL=$2
shift 2
;;
deploy|replay|restore|patch|hydrate|backup|log|migrator|diff|pull_staging|test|check|cleanup|init)
if [ "${SUBCOMMAND+x}" ]; then
REMAINING_ARGS="$REMAINING_ARGS $(quote "$1")"
else
SUBCOMMAND="$1"
fi
shift
;;
-i|--inherits)
INHERITS_LIST="${INHERITS_LIST+$INHERITS_LIST\"}$2"
shift 2
;;
*) REMAINING_ARGS="$REMAINING_ARGS $(quote "$1")"; shift ;;
esac
done
[ "${SUBCOMMAND+x}" ] || ( log error "no subcommand specified. Use '--help' for usage information."; exit 1; )
# SAFETY: do not allow use on remote database
#if ! printf '%s\n' "$ALLOWED_ACTIONS" | grep "$SUBCOMMAND"; then
# IS_REMOTE="$(psql "$PGURL" -tAc "SELECT (inet_client_addr() IS NOT NULL)::int;")"
# [ "$IS_REMOTE" = 1 ] && (log error "THIS IS NOT LOCAL DATABSE, BASTARD"; exit 1)
#fi
[ "${INHERITS_LIST+x}" ] && {
INHERITS_LIST="$(printf '%s' "$INHERITS_LIST" | sed -E 's/"/,/g; s/([^,]+)/"\1"/g')"
old_IFS="$IFS"
IFS=','
check_inherits=
for table in $INHERITS_LIST; do
check_inherits="$(printf '%s\nSELECT 1 FROM %s LIMIT 1;' "$check_inherits" "$table")"
done
IFS="$old_IFS"
check_inherits=$(printf '%s\n' \
'BEGIN;' \
"$check_inherits" \
'COMMIT;')
if ! db_exec "$check_inherits"; then
log error "init failed: ${WHITE}one of inherits table does not exists: ${CYAN}$INHERITS_LIST"
exit 5
fi
}
parsed_uri=$(mktemp)
trap 'rm -rf "$parsed_uri"' EXIT INT HUP
parse-uri "${PGURL:-}" > "$parsed_uri"
# shellcheck disable=SC1090
. "$parsed_uri"
[ "$REMAINING_ARGS" = '' ] || eval "set -- $REMAINING_ARGS"; REMAINING_ARGS=
"subcommand_$SUBCOMMAND" "$@"
fi