From d197fc04202e4650b183923da534cbde8dbf5881 Mon Sep 17 00:00:00 2001 From: Jephte Clain Date: Thu, 9 May 2019 12:21:07 +0400 Subject: [PATCH] sqlmig: support de l'importation directe des fichiers csv --- sqlmig | 263 +++++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 208 insertions(+), 55 deletions(-) diff --git a/sqlmig b/sqlmig index c795aa7..8d6802e 100755 --- a/sqlmig +++ b/sqlmig @@ -80,13 +80,53 @@ OPTIONS spécifié au nom de la base de données. Cette valeur peut être spécifiée dans la section [sqlmig] du fichier my.cnf ou avec la variable SUFFIX pour ora.conf + --csv2sql + Convertir les fichiers CSV en SQL. C'est la valeur par défaut. Ce + paramètre peut être spécifié dans my.cnf sous la forme + [sqlmig] + csv2sql=1 + ou dans ora.conf + CSV2SQL=1 + Pour le moment, cette conversion n'est supportée que pour MySQL. Tous + les fichiers de la forme [NUM]TABLE[-data].csv sont transformés en une + suite d'insertion dans la table TABLE. Une variante insère les données + dans la table après l'avoir vidée avec 'truncate table'. Les fichiers + doivent être de la forme [NUM]TABLE-data_truncate.csv --csv-null VALUE Lors de la conversion des fichiers .csv en .sql, considérer que VALUE représente la valeur NULL. Par défaut, utiliser la chaine vide + --csv-null-empty --csv-null-mysql --csv-null-upper - Aliases pour --csv-null '\\N' et --csv-null NULL respectivement - + Aliases pour --csv-null '', --csv-null '\\N' et --csv-null NULL respectivement + --no-csv2sql + Ne pas convertir les fichiers CSV en SQL. Ce paramètre peut être + spécifié dans my.cnf sous la forme + [sqlmig] + csv2sql=0 + ou dans ora.conf + CSV2SQL=0 + --load-data DEFAULT|LOCAL|SERVER + Si l'option --no-csv2sql est activée, spécifier le type de chargement + effectué pour les fichiers CSV. + * Avec LOCAL (la valeur par défaut), la directive 'load data local' est + utilisée. Cependant les erreurs éventuelles sont ignorées parce que le + serveur n'a pas de moyen pour arrêter la transmission des informations + par le client. + * Avec SERVER, la directive 'load data' est utilisée. Les erreurs + éventuelles sont affichées et arrêtent l'importation des données. Par + contre, le fichier doit être accessible par le serveur MySQL puisque + les données sont chargées directement par le serveur. + NB: avec MariaDB sur systemd, l'accès à /home, /root et /run/user est + désactivé par défaut. Pour pouvoir utiliser ce mode avec des fichiers + de l'utilisateur, il faut lancer ces commandes: + [ \$(id -u) == 0 ] && sudo= || sudo=sudo + \$sudo mkdir -p /etc/systemd/system/mariadb.service.d + \$sudo tee /etc/systemd/system/mariadb.service.d/dontprotecthome.conf <<<\$'[Service]\nProtectHome=false' + \$sudo systemctl daemon-reload + * Avec DEFAULT, utiliser SERVER si on communique avec le serveur MySQL + local (c'est à dire si host n'est pas spécifié ou vaut 127.*, ::1 ou + localhost). Sinon, utiliser LOCAL --profile PROFILE -P, --prod -T, --test @@ -156,14 +196,9 @@ OPTIONS --force, --continue-on-error Ne pas s'arrêter en cas d'erreur de mise à jour --no-data-csv - Ne pas convertir les fichiers *-data.csv en fichier .sql - correspondant. Cette conversion n'est supportée que pour MySQL pour le - moment, et un fichier de la forme NUMTABLE-data.csv où NUM est une valeur - numérique est transformé en une suite d'insertions dans la table TABLE. - La variante NUMTABLE-data_truncate.csv ajoute les données dans la table - après l'avoir vidée avec truncate. + Ne pas procéder à la conversion des fichiers CSV en SQL --force-data-csv - Forcer la conversion des fichiers *-data.csv. Par défaut, la conversion + Forcer la conversion des fichiers CSV en SQL. Par défaut, la conversion n'est faite que si le fichier csv est plus récent que le fichier sql correspondant. --devel-mode @@ -326,12 +361,60 @@ function set_dbdirs() { fi } +function fix_csv2sql() { + if [ -z "$csv2sql" ]; then + # valeur par défaut + csv2sql=1 + elif is_yes "$csv2sql"; then + csv2sql=1 + elif is_no "$csv2sql"; then + csv2sql=0 + fi +} function fix_csv_null() { + # si csv_null a une valeur vide, c'est déjà la valeur par défaut case "$csv_null" in + empty) csv_null=;; mysql) csv_null='\N';; upper) csv_null=NULL;; esac } +function fix_load_data() { + if [ -z "$load_data" ]; then + # valeur par défaut + load_data=local + else + case "${load_data,,}" in + default|d*) load_data=default;; + local|l*) load_data=local;; + server|s*) load_data=server;; + esac + fi + if [ "$load_data" == default ]; then + case "$host" in + ""|localhost|127.*|::1) load_data=server;; + *) load_data=local;; + esac + fi +} + +function get_csvinfo() { + # afficher les informations sur un fichier csv: nom de la table, et s'il + # faut faire un truncate + local csvname="$(basename -- "$1")" + local script='{ + truncate = ($0 ~ /-data_truncate(.devel)?.csv$/)? "1": "" + sub(/^.*\//, "") + sub(/^(([A-Z][-.A-Z0-9]*[0-9]-?)|([0-9][-.0-9]*-?))/, "") + sub(/\.csv$/, "") + sub(/\.devel$/, "") + sub(/-data(_[a-z]+*)?$/, "") + print "truncate=" truncate + gsub(/'\''/, "'\'\\\\\'\''") + print "table='\''" $0 "'\''" +}' + awk "$script" <<<"$csvname" +} ################################################################################ # MySQL @@ -476,6 +559,13 @@ function mysql__mconf_get() { setx tmp=mconf_get "$defaults" sqlmig suffix [ -n "$tmp" ] && suffix="$tmp" fi + if [ -n "$set_csv2sql" ]; then + setx tmp=mconf_get "$defaults" sqlmig csv2sql + [ -n "$tmp" ] && { + csv2sql="$tmp" + fix_csv2sql + } + fi if [ -n "$set_csv_null" ]; then setx tmp=mconf_get "$defaults" sqlmig csv_null [ -n "$tmp" ] && { @@ -483,14 +573,27 @@ function mysql__mconf_get() { fix_csv_null } fi + if [ -n "$set_load_data" ]; then + if [ -z "$host" ]; then + # load_data==default requière host + setx host=mconf_get "$defaults" client host + fi + setx tmp=mconf_get "$defaults" sqlmig load-data + [ -n "$tmp" ] && { + load_data="$tmp" + fix_load_data + } + fi } function mysql_set_userargs() { local dir="$1" dbname="$2" defaults - local set_suffix set_csv_null + local set_suffix set_csv2sql set_csv_null set_load_data userargs=() setx defaults=mysql_get_defaults "$dir" [ -z "$suffix" ] && set_suffix=1 + [ -z "$csv2sql" ] && set_csv2sql=1 [ -z "$csv_null" ] && set_csv_null=1 + [ -z "$load_data" ] && set_load_data=1 if [ -f "$defaults" ]; then array_add userargs --defaults-file="$defaults" mysql__mconf_get "$defaults" @@ -509,6 +612,10 @@ function mysql_set_userargs() { mysql__mconf_get "$dir/my-${dbname}.cnf" fi [ ${#userargs[*]} -gt 0 ] || array_add userargs --default-character-set utf8 + # initialiser les valeurs par défaut + fix_csv2sql + fix_csv_null + fix_load_data } function mysql_set_mysqlargs() { mysqlargs=() @@ -561,7 +668,26 @@ function mysql_user_update() { if [ -z "$updateone" ]; then mysql_before_update "$dbname" || die fi - cat "$update" | mysql_user_ve || abort_on_error + if [[ "$update" == *.sql ]]; then + # SQL + cat "$update" | mysql_user_ve || abort_on_error + else + # CSV + local truncate table local sql + eval "$(get_csvinfo "$update")" + [ -n "$truncate" ] && sql="truncate table $table;" + [ "$load_data" == local ] && local=1 || local= + sql="${sql}load data${local:+ local} infile '$update' into table \`$table\` + character set 'utf8' + fields terminated by ',' + optionally enclosed by '\\\"' + escaped by '\\\\' + lines terminated by '\\n' + starting by '' + ignore 1 lines + ($(<"$update" awk '{ print; exit }'));" + echo "$sql" | mysql_user_ve || abort_on_error + fi if [ -z "$updateone" ]; then mysql_after_update "$dbname" || die fi @@ -827,7 +953,7 @@ function oracle_source_adminconf() { } function oracle_source_userconf() { local dir="$1" dbname="$2" - unset ORACLE_SID NLS_LANG ADMINCONNECT USERCONNECT SQLMIGLOG SUFFIX CSV_NULL + unset ORACLE_SID NLS_LANG ADMINCONNECT USERCONNECT SQLMIGLOG SUFFIX CSV2SQL CSV_NULL setx defaults=oracle_get_defaults "$dir" [ -f "$defaults" ] && source "$defaults" [ -f "$dir/ora-${dbname}.conf" ] && source "$dir/ora-${dbname}.conf" @@ -851,10 +977,10 @@ function oracle_source_userconf() { fi [ -n "$SQLMIGLOG" ] || SQLMIGLOG="/tmp/sqlmig-${ORACLE_SID}-${dbname}.log" [ -z "$suffix" ] && suffix="$SUFFIX" - [ -z "$csv_null" ] && { - csv_null="$CSV_NULL" - fix_csv_null - } + [ -z "$csv2sql" ] && csv2sql="$CSV2SQL" + [ -z "$csv_null" ] && csv_null="$CSV_NULL" + fix_csv2sql + fix_csv_null } ORACLE_ADMIN_CONF_DONE= function oracle_admin_update() { @@ -918,7 +1044,9 @@ charset= oracle_sid= nls_lang= suffix= +csv2sql= csv_null= +load_data= profile= type=auto action=update @@ -946,9 +1074,13 @@ args=( -s:,--oracle-sid: oracle_sid= --nls-lang: nls_lang= --suffix: suffix= + --csv2sql csv2sql=1 --csv-null: csv_null= + --csv-null-empty csv_null=empty --csv-null-mysql csv_null='\N' --csv-null-upper csv_null=NULL + --no-csv2sql csv2sql=0 + --load-data: load_data= --profile: profile= -P,--prod profile=prod -T,--test profile=test @@ -1060,7 +1192,9 @@ default-character-set=utf8 [sqlmig] #suffix= -csv_null=" +#csv2sql=0 +csv_null= +#load-data=default" fi if [ ! -f "$dbdir/my-${dbname}.cnf" ]; then @@ -1110,7 +1244,8 @@ USERCONNECT=$dbname/password SQLMIGLOG=\"/tmp/sqlmig-\${ORACLE_SID}-${dbname}.log\" # divers #SUFFIX= -#CSV_NULL=" +#CSV2SQL=0 +CSV_NULL=" fi else @@ -1289,9 +1424,9 @@ function should_update() { elif [ "${name#maint-}" != "$name" ]; then # ignorer les opérations de maintenance par défaut return 1 - elif [ "$dbmode" != devel -a "${name%.devel.sql}" != "$name" ]; then + elif [ "$dbmode" != devel -a "${name%.devel.*}" != "$name" ]; then # si on est en mode autre que devel, le nom ne doit pas se terminer - # par .devel.sql + # par .devel.* return 1 fi return 0 @@ -1347,13 +1482,17 @@ for dbdir in "${dbdirs[@]}"; do ensure_dbtype "$dbdir" "$type" ensure_dbmode "$dbtype" "$mode" - if [ -n "$data_csv" ]; then - # Conversion csv --> sql - array_lsfiles csvs "$dbdir" "*.csv" - if [ "$dbtype" == mysql ]; then - setx defaults=mysql_get_defaults "$dbdir" - set_csv_null=1 - mysql__mconf_get "$defaults" + if [ "$dbtype" == mysql ]; then + setx defaults=mysql_get_defaults "$dbdir" + set_csv2sql=1 + set_csv_null=1 + mysql__mconf_get "$defaults" + fix_csv2sql + fix_csv_null + + if is_yes "$csv2sql" && [ -n "$data_csv" ]; then + # Conversion csv --> sql + array_lsfiles csvs "$dbdir" "*.csv" etitled "Conversion" for csv in "${csvs[@]}"; do @@ -1364,19 +1503,14 @@ for dbdir in "${dbdirs[@]}"; do fi estep "$csvname --> ${csvname%.csv}.sql" - script='{ - truncate = ($0 ~ /-data_truncate(.devel)?.csv$/)? "1": "" - sub(/^.*\//, "") - sub(/^[A-Z0-9.]*[0-9]-?/, "") - sub(/\.csv$/, "") - sub(/\.devel$/, "") - sub(/-data(_[a-z]+*)?$/, "") - print "truncate=" truncate - gsub(/'\''/, "'\'\\\\\'\''") - print "table='\''" $0 "'\''" -}' - eval "$(awk "$script" <<<"$csvname")" - "$scriptdir/mysqlloadcsv" >"$sql" ${truncate:+-T} -Z "$csv_null" -nIf "$csv" "$table" --prefix "-- -*- coding: utf-8 mode: sql -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8" + eval "$(get_csvinfo "$csvname")" + mysqlloadcsv_args=( + ${truncate:+-T} + -Z "$csv_null" + --prefix "-- -*- coding: utf-8 mode: sql -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8" + -nIf "$csv" "$table" + ) + "$scriptdir/mysqlloadcsv" "${mysqlloadcsv_args[@]}" >"$sql" done eend; eclearp fi @@ -1386,14 +1520,21 @@ for dbdir in "${dbdirs[@]}"; do drops=() creates=() updates=() - array_lsfiles files "$dbdir" "*.sql" + have_csv= + array_lsfiles files "$dbdir" "*.sql" "*.csv" for file in "${files[@]}"; do - if have_tag drop "$file"; then - array_add drops "$file" - elif have_tag create "$file"; then - array_add creates "$file" - else + if [[ "$file" == *.sql ]]; then + if have_tag drop "$file"; then + array_add drops "$file" + elif have_tag create "$file"; then + array_add creates "$file" + else + array_add updates "$file" + fi + elif [ ! -f "${file%.csv}.sql" ]; then + # n'ajouter le CSV que si le fichier SQL correspondant n'existe pas array_add updates "$file" + have_csv=1 fi done @@ -1408,6 +1549,9 @@ for dbdir in "${dbdirs[@]}"; do estepi "Suffixe: $dbname --> $dbname$suffix" dbname="$dbname$suffix" fi + if is_no "$csv2sql" && [ -n "$have_csv" ]; then + estepi "Chargement des fichiers CSV avec la méthode $load_data" + fi # Suppression if [ -n "$drop" ]; then @@ -1449,10 +1593,16 @@ for dbdir in "${dbdirs[@]}"; do mysql_tbconf "$dbname" for update in "${updates[@]}"; do should_update "$update" || continue - if have_tag admin "$update"; then - [ -n "$updatedir" ] && name="${update#$updatedir/}" || name="${update#$dbdir/}" - mysql_admin_update "$name" "$update" "$updateone" - else + if [[ "$update" == *.sql ]]; then + # fichier SQL + if have_tag admin "$update"; then + [ -n "$updatedir" ] && name="${update#$updatedir/}" || name="${update#$dbdir/}" + mysql_admin_update "$name" "$update" "$updateone" + else + mysql_user_update "${update#$dbdir/}" "$update" "$dbname" "$updateone" + fi + elif is_no "$csv2sql"; then + # fichier CSV, ne les traiter que si on est en mode --no-csv2sql mysql_user_update "${update#$dbdir/}" "$update" "$dbname" "$updateone" fi done @@ -1511,11 +1661,14 @@ for dbdir in "${dbdirs[@]}"; do oracle_tbconf "$dbname" for update in "${updates[@]}"; do should_update "$update" || continue - if have_tag admin "$update"; then - [ -n "$updatedir" ] && name="${update#$updatedir/}" || name="${update#$dbdir/}" - oracle_admin_update "$name" "$update" "$updateone" - else - oracle_user_update "${update#$dbdir/}" "$update" "$dbname" "$updateone" + if [[ "$update" == *.sql ]]; then + # fichier SQL + if have_tag admin "$update"; then + [ -n "$updatedir" ] && name="${update#$updatedir/}" || name="${update#$dbdir/}" + oracle_admin_update "$name" "$update" "$updateone" + else + oracle_user_update "${update#$dbdir/}" "$update" "$dbname" "$updateone" + fi fi done eend; eclearp