#!/bin/dash # error codes # 1 - generic error # 2 - ambiguous, when you try to use something that cannot be used in same time # 3 - missing required argument / variable # 4 - # 5 - provided table that not exists # 9 - argument or command not found # 13 - program bug / unexpected system / database incompatibles # 127 - command not found (dependency) set -eu if ! command -v psql >/dev/null; then log error "Required tool (psql) are not installed." exit 127 fi VERSION='0.0.1' MIGRATION_DIR="${MIGRATION_DIR:-migration}" quote() { printf "'%s'" "$(printf %s "$1" | sed "s/'/'\\\\''/g")"; } REMAINING_ARS= # cat filename | sha256sum() # sha256sum(filename) sha256sum() { local file file="${1:-'-'}" cksum --algorithm=sha256 --untagged "$file" | awk '{printf $1}' } while [ $# -gt 0 ]; do log debug "$1" case $1 in migrate|create|fetch|list|init) [ "${SUBCOMMAND+x}" ] && { log error "ambiguous subcommand, decide ${WHITE}$SUBCOMMAND ${NC}or ${WHITE}$1"; exit 2; } SUBCOMMAND=$1 shift ;; --migration-dir|-d) MIGRATION_DIR=$2 shift 2 ;; --inherits) INHERITS_LIST="${INHERITS_LIST+$INHERITS_LIST\"}$2" shift 2 ;; --*|-*) REMAINING_ARS="$REMAINING_ARS $(quote "$1")"; shift ;; # unknown global -> pass through *) REMAINING_ARS="$REMAINING_ARS $(quote "$1")"; shift ;; esac done [ ${INHERITS_LIST+x} ] && INHERITS_LIST="$(printf '%s' "$INHERITS_LIST" | sed -E 's/"/,/g; s/([^,]+)/"\1"/g')" # shellcheck disable=SC2120 init() { while [ $# -gt 0 ]; do case $1 in --dry-run) INIT_DRY_RUN=1 shift ;; --db-url|-u) DB_URL="$2" shift 2 ;; --set|-v) VARIABLE_LIST="${VARIABLE_LIST+$VARIABLE_LIST }$2" shift 2 ;; --*|-*) printf 'init argument %s does not exists' "$1" exit 9 ;; *) printf 'init command %s does not exists' "$1" exit 9 ;; esac done [ "${INIT_DRY_RUN+x}" ] && { printf '%s\n' "$(init_sql)"; exit; } error_handler_no_db_url psql_args="$(form_psql_args)" [ ${INHERITS_LIST+x} ] && { oldIFS="$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="$oldIFS" check_inherits=$(printf '%s\n' \ 'BEGIN;' \ "$check_inherits" \ 'COMMIT;') # shellcheck disable=SC2086 if ! psql $psql_args -c "$check_inherits"; then log error "init failed: ${WHITE}one of inherits table does not exists: ${CYAN}$INHERITS_LIST" exit 5 fi } # shellcheck disable=SC2086 if ! psql $psql_args -c "$(init_sql)"; then log error "init failed" exit 13 fi } # error_handler_no_db_url() error_handler_no_db_url() { [ "${DB_URL+x}" ] || { log error "no ${WHITE}DB_URL${NC} or ${WHITE}--db-url${NC} specified"; exit 3; } } init_sql() { local sql sql="$(printf '%s\n' \ "BEGIN;" \ '' \ 'CREATE SCHEMA IF NOT EXISTS hectic;' \ '' \ "CREATE DOMAIN hectic.migration_name AS TEXT CHECK (VALUE ~ '^[0-9]{15}-.*$');" \ '' \ "CREATE DOMAIN hectic.sha256 AS CHAR(64) CHECK (VALUE ~ '^[0-9a-f]{64}$');" \ '' \ 'CREATE FUNCTION hectic.sha256_lower() RETURNS trigger AS $$' \ 'BEGIN' \ ' NEW.hash = lower(NEW.hash);' \ ' RETURN NEW;' \ 'END;' \ '$$ LANGUAGE plpgsql;' \ '' \ 'CREATE TABLE IF NOT EXISTS hectic.version (' \ ' name TEXT PRIMARY KEY,' \ ' version TEXT NOT NULL,' \ ' installed_at TIMESTAMPTZ NOT NULL DEFAULT NOW()' \ ');' \ '' \ "INSERT INTO hectic.version (name, version) VALUES ('migrator', '$VERSION')" \ '' \ 'CREATE TABLE IF NOT EXISTS hectic.migration (' \ ' id SERIAL PRIMARY KEY,' \ ' name hectic.migration_name UNIQUE NOT NULL,' \ ' hash hectic.sha256 UNIQUE NOT NULL,' \ ' applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW()' \ ')')" [ ${INHERITS_LIST+x} ] && sql="$(printf '%s INHERITS(%s)' "$sql" "$INHERITS_LIST")" sql="$(printf '%s;\n' "$sql")" printf '%s\n' \ "$sql" \ 'CREATE TRIGGER hectic_t_sha256_lower' \ 'BEFORE INSERT OR UPDATE ON hectic.migration' \ 'FOR EACH ROW EXECUTE FUNCTION hectic.sha256_lower();' \ 'COMMIT;' } [ "${SUBCOMMAND+x}" ] || { log error "no subcomand specified"; exit 1; } help() { # inherits: List one or more tables the migration table must inherit from echo help } migrate_down() { DOWN_NUMBER=1 while [ $# -gt 0 ]; do case $1 in --*|-*) printf 'migrate argument %s does not exists' "$1" exit 1 ;; ''|*[!0-9]*) log error "down argument not a number"; exit 1; ;; *) DOWN_NUMBER=$2 shift 2; ;; esac done : "$DOWN_NUMBER" } migrate_up() { UP_NUMBER=1 while [ $# -gt 0 ]; do case $1 in --*|-*) printf 'migrate argument %s does not exists' "$1" exit 1 ;; ''|*[!0-9]*) log error "up argument not a number"; exit 1; ;; *) UP_NUMBER=$2 shift 2; ;; esac done : "$UP_NUMBER" #ls "$MIGRATION_DIR" -1 | sort } migrate_to() { while [ $# -gt 0 ]; do case $1 in --*|-*) printf 'migrate argument %s does not exists' "$1" exit 1 ;; *) MIGRATION_NAME= ;; esac done [ "${MIGRATION_NAME+x}" ] || { log error "no migration name specified"; exit 1; } } migration_list() { find "$MIGRATION_DIR" -maxdepth 1 -type d -regextype posix-extended -regex '^.*/[0-9]{14}-.*$' -printf '%f\n' | sort } migrate() { local fs_migrations db_migrations db_migration fs_migration psql_args var #target_migration while [ $# -gt 0 ]; do case $1 in up|down|to) [ "${MIGRATE_SUBCOMMAND+x}" ] && { log error "ambiguous migrate subcommand, decide ${WHITE}$MIGRATE_SUBCOMMAND ${NC}or ${WHITE}$1"; exit 2 } MIGRATE_SUBCOMMAND="$1" shift ;; --db-url|-u) DB_URL="$2" shift 2 ;; --force|-f) FORCE=1 shift ;; --set|-v) VARIABLE_LIST="${VARIABLE_LIST+$VARIABLE_LIST }$2" shift 2 ;; --*|-*) REMAINING_ARS="$REMAINING_ARS $(quote "$1")"; shift ;; # unknown global -> pass through *) REMAINING_ARS="$REMAINING_ARS $(quote "$1")"; shift ;; esac done error_handler_no_db_url [ "${FORCE+x}" ] && { log error "migrate --force not implemented" exit 1 } init fs_migrations=$(migration_list) db_migrations=$( psql "$DB_URL" --no-align --tuples-only --quiet \ --command "SELECT name FROM hectic.migration ORDER BY name ASC" \ | awk NF ) db_mig_count=$(printf '%s' "$db_migrations" | wc) log debug "$db_mig_count" # Check if the DB migrations form a proper prefix of disk migrations # (meaning all DB-applied migration filenames should appear in the same order at the start). i=0 for db_migration in $db_migrations; do fs_migration=$(printf '%s' "$fs_migrations" | sed -n "$((i+1))p") if [ -z "$fs_migration" ] || [ "$fs_migration" != "$db_migration" ]; then if [ -z "$FORCE" ]; then log error "unrelated migration tree detected. Use --force to proceed." exit 2 else log error "unrelated migration tree forced. Proceeding..." break fi fi i=$((i+1)) done eval "set -- $REMAINING_ARS" #target_migration="$("migrate_$MIGRATE_SUBCOMMAND" "$@")" } form_psql_args() { psql_args="-d $DB_URL -v ON_ERROR_STOP=1" for var in ${VARIABLE_LIST:-}; do psql_args="$psql_args -v $var" done } migrate_inner() { printf '%s\n' "$fs_migrations" | while IFS= read -r fs_migration; do # skip already applied migrations printf '%s' "$db_migrations" | grep -qxF "$fs_migration" && continue psql_args="$(form_psql_args)" direction=1 mig_direction=$([ "$direction" -gt 0 ] && printf 'up.sql' || printf 'down.sql') escaped_name=$(printf '%s' "$fs_migration" | sed "s/'/''/g") mig_path=$(printf '%s/%s/%s' "$MIGRATION_DIR" "$fs_migration" "$mig_direction") escaped_path=$(printf '%s' "$mig_path" | sed "s/'/''/g") log trace "mig name: $escaped_name; mig path: $escaped_path" # shellcheck disable=SC2086 if ! psql $psql_args < "$file_path" log notice "created migration: ${WHITE}${file_path}${NC}" } fetch() { while [ $# -gt 0 ]; do case $1 in --db-url|-u) # shellcheck disable=SC2034 DB_URL=$2 shift 2 ;; esac done error_handler_no_db_url } list() { while [ $# -gt 0 ]; do case $1 in --raw|-r) RAW=1 shift ;; --*|-*) log error "init argument $1 does not exists" exit 9 ;; *) log error "init subcommand $1 does not exists" exit 9 ;; esac done [ "${RAW+x}" ] && { migration_list exit } migration_list | while read -r name; do dir="./${MIGRATION_DIR}/${name}" up="$dir/up.sql" down="$dir/down.sql" if [ ! -f "$up" ] || [ ! -f "$down" ]; then echo "$name: missing $( [ ! -f "$up" ] && echo up.sql ) $( [ ! -f "$down" ] && echo down.sql )" else echo "$name" fi done } generate_word() { C="b c d f g h j k l m n p r s t v w z" V="a e i o u" N=${N:-5} w= for i in $(seq 3); do c=$(echo "$C" | tr ' ' '\n' | shuf -n1) v=$(echo "$V" | tr ' ' '\n' | shuf -n1) w="${w}${c}${v}" done printf '%s' "$w" } log debug "subcommand: $WHITE$SUBCOMMAND" log debug "subcommand args: $WHITE$REMAINING_ARS" eval "set -- $REMAINING_ARS" "$SUBCOMMAND" "$@"