506 lines
15 KiB
Bash
Executable File
506 lines
15 KiB
Bash
Executable File
#!/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
|