#!/bin/bash
# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
source "$(dirname "$0")/lib/ulib/ulib" || exit 1
urequire DEFAULTS awk

function display_help() {
    uecho "$scriptname: Charger une table MySQL avec un fichier csv

USAGE
    $scriptname [db.]table [fields...] [-- mysql options]

db      est le nom de la base de données
table   est le nom de la table à charger
fields  est la liste des colonnes. Si cette valeur est spécifiée, il faudra
        peut-être utiliser l'option -s pour ignorer le cas échéant la ligne des
        en-têtes dans le fichier en entrée. Sinon, les colonnes à utiliser sont
        calculées à partir du fichier en entrée.

Dans les données en entrées, qui doivent être en UTF8, les conversions suivantes
sont effectuées par MySQL:

    \\0 --> <caractère NUL>
    \\b --> <backspace>
    \\n --> <newline>
    \\r --> <carriage return>
    \\t --> <tab>
    \\Z --> Ctrl+Z
    \\N --> NULL

OPTIONS
    -h, --host host
    -P, --port port
    -u, --user user
    -ppassword
        Informations de connexion à la base de données
    -C, --config CONFIG
        Prendre les informations de connexion depuis le fichier spécifié.
        Le fichier doit être de la forme
            host=HOST.TLD
            #post=3306
            user=USER
            password=PASS
            #dbtable=DB.TABLE
            #fields=(FIELDS...)
            # Il est possible aussi de spécifier DB et TABLE séparément:
            #database=DB
            #table=TABLE
        Les variables port, dbtable et fields sont facultatives.
        Les valeurs définies dans ce fichier sont prioritaires par rapport à
        celles qui auraient été spécifiées sur la ligne de commande.
        Utiliser password=--NOT-SET-- s'il faut se connecter sans mot de passe
        Cette option peut être utilisée plusieurs fois, auquel cas les fichiers
        sont chargés dans l'ordre.
    --profile PROFILE
        La variable \$PROFILE est définie avec la valeur spécifiée avant de
        sourcer les fichiers de configuration. Cela permet d'avoir des fichiers
        de configuration qui calculent dynamiquement les paramètres en fonction
        de la valeur du profil.
    -f, --input INPUT
        Fichier en entrée. Ne pas spécifier cette option ou utiliser '-' pour
        lire depuis l'entrée standard.
    -d, --auto-dbtable DB
        Spécifier la base de données avec laquelle se connecter. De plus, si le
        nom de la table n'est pas spécifié, prendre par défaut le nom de base du
        fichier spécifié avec l'option -f
    -s, --skip-lines NBLINES
        Nombre de lignes à sauter dans le fichier en entrée
    -n, --fake
        Ne pas effectuer l'opération. Afficher simplement la commande SQL.
    --run
        Forcer le lancement de l'opération. Utiliser cette option avec -A pour
        créer la table avec les paramètres analysés.
    -T, --truncate
        Vider la table avant d'effectuer le chargement
    -L, --load-data
        Charger les données avec la commande 'load data local'. C'est l'option
        par défaut. Le fichier est transmis tel quel à MySQL.
    -I, --insert-data
        Charger les données en générant des commandes 'insert into'. L'effet est
        en principe le même avec l'option -L (sauf que certains formats de date
        exotiques seront correctement importés). Cette option peut aussi être
        utilisée avec l'option -n pour générer une liste de commande à corriger
        et/ou adapter.
    -U, -k, --update-data KEY
        Au lieu de charger de nouvelles données, essayer de mettre à jour la
        table. KEY est le nom de la colonne qui est utilisée comme clé. Toutes
        les autres colonnes sont les nouvelles données à mettre à jour. Si
        aucune ligne ne correspond à une clé donnée, la mise à jour pour cette
        ligne est ignorée.
        Note: utiliser les options -T et -U ensemble n'a pas de sens, mais -T
        est quand même honoré.
    -Z, --null-value VALUE
        Avec les options -I et -U, considérer que NULL est représenté par la
        chaine spécifiée. Par défaut, utiliser \N
    -z, --null-is-empty
        Avec les options -I et -U, considérer que NULL est représenté par la
        chaine vide. Cette option est équivalente à -Z ''
    -t, --types [DEFAULT_TYPE,]FIELD:TYPE,...
        Spécifier pour chaque champ mentionné le type de donnée à forcer. Le
        type 'auto' signifie que le type est autodétecté. C'est la valeur par
        défaut. Les autres types valides sont 'str', 'int' et 'date'
        Cette option est ignorée avec l'option -L
    -A, --analyse
        Analyser les données et afficher une requête pour créer une table qui
        pourrait contenir ces données.
        Cette option implique --fake et affiche simplement la commande SQL.
        Pour lancer la commande SQL générée, il faut ajouter l'option --run
        APRES cette option"
}

function norm_type() {
    case "$1" in
    string|str|s|varchar) echo varchar;;
    integer|int|i) echo integer;;
    #datetime|dt) echo datetime;;
    date|d) echo date;;
    auto|a) echo auto;;
    *) echo "$1";;
    esac
}

__AWK_MYSQLFUNCS='
function is_null(value) {
  return value == null_value
}
function is_integer(value) {
  return value ~ /^[0-9]+$/
}
function is_date_dmy(value) {
  return value ~ /^[0-9][0-9]\/[0-9][0-9]\/[0-9][0-9]$/
}
function is_date_dmY(value) {
  return value ~ /^[0-9][0-9]\/[0-9][0-9]\/[0-9][0-9][0-9][0-9]$/
}
function is_date_Ymd1(value) {
  return value ~ /^[0-9][0-9][0-9][0-9]\/[0-9][0-9]\/[0-9][0-9]$/
}
function is_date_Ymd2(value) {
  return value ~ /^[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]$/
}
function is_date(value) {
  return is_date_dmy(value) || is_date_dmY(value) || is_date_Ymd1(value) || is_date_Ymd2(value)
}
#function is_datetime(value) {
#}
function format_string(value) {
  gsub(/'\''/, "'\'\''", value)
  value = "'\''" value "'\''"
  return value
}
function format_integer(value) {
  if (is_integer(value)) {
    if (value != "0") {
      gsub(/^0+/, "", value)
    }
  } else {
    value = format_string(value)
  }
  return value
}
function format_date(value) {
  if (is_date_dmy(value)) {
    value = "str_to_date('\''" value "'\'', '\''%d/%m/%y'\'')"
  } else if (is_date_dmY(value)) {
    value = "str_to_date('\''" value "'\'', '\''%d/%m/%Y'\'')"
  } else if (is_date_Ymd1(value)) {
    value = "str_to_date('\''" value "'\'', '\''%Y/%m/%d'\'')"
  } else if (is_date_Ymd2(value)) {
    value = "str_to_date('\''" value "'\'', '\''%Y-%m-%d'\'')"
  } else {
    value = format_string(value)
  }
  return value
}
function format_sqlvalue(value, type) {
  if (type == "varchar" || type == "string" || type == "str" || type == "s") {
    value = format_string(value)
  } else if (type == "integer" || type == "int" || type == "i") {
    value = format_integer(value)
  } else if (type == "date" || type == "d") {
    value = format_date(value)
  #} else if (type == "datetime" || type == "dt") {
  #  value = format_datetime(value)
  } else {
    # par defaut, autodétecter
    if (is_null(value)) {
      value = "NULL"
    } else if (is_integer(value)) {
      value = format_integer(value)
    } else if (is_date(value)) {
      value = format_date(value)
    } else {
      value = format_string(value)
    }
  }
  return value
}
'

host=
port=
user=
password=--NOT-SET--
configs=()
profile=
input=
auto_db=
skip_lines=
truncate=
fake=
method=load
update_key=
null_value='\N'
fieldtypes=()
parse_opts "${PRETTYOPTS[@]}" \
    --help '$exit_with display_help' \
    -h:,-H:,--host: host= \
    -P:,--port: port= \
    -u:,--user: user= \
    -p::,--passwd:,--password: password= \
    -C:,--config: configs \
    -f:,--input: input= \
    -d:,--auto-dbtable: auto_db= \
    -s:,--skip-lines: skip_lines= \
    -n,--fake fake=1 \
    --run fake= \
    -T,--truncate truncate=1 \
    -L,--load-data method=load \
    -I,--insert-data method=insert \
    -U:,-k:,--update-data: '$method=update; set@ update_key' \
    -Z:,--null-value: null_value= \
    -z,--null-is-empty null_value= \
    -t:,--types: fieldtypes \
    -A,--analyse '$method=analyse; fake=1' \
    @ args -- "$@" && set -- "${args[@]}" || die "$args"

dbtable="$1"; shift
fields=()
while [ "$#" -gt 0 -a "$1" != "--" ]; do
    fields=("${fields[@]}" "$1")
    shift
done
[ "$1" == "--" ] && shift

splitfsep2 "$dbtable" . database table
if [ -z "$database" -a -n "$auto_db" ]; then
    database="$auto_db"
    if [ -z "$table" -a -n "$input" -a "$input" != "-" ]; then
        table="$(basename -- "$input")"
        table="${table%.*}"
    fi
fi

if [ -n "${configs[*]}" ]; then
    PROFILE="$profile"
    array_fix_paths configs
    for config in "${configs[@]}"; do
        [ -f "$config" ] || die "Fichier introuvable: $config"
        source "$(abspath "$config")"
    done
fi
[ -n "$table" ] || die "Vous devez spécifier la table dans laquelle se fera l'importation"

if [ -n "$input" -a "$input" != "-" ]; then
    [ -f "$input" ] || die "$input: fichier introuvable"
fi

isnum "$skip_lines" || skip_lines=0

if [ -n "${fields[*]}" ]; then
    # nous savons quels champs utiliser
    cfields="$(array_join fields ,)"
    if [ -z "$input" -o "$input" == "-" ]; then
        ac_set_tmpfile input
        awkcsv -s "$skip_lines" -k "$cfields" >"$input"
    else
        ac_set_tmpfile tmpinput
        <"$input" awkcsv -s "$skip_lines" -k "$cfields" >"$tmpinput"
        input="$tmpinput"
    fi
else
    # les champs seront calculés à partir de l'entrée
    if [ -z "$input" -o "$input" == "-" ]; then
        ac_set_tmpfile input
        cat >"$input"
    fi
    array_split fields "$(awkcsv -s "$skip_lines" -e '{ print array_join(HEADERS, ","); exit }' <"$input")" ","
    cfields="$(array_join fields ,)"
fi

# calculer les types à utiliser pour chaque champ
types=()
for field in "${fields[@]}"; do
    array_add types ""
done
default_type=auto
array_fix_paths fieldtypes ,
for ft in "${fieldtypes[@]}"; do
    splitpair "$ft" f t
    if [ -n "$t" ]; then
        let i=0
        for field in "${fields[@]}"; do
            if [ "$field" == "$f" ]; then
                types[$i]="$(norm_type "$t")"
                break
            fi
            let i=$i+1
        done
    else
        default_type="$(norm_type "$f")"
    fi
done
let i=0
for type in "${types[@]}"; do
    [ -n "$type" ] || types[$i]="$default_type"
    let i=$i+1
done

mysqlargs=(
    ${host:+-h "$host"} ${port:+-P "$port"}
    ${user:+-u "$user"} ${database:+-D "$database"}
)
[ "$password" != "--NOT-SET--" ] && mysqlargs=("${mysqlargs[@]}" -p"$password")

[ -n "$truncate" ] && truncate="truncate table \`$table\`;"

if [ "$method" == load ]; then
    skip_lines=$(($skip_lines + 1))
    loadcsv="load data local infile '$input' into table \`$table\` character set 'utf8' fields terminated by ',' optionally enclosed by '\\\"' escaped by '\\\\' lines terminated by '\\n' starting by '' ignore $skip_lines lines ($cfields);"
    mysqlargs=("${mysqlargs[@]}"
        "$truncate$loadcsv"
        --
        --local-infile=1
    )
    cmd=("$scriptdir/mysqlcsv" "${mysqlargs[@]}" "$@")

    r=0
    if [ -n "$fake" ]; then
        echo "-- Requêtes SQL:"
        [ -n "$truncate" ] && echo "$truncate"
        echo "$loadcsv"
        echo "-- Commande à lancer pour importer la table dans MySQL:"
        echo "--     $(qvals "${cmd[@]}")"
    else
        "${cmd[@]}"; r=$?
    fi

    exit $r

elif [ "$method" == insert ]; then
    ac_set_tmpfile inserts
    [ -n "$truncate" ] && echo "$truncate" >>"$inserts"
    awkcsv <"$input" >>"$inserts" -s "$skip_lines" -v table="$table" \
        -v null_value="$null_value" -v types[@] -a "$__AWK_MYSQLFUNCS"'{
  count = length(ORIGHEADERS)
  fields = ""
  for (i = 1; i <= count; i++) {
    if (i > 1) fields = fields ", "
    fields = fields "`" ORIGHEADERS[i] "`"
  }
  values = ""
  for (i = 1; i <= count; i++) {
    if (i > 1) values = values ", "
    values = values format_sqlvalue($i, types[i])
  }
  print "insert into `" table "` (" fields ") values (" values ");"
}
END {
  print "commit;"
}'
    r=0
    if [ -n "$fake" ]; then
        cat "$inserts"
    else
        cat "$inserts" | "$scriptdir/mysqlcsv" "${mysqlargs[@]}" -f -; r=$?
    fi

    ac_clean "$inserts"
    exit $r

elif [ "$method" == update ]; then
    array_contains fields "$update_key" || die "$update_key: ce champ n'existe pas dans la source"

    ac_set_tmpfile updates
    [ -n "$truncate" ] && echo "$truncate" >>"$updates"
    awkcsv <"$input" >>"$updates" -s "$skip_lines" \
        -v table="$table" -v update_key="$update_key" \
        -v null_value="$null_value" -v types[@] -a "$__AWK_MYSQLFUNCS"'{
  set_values = ""
  cond = ""
  count = length(ORIGHEADERS)
  for (i = 1; i <= count; i++) {
    field = ORIGHEADERS[i]
    value = format_sqlvalue($i, types[i])
    if (field == update_key) {
      cond = "`" field "`=" value
    } else {
      if (set_values) set_values = set_values ", "
      set_values = set_values "`" field "`=" value
    }
  }
  print "update `" table "` set " set_values " where " cond ";"
}
END {
  print "commit;"
}'
    r=0
    if [ -n "$fake" ]; then
        cat "$updates"
    else
        cat "$updates" | "$scriptdir/mysqlcsv" "${mysqlargs[@]}" -f -; r=$?
    fi

    ac_clean "$updates"
    exit $r

elif [ "$method" == analyse ]; then
    ac_set_tmpfile create
    awkcsv <"$input" >"$create" -s "$skip_lines" -v table="$table" \
        -v null_value="$null_value" -v types[@] -a "$__AWK_MYSQLFUNCS"'
function set_integer(i) {
  if (COL_TYPES[i] == "" || COL_TYPES[i] == "integer") {
    COL_TYPES[i] = "integer"
    return 1
  }
  return 0
}
function set_datetime(i) {
  if (COL_TYPES[i] == "" || COL_TYPES[i] == "datetime") {
    COL_TYPES[i] = "datetime"
    return 1
  }
  return 0
}
function set_date(i) {
  if (COL_TYPES[i] == "" || COL_TYPES[i] == "date") {
    COL_TYPES[i] = "date"
    return 1
  }
  return 0
}
function set_varchar(i, col_size) {
  COL_TYPES[i] = "varchar"
  if (COL_SIZES[i] == "") {
    COL_SIZES[i] = col_size
  } else if (col_size > COL_SIZES[i]) {
    COL_SIZES[i] = col_size
  }
    return 1
}
{
  if (do_once("init")) {
    count = length(ORIGHEADERS)
    array_newsize(COL_TYPES, count)
    array_newsize(COL_SIZES, count)
    for (i = 1; i <= count; i++) {
      if (types[i] != "auto") {
        COL_TYPES[i] = types[i]
      }
    }
  }
  for (i = 1; i <= count; i++) {
    if (is_null($i)) continue
    if (is_integer($i) && set_integer(i)) continue
#    if (is_datetime($i) && set_datetime(i)) continue
    if (is_date($i) && set_date(i)) continue
    set_varchar(i, length($i))
  }
}
END {
  print "drop table if exists `" table "`;"
  print "create table `" table "` ("
  for (i = 1; i <= count; i++) {
    line = ""
    if (i == 1) line = line " "
    else line = line ","
    line = line "`" ORIGHEADERS[i] "` "
    col_type = COL_TYPES[i]
    if (col_type == "") col_type = "varchar"
    if (col_type == "integer") {
      line = line col_type
    } else if (col_type == "datetime" || col_type == "date") {
      line = line col_type
    } else if (col_type == "varchar") {
      col_size = COL_SIZES[i]
      if (col_size == "") col_size = "80"
      line = line col_type "(" col_size ")"
    }
    print line
  }
  print ");"
}'
    r=0
    if [ -n "$fake" ]; then
        cat "$create"
    else
        cat "$create" | "$scriptdir/mysqlcsv" "${mysqlargs[@]}" -f -; r=$?
    fi

    ac_clean "$create"
    exit $r

else
    die "BUG: $method: method non implémentée"
fi
