# 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}/hydate.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!' \ -c 'runtime! plugin/*.vim' \ -c 'set conceallevel=3' \ $@ \ - } help_log() { # shellcheck disable=SC2059 printf "$(cat <|] 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 $NC Open/print one file by numeric index from list ${BCYAN}log $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 < [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 <${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 <&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" < "$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 secrets hook" # shellcheck disable=SC2059 printf "${BBLACK}" sh "${LOCAL_DIR}/lib/hook/postgres-secrets.sh" "$PGURL" "$ENVIRONMENT" # shellcheck disable=SC2059 printf "${NC}" else log info "skipping hectic secrets 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 </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