#!/usr/bin/perl
# Načte textové soubory vyexportované z Microsoft Accessu a importuje je do MySQL.
# Copyright © 2005, 2012, 2015, 2019 Daniel Zeman <zeman@ufal.mff.cuni.cz>
# Licence: GNU GPL
# 25.7.2015: přechod na nový kub.cz
# 30.9.2019: po upgradu systému a MySQL na kubu se import dramaticky zpomalil;
#            i když primární příčina pravděpodobně tkví ve změněném nastavení
#            serveru, přišel čas hledat rezervy i v tomhle skriptu
# 1.10.2019: INSERT volán dávkově pro 100 řádků tabulky najednou (místo 35 až 51 minut to najednou trvá celé 81 sekund)
# Doporučeno volat s nohupem, aby import nespadl, když spadne spojení SSH:
# nohup perl /var/web/lib/cgi/hrygit-master-release/vnitro/import-mysql.pl mso 2>> import-mso.log &

use utf8;
use open ':utf8';
binmode(STDIN, ':utf8');
binmode(STDOUT, ':utf8');
binmode(STDERR, ':utf8');
use Encode;
use DBI;
# Kvůli sitesql raději použijeme stejné knihovny, jaké používají skripty CGI.
# Jinak by asi stačila kopie v /home/dan/lib, na kterou ukazuje můj $PERLLIB.
use lib '/s/w/lib/dan';
use access;
use sitesql;
use cas;
my $global_cas_start = cas::ted();
my $global_n_insertu = 0;
my $global_n_radku = 0;
my $global_flush_rate = 100;
if($ARGV[0] =~ m/^(deskohrani|mso)$/i)
{
    importovat_deskohrani();
}
elsif($ARGV[0] eq 'deskohraniletos')
{
    ###!!! EXPERIMENTÁLNÍ. POZOR, ČÍST KOMENTÁŘE V TÉ FUNKCI DOLE, POTENCIÁLNĚ TO MŮŽE OŘEZÁVAT PŘENÁŠENÁ DATA!
    importovat_deskohrani_letos();
}
else
{
    importovat_hry(); # včetně databáze obchod
}
my $ted = vypsat_statistiku(); # vypíše stat na STDERR a navíc nám řekne, kolik zjistil, že je hodin
print STDERR ("$ted->{rmdhms} končím.\n");



#------------------------------------------------------------------------------
# Uloží databázi her.
#------------------------------------------------------------------------------
sub importovat_hry
{
    my $databaze = sitesql::connect_obchod('hry') or die;
    my $path = '/home/klara/access_txt_hry';
    importovat_tabulku($databaze, "$path/hry.txt", 'hry');
    importovat_tabulku($databaze, "$path/nazvy.txt", 'nazvy');
    importovat_tabulku($databaze, "$path/odkazy.txt", 'odkazy');
    importovat_tabulku($databaze, "$path/provedeni.txt", 'provedeni');
    importovat_tabulku($databaze, "$path/exemplare.txt", 'exemplare');
    importovat_tabulku($databaze, "$path/prodej.txt", 'prodej');
    importovat_tabulku($databaze, "$path/zbozi.txt", 'zbozi');
    importovat_tabulku($databaze, "$path/slevkody.txt", 'slevkody');
    $databaze = sitesql::connect_obchod('web_hrejsi_obchod') or die;
    $path = '/home/klara/access_txt_obchod';
    importovat_tabulku($databaze, "$path/skupiny.txt", 'skupiny');
    importovat_tabulku($databaze, "$path/zbozi.txt", 'zbozi');
    importovat_tabulku($databaze, "$path/prodej.txt", 'prodej');
    importovat_tabulku($databaze, "$path/odkazy.txt", 'odkazy');
    importovat_tabulku($databaze, "$path/slevkody.txt", 'slevkody');
}



#------------------------------------------------------------------------------
# Uloží databázi Deskohraní.
#------------------------------------------------------------------------------
sub importovat_deskohrani
{
    my $heslo = `cat /home/dan/.dbimphsl`;
    $heslo =~ s/\r?\n$//;
    my $databaze = sitesql::connect('web_deskohrani', 'dan', $heslo) or die;
    my $path = '/home/klara/access_txt_deskohrani';
    importovat_tabulku($databaze, "$path/rocniky.txt", 'rocniky');
    importovat_tabulku($databaze, "$path/akce.txt", 'akce');
    importovat_tabulku($databaze, "$path/hry.txt", 'hry');
    importovat_tabulku($databaze, "$path/osoby.txt", 'osoby');
    importovat_tabulku($databaze, "$path/zeme.txt", 'zeme');
    importovat_tabulku($databaze, "$path/prihlasky.txt", 'prihlasky');
    importovat_tabulku($databaze, "$path/mistnosti.txt", 'mistnosti');
    importovat_tabulku($databaze, "$path/rezervace.txt", 'rezervace');
    importovat_tabulku($databaze, "$path/vysledky.txt", 'vysledky');
}



#------------------------------------------------------------------------------
# Uloží do databáze Deskohraní letošní data, starých se nedotkne.
#------------------------------------------------------------------------------
sub importovat_deskohrani_letos
{
    my $heslo = `cat /home/dan/.dbimphsl`;
    $heslo =~ s/\r?\n$//;
    my $databaze = sitesql::connect('web_deskohrani', 'dan', $heslo) or die;
    my $path = '/home/klara/access_txt_deskohrani';
    importovat_tabulku($databaze,      "$path/rocniky.txt", 'rocniky');
    importovat_letosni_data($databaze, "$path/akce.txt", 'akce');
    importovat_tabulku($databaze,      "$path/hry.txt", 'hry');
    importovat_tabulku($databaze,      "$path/osoby.txt", 'osoby');
    importovat_tabulku($databaze,      "$path/zeme.txt", 'zeme');
    importovat_letosni_data($databaze, "$path/prihlasky.txt", 'prihlasky');
    importovat_tabulku($databaze,      "$path/mistnosti.txt", 'mistnosti');
    importovat_letosni_data($databaze, "$path/rezervace.txt", 'rezervace');
    importovat_letosni_data($databaze, "$path/vysledky.txt", 'vysledky');
}



#------------------------------------------------------------------------------
# Načte tabulku vyexportovanou z Accessu a data uloží na server MySQL. Původní
# tabulku v databázi zlikviduje!
#------------------------------------------------------------------------------
sub importovat_tabulku
{
    my $databaze = shift;
    my $soubor = shift; # název souboru s daty k imporut
    my $tbl = shift; # název tabulky v databázi, kam se mají data importovat.
    # Načíst data z textového souboru, do nějž byla vyexportována z Accessu.
    my @nazvy;
    my $tabulka = access::cist_tabulku($soubor, \@nazvy);
    # Zapomenout sloupce s prázdným názvem (zřejmě se jeden takový objevuje na konci).
    @nazvy = grep{$_ !~ m/^\s*$/}(@nazvy);
    # Upravit názvy sloupců, aby neobsahovaly mezery, pomlčky ani tečky.
    my %uprnazvy;
    upravit_nazvy_sloupcu(\@nazvy, \%uprnazvy);
    # Proběhnout tabulku a pro každý sloupec zjistit maximální délku hodnoty.
    %maxdelky;
    # Žádnému sloupci nedovolit nulovou délku, to by SQL server nemusel strávit.
    foreach my $sloupec (@nazvy)
    {
        $maxdelky{$sloupec} = 1;
    }
    foreach my $radek (@{$tabulka})
    {
        foreach my $sloupec (@nazvy)
        {
            my $delka = length($radek->{$sloupec});
            if($delka>$maxdelky{$sloupec})
            {
                $maxdelky{$sloupec} = $delka;
            }
        }
    }
    # Převést pole maximálních délek na pole datových typů.
    # Protože máme nějaký problém se vstupem textových dat v UTF-8 a protože nám
    # server ořezává data ne na 255 znaků, ale 255 bajtů, násobit délku všech polí
    # třema (rozumný limit počtu bajtů na UTF-8 znak) a u polí, kde tento trojnásobek
    # přesahuje 255 bajtů, zadat jako typ TEXT (místo CHAR).
    # Ten může být až 65535 znaků (bajtů) dlouhý.
    my @sloupce = map{"$uprnazvy{$_} ".($maxdelky{$_}*3>255 ? "TEXT" : "CHAR(".($maxdelky{$_}*3).")")}(@nazvy);
#    my @sloupce = map{"$uprnazvy{$_} CHAR($maxdelky{$_})"}(@nazvy);
    # Smazat dosavadní tabulku v databázi.
    $databaze->do("DROP TABLE $tbl;");
    # Vytvořit v databázi příslušnou tabulku.
    my $dotaz = "CREATE TABLE $tbl (".join(", ", @sloupce).") CHARACTER SET utf8 COLLATE utf8_czech_ci;";
    print STDERR ("$dotaz\n");
    $databaze->do($dotaz) or die("Nelze spustit dotaz.\n");
    # Nalít do tabulky data.
    my $seznam_poli = join(', ', map {$uprnazvy{$_}} (@nazvy));
    my @seznamy_hodnot;
    foreach my $radek (@{$tabulka})
    {
        # V hodnotách nesmí být ASCII apostrof, protože ho používáme na ohraničení řetězců v SQL.
        # Zdvojený apostrof neukončuje řetězec.
        my $seznam_hodnot = join(', ', map {$radek->{$_} =~ s/'/''/g; "_utf8'$radek->{$_}'"} @nazvy); #'
        push(@seznamy_hodnot, $seznam_hodnot);
        # Posílat na server N řádků najednou, abychom ušetřili na režii.
        if(scalar(@seznamy_hodnot) >= $global_flush_rate)
        {
            flush($databaze, $tbl, $seznam_poli, @seznamy_hodnot);
            @seznamy_hodnot = ();
        }
    }
    if(scalar(@seznamy_hodnot) > 0)
    {
        flush($databaze, $tbl, $seznam_poli, @seznamy_hodnot);
    }
}



#------------------------------------------------------------------------------
# Načte tabulku vyexportovanou z Accessu, vyhledá v ní řádky vztahující se
# k aktuálnímu roku a uloží je na server MySQL. Z cílové tabulky nejdříve
# odstraní všechny existující záznamy vztahující se k aktuálnímu roku, zbytek
# tabulky ale zachová. Nutnou podmínkou je, aby tabulka měla pole s názvem
# "rok".
#------------------------------------------------------------------------------
sub importovat_letosni_data
{
    my $databaze = shift;
    my $soubor = shift; # název souboru s daty k imporut
    my $tbl = shift; # název tabulky v databázi, kam se mají data importovat.
    my $letos = $global_cas_start->{rok};
    # Načíst data z textového souboru, do nějž byla vyexportována z Accessu.
    my @nazvy;
    my $tabulka = access::cist_tabulku($soubor, \@nazvy);
    # Zapomenout sloupce s prázdným názvem (zřejmě se jeden takový objevuje na konci).
    @nazvy = grep{$_ !~ m/^\s*$/}(@nazvy);
    # Upravit názvy sloupců, aby neobsahovaly mezery, pomlčky ani tečky.
    my %uprnazvy;
    upravit_nazvy_sloupcu(\@nazvy, \%uprnazvy);
    # Smazat existující letošní data v tabulce.
    my $dotaz = "DELETE FROM $tbl WHERE rok = '$letos';";
    print STDERR ("$dotaz\n");
    $databaze->do($dotaz) or die("Nelze spustit dotaz.\n");
    # Nalít do tabulky data.
    ###!!! POZOR! Když jsme tabulku vytvářeli (viz funkci importovat_tabulku() výše),
    ###!!! deklarovali jsme pro každý sloupec tolik znaků, kolik bylo potřeba pro
    ###!!! tehdy známá data. Jenže je možné, že v nových datech bude nějaká hodnota delší!
    ###!!! S tím je potřeba něco udělat, jinak dojde k oříznutí hodnot nebo to spadne.
    my $seznam_poli = join(', ', map {$uprnazvy{$_}} (@nazvy));
    my @seznamy_hodnot;
    foreach my $radek (@{$tabulka})
    {
        # Přeskočit řádky, které se nevztahují k aktuálnímu roku.
        next if(!defined($radek->{rok}) || $radek->{rok} ne $letos);
        # V hodnotách nesmí být ASCII apostrof, protože ho používáme na ohraničení řetězců v SQL.
        # Zdvojený apostrof neukončuje řetězec.
        my $seznam_hodnot = join(', ', map {$radek->{$_} =~ s/'/''/g; "_utf8'$radek->{$_}'"} @nazvy); #'
        push(@seznamy_hodnot, $seznam_hodnot);
        # Posílat na server N řádků najednou, abychom ušetřili na režii.
        if(scalar(@seznamy_hodnot) >= $global_flush_rate)
        {
            flush($databaze, $tbl, $seznam_poli, @seznamy_hodnot);
            @seznamy_hodnot = ();
        }
    }
    if(scalar(@seznamy_hodnot) > 0)
    {
        flush($databaze, $tbl, $seznam_poli, @seznamy_hodnot);
    }
}



#------------------------------------------------------------------------------
# Upraví názvy sloupců, aby neobsahovaly mezery, pomlčky ani tečky.
#------------------------------------------------------------------------------
sub upravit_nazvy_sloupcu
{
    my $nazvy = shift; # array ref
    my $uprnazvy = shift; # hash ref
    foreach my $nazev (@{$nazvy})
    {
        $uprnazvy->{$nazev} = $nazev;
        $uprnazvy->{$nazev} =~ s/[-.\s]+/_/g;
    }
}



#------------------------------------------------------------------------------
# Provede vlastní INSERT jednoho nebo několika řádků do tabulky. Počet řádků
# může být teoreticky libovolný a záleží na volající funkci, kolik řádků si
# nasbírá před tím, než zavolá flush().
#------------------------------------------------------------------------------
sub flush
{
    my $databaze = shift;
    my $tbl = shift; # název tabulky v databázi, kam se mají data importovat.
    my $seznam_poli = shift;
    # Každý řádek v poli už musí být serializovaný jako řetězec (ale zatím bez závorek kolem).
    my @radky = @_;
    my $n_novych_radku = scalar(@radky);
    my $seznamy_hodnot = join(', ', map {"($_)"} (@radky));
    my $dotaz = "INSERT INTO $tbl ($seznam_poli) VALUES $seznamy_hodnot;";
    # Dotaz může být tak dlouhý, že vytlačí ostatní zprávy z obrazovky. Zároveň
    # ovšem právě provádění dotazu může poskytnout dost dlouhou pauzu, abychom
    # průběžné informace přečetli. Proto vypíšeme nejdříve dotaz a pak průběžné
    # statistiky, přestože ty se týkají stavu po provedení předcházejícího dotazu.
    print STDERR ("$dotaz\n");
    my $ted = vypsat_statistiku(); # vypíše stat na STDERR a navíc nám řekne, kolik zjistil, že je hodin
    print STDERR ("$ted->{rmdhms} spouštím výše uvedený dotaz.\n");
    $databaze->do($dotaz) or die("Nelze spustit dotaz.\n");
    $global_n_insertu++;
    $global_n_radku += $n_novych_radku;
}



#------------------------------------------------------------------------------
# Vypíše statistiku, kolik jsme toho udělali a jak dlouho to trvalo.
#------------------------------------------------------------------------------
sub vypsat_statistiku
{
    my $ted = cas::ted();
    my $esek_start = $global_cas_start->{esek};
    my $esek_ted = $ted->{esek};
    my $n_sek_beh = $esek_ted - $esek_start;
    print STDERR ("Dosud běh $n_sek_beh sekund; zpracováno $global_n_insertu insertů a $global_n_radku řádků.\n");
    if($n_sek_beh > 0 && $global_n_insertu > 0 && $global_n_radku > 0)
    {
        my $prumer_sekund_na_insert = $n_sek_beh / $global_n_insertu;
        my $prumer_insertu_za_sekundu = $global_n_insertu / $n_sek_beh;
        my $prumer_sekund_na_radek = $n_sek_beh / $global_n_radku;
        my $prumer_radku_za_sekundu = $global_n_radku / $n_sek_beh;
        printf STDERR ("Dosud průměr 1 insert / %f s (%f insertů / s).\n", $prumer_sekund_na_insert, $prumer_insertu_za_sekundu);
        printf STDERR ("Dosud průměr 1 řádek  / %f s (%f řádků / s).\n", $prumer_sekund_na_radek, $prumer_radku_za_sekundu);
    }
    # Aktuální čas se může hodit i volajícímu, tak mu ho dáme, když už ho máme.
    return $ted;
}
