Versioning

Fork/mirror of https://gitlab.com/depesz/Versioning
git clone https://ccx.te2000.cz/git/Versioning
Log | Files | Refs | README | LICENSE

commit 851774b596b11b5d5620f38f13fc2957345e6b2d
parent a6092c90e099d447ae6a7464bcad5443252cfe17
Author: Jan Pobříslo <ccx@te2000.cz>
Date:   Mon,  4 Dec 2023 22:50:15 +0100

Prototype of apply_patchset script in zsh.

Diffstat:
Apatch_file_format.md | 26++++++++++++++++++++++++++
At/patches/000-base.sql | 3+++
At/patches/001-users.sql | 4++++
Atools/apply_patchset | 191+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atools/common.zsh | 73+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
5 files changed, 297 insertions(+), 0 deletions(-)

diff --git a/patch_file_format.md b/patch_file_format.md @@ -0,0 +1,26 @@ +# Purpose + +The patch format for `apply_patchset` script slightly differs from regular **Versioning** patch format. +The reason for this is to make the patch metadata easier to parse and thus allow reasoning about them before patch +application, as well as validation of the format and removing boilerplate such as transaction management. + +This format equates filename with patch name. +While it's less flexible than being able to assign arbitrary names to patches it greatly simplifies the automation +required to get the correct patches applied. + +# Format + +Every patch file has a header which starts with `-- VPATCH` and ends with empty line (required). +In between those lines you can put dependency and conflict lines in arbitrary order, such as: + +``` +-- VPATCH +-- VDEP:000-base.sql +-- VDEP:001-extensions.sql +-- VCON:009-special.sql + +create table foo (foo_id serial primary key, bar text); +``` + +Note the lack of transaction management and calls to `_v.patch()`. +That is all handled by the script applying the patches so you can have just the relevant SQL code below the header. diff --git a/t/patches/000-base.sql b/t/patches/000-base.sql @@ -0,0 +1,3 @@ +-- VPATCH + +create table users (id serial primary key, username text); diff --git a/t/patches/001-users.sql b/t/patches/001-users.sql @@ -0,0 +1,4 @@ +-- VPATCH +-- VDEP:000-base.sql + +insert into users (username) values ('depesz'); diff --git a/tools/apply_patchset b/tools/apply_patchset @@ -0,0 +1,191 @@ +#!/bin/zsh +. $0:h/common.zsh || exit $? + +main() { + if (( $# < 1 )); then + warn_msg usage: "cd <patchdir> && apply_patchset <one or more patch names>" + exit 100 + fi + local patch + for patch in "$@"; do + if [[ $patch == *[\\\']* ]]; then + err_msg ERROR: "disallowed character in patch name: $1" + exit 100 + fi + done + - parse_patch_set $@ + - load_installed_patches + - verify_patch_set $@ + - install_patch_set $@ +} + +# --- + +load_installed_patches() { + typeset -ga installed_patches conflicting_patches + installed_patches=( ${(f)"$( psql -AP tuples_only=on -c 'select patch_name from _v.patches' )"} ) || \ + exit $? + conflicting_patches=( ${(f)"$( psql -AP tuples_only=on -c 'select distinct con from _v.patches, lateral unnest(conflicts) con;' )"} ) || \ + exit $? +} + +# --- + +parse_patch_set() { + typeset -gA patch_parsed patch_deps patch_cons + local patch + for patch in "$@"; do + - parse_patch $patch + done +} + +parse_patch() { + (( $+patch_parsed[$1] )) && return 0 + if [[ $1 == *[\\\']* ]]; then + printf >&2 'ERROR: disallowed character in patch name: %s\n' $1 + exit 1 + fi + + local line have_start=false + local -a deps cons + <$1 while IFS= read line; do + if ! $have_start; then + if [[ $line == "-- VPATCH" ]]; then + have_start=true + continue + else + die "$1 doesn't have VPATCH header" + fi + else + if [[ $line == '' ]]; then + break + fi + line=${line%%--[[:space:]]#} + case $line in + (VDEP:*) deps+=${line#VDEP:};; + (VCON:*) cons+=${line#VCON:};; + (*) die "unexpected line in $1: ${(qqq)line}"; + esac + fi + done + + patch_deps[$1]=${(pj:\0:)deps} + patch_cons[$1]=${(pj:\0:)cons} + patch_parsed[$1]=1 + + local dep + for dep in $deps; do + - parse_patch $dep + done +} + +# --- + +verify_patch_set() { + unset verify_install + unset verify_conflict + typeset -gA verify_install + typeset -gA verify_conflict + local patch + for patch in "$@"; do + - verify_patch $patch '<commandline>' + done +} + +print_dep_traceback() { + local dep indent + indent=${1:->} + (($+verify_install[$1])) || return + for dep in ${(f)verify_install[$1]}; do + - warn_msg $indent patch ${(qqq)1} was pulled in by ${(qqq)dep} + - print_dep_traceback $dep "$indent >" + done +} + +print_con_traceback() { + local dep indent + indent=${1:-!} + (($+verify_install[$1])) || return + for dep in ${(f)verify_install[$1]}; do + - warn_msg $indent conflict with ${(qqq)1} is caused by ${(qqq)dep} + - print_dep_traceback $dep "$indent >" + done +} + +extend_install_list() { + [[ $# == 2 ]] || die "exactly two arguments required" + verify_install[$1]+=$2$'\n' + if (( ${conflicting_patches[(I)$1]} )); then + - err_msg ERROR cannot install patch $1 as it conflicts with current database + - print_dep_traceback $1 + exit 3 + elif (( $+verify_conflict[$1] )); then + - err_msg ERROR cannot install patch $1 as it conflicts with another patch + - print_con_traceback $1 + - print_dep_traceback $1 + exit 3 + fi +} + +extend_conflict_list() { + [[ $# == 2 ]] || die "exactly two arguments required" + verify_conflict[$1]+=$2$'\n' + if (( ${installed_patches[(I)$1]} )); then + - err_msg ERROR cannot install patch $2 as it creates conflict with $1 in the current database + - print_con_traceback $1 + exit 3 + elif (( $+verify_patch[$1] )); then + - err_msg ERROR cannot install patch $2 as it creates conflict with another patch + - print_con_traceback $1 + - print_dep_traceback $1 + exit 3 + fi +} + +verify_patch() { + [[ $# == 2 ]] || die "exactly two arguments required" + # (( ${installed_patches[(I)$1]} )) && return 0 + $+verify_patch[$1] && return 0 + local -a deps cons + local dep con + extend_install_list $1 $2 + for dep in ${(ps:\0:)patch_deps[$1]}; do + - verify_patch $dep $1 + done + for con in ${(ps:\0:)patch_cons[$1]}; do + - extend_conflict_list $con $1 + done +} + +# --- + +install_patch_set() { + local patch + for patch in "$@"; do + - install_patch $patch + done +} + +install_patch() { + (( ${installed_patches[(I)$1]} )) && return 0 + if [[ $1 == *[\\\']* ]]; then + printf >&2 'ERROR: disallowed character in patch name: %s\n' $1 + exit 1 + fi + + local dep + for dep in ${(ps:\0:)patch_deps[$1]}; do + - install_patch $dep + done + + printf '%s\n' \ + '\set ON_ERROR_STOP' \ + "select _v.register_patch('$1', NULL, NULL);" \ + "\i $1" \ + | psql -1 -e -f - || die ERROR: "installing patch ${(qqq)1} failed with exitcode $#" + installed_patches+=$1 +} + +# --- + +main "$@" diff --git a/tools/common.zsh b/tools/common.zsh @@ -0,0 +1,73 @@ +setopt no_unset warn_create_global # be strict +setopt extended_glob # allow glob specifiers + +# nice PS4 with ellapsed seconds (to two decimal places) {{{1 +zmodload zsh/system # to get actual pid +setopt PROMPT_SUBST +if [[ ${TERM:-dumb} == (xterm|rxvt|screen|linux|console|Eterm|putty)* ]]; then + # first color is for main shell process, second is subshell + PS4_PID_COLORS=(cyan magenta) + PS4='+%B${SECONDS} %F{${PS4_PID_COLORS[1+($$ != ${sysparams[pid]})]}}%N%f:%F{yellow}%i%f>%b ' +else + PS4='+%B${SECONDS} ${sysparams[pid]} %N:%i>%b ' +fi + +# two digits after the decimal point +typeset -g -F 2 SECONDS + +### Color and text definition {{{1 + +typeset -g hl_fatal hl_reset + +# die messages +if (( $terminfo[colors] >= 8 )); then + hl_fatal='%F{red}%B'; hl_fatal=${(%)hl_fatal} + hl_warn='%F{yellow}%B'; hl_warn=${(%)hl_warn} + hl_reset='%b%f'; hl_reset=${(%)hl_reset} +fi + +### Utility functions {{{1 +err_msg() { + local first=$1 + shift + printf >&2 '%s%s%s%s\n' "$hl_fatal" "$first" "$hl_reset" "$*" +} + +warn_msg() { + local first=$1 + shift + printf >&2 '%s%s%s%s\n' "$hl_warn" "$first" "$hl_reset" "$*" +} + +# helper that prints out stack, error message and exits +die_ret() { + set +x + local ret n + ret=$1 + shift + print -r - >&2 "${hl_fatal}Fatal$hl_reset error occurend in:" + for n in {${#funcfiletrace}..1}; do + printf >&2 '%d> %s (%s)\n' $n "$funcfiletrace[$n]" "$functrace[$n]" + done + printf >&2 '%s\n' "${hl_fatal}*$hl_reset $^@" + exit $ret +} + +die() { + set +x + die_ret 1 "$@" +} +die100() { # 100: wrong usage + set +x + die_ret 100 "$@" +} +die111() { # 111: system call failed + set +x + die_ret 111 "$@" +} + +-() { # Run command and die on nonzero exitcode + "$@" || die_ret $? "command failed with exitcode $?: ${(j: :)${(q)@}}" +} + +