Skip to content

Instantly share code, notes, and snippets.

@mkw
Last active September 26, 2016 02:34
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save mkw/be60022d2c4879a5df6f5daa97749856 to your computer and use it in GitHub Desktop.
Save mkw/be60022d2c4879a5df6f5daa97749856 to your computer and use it in GitHub Desktop.
Script to copy part or all of PostgreSQL databases between different connections
#!/usr/bin/env bash
# MIT License
#
# Copyright (c) 2016 Michael K. Werle
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.#
# Make failed commands exit the script
set -e
# Determine script directory
if [ "$(uname -s | cut -c 1-9)" = "MINGW.._NT" ]; then
SCRIPT=$(basename ${BASH_SOURCE[0]})
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
else
SOURCE="${BASH_SOURCE[0]}"
SCRIPT=$(basename ${SOURCE})
while [ -h "$SOURCE" ]; do
DIR="$( cd -P "$( dirname"$SOURCE" )" && pwd )"
SOURCE="$(readlink"$SOURCE")"
[[ ${SOURCE} != /* ]] && SOURCE="$DIR/$SOURCE"
done
DIR="$( cd -P "$(dirname "$SOURCE")" && pwd )"
fi
# Some constants
ENTIRE_DATABASE='__ENTIRE_DATABASE__'
ENTIRE_SCHEMA='__ENTIRE_SCHEMA__'
PG_SERVICE_CONF=~/.pg_service.conf
LF=$'\n'
QUOT='"'
NORM=`tput sgr0`
BOLD=`tput bold`
REV=`tput smso`
CC_RED='\033[0;31m'
CC_YELLOW='\033[0;33m'
CC_GREEN='\033[32m'
CC_END='\033[0m'
echo_red() { echo -e "${CC_RED}$@${CC_END}"; }
echo_yellow() { echo -e "${CC_YELLOW}$@${CC_END}"; }
echo_green() { echo -e "${CC_GREEN}$@${CC_END}"; }
show_help() {
echo -e "Help documentation for ${BOLD}${SCRIPT}.${NORM}"
echo
echo -e "Basic usage: ${BOLD}$SCRIPT${NORM}"
echo
echo -e "All command line switches are optional. The following switches are recognized."
echo
echo -e "\t${BOLD}-b${NORM} Save backups after a successful copy."
echo -e "\t${BOLD}-B [backup location]${NORM} Backup location (resolved relative to the current working dir)."
echo -e "\t${BOLD}-d [destination]${NORM} Destination database service name."
echo -e "\t${BOLD}-D${NORM} Copy entire database."
echo -e "\t${BOLD}-f [source]${NORM} Source database service name."
echo -e "\t${BOLD}-B${NORM} No backup (no way to undo failed copies)."
echo -e "\t${BOLD}-s [schema name]${NORM} Schema name."
echo -e "\t${BOLD}-S${NORM} Copy entire schema."
echo -e "\t${BOLD}-t [table_name]${NORM} Table to copy (multiple accepted)."
echo -e "\t${BOLD}-T${NORM} Restore in transaction (slower, but atomic success/failure)."
echo -e "\t${BOLD}-u${NORM} Unattended operation (skip confirmations)."
echo
exit 0
}
# Checks for script requirements
init() {
if [[ ! -f ${PG_SERVICE_CONF} ]]; then
echo_red '************************'
echo_red 'Connection file missing!'
echo_red '************************'
echo
echo_yellow Please create a ${PG_SERVICE_CONF} file with host, port, dbname, user and password.
echo_yellow ' See https://www.postgresql.org/docs/9.5/static/libpq-pgservice.html'
echo_yellow ' and https://www.postgresql.org/docs/9.5/static/libpq-connect.html#LIBPQ-PARAMKEYWORDS'
echo
echo Example configuration section for ${PG_SERVICE_CONF}:
echo
echo -e '\t[my-connection-name]'
echo -e '\thost=my.host.name.com'
echo -e '\tport=1234'
echo -e '\tdbname=my_db_name'
echo -e '\tuser=my_user'
echo -e '\tpassword=my_password'
echo
exit 1
fi
for cmd in psql pg_dump pg_restore; do
if [[ ! -x "$(command -v ${cmd})" ]]; then
echo_red '*************************'
echo_red 'Required command missing!'
echo_red '*************************'
echo_red "${cmd} command missing; please install it"
exit 2
fi
done
}
ini_parser () {
# From; http://theoldschooldevops.com/2008/02/09/bash-ini-parser/
local oifs="${IFS}"
local ini="$(<$1)" # read the file
ini="${ini//[/\\[}" # escape [
ini="${ini//]/\\]}" # escape ]
local IFS=$'\n' && ini=( ${ini} ) # convert to line-array
ini=( ${ini[*]//\;*/} ) # remove comments with ;
ini=( ${ini[*]/\ =/=} ) # remove tabs before =
ini=( ${ini[*]/=\ /=} ) # remove tabs be =
ini=( ${ini[*]/\ =\ /=} ) # remove anything with a space around =
ini=( ${ini[*]/#\\[/\}$'\n'ini.section.} ) # set section prefix
ini=( ${ini[*]/%\\]/ \(} ) # convert text2function (1)
ini=( ${ini[*]/=/=\( } ) # convert item to array
ini=( ${ini[*]/%/ \)} ) # close array parenthesis
ini=( ${ini[*]/%\\ \)/ \\} ) # the multiline trick
ini=( ${ini[*]/%\( \)/\(\) \{} ) # convert text2function (2)
ini=( ${ini[*]/%\} \)/\}} ) # remove extra parenthesis
ini[0]="" # remove first element
ini[${#ini[*]} + 1]='}' # add the last brace
eval "$(echo "${ini[*]}")" # eval the result
IFS="${oifs}"
}
parse_ini_info() {
local service="${1}"
local _return_host="${2}"
local _return_port="${3}"
local _return_dbname="${4}"
local _return_user="${5}"
local _return_password="${6}"
ini_parser "${PG_SERVICE_CONF}"
if [[ "$(type -t ini.section.${service})" != "function" ]]; then
echo_red '******************'
echo_red 'Service not found!'
echo_red '******************'
echo_red "Missing service ${service} in ${PG_SERVICE_CONF}"
exit 3
fi
ini.section.${service}
printf -v "${_return_host}" '%s' "${host}"
printf -v "${_return_port}" '%s' "${port}"
printf -v "${_return_dbname}" '%s' "${dbname}"
printf -v "${_return_user}" '%s' "${user}"
printf -v "${_return_password}" '%s' "${password}"
}
select_db() {
local db_alias=$1
local _return_service=$2
local db_service
PS3="Choose a ${BOLD}${db_alias}${NORM} database: "
select db_service in $(cat "${PG_SERVICE_CONF}" | egrep '^\[.*\]' | sed -E 's/\[|\]//g'); do
printf -v "${_return_service}" '%s' "${db_service}"
break;
done
}
echo_db_info() {
local db_alias="${1}"
local db_service="${2}"
local db_host="${3}"
local db_port="${4}"
local db_dbname="${5}"
local db_user="${6}"
local db_password="${7}"
echo
echo ${BOLD}${db_alias}${NORM} database connection information:
echo
echo -e "\t${BOLD}${db_alias}${NORM} Host: ${!db_host}"
echo -e "\t${BOLD}${db_alias}${NORM} Port: ${!db_port}"
echo -e "\t${BOLD}${db_alias}${NORM} DB Name: ${!db_dbname}"
echo -e "\t${BOLD}${db_alias}${NORM} Username: ${!db_user}"
echo -e "\t${BOLD}${db_alias}${NORM} Password: $([[ -n "${!db_password}" ]] && echo '***' || echo_yellow "Password NOT set in ${PG_SERVICE_CONF}" )"
echo
}
psql_cmd() {
local service_name=$1
shift
psql -d "service=${service_name}" "$@"
}
array_contains() {
local el="${1}"
shift
local arr=("${@}")
local e
for e in "${arr[@]}"; do
if [[ "${e}" == "${el}" ]]; then
echo -n y
return
fi
done
echo -n n
}
join_by() {
local _result=${1}
local sep=${2}
local arr=${3}
shift 3 || shift $(($#))
printf -v "${_result}" "%s" "${arr}${@/#/${sep}}"
}
list_tables() {
local service="${1}"
local schema="${2}"
psql_cmd "${service}" -tAc "select table_name from information_schema.tables where table_schema='${schema}'"
}
select_tables() {
local service="${1}"
local schema="${2}"
local tifs="${3}"
local _result="${4}"
local all_tables=($(list_tables ${service} ${schema}))
local entire_schema="${BOLD}Entire Schema${NORM}"
local done_tables="${BOLD}Done selecting tables${NORM}"
local tables=()
PS3="Choose tables: "
select table in "${all_tables[@]}" "${entire_schema}" "${done_tables}"; do
case ${table} in
${entire_schema})
echo
echo "${BOLD}All tables selected${NORM}; copying entire schema"
echo
tables=(${ENTIRE_SCHEMA})
break;
;;
${done_tables})
# TODO: Check to make sure that at least one table is selected
break;
;;
*)
if [[ "$(array_contains ${table} "${tables[@]}")" == "y" ]]; then
for i in "${!tables[@]}"; do
[[ "${tables[$i]}" == "${table}" ]] && unset "tables[$i]"
done
local removed_tables
join_by removed_tables "," "${tables[@]}"
echo_green "Removed ${table} (${removed_tables})"
else
tables+=("${table}")
local added_tables
join_by added_tables "," "${tables[@]}"
echo_green "Added ${table} (${added_tables})"
fi
;;
esac
done
join_by ${_result} "${tifs}" "${tables[@]}"
}
create_backup_location() {
local base_name="${1}"
local backup="${2}"
local location="${3}"
local unattended="${4}"
local _result="${5}"
# If we are saving the backup, default to the working directory
if [[ "${backup}" == "save" || -n "${location}" ]]; then
local working_dir="$(pwd)"
local default_location="${working_dir}/${base_name}.dmp"
if [[ -z "${location}" && "${unattended}" != "true" ]]; then
echo "Relative backup paths will be resolved relative to '${working_dir}'"
local input
read -r -p "Backup location (${default_location}): " input
location=${input:-${default_location}}
else
location="${location:-${default_location}}"
fi
# Crudely resolve relative paths (does not simplify)
[[ "${location:0:1}" == "/" ]] || location="${working_dir}/${location}"
if [[ -e "${location}" ]]; then
echo
echo_yellow "*******************************"
echo_yellow "Cannot overwrite existing file."
echo_yellow "*******************************"
echo_yellow "Found existing file at: '${location}'"
exit 4;
fi
# Make an empty backup file
touch "${location}"
else
# This results in the file existing
location="$(mktemp)"
fi
printf -v "${_result}" '%s' "${location}"
}
create_backup() {
local backup_location="${1}"
local backup_cmd="${2}"
# Append to the location because it should already have been created
local start_msg="Creating backup at ${backup_location}"
if [[ -x "$(command -v pv)" ]]; then
echo_green "${start_msg} (progress total will be less than DB size)..."
${backup_cmd} | pv >> "${backup_location}"
else
echo_green "${start_msg} (may be slow; install 'pv' for progress)..."
${backup_cmd} >> "${backup_location}"
fi
echo_green "Backup completed successfully."
}
check_database() {
local service="${1}"
set +e
psql_cmd "${service}" --command="SELECT version();" >/dev/null 2>&1
local retval=$?
set -e
[[ ${retval} -eq 0 ]] && echo -n 'y' || echo -n 'n'
}
copy_database() {
local db_source_service="${1}"
local db_source_host="${2}"
local db_source_port="${3}"
local db_source_dbname="${4}"
local db_source_user="${5}"
local db_source_password="${6}"
local db_destination_service="${7}"
local db_destination_host="${8}"
local db_destination_port="${9}"
local db_destination_dbname="${10}"
local db_destination_user="${11}"
local db_destination_password="${12}"
local backup="${13}"
local backup_location="${14}"
local transaction="${15}"
local unattended="${16}"
local db_source_size=$(psql_cmd "${db_source_service}" -tAc "select pg_size_pretty(pg_database_size('${db_source_dbname}'));")
if [[ "${unattended}" != "true" ]]; then
echo
echo "About to copy entire database from:"
echo
echo_green "\t${db_source_user}/***@${db_source_host}:${db_source_port}/${db_source_dbname}"
echo
echo to:
echo
echo_red "\t${db_destination_user}/***@${db_destination_host}:${db_destination_port}/${db_destination_dbname}"
echo
echo_yellow "Please check for available disk space; ${BOLD}${db_source_size}${NORM} required."
echo
read -r -p "Are you sure? (yes to confirm) " confirm
case ${confirm} in
[yY][eE][sS])
# Let fall through to do the copy
;;
*)
echo
echo_green "Aborted"
exit 0
;;
esac
echo
fi
local backup_location
local dump_cmd="pg_dump -d service=${db_source_service} -Fc"
local restore_cmd="pg_restore -d service=${db_destination_service} -c --if-exists"
echo_green "Checking for existing DB..."
local database_status="$(check_database ${db_destination_service})"
if [[ "${database_status}" != "y" ]]; then
echo_red "*****************"
echo_red "Database missing!"
echo_red "*****************"
echo_red "Database ${db_destination_dbname} does not exist on ${db_destination_host}"
return -1
fi
# Only backup if we are not doing the restore in a transaction
if [[ "${transaction}" == "true" ]]; then
restore_cmd="${restore_cmd} --single-transaction"
elif [[ "${backup}" != "skip" ]]; then
create_backup_location "${db_destination_dbname}" "${backup}" "${backup_location}" "${unattended}" backup_location
create_backup "${backup_location}" "${dump_cmd}"
fi
local start_msg="Executing copy of entire DB from ${db_source_host} to ${db_destination_host}"
set +e
if [[ -x "$(command -v pv)" ]]; then
echo_green "${start_msg} (progress total will be less than DB size)..."
${dump_cmd} | pv | ${restore_cmd}
else
echo_green "${start_msg} (may be slow; install 'pv' for progress)..."
${dump_cmd} | ${restore_cmd}
fi
local retval=$?
set -e
if [[ ${retval} -eq 0 ]]; then
echo_green "Copy completed."
[[ "${backup}" != "save" ]] && rm -f ${backup_location}
return 0;
fi
echo_red "************"
echo_red "Copy failed!"
echo_red "************"
echo_red "Note that full database restores must be done as a super-user"
# No need to restore if we have a failed transaction
if [[ -r "${backup_location}" && "${transaction}" != "true" ]]; then
# Attempt to restore the backup
echo_yellow "Attempting to restore target DB from backup..."
${restore_cmd} "${backup_location}"
echo_yellow "Restore of backup completed successfully."
[[ "${backup}" != "save" ]] && rm -f ${backup_location}
fi
return ${retval}
}
list_schemas() {
local service="${1}"
psql_cmd "${service}" -tA << EOF
select schema_name
from information_schema.schemata
where schema_name not like 'pg_%' and schema_name <> 'information_schema'
EOF
}
select_schema() {
local service="${1}"
local _result="${2}"
local entire_database="${BOLD}Entire Database${NORM}"
all_source_schemas=$(list_schemas ${service})
PS3="Choose a schema to copy: "
select schema in ${all_source_schemas} "${entire_database}"; do
[[ "${schema}" == "${entire_database}" ]] && schema="${ENTIRE_DATABASE}"
printf -v "${_result}" '%s' "${schema}"
break;
done
}
check_schema() {
local service="${1}"
local schema="${2}"
psql_cmd "${service}" -tAc "select schema_name from information_schema.schemata where schema_name='${schema}'"
}
schema_size() {
local service="${1}"
local schema_name="${2}"
psql_cmd "${service}" -tA <<EOF
SELECT pg_size_pretty(SUM(pg_total_relation_size(quote_ident(schemaname) || '.' || quote_ident(tablename)))::BIGINT)
FROM pg_tables
WHERE schemaname = '${schema_name}';
EOF
}
copy_schema() {
local db_source_service="${1}"
local db_source_host="${2}"
local db_source_port="${3}"
local db_source_dbname="${4}"
local db_source_user="${5}"
local db_source_password="${6}"
local db_destination_service="${7}"
local db_destination_host="${8}"
local db_destination_port="${9}"
local db_destination_dbname="${10}"
local db_destination_user="${11}"
local db_destination_password="${12}"
local schema_name="${13}"
local backup="${14}"
local backup_location="${15}"
local transaction="${16}"
local unattended="${17}"
local source_schema_size=$(schema_size ${db_source_service} ${schema_name})
if [[ "${unattended}" != "true" ]]; then
echo
echo "About to copy ${BOLD}${schema_name}${NORM} (${BOLD}${source_schema_size}${NORM}) from:"
echo
echo_green "\t${db_source_user}/***@${db_source_host}:${db_source_port}/${db_source_dbname} (schema: ${BOLD}${schema_name}${NORM})"
echo
echo to:
echo
echo_red "\t${db_destination_user}/***@${db_destination_host}:${db_destination_port}/${db_destination_dbname} (schema: ${BOLD}${schema_name}${NORM})"
echo
echo_yellow "Please check for available disk space; ${BOLD}${source_schema_size}${NORM} required."
echo
read -r -p "Are you sure? (yes to confirm) " confirm
case ${confirm} in
[yY][eE][sS])
# Let fall through to do the copy
;;
*)
echo
echo_green "Aborted"
exit 0
;;
esac
echo
fi
local backup_location
local dump_cmd="pg_dump -d service=${db_source_service} -n ${schema_name} -Fc"
local restore_cmd="pg_restore -d service=${db_destination_service} -c --if-exists"
echo_green "Checking for existing schema..."
local existing_schema=$(check_schema ${db_destination_service} ${schema_name})
if [[ -z "${existing_schema}" && "${backup}" == "save" ]]; then
echo
echo_yellow "********************************************************"
echo_yellow "Invalid state: Cannot backup schema that does not exist."
echo_yellow "********************************************************"
return 5
fi
# Only backup if we are not doing the restore in a transaction, there is an existing schema, and it is not explicitly skipped
if [[ "${transaction}" == "true" ]]; then
restore_cmd="${restore_cmd} --single-transaction"
elif [[ -n "${existing_schema}" && "${backup}" != "skip" ]]; then
create_backup_location "${db_destination_dbname}__${schema_name}" "${backup}" "${backup_location}" "${unattended}" backup_location
# Append to the location because we just created an empty file
create_backup "${backup_location}" "${dump_cmd}"
fi
local start_msg="Executing copy of ${schema_name} from ${db_source_host} to ${db_destination_host}"
set +e
if [[ -x "$(command -v pv)" ]]; then
echo_green "${start_msg} (progress total will be less than DB size)..."
${dump_cmd} | pv | ${restore_cmd}
else
echo_green "${start_msg} (may be slow; install 'pv' for progress)..."
${dump_cmd} | ${restore_cmd}
fi
local retval=$?
set -e
if [[ ${retval} -eq 0 ]]; then
echo_green "Copy completed."
[[ "${backup}" != "save" ]] && rm -f ${backup_location}
return 0;
fi
echo_red "************"
echo_red "Copy failed!"
echo_red "************"
# No need to restore if we have a failed transaction
if [[ -r "${backup_location}" && "${transaction}" != "true" ]]; then
# Attempt to restore the backup
echo_yellow "Attempting to restore target schema from backup..."
${restore_cmd} "${backup_location}"
echo_yellow "Restore of backup completed successfully."
[[ "${backup}" != "save" ]] && rm -f ${backup_location}
fi
return ${retval}
}
check_tables() {
local service="${1}"
local schema="${2}"
shift 2
local tables=("${@}")
local tables_param
join_by tables_param "', '" "${tables[@]}"
tables_param="'${tables_param}'"
psql_cmd "${service}" -tAc "select tablename from pg_tables where schemaname='${schema}' and tablename in (${tables_param})"
}
table_type() {
local service="${1}"
local schema="${2}"
local table="${3}"
local table_type="$(psql_cmd "${service}" -tAc "select table_type from information_schema.tables where table_schema='${schema}' and table_name='${table}'")"
[[ "${table_type}" == "VIEW" ]] && echo -n "VIEW" || echo -n "TABLE"
}
tables_size() {
local service="${1}"
local schema_name="${2}"
shift 2
local tables=("${@}")
local tables_param
join_by tables_param "', '" "${tables[@]}"
tables_param="'${tables_param}'"
psql_cmd "${service}" -tA <<EOF
SELECT pg_size_pretty(SUM(pg_total_relation_size(quote_ident(schemaname) || '.' || quote_ident(tablename)))::BIGINT)
FROM pg_tables
WHERE schemaname = '${schema_name}'
AND tablename in (${tables_param});
EOF
}
copy_tables() {
local db_source_service="${1}"
local db_source_host="${2}"
local db_source_port="${3}"
local db_source_dbname="${4}"
local db_source_user="${5}"
local db_source_password="${6}"
local db_destination_service="${7}"
local db_destination_host="${8}"
local db_destination_port="${9}"
local db_destination_dbname="${10}"
local db_destination_user="${11}"
local db_destination_password="${12}"
local schema_name="${13}"
local backup="${14}"
local backup_location="${15}"
local transaction="${16}"
local unattended="${17}"
shift 17
local tables=("${@}")
local source_tables_size=$(tables_size "${db_source_service}" "${schema_name}" "${tables[@]}")
if [[ "${unattended}" != "true" ]]; then
echo
echo "About to copy (${BOLD}${source_tables_size}${NORM})"
echo
for table in "${tables[@]}"; do
echo -e "\t${table}"
done
echo
echo "from:"
echo
echo_green "\t${db_source_user}/***@${db_source_host}:${db_source_port}/${db_source_dbname} (schema: ${BOLD}${schema_name}${NORM})"
echo
echo "to:"
echo
echo_red "\t${db_destination_user}/***@${db_destination_host}:${db_destination_port}/${db_destination_dbname} (schema: ${BOLD}${schema_name}${NORM})"
echo
echo_yellow "Please check for available disk space; ${BOLD}${source_tables_size}${NORM} required."
echo
read -r -p "Are you sure? (yes to confirm) " confirm
case ${confirm} in
[yY][eE][sS])
# Let fall through to do the copy
;;
*)
echo
echo_green "Aborted"
exit 0
;;
esac
echo
fi
echo_green "Checking for existing schema..."
local existing_schema=$(check_schema ${db_destination_service} ${schema_name})
local existing_tables=()
if [[ -n "$existing_schema" ]]; then
echo_green "Checking for existing tables..."
existing_tables=($(check_tables "${db_destination_service}" "${schema_name}" "${tables[@]}"))
fi
local backup_location
if [[ "${backup}" == "save" && (-z "$existing_schema" || ${#existing_tables[@]} -eq 0) ]]; then
echo
echo_yellow "******************************************************"
echo_yellow "Invalid state: Cannot backup tables that do not exist."
echo_yellow "******************************************************"
return 5
elif [[ -z "$existing_schema" ]]; then
echo
echo_yellow "Schema does not exist! Creating...."
echo
# Copy the DDL from source to destination for the whole schema to get it created
pg_dump -d "service=${db_source_service}" -s -n "${schema_name}" -Fc | pg_restore -d "service=${db_destination_service}"
# Drop all the tables in the schema we just crated
local all_destination_tables=($(list_tables ${db_destination_service} ${schema_name}))
local drop_empty_sql="BEGIN;${LF}"
for table in "${all_destination_tables[@]}"; do
local tt="$(table_type ${db_destination_service} ${schema_name} ${table})"
drop_empty_sql+="drop ${tt} if exists ${QUOT}${schema_name}${QUOT}.${QUOT}${table}${QUOT} cascade;${LF}"
done
drop_empty_sql+="COMMIT;${LF}"
psql_cmd "${db_destination_service}" -tAc "${drop_empty_sql}"
echo_green "Created schema ${schema_name}...."
echo
elif [[ "${backup_location}" != "skip" && "${transaction}" != "true" && ${#existing_tables[@]} -gt 0 ]]; then
# Means that we need to do a backup
create_backup_location "${db_destination_dbname}__${schema_name}__${#existing_tables}_tables" "${backup}" "${backup_location}" "${unattended}" backup_location
local existing_table_flags=()
for table in "${existing_tables[@]}"; do
existing_table_flags+=("-t")
existing_table_flags+=("${schema_name}.${table}")
done
local backup_cmd="pg_dump -d service=${db_source_service} -n ${schema_name} ${existing_table_flags[@]}"
create_backup "${backup_location}" "${backup_cmd}"
fi
local table_flags=()
for table in "${tables[@]}"; do
table_flags+=("-t")
table_flags+=("${schema_name}.${table}")
done
local dump_cmd="pg_dump -d service=${db_source_service} ${table_flags[@]} -Fc"
local restore_cmd="pg_restore -d service=${db_destination_service} -c --if-exists"
local start_msg="Executing copy from ${db_source_host} to ${db_destination_host}"
[[ "${transaction}" == "true" ]] && restore_cmd="${restore_cmd} --single-transaction"
set +e
if [[ -x "$(command -v pv)" ]]; then
echo_green "${start_msg} (progress total will be less than DB size)..."
${dump_cmd} | pv | ${restore_cmd}
else
echo_green "${start_msg} (may be slow; install 'pv' for progress)..."
${dump_cmd} | ${restore_cmd}
fi
local retval=$?
set -e
if [[ ${retval} -eq 0 ]]; then
echo_green "Copy completed."
[[ "${backup}" != "save" ]] && rm -f ${backup_location}
return 0;
fi
echo_red "************"
echo_red "Copy failed!"
echo_red "************"
# No need to restore if we have a failed transaction
if [[ -r "${backup_location}" && "${transaction}" != "true" ]]; then
# Attempt to restore the backup
echo_yellow "Attempting to restore target tables from backup..."
${restore_cmd} "${backup_location}"
echo_yellow "Restore of backup completed successfully."
[[ "${backup}" != "save" ]] && rm -f ${backup_location}
fi
return ${retval}
}
main() {
local db_source_service
local db_destination_service
local backup="delete"
local backup_location
local schema_name
local tables=()
local transaction="false"
local unattended="false"
OPTIND=1
while getopts "h?bB:d:Df:ns:St:Tu" opt; do
case "${opt}" in
h|\?)
show_help
exit 0
;;
b) backup="save"
;;
B) backup_location="${OPTARG}"
;;
d) db_destination_service="${OPTARG}"
;;
D) schema_name="${ENTIRE_DATABASE}"
;;
f) db_source_service="${OPTARG}"
;;
n) backup="skip"
;;
s) schema_name="${OPTARG}"
;;
S) tables=("${ENTIRE_SCHEMA}")
;;
t) tables+=("${OPTARG}")
;;
T) transaction="true"
;;
u) unattended="true"
;;
esac
done
local db_source_host
local db_source_port
local db_source_dbname
local db_source_user
local db_source_password
local db_destination_host
local db_destination_port
local db_destination_dbname
local db_destination_user
local db_destination_password
[[ -n "${db_source_service}" ]] || select_db source db_source_service
parse_ini_info ${db_source_service} db_source_host db_source_port db_source_dbname db_source_user db_source_password
echo_db_info Source db_source_service db_source_host db_source_port db_source_dbname db_source_user db_source_password
[[ -n "${db_destination_service}" ]] || select_db destination db_destination_service
parse_ini_info ${db_destination_service} db_destination_host db_destination_port db_destination_dbname db_destination_user db_destination_password
echo_db_info Destination db_destination_service db_destination_host db_destination_port db_destination_dbname db_destination_user db_destination_password
[[ -n "${schema_name}" ]] || select_schema ${db_source_service} schema_name
if [[ "${schema_name}" == "${ENTIRE_DATABASE}" ]]; then
copy_database "${db_source_service}" "${db_source_host}" "${db_source_port}" "${db_source_dbname}" "${db_source_user}" "${db_source_password}" \
"${db_destination_service}" "${db_destination_host}" "${db_destination_port}" "${db_destination_dbname}" "${db_destination_user}" "${db_destination_password}" \
"${backup}" "${backup_location}" "${transaction}" "${unattended}"
return 0
fi
echo_green "Selected schema: ${schema_name}"
echo
if [[ ${#tables[@]} -eq 0 ]]; then
local table_list
select_tables "${db_source_service}" "${schema_name}" $'\t' table_list
local oifs="${IFS}"
local IFS=$'\t'
tables=(${table_list})
IFS="${oifs}";
fi
if [[ "${tables[0]}" == "${ENTIRE_SCHEMA}" ]]; then
copy_schema "${db_source_service}" "${db_source_host}" "${db_source_port}" "${db_source_dbname}" "${db_source_user}" "${db_source_password}" \
"${db_destination_service}" "${db_destination_host}" "${db_destination_port}" "${db_destination_dbname}" "${db_destination_user}" "${db_destination_password}" \
"${schema_name}" "${backup}" "${backup_location}" "${transaction}" "${unattended}"
return 0
fi
local selected_tables
join_by selected_tables "," "${tables[@]}"
echo_green "Tables selected (${#tables[@]}): ${selected_tables}"
echo
if [[ ${#tables[@]} -gt 0 ]]; then
copy_tables "${db_source_service}" "${db_source_host}" "${db_source_port}" "${db_source_dbname}" "${db_source_user}" "${db_source_password}" \
"${db_destination_service}" "${db_destination_host}" "${db_destination_port}" "${db_destination_dbname}" "${db_destination_user}" "${db_destination_password}" \
"${schema_name}" "${backup}" "${backup_location}" "${transaction}" "${unattended}" "${tables[@]}"
return 0
fi
}
init
main "${@}"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment