#!/bin/dash #version="$(psql "$DB_URL" -c "SELECT version FROM hectic.version WHERE name = 'migrator';")" # 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 VERSION='0.0.1' MIGRATION_DIR="${MIGRATION_DIR:-migration}" REMAINING_ARS= quote() { printf "'%s'" "$(printf %s "$1" | sed "s/'/'\\\\''/g")"; } # cat filename | sha256sum() # sha256sum(filename) sha256sum() { local file file="${1:-'-'}" cksum --algorithm=sha256 --untagged "$file" | awk '{printf $1}' } # detect_db_type() # Returns: "postgresql" or "sqlite" detect_db_type() { case "$DB_URL" in postgresql://*|postgres://*) printf 'postgresql' ;; sqlite://*|*.db|*.sqlite|*.sqlite3) printf 'sqlite' ;; *) log error "unsupported database URL format: ${WHITE}$DB_URL${NC}" log error "supported formats: postgresql://... or sqlite://... or *.db" exit 1 ;; esac } # get_sqlite_path() get_sqlite_path() { case "$DB_URL" in sqlite://*) printf '%s' "$DB_URL" | sed 's|^sqlite://||' ;; *) printf '%s' "$DB_URL" ;; esac } # db_exec(sql) db_exec() { local sql="$1" local db_type db_type=$(detect_db_type) case "$db_type" in postgresql) local psql_args psql_args="$(form_psql_args)" # shellcheck disable=SC2086 printf '%s' "$sql" | psql $psql_args "$DB_URL" ;; sqlite) local db_path db_path=$(get_sqlite_path) printf '%s' "$sql" | sqlite3 "$db_path" ;; esac } # db_query(sql) db_query() { local sql="$1" local db_type db_type=$(detect_db_type) case "$db_type" in postgresql) psql "$DB_URL" --no-align --tuples-only --quiet --command "$sql" | awk NF ;; sqlite) local db_path db_path=$(get_sqlite_path) sqlite3 "$db_path" "$sql" ;; esac } # db_exec_file(file_path) db_exec_file() { local file_path="$1" local db_type db_type=$(detect_db_type) case "$db_type" in postgresql) local psql_args escaped_path psql_args="$(form_psql_args)" escaped_path=$(printf '%s' "$file_path" | sed "s/'/''/g") # shellcheck disable=SC2086 psql $psql_args "$DB_URL" < pass through *) MIGRATOR_REMAINING_ARS="$MIGRATOR_REMAINING_ARS $(quote "$1")"; shift ;; esac done log debug "migrate REMAINING_ARGS: $WHITE$MIGRATOR_REMAINING_ARS" [ "${FORCE+x}" ] && { log error "migrate --force not implemented" exit 1 } init fs_migrations=$(migration_list) db_type=$(detect_db_type) case "$db_type" in postgresql) db_migrations=$(db_query "SELECT name FROM hectic.migration ORDER BY name ASC") ;; sqlite) db_migrations=$(db_query "SELECT name FROM hectic_migration ORDER BY name ASC") ;; esac log debug "db mig: $db_migrations" db_mig_count=$(printf '%s' "$db_migrations" | wc -l) log debug "mig count: $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 -- $MIGRATOR_REMAINING_ARS" target_migration="$("migrate_$MIGRATE_SUBCOMMAND" "$@")" if [ -z "$db_migrations" ]; then log info "it'll firs migration" current_idx=0 else current_migration=$(printf '%s\n' "$db_migrations" | tail -n1) current_idx=$(index_of "$fs_migrations" "$current_migration") fi log debug "[$WHITE$fs_migrations$NC]" log debug "$target_migration" if [ -z "$target_migration" ]; then target_idx=0 else target_idx=$(index_of "$fs_migrations" "$target_migration") fi log debug "indexes $WHITE$current_idx$NC $WHITE${target_idx}" if [ "$target_idx" -eq "$current_idx" ]; then if [ "$target_idx" -eq 0 ]; then log notice "database already at clean state (no migrations)" else log notice "database already at ${WHITE}$target_migration${NC}" fi exit 0 fi # Apply migrations psql_args="$(form_psql_args)" if [ "$target_idx" -gt "$current_idx" ]; then # Migrate UP log info "migrating up from index $current_idx to $target_idx" i=$((current_idx + 1)) while [ "$i" -le "$target_idx" ]; do fs_migration=$(printf '%s' "$fs_migrations" | sed -n "${i}p") escaped_name=$(printf '%s' "$fs_migration" | sed "s/'/''/g") mig_path="$MIGRATION_DIR/$fs_migration/up.sql" escaped_path=$(printf '%s' "$mig_path" | sed "s/'/''/g") if [ ! -f "$mig_path" ]; then log error "migration file not found: ${WHITE}$mig_path${NC}" exit 1 fi mig_hash=$(sha256sum "$mig_path") log info "applying migration ${WHITE}$fs_migration${NC} (up)" case "$db_type" in postgresql) local psql_args psql_args="$(form_psql_args)" # shellcheck disable=SC2086 if ! psql $psql_args "$DB_URL" < "$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" } check_db_dependencies() { [ "${DB_URL+x}" ] || return 0 # Skip if no DB_URL yet db_type=$(detect_db_type) case "$db_type" in postgresql) if ! command -v psql >/dev/null; then log error "Required tool (psql) is not installed." log error "PostgreSQL client tools are required for postgresql:// URLs" exit 127 fi ;; sqlite) if ! command -v sqlite3 >/dev/null; then log error "Required tool (sqlite3) is not installed." log error "SQLite3 client is required for sqlite:// URLs" exit 127 fi ;; esac } if ! [ "${AS_LIBRARY+x}" ]; then while [ $# -gt 0 ]; do log debug "arg: $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')" [ "${SUBCOMMAND+x}" ] || { log error "no subcomand specified"; exit 1; } log debug "subcommand: $WHITE$SUBCOMMAND" log debug "subcommand args: $WHITE$REMAINING_ARS" eval "set -- $REMAINING_ARS" "$SUBCOMMAND" "$@" fi