#!/usr/bin/env perl
use warnings;
use strict;
use 5.10.1;
use open qw(:utf8);
no warnings 'utf8';

our $VERSION = '0.018';

use Encode                qw( encode decode );
use File::Basename        qw( basename );
use File::Spec::Functions qw( catfile catdir );
use FindBin               qw( $RealBin $RealScript );
use Getopt::Long          qw( GetOptions );
use List::Util            qw( sum );
use Pod::Usage            qw( pod2usage );
use Scalar::Util          qw( looks_like_number );

use Clone               qw( clone );
use Encode::Locale      qw( decode_argv );
use JSON::XS            qw();
use File::HomeDir       qw( my_home );
use List::MoreUtils     qw( any first_index pairwise );
use Term::Choose        qw( choose );
use Term::TablePrint    qw( print_table );
use Term::ReadKey       qw( ReadKey ReadMode GetTerminalSize );
use Text::LineFold      qw();
use Unicode::GCString   qw();

# See at the end:
# App::DBBrowser::DB;

binmode STDIN,  ':encoding(console_in)';
binmode STDOUT, ':encoding(console_out)';
binmode STDERR, ':encoding(console_out)';

BEGIN {
    decode_argv(); # not at the end of the BEGIN block if perl < 5.16.0
    if ( $^O eq 'MSWin32' ) {
        require Term::Size::Win32;
        Term::Size::Win32::->import( 'chars' );
        require Win32::Console::ANSI;
    }
}

sub term_size {
    if ( $^O eq 'MSWin32' ) {
        my ( $width, $heigth ) = chars();
        return $width - 1, $heigth;
    }
    else {
        return GetTerminalSize;
    }
}


END { ReadMode 0 }

use constant {
    BSPACE         => 0x7f,
    CLEAR_SCREEN   => "\e[1;1H\e[0J",
};


my $home = decode( 'locale', File::HomeDir->my_home() );
if ( ! $home ) {
    say "'File::HomeDir->my_home()' could not find the home directory!";
    say "A home directory is needed to be able to use 'db-browser'";
    exit;
}
my $my_data = decode( 'locale', File::HomeDir->my_data() );
my $app_dir = $my_data ? catdir( $my_data, 'db_browser_conf' ) : catdir( $home, '.db_browser_conf' );
mkdir $app_dir or die $! if ! -d $app_dir;


my $info = {
    home              => $home,
    app_dir           => $app_dir,
    config_file       => catfile( $app_dir, 'config.json' ),
    db_cache_file     => catfile( $app_dir, 'cache_db_search.json' ),
    lyt_1             => {                      layout => 1, order => 0, justify => 2, clear_screen => 1, undef => '<<'     },
    lyt_stmt_h        => { prompt => 'Choose:', layout => 1, order => 0, justify => 2, clear_screen => 0, undef => '<<'     },
    lyt_3             => {                      layout => 3,             justify => 0, clear_screen => 1, undef => '  BACK' },
    lyt_stmt_v        => { prompt => 'Choose:', layout => 3,             justify => 0, clear_screen => 0, undef => '  BACK' },
    lyt_stop          => {                                                             clear_screen => 0                    },
    back              => 'BACK',
    confirm           => 'CONFIRM',
    ok                => '- OK -',
    _exit             => '  EXIT',
    _help             => '  HELP',
    _back             => '  BACK',
    _confirm          => '  CONFIRM',
    _continue         => '  CONTINUE',
    _info             => '  INFO',
    _reset            => '  RESET',
    yes_no            => [ 'YES', 'NO' ],
    line_fold         => { Charset=> 'utf8', OutputCharset => '_UNICODE_', Urgent => 'FORCE' },
    stmt_init_tab     => 4,
    tbl_info_width    => 140,
    avail_aggregate   => [ "AVG(X)", "COUNT(X)", "COUNT(*)", "MAX(X)", "MIN(X)", "SUM(X)" ],
    cached            => '',
    avail_operators   => [ "REGEXP", "NOT REGEXP", "LIKE", "NOT LIKE", "IS NULL", "IS NOT NULL", "IN", "NOT IN",
                           "BETWEEN", "NOT BETWEEN", " = ", " != ", " <> ", " < ", " > ", " >= ", " <= ", "LIKE col",
                           "NOT LIKE col", "LIKE %col%", "NOT LIKE %col%", " = col", " != col", " <> col", " < col",
                           " > col", " >= col", " <= col" ],
                           # "LIKE col%", "NOT LIKE col%", "LIKE %col", "NOT LIKE %col",
    avail_db_drivers  => [ 'SQLite', 'mysql', 'Pg' ],
    hidd_func_pr      => { Epoch_to_Date => 'DATE', Truncate => 'TRUNC', Epoch_to_DateTime => 'DATETIME',
                           Bit_Length => 'BIT_LENGTH', Char_Length => 'CHAR_LENGTH' },
    keys_hidd_func_pr => [ qw( Epoch_to_Date Bit_Length Truncate Char_Length Epoch_to_DateTime ) ],
};

my $defaults = {
    db_defaults          => 'Dummy',
    db_drivers           => [ 'SQLite', 'mysql', 'Pg' ],
    db_login_once        => 0,
    _env_dbi            => 1,
    env_dbi_user         => 0,
    env_dbi_pass         => 0,
    _expand              => 'Dummy',
    keep_db_choice       => 0,
    keep_schema_choice   => 0,
    keep_table_choice    => 0,
    table_expand         => 1,
    lock_stmt            => 0,
    max_rows             => 50_000,
    metadata             => 0,
    mouse                => 0,
    min_col_width        => 30,
    operators            => [ "REGEXP", " = ", " != ", " < ", " > ", "IS NULL", "IS NOT NULL" ],
    _parentheses         => 'Dummy',
    w_parentheses        => 0,
    h_parentheses        => 0,
    progress_bar         => 20_000,
    regexp_case          => 0,
    sssc_mode            => 0,
    tab_width            => 2,
    undef                => '',
    binary_string        => 'BNRY',
    thsd_sep             => ',',
    header_row           => 1,
    choose_columns       => 0,
    db_browser_mode      => 1,
    SQLite => {
        _reset_cache_cmdline_only  => 0,
        sqlite_unicode             => 1,
        sqlite_see_if_its_a_number => 1,
        _binary_filter             => 0,
    },
    mysql => {
        host                => '',
        port                => '',
        mysql_enable_utf8   => 1,
        _binary_filter      => 0,
    },
    Pg => {
        host                => '',
        port                => '',
        pg_enable_utf8      => 1,
        _binary_filter      => 0,
    },
};

$info->{SQLite}{options} = [ qw( sqlite_unicode sqlite_see_if_its_a_number _binary_filter ) ];
$info->{mysql}{options}  = [ qw( mysql_enable_utf8 _binary_filter host port ) ];
$info->{Pg}{options}     = [ qw( pg_enable_utf8 _binary_filter host port ) ];

my $opt;

if ( ! eval {
    $opt = read_config_file( $defaults, $info->{config_file} );
    for my $lyt ( grep { /^lyt_/ } keys %$info ) {
        $info->{$lyt}{mouse} = $opt->{mouse};
    }
    my $help;
    GetOptions (
        'h|?|help' => \$help,
        's|search' => \$opt->{SQLite}{_reset_cache_cmdline_only},
    );
    $opt = options( $info, $opt ) if $help;
    1 }
) {
    say 'Configfile/Options:';
    print_error_message( $@ );
    $opt = $defaults;
}

for my $lyt ( grep { /^lyt_/ } keys %$info ) {
    $info->{$lyt}{mouse} = $opt->{mouse};
}
$info->{ok} = '<OK>' if $opt->{sssc_mode};
my $sqlite_search = 0;
$sqlite_search = 1 if $opt->{SQLite}{_reset_cache_cmdline_only};
$sqlite_search = 1 if @ARGV;


sub available_databases_cached {
    my ( $info, $dbh, $metadata, @dirs ) = @_;
    my $cache_key = $info->{db_driver} . '_' . join ' ', @dirs;
    $info->{cache} = read_json( $info->{db_cache_file} );
    if ( $opt->{$info->{db_driver}}{_reset_cache_cmdline_only} ) {
        delete $info->{cache}{$cache_key};
    }
    if ( $info->{cache}{$cache_key} ) {
        $info->{cached} = ' (cached)';
        return $info->{cache}{$cache_key};
    }
    my $new = App::DBBrowser::DB->new( $info );
    my $databases = $new->available_databases( $dbh, $metadata, @dirs );
    $info->{cache}{$cache_key} = $databases;
    write_json( $info->{db_cache_file}, $info->{cache} );
    return $databases;
}


sub get_username {
    my ( $info, $opt, $db ) = @_;
    my $db_driver = $info->{db_driver};
    return '' if $db_driver eq 'SQLite';
    return $ENV{DBI_USER} if $opt->{env_dbi_user};
    my ( $user );
    if ( $opt->{db_login_once} ) {
        $user = $info->{login}{$db_driver}{user};
    }
    else {
        $user = $info->{login}{$db_driver}{$db}{user};
    }
    # Readline
    if ( ! defined $user ) {
        $user = local_readline( $info, { prompt => 'User: ', status => "DB: $db" } );
    }

    if ( $opt->{db_login_once} ) {
        $info->{login}{$db_driver}{user} = $user;
    }
    else {
        $info->{login}{$db_driver}{$db}{user} = $user;
    }
    return $user;
}


sub get_password {
    my ( $info, $opt, $db, $user ) = @_;
    my $db_driver = $info->{db_driver};
    return '' if $db_driver eq 'SQLite';
    return $ENV{DBI_PASS} if $opt->{env_dbi_pass};
    my ( $passwd );
    if ( $opt->{db_login_once} ) {
        $passwd = $info->{login}{$db_driver}{passwd};
    }
    else {
        $passwd = $info->{login}{$db_driver}{$db}{passwd};
    }
    # Readline
    if ( ! defined $passwd ) {
        my $status = "DB: $db\nUser: $user";
        $passwd = local_readline( $info, { prompt => 'Password: ', status => $status, no_echo => 1 } );
    }
    if ( $opt->{db_login_once} ) {
        $info->{login}{$db_driver}{passwd} = $passwd;
    }
    else {
        $info->{login}{$db_driver}{$db}{passwd} = $passwd;
    }
    return $passwd;
}



DB_DRIVER: while ( 1 ) {

    if ( $sqlite_search ) {
        $info->{db_driver} = 'SQLite';
        $sqlite_search = 0;
    }
    else {
        if ( @{$opt->{db_drivers}} == 1 ) {
            $info->{one_db_driver} = 1;
            $info->{db_driver} = $opt->{db_drivers}[0];
        }
        else {
            $info->{one_db_driver} = 0;
            # Choose
            $info->{db_driver} = choose(
                [ undef, @{$opt->{db_drivers}} ],
                { %{$info->{lyt_1}}, prompt => 'Database Driver: ', undef => 'Quit' }
            );
            last DB_DRIVER if ! defined $info->{db_driver};
        }
    }

    $info->{cached} = '';
    my $db_driver = $info->{db_driver};
    my $metadata  = $opt->{metadata};
    my $databases = [];
    my $new = App::DBBrowser::DB->new( $info );
    if ( ! eval {
        my $db = $new->info_database();
        my $user   = get_username( $info, $opt, $db );
        my $passwd = get_password( $info, $opt, $db, $user );
        my $dbh = $new->get_db_handle( $opt, $db, $user, $passwd );
        if ( $db_driver eq 'SQLite' ) {
            my @dirs = @ARGV ? @ARGV : ( $info->{home} );
            $databases = available_databases_cached( $info, $dbh, $metadata, @dirs );
        }
        else {
            $databases = $new->available_databases( $dbh, $metadata );
        }
        1 }
    ) {
        say 'Available databases:';
        delete $info->{login}{$db_driver};
        print_error_message( $@ );
        next DB_DRIVER;
    }
    if ( ! @$databases ) {
        print_error_message( "no $db_driver-databases found\n" );
        next DB_DRIVER;
    }
    my $regexp_system;
    $regexp_system = $new->regexp_system( 'database' ) if $metadata;
    if ( defined $regexp_system ) {
        my $regexp = join( '|', @$regexp_system );
        my ( @data, @system );
        for my $database ( @{$databases} ) {
            if ( $database =~ /(?:$regexp)/ ) {
                push @system, $database;
            }
            else {
                push @data, $database;
            }
        }
        if ( $db_driver eq 'SQLite' ) {
            $databases = [ @data, @system ];
        }
        else {
            $databases = [ map( "- $_", @data ), map( "  $_", @system ) ];
        }
    }
    else {
        if ( $db_driver ne 'SQLite' ) {
            $databases = [ map{ "- $_" } @$databases ];
        }
    }

    my $data = {};
    my $old_idx_db = 0;
    my $new_db_settings = 0;
    my $db;

    DATABASE: while ( 1 ) {

        if ( $new_db_settings ) {
            $new_db_settings = 0;
            $data = {};
        }
        else {
            my $back = ( $db_driver eq 'SQLite' ? '' : ' ' x 2 ) . ( $info->{one_db_driver} ? 'Quit' : 'BACK' );
            my $prompt = 'Choose Database' . $info->{cached};
            my $choices = [ undef, @$databases ];
            # Choose
            my $idx_db = choose(
                $choices,
                { %{$info->{lyt_3}}, prompt => $prompt, index => 1, default => $old_idx_db, undef => $back }
            );
            $db = undef;
            $db = $choices->[$idx_db] if defined $idx_db;
            if ( ! defined $db ) {
                last DB_DRIVER if   $info->{one_db_driver};
                next DB_DRIVER if ! $info->{one_db_driver};
            }
            if ( $opt->{keep_db_choice} ) {
                if ( $old_idx_db == $idx_db ) {
                    $old_idx_db = 0;
                    next DATABASE;
                }
                else {
                    $old_idx_db = $idx_db;
                }
            }
            $db =~ s/^[-\ ]\s// if $db_driver ne 'SQLite';
            die "'$db': $!. Maybe the cached data is not up to date." if $db_driver eq 'SQLite' && ! -f $db;
        }

        my $dbh;
        if ( ! eval {
            my $user   = get_username( $info, $opt, $db );
            my $passwd = get_password( $info, $opt, $db, $user );
            $dbh = $new->get_db_handle( $opt, $db, $user, $passwd );
            $data->{$db}{schemas} = $new->get_schema_names( $dbh, $db, $metadata ) if ! defined $data->{$db}{schemas};
            1 }
        ) {
            say 'Get database handle and schema names:';
            delete $info->{login}{$db_driver}{$db};
            print_error_message( $@ );
            # remove database from @databases
            next DATABASE;
        }
        my $old_idx_sch = 0;

        SCHEMA: while ( 1 ) {

            my $schema;
            if ( @{$data->{$db}{schemas}} == 1 ) {
                $schema = $data->{$db}{schemas}[0];
            }
            elsif ( @{$data->{$db}{schemas}} > 1 ) {
                my $choices;
                my $idx_sch;
                my $regexp_system;
                $regexp_system = $new->regexp_system( 'schema' ) if $metadata;
                if ( defined $regexp_system ) {
                    my $regexp = join( '|', @$regexp_system );
                    my ( @data, @system );
                    for my $schema ( @{$data->{$db}{schemas}} ) {
                        if ( $schema =~ /(?:$regexp)/ ) {
                            push @system, $schema;
                        }
                        else {
                            push @data, $schema;
                        }
                    }
                    $choices = [ undef, map( "- $_", @data ), map( "  $_", @system ) ];
                }
                else {
                    $choices = [ undef, map( "- $_", @{$data->{$db}{schemas}} ) ];
                }
                my $prompt = 'DB "'. basename( $db ) . '" - choose Schema:';
                # Choose
                $idx_sch = choose(
                    $choices,
                    { %{$info->{lyt_3}}, prompt => $prompt, index => 1, default => $old_idx_sch }
                );
                $schema = $choices->[$idx_sch] if defined $idx_sch;
                next DATABASE if ! defined $schema;
                if ( $opt->{keep_schema_choice} ) {
                    if ( $old_idx_sch == $idx_sch ) {
                        $old_idx_sch = 0;
                        next SCHEMA;
                    }
                    else {
                        $old_idx_sch = $idx_sch;
                    }
                }
                $schema =~ s/^[-\ ]\s//;
            }

            if ( ! eval {
                if ( ! defined $data->{$db}{$schema}{tables} ) {
                    $data->{$db}{$schema}{tables} = $new->get_table_names( $dbh, $schema, $metadata );
                }
                1 }
            ) {
                say 'Get table names:';
                print_error_message( $@ );
                next DATABASE;
            }

            my $join       = '  Join';
            my $union      = '  Union';
            my $db_setting = '  Database settings';
            my @tables = ();
            my $regexp_system;
            $regexp_system = $new->regexp_system( 'table' ) if $metadata;
            if ( defined $regexp_system ) {
                my $regexp = join( '|', @$regexp_system );
                my ( @data, @system );
                for my $table ( @{$data->{$db}{$schema}{tables}} ) {
                    if ( $table =~ /(?:$regexp)/ ) {
                        push @system, $table;
                    }
                    else {
                        push @data, $table;
                    }
                }
                @tables = ( map( "- $_", @data ), map( "  $_", @system ) );
            }
            else {
                @tables = map { "- $_" } @{$data->{$db}{$schema}{tables}};
            }
            my $old_idx_tbl = 0;

            TABLE: while ( 1 ) {

                my $prompt = 'DB: "'. basename( $db );
                $prompt .= '.' . $schema if defined $data->{$db}{schemas} && @{$data->{$db}{schemas}} > 1;
                $prompt .= '"';
                my $choices = [ undef, @tables, $join, $union, $db_setting ];
                # Choose
                my $idx_tbl = choose(
                    $choices,
                    { %{$info->{lyt_3}}, prompt => $prompt, index => 1, default => $old_idx_tbl }
                );
                my $table;
                $table = $choices->[$idx_tbl] if defined $idx_tbl;
                if ( ! defined $table ) {
                    next SCHEMA if defined $data->{$db}{schemas} && @{$data->{$db}{schemas}} > 1;
                    next DATABASE;
                }
                if ( $opt->{keep_table_choice} ) {
                    if ( $old_idx_tbl == $idx_tbl ) {
                        $old_idx_tbl = 0;
                        next TABLE;
                    }
                    else {
                        $old_idx_tbl = $idx_tbl;
                    }
                }
                my $multi_table;
                if ( $table eq $db_setting ) {
                    if ( ! eval {
                        $new_db_settings = database_setting( $info, $opt, $db );
                        1 }
                    ) {
                        say 'Database settings:';
                        print_error_message( $@ );
                    }
                    next DATABASE if $new_db_settings;
                    next TABLE;
                }
                elsif ( $table eq $join ) {
                    if ( ! eval {
                        $multi_table = join_tables( $info, $dbh, $db, $schema, $data );
                        $table = 'joined_tables';
                        1 }
                    ) {
                        say 'Join tables:';
                        print_error_message( $@ );
                    }
                    next TABLE if ! defined $multi_table;
                }
                elsif ( $table eq $union ) {
                    if ( ! eval {
                        $multi_table = union_tables( $info, $dbh, $db, $schema, $data );
                        $table = 'union_tables';
                        1 }
                    ) {
                        say 'Union tables:';
                        print_error_message( $@ );
                    }
                    next TABLE if ! defined $multi_table;
                }
                else {
                    $table =~ s/^[-\ ]\s//;
                }
                if ( ! eval {
                    my $qt_columns = {};
                    my $pr_columns = [];
                    my $sql;
                    $sql->{strg_keys} = [ qw( distinct_stmt where_stmt group_by_stmt having_stmt order_by_stmt limit_stmt ) ];
                    $sql->{list_keys} = [ qw( chosen_cols aggr_cols where_args group_by_cols having_args limit_args hidd ) ];
                    reset_sql( $sql, $qt_columns );

                    $info->{lock} = $opt->{lock_stmt};

                    my $select_from_stmt = '';
                    if ( $multi_table ) {
                        $select_from_stmt = $multi_table->{quote}{stmt};
                        for my $col ( @{$multi_table->{pr_columns}} ) {
                            $qt_columns->{$col} = $multi_table->{qt_columns}{$col};
                            push @$pr_columns, $col;
                        }
                    }
                    else {
                        $select_from_stmt = "SELECT * FROM " . $dbh->quote_identifier( undef, $schema, $table );
                        my $sth = $dbh->prepare( $select_from_stmt . " WHERE 1 = 0" );
                        $sth->execute();
                        for my $col ( @{$sth->{NAME}} ) {
                            $qt_columns->{$col} = $dbh->quote_identifier( $col );
                            push @$pr_columns, $col;
                        }
                    }

                    PRINT_TABLE: while ( 1 ) {
                        my $all_arrayref = read_table( $info, $opt, $sql, $dbh, $table,
                                                       $select_from_stmt, $qt_columns, $pr_columns );
                        last PRINT_TABLE if ! defined $all_arrayref;
                        delete @{$info}{qw(width_head width_cols not_a_number)};
                        print_table( $all_arrayref, $opt );
                    }

                    1 }
                ) {
                    say 'Print table:';
                    print_error_message( $@ );
                }
            }
        }
        $dbh->disconnect();
    }
}


sub print_error_message {
    my ( $message ) = @_;
    utf8::decode( $message );
    print $message;
    choose(
        [ 'Press ENTER to continue' ],
        { %{$info->{lyt_stop}}, prompt => '' }
    );
}


sub union_tables {
    my ( $info, $dbh, $db, $schema, $data ) = @_;
    my $u = $data->{$db}{$schema};
    if ( ! defined $u->{col_names} || ! defined $u->{col_types} ) {
        my $new = App::DBBrowser::DB->new( $info );
        $data = $new->column_names_and_types( $dbh, $db, $schema, $data );
    }
    my $union = {
        unused_tables => [ map { "- $_" } @{$u->{tables}} ],
        used_tables   => [],
        used_cols     => {},
        saved_cols    => [],
    };

    UNION_TABLE: while ( 1 ) {
        my $enough_tables = '  Enough TABLES';
        print_union_statement( $info, $union );
        # Choose
        my $union_table = choose(
            [ undef, $enough_tables, map( "+ $_", @{$union->{used_tables}} ), @{$union->{unused_tables}}, $info->{_info} ],
            { %{$info->{lyt_stmt_v}}, prompt => 'Choose UNION table:' }
        );
        if ( ! defined $union_table ) {
            return;
        }
        elsif ( $union_table eq $info->{_info} ) {
            if ( ! defined $u->{tables_info} ) {
                $u->{tables_info} = get_tables_info( $info, $dbh, $db, $schema, $data );
            }
            my $tbls_info = print_tables_info( $info, $u );
            choose(
                $tbls_info,
                { %{$info->{lyt_3}}, prompt => '' }
            );
            next UNION_TABLE;
        }
        elsif ( $union_table eq $enough_tables ) {
            return if ! @{$union->{used_tables}};
            last UNION_TABLE;
        }
        my $backup_union = clone( $union );
        my $idx = first_index { $_ eq $union_table } @{$union->{unused_tables}};
        $union_table =~ s/^[-+]\s//;
        if ( $idx == -1 ) {
            delete $union->{used_cols}{$union_table};
        }
        else {
            splice( @{$union->{unused_tables}}, $idx, 1 );
            push @{$union->{used_tables}}, $union_table;
        }

        UNION_COLUMN: while ( 1 ) {
            my ( $all_cols, $privious_cols, $void ) = ( q['*'], q['^'], q[' '] );
            my @short_cuts = ( ( @{$union->{saved_cols}} ? $privious_cols : $void ), $all_cols );
            my $choices = [ $info->{ok}, @short_cuts, @{$u->{col_names}{$union_table}} ];
            unshift @$choices, undef if $opt->{sssc_mode};
            print_union_statement( $info, $union );
            # Choose
            my $col = choose(
                $choices,
                { %{$info->{lyt_stmt_h}}, prompt => 'Choose Column:' }
            );
            if ( ! defined $col ) {
                if ( defined $union->{used_cols}{$union_table} ) {
                    delete $union->{used_cols}{$union_table};
                    next UNION_COLUMN;
                }
                else {
                    $union = clone( $backup_union );
                    last UNION_COLUMN;
                }
            }
            if ( $col eq $info->{ok} ) {
                if ( ! defined $union->{used_cols}{$union_table} ) {
                    my $idx = first_index { $_ eq $union_table } @{$union->{used_tables}};
                    my $tbl = splice( @{$union->{used_tables}}, $idx, 1 );
                    push @{$union->{unused_tables}}, "- $tbl";
                }
                last UNION_COLUMN;
            }
            if ( $col eq $void ) {
                next UNION_COLUMN;
            }
            if ( $col eq $privious_cols ) {
                $union->{used_cols}{$union_table} = $union->{saved_cols};
                next UNION_COLUMN if $opt->{sssc_mode};
                last UNION_COLUMN;
            }
            if ( $col eq $all_cols ) {
                @{$union->{used_cols}{$union_table}} = @{$u->{col_names}{$union_table}};
                next UNION_COLUMN if $opt->{sssc_mode};
                last UNION_COLUMN;
            }
            else {
                push @{$union->{used_cols}{$union_table}}, $col;
            }
        }
        $union->{saved_cols} = $union->{used_cols}{$union_table} if defined $union->{used_cols}{$union_table};
    }
    # column names in the result-set of a UNION are taken from the first query.
    my $first_table = $union->{used_tables}[0];
    $union->{pr_columns} = $union->{used_cols}{$first_table};
    for my $col ( @{$union->{pr_columns}} ) {
        $union->{qt_columns}{$col} = $dbh->quote_identifier( $col );
    }
    $union->{quote}{stmt} = "SELECT * FROM (";
    my $c;
    for my $table ( @{$union->{used_tables}} ) {
        $c++;
        $union->{quote}{stmt} .= " SELECT ";
        $union->{quote}{stmt} .= join( ', ', map { $dbh->quote_identifier( $_ ) } @{$union->{used_cols}{$table}} );
        $union->{quote}{stmt} .= " FROM " . $dbh->quote_identifier( undef, $schema, $table );
        $union->{quote}{stmt} .= $c < @{$union->{used_tables}} ? " UNION ALL " : " )";
    }
    $union->{quote}{stmt} .= " AS " . join '_', @{$union->{used_tables}};
    return $union;
}


sub print_union_statement {
    my ( $info, $union ) = @_;
    my $str = "SELECT * FROM (\n";
    my $c = 0;
    for my $table ( @{$union->{used_tables}} ) {
        ++$c;
        $str .= "  SELECT ";
        $str .= @{$union->{used_cols}{$table}//[]} ? join( ', ', @{$union->{used_cols}{$table}} ) : '?';
        $str .= " FROM $table";
        $str .= $c < @{$union->{used_tables}} ? " UNION ALL\n" : "\n";
    }
    if ( @{$union->{used_tables}} ) {
        $str .= ") AS " . join( '_', @{$union->{used_tables}} ) . " \n";
    }
    $str .= "\n";
    my $line_fold = Text::LineFold->new( %{$info->{line_fold}}, ColMax => ( term_size() )[0] - 2 );
    print CLEAR_SCREEN;
    print $line_fold->fold( '', ' ' x $info->{stmt_init_tab}, $str );
}


sub get_tables_info {
    my ( $info, $dbh, $db, $schema, $data ) = @_;
    my $tables_info = {};
    my $sth;
    my $new = App::DBBrowser::DB->new( $info );
    my ( $pk, $fk ) = $new->primary_and_foreign_keys( $dbh, $db, $schema, $data );
    for my $table ( @{$data->{$db}{$schema}{tables}} ) {
        push @{$tables_info->{$table}}, [ 'Table: ', '== ' . $table . ' ==' ];
        push @{$tables_info->{$table}}, [
            'Columns: ',
            join( ' | ',
                pairwise { no warnings q(once); lc( $a ) . ' ' . $b }
                    @{$data->{$db}{$schema}{col_types}{$table}},
                    @{$data->{$db}{$schema}{col_names}{$table}}
            )
        ];
        if ( @{$pk->{$table}} ) {
            push @{$tables_info->{$table}}, [ 'PK: ', 'primary key (' . join( ',', @{$pk->{$table}} ) . ')' ];
        }
        for my $fk_name ( sort keys %{$fk->{$table}} ) {
            if ( $fk->{$table}{$fk_name} ) {
                push @{$tables_info->{$table}}, [
                    'FK: ',
                    'foreign key (' . join( ',', @{$fk->{$table}{$fk_name}{foreign_key_col}} ) .
                    ') references ' . $fk->{$table}{$fk_name}{reference_table} .
                    '(' . join( ',', @{$fk->{$table}{$fk_name}{reference_key_col}} ) .')'
                ];
            }
        }
    }
    return $tables_info;
}


sub print_tables_info {
    my ( $info, $ref ) = @_;
    my $len_key = 10;
    my $col_max = ( term_size() )[0] - 1;
    my $line_fold = Text::LineFold->new( %{$info->{line_fold}} );
    $line_fold->config( 'ColMax', $col_max > $info->{tbl_info_width} ? $info->{tbl_info_width} : $col_max );
    my $ch_info = [ 'Close with ENTER' ];
    for my $table ( @{$ref->{tables}} ) {
        push @{$ch_info}, " ";
        for my $line ( @{$ref->{tables_info}{$table}} ) {
            my $text = sprintf "%*s%s", $len_key, @$line;
            $text = $line_fold->fold( '' , ' ' x $len_key, $text );
            push @{$ch_info}, split /\R+/, $text;
        }
    }
    return $ch_info;
}


sub join_tables {
    my ( $info, $dbh, $db, $schema, $data ) = @_;
    my $join = {};
    $join->{quote}{stmt} = "SELECT * FROM";
    $join->{print}{stmt} = "SELECT * FROM";
    my $j = $data->{$db}{$schema};
    if ( ! defined $j->{col_names} || ! defined $j->{col_types} ) {
        my $new = App::DBBrowser::DB->new( $info );
        $data = $new->column_names_and_types( $dbh, $db, $schema, $data );
    }
    my @tables = map { "- $_" } @{$j->{tables}};

    MASTER: while ( 1 ) {
        print_join_statement( $info, $join->{print}{stmt} );
        # Choose
        my $master = choose(
            [ undef, @tables, $info->{_info} ],
            { %{$info->{lyt_stmt_v}}, prompt => 'Choose MASTER table:' }
        );
        if ( ! defined $master ) {
            return;
        }
        if ( $master eq $info->{_info} ) {
            if ( ! defined $j->{tables_info} ) {
                $j->{tables_info} = get_tables_info( $info, $dbh, $db, $schema, $data );
            }
            my $tbls_info = print_tables_info( $info, $j );
            # Choose
            choose(
                $tbls_info,
                { %{$info->{lyt_3}}, prompt => '' }
            );
            next MASTER;
        }
        my $idx = first_index { $_ eq $master } @tables;
        splice( @tables, $idx, 1 );
        $master =~ s/^-\s//;
        $join->{used_tables}  = [ $master ];
        $join->{avail_tables} = [ @tables ];
        $join->{quote}{stmt}  = "SELECT * FROM " . $dbh->quote_identifier( undef, $schema, $master );
        $join->{print}{stmt}  = "SELECT * FROM " .                                         $master  ;
        $join->{primary_keys} = [];
        $join->{foreign_keys} = [];
        my $backup_master = clone( $join );

        JOIN: while ( 1 ) {
            my $enough_slaves = '  Enough TABLES';
            my $slave;
            my $backup_join = clone( $join );

            SLAVE: while ( 1 ) {
                print_join_statement( $info, $join->{print}{stmt} );
                # Choose
                $slave = choose(
                    [ undef, $enough_slaves, @{$join->{avail_tables}}, $info->{_info} ],
                    { %{$info->{lyt_stmt_v}}, prompt => 'Add a SLAVE table:', undef => $info->{_reset} }
                );
                if ( ! defined $slave ) {
                    if ( @{$join->{used_tables}} == 1 ) {
                        @tables = map { "- $_" } @{$j->{tables}};
                        $join->{quote}{stmt} = "SELECT * FROM";
                        $join->{print}{stmt} = "SELECT * FROM";
                        next MASTER;
                    }
                    else {
                        $join = clone( $backup_master );
                        next JOIN;
                    }
                }
                elsif ( $slave eq $enough_slaves ) {
                    last JOIN;
                }
                elsif ( $slave eq $info->{_info} ) {
                    if ( ! defined $j->{tables_info} ) {
                        $j->{tables_info} = get_tables_info( $info, $dbh, $db, $schema, $data );
                    }
                    my $tbls_info = print_tables_info( $info, $j );
                    # Choose
                    choose(
                        $tbls_info,
                        { %{$info->{lyt_3}}, prompt => '' }
                    );
                    next SLAVE;
                }
                else {
                    last SLAVE;
                }
            }
            my $idx = first_index { $_ eq $slave } @{$join->{avail_tables}};
            splice( @{$join->{avail_tables}}, $idx, 1 );
            $slave =~ s/^-\s//;
            $join->{quote}{stmt} .= " LEFT OUTER JOIN " . $dbh->quote_identifier( undef, $schema, $slave ) . " ON";
            $join->{print}{stmt} .= " LEFT OUTER JOIN " .                                         $slave   . " ON";
            my %avail_pk_cols = ();
            for my $used_table ( @{$join->{used_tables}} ) {
                for my $col ( @{$j->{col_names}{$used_table}} ) {
                    $avail_pk_cols{ $used_table . '.' . $col } = $dbh->quote_identifier( undef, $used_table, $col );
                }
            }
            my %avail_fk_cols = ();
            for my $col ( @{$j->{col_names}{$slave}} ) {
                $avail_fk_cols{ $slave . '.' . $col } = $dbh->quote_identifier( undef, $slave, $col );
            }
            my $AND = '';

            ON: while ( 1 ) {
                print_join_statement( $info, $join->{print}{stmt} );
                # Choose
                my $pk_col = choose(
                    [ undef, $info->{_continue}, map( "- $_", sort keys %avail_pk_cols ) ],
                    { %{$info->{lyt_stmt_v}}, prompt => 'Choose PRIMARY KEY column:', undef => $info->{_reset} }
                );
                if ( ! defined $pk_col ) {
                    $join = clone( $backup_join );
                    next JOIN;
                }
                if ( $pk_col eq $info->{_continue} ) {
                    if ( @{$join->{primary_keys}} == @{$backup_join->{primary_keys}} ) {
                        $join = clone( $backup_join );
                        next JOIN;
                    }
                    last ON;
                }
                $pk_col =~ s/^-\s//;
                push @{$join->{primary_keys}}, $avail_pk_cols{$pk_col};
                $join->{quote}{stmt} .= $AND;
                $join->{print}{stmt} .= $AND;
                $join->{quote}{stmt} .= ' ' . $avail_pk_cols{$pk_col} . " =";
                $join->{print}{stmt} .= ' ' .                $pk_col  . " =";
                print_join_statement( $info, $join->{print}{stmt} );
                # Choose
                my $fk_col = choose(
                    [ undef, map( "- $_", sort keys %avail_fk_cols ) ],
                    { %{$info->{lyt_stmt_v}}, prompt => 'Choose FOREIGN KEY column:', undef => $info->{_reset} }
                );
                if ( ! defined $fk_col ) {
                    $join = clone( $backup_join );
                    next JOIN;
                }
                $fk_col =~ s/^-\s//;
                push @{$join->{foreign_keys}}, $avail_fk_cols{$fk_col};
                $join->{quote}{stmt} .= ' ' . $avail_fk_cols{$fk_col};
                $join->{print}{stmt} .= ' ' .                $fk_col;
                $AND = " AND";
            }
            push @{$join->{used_tables}}, $slave;
        }
        last MASTER;
    }

    #my @not_unique_col;
    #my %seen;
    #for my $table (@{$join->{used_tables}} ) {
    #    for my $col ( @{$j->{col_names}{$table}} ) {
    #        $seen{$col}++;
    #        push @not_unique_col, $col if $seen{$col} == 2;
    #    }
    #}
    my $col_stmt = '';
    for my $table ( @{$join->{used_tables}} ) {
        for my $col ( @{$j->{col_names}{$table}} ) {
            my $col_qt = $dbh->quote_identifier( undef, $table, $col );
            my $col_pr = $col;
            if ( any { $_ eq $col_qt } @{$join->{foreign_keys}} ) {
                next;
            }
            #if ( any { $_ eq $col_pr } @not_unique_col ) {
                $col_pr .= '_' . $table;
                $col_qt .= " AS " . $col_pr;
            #}
            push @{$join->{pr_columns}}, $col_pr;
            $join->{qt_columns}{$col_pr} = $col_qt;
            $col_stmt .= ', ' . $col_qt;
        }
    }
    $col_stmt =~ s/^,\s//;
    $join->{quote}{stmt} =~ s/\s\*\s/ $col_stmt /;
    return $join;
}


sub print_join_statement {
    my ( $info, $join_stmt_pr ) = @_;
    $join_stmt_pr =~ s/(?=\sLEFT\sOUTER\sJOIN)/\n\ /g;
    $join_stmt_pr .= "\n\n";
    my $line_fold = Text::LineFold->new( %{$info->{line_fold}}, ColMax => ( term_size() )[0] - 2 );
    print CLEAR_SCREEN;
    print $line_fold->fold( '', ' ' x $info->{stmt_init_tab}, $join_stmt_pr );
}


sub print_select_statement {
    my ( $info, $sql, $table, $status ) = @_;
    my $cols_sql;
    if ( $sql->{select_type} eq '*' ) {
        $cols_sql = ' *';
    }
    elsif ( $sql->{select_type} eq 'chosen_cols' ) {
        $cols_sql = ' ' . join( ', ', @{$sql->{print}{chosen_cols}} );
    }
    elsif ( @{$sql->{print}{aggr_cols}} || @{$sql->{print}{group_by_cols}} ) {
        $cols_sql = ' ' . join( ', ', @{$sql->{print}{group_by_cols}}, @{$sql->{print}{aggr_cols}} );
    }
    else {
        $cols_sql = ' *';
    }
    my $str = "SELECT";
    $str .= $sql->{print}{distinct_stmt} if $sql->{print}{distinct_stmt};
    $str .= $cols_sql . "\n";
    $str .= "  FROM $table" . "\n";
    $str .= ' ' . $sql->{print}{where_stmt}    . "\n" if $sql->{print}{where_stmt};
    $str .= ' ' . $sql->{print}{group_by_stmt} . "\n" if $sql->{print}{group_by_stmt};
    $str .= ' ' . $sql->{print}{having_stmt}   . "\n" if $sql->{print}{having_stmt};
    $str .= ' ' . $sql->{print}{order_by_stmt} . "\n" if $sql->{print}{order_by_stmt};
    $str .= ' ' . $sql->{print}{limit_stmt}    . "\n" if $sql->{print}{limit_stmt};
    $str .= "\n";
    my $line_fold = Text::LineFold->new( %{$info->{line_fold}}, ColMax => ( term_size() )[0] - 2 );
    return $line_fold->fold( '', ' ' x $info->{stmt_init_tab}, $str ) if $status;
    print CLEAR_SCREEN;
    print $line_fold->fold( '', ' ' x $info->{stmt_init_tab}, $str );
}


sub reset_sql {
    my ( $sql, $qt_columns ) = @_;
    $sql->{select_type} = '*';
    @{$sql->{print}}{ @{$sql->{strg_keys}} } = ( '' ) x  @{$sql->{strg_keys}};
    @{$sql->{quote}}{ @{$sql->{strg_keys}} } = ( '' ) x  @{$sql->{strg_keys}};
    @{$sql->{print}}{ @{$sql->{list_keys}} } = map{ [] } @{$sql->{list_keys}};
    @{$sql->{quote}}{ @{$sql->{list_keys}} } = map{ [] } @{$sql->{list_keys}};
    delete $sql->{backup_in_hidd};
}


sub read_table {
    my ( $info, $opt, $sql, $dbh, $table, $select_from_stmt, $qt_columns, $pr_columns ) = @_;
    my %lyt_stmt_h = %{$info->{lyt_stmt_h}};
    my @keys = ( qw( print_table columns aggregate distinct where group_by having order_by limit lock ) );
    my $lk = [ '  Lk0', '  Lk1' ];
    my %customize = (
        hidden          => 'Customize:',
        print_table     => 'Print TABLE',
        columns         => '- COLUMNS',
        aggregate       => '- AGGREGATE',
        distinct        => '- DISTINCT',
        where           => '- WHERE',
        group_by        => '- GROUP BY',
        having          => '- HAVING',
        order_by        => '- ORDER BY',
        limit           => '- LIMIT',
        lock            => $lk->[$info->{lock}],
    );
    my ( $DISTINCT, $ALL, $ASC, $DESC, $AND, $OR ) = ( "DISTINCT", "ALL", "ASC", "DESC", "AND", "OR" );
    if ( $info->{lock} == 0 ) {
        reset_sql( $sql, $qt_columns );
    }

    CUSTOMIZE: while ( 1 ) {
        my $backup_sql = clone( $sql );
        print_select_statement( $info, $sql, $table );
        # Choose
        my $custom = choose(
            [ $customize{hidden}, undef, @customize{@keys} ],
            { %{$info->{lyt_stmt_v}}, prompt => '', default => 1, undef => $info->{back} }
        );
        if ( ! defined $custom ) {
            last CUSTOMIZE;
        }
        elsif ( $custom eq $customize{'lock'} ) {
            if ( $info->{lock} == 1 ) {
                $info->{lock} = 0;
                $customize{lock} = $lk->[0];
                reset_sql( $sql, $qt_columns );
            }
            elsif ( $info->{lock} == 0 )   {
                $info->{lock} = 1;
                $customize{lock} = $lk->[1];
            }
        }
        elsif( $custom eq $customize{'columns'} ) {
            if ( ! ( $sql->{select_type} eq '*' || $sql->{select_type} eq 'chosen_cols' ) ) {
                reset_sql( $sql, $qt_columns );
            }
            my @cols = ( @$pr_columns );
            $sql->{quote}{chosen_cols} = [];
            $sql->{print}{chosen_cols} = [];
            $sql->{select_type} = 'chosen_cols';

            COLUMNS: while ( 1 ) {
                my $choices = [ $info->{ok}, @cols ];
                unshift @$choices, undef if $opt->{sssc_mode};
                print_select_statement( $info, $sql, $table );
                # Choose
                my $print_col = choose(
                    $choices,
                    { %lyt_stmt_h }
                );
                if ( ! defined $print_col ) {
                    if ( @{$sql->{quote}{chosen_cols}} ) {
                        $sql->{quote}{chosen_cols} = [];
                        $sql->{print}{chosen_cols} = [];
                        delete $sql->{backup_in_hidd}{chosen_cols};
                        next COLUMNS;
                    }
                    else {
                        $sql = clone( $backup_sql );
                        last COLUMNS;
                    }
                }
                if ( $print_col eq $info->{ok} ) {
                    if ( ! @{$sql->{quote}{chosen_cols}} ) {
                        $sql->{select_type} = '*';
                    }
                    delete $sql->{backup_in_hidd}{chosen_cols};
                    $sql->{print}{hidd} = [];
                    last COLUMNS;
                }
                push @{$sql->{quote}{chosen_cols}}, $qt_columns->{$print_col};
                push @{$sql->{print}{chosen_cols}}, $print_col;
            }
        }
        elsif( $custom eq $customize{'distinct'} ) {
            $sql->{quote}{distinct_stmt} = '';
            $sql->{print}{distinct_stmt} = '';

            DISTINCT: while ( 1 ) {
                my $choices = [ $info->{ok}, $DISTINCT, $ALL ];
                unshift @$choices, undef if $opt->{sssc_mode};
                print_select_statement( $info, $sql, $table );
                # Choose
                my $select_distinct = choose(
                    $choices,
                    { %lyt_stmt_h }
                );
                if ( ! defined $select_distinct ) {
                    if ( $sql->{quote}{distinct_stmt} ) {
                        $sql->{quote}{distinct_stmt} = '';
                        $sql->{print}{distinct_stmt} = '';
                        next DISTINCT;
                    }
                    else {
                        $sql = clone( $backup_sql );
                        last DISTINCT;
                    }
                }
                if ( $select_distinct eq $info->{ok} ) {
                    last DISTINCT;
                }
                $sql->{quote}{distinct_stmt} = ' ' . $select_distinct;
                $sql->{print}{distinct_stmt} = ' ' . $select_distinct;
            }
        }
        elsif( $custom eq $customize{'aggregate'} ) {
            if ( $sql->{select_type} eq '*' || $sql->{select_type} eq 'chosen_cols' ) {
                reset_sql( $sql, $qt_columns );
            }
            my @cols = ( @$pr_columns );
            $sql->{quote}{aggr_cols} = [];
            $sql->{print}{aggr_cols} = [];
            $sql->{select_type} = 'aggr_cols';

            AGGREGATE: while ( 1 ) {
                my $choices = [ $info->{ok}, @{$info->{avail_aggregate}} ];
                unshift @$choices, undef if $opt->{sssc_mode};
                print_select_statement( $info, $sql, $table );
                # Choose
                my $aggr = choose(
                    $choices,
                    { %lyt_stmt_h }
                );
                if ( ! defined $aggr ) {
                    if ( @{$sql->{quote}{aggr_cols}} ) {
                        $sql->{quote}{aggr_cols} = [];
                        $sql->{print}{aggr_cols} = [];
                        delete $sql->{backup_in_hidd}{aggr_cols};
                        next AGGREGATE;
                    }
                    else {
                        $sql = clone( $backup_sql );
                        last AGGREGATE;
                    }
                }
                if ( $aggr eq $info->{ok} ) {
                    delete $sql->{backup_in_hidd}{aggr_cols};
                    if ( ! @{$sql->{quote}{aggr_cols}} && ! @{$sql->{quote}{group_by_cols}} ) {
                        $sql->{select_type} = '*';
                    }
                    last AGGREGATE;
                }
                my $i = @{$sql->{quote}{aggr_cols}};
                if ( $aggr eq 'COUNT(*)' ) {
                    $sql->{print}{aggr_cols}[$i] = $aggr;
                    $sql->{quote}{aggr_cols}[$i] = $aggr;
                }
                else {
                    $aggr =~ s/\(\S\)\z//;
                    $sql->{quote}{aggr_cols}[$i] = $aggr . "(";
                    $sql->{print}{aggr_cols}[$i] = $aggr . "(";
                    if ( $aggr eq 'COUNT' ) {
                        my $choices = [ $ALL, $DISTINCT ];
                        unshift @$choices, undef if $opt->{sssc_mode};
                        print_select_statement( $info, $sql, $table );
                        # Choose
                        my $all_or_distinct = choose(
                            $choices,
                            { %lyt_stmt_h }
                        );
                        if ( ! defined $all_or_distinct ) {
                            $sql->{quote}{aggr_cols} = [];
                            $sql->{print}{aggr_cols} = [];
                            next AGGREGATE;
                        }
                        if ( $all_or_distinct eq $DISTINCT ) {
                            $sql->{quote}{aggr_cols}[$i] .= $DISTINCT . ' ';
                            $sql->{print}{aggr_cols}[$i] .= $DISTINCT . ' ';
                        }
                    }
                    my $choices = [ @cols ];
                    unshift @$choices, undef if $opt->{sssc_mode};
                    print_select_statement( $info, $sql, $table );
                    # Choose
                    my $print_col = choose(
                        $choices,
                        { %lyt_stmt_h }
                    );
                    if ( ! defined $print_col ) {
                        $sql->{quote}{aggr_cols} = [];
                        $sql->{print}{aggr_cols} = [];
                        next AGGREGATE;
                    }
                    ( my $quote_col = $qt_columns->{$print_col} ) =~ s/\sAS\s\S+\z//;
                    $sql->{print}{aggr_cols}[$i] .= $print_col . ")";
                    $sql->{quote}{aggr_cols}[$i] .= $quote_col . ")";
                }
                $sql->{print}{aggr_cols}[$i] = unambiguous_key( $sql->{print}{aggr_cols}[$i], $pr_columns );
                $sql->{quote}{aggr_cols}[$i] .= " AS " . $dbh->quote_identifier( $sql->{print}{aggr_cols}[$i] );
                my $print_aggr = $sql->{print}{aggr_cols}[$i];
                my $quote_aggr = $sql->{quote}{aggr_cols}[$i];
                ( $qt_columns->{$print_aggr} = $quote_aggr ) =~ s/\sAS\s\S+//;
            }
        }
        elsif ( $custom eq $customize{'where'} ) {
            my @cols = ( @$pr_columns, @{$sql->{print}{hidd}} );
            my $AND_OR = ' ';
            $sql->{quote}{where_args} = [];
            $sql->{quote}{where_stmt} = " WHERE";
            $sql->{print}{where_stmt} = " WHERE";
            my $unclosed = 0;
            my $count = 0;

            WHERE: while ( 1 ) {
                my $choices = [ $info->{ok}, @cols ];
                if ( $opt->{w_parentheses} ) {
                    push @$choices, $unclosed ? ')' : '(';
                }
                unshift @$choices, undef if $opt->{sssc_mode};
                print_select_statement( $info, $sql, $table );
                # Choose
                my $print_col = choose(
                    $choices,
                    { %lyt_stmt_h }
                );
                if ( ! defined $print_col ) {
                    if ( $sql->{quote}{where_stmt} ne " WHERE" ) {
                        $sql->{quote}{where_args} = [];
                        $sql->{quote}{where_stmt} = " WHERE";
                        $sql->{print}{where_stmt} = " WHERE";
                        $count = 0;
                        $AND_OR = ' ';
                        next WHERE;
                    }
                    else {
                        $sql = clone( $backup_sql );
                        last WHERE;
                    }
                }
                if ( $print_col eq $info->{ok} ) {
                    if ( $count == 0 ) {
                        $sql->{quote}{where_stmt} = '';
                        $sql->{print}{where_stmt} = '';
                    }
                    last WHERE;
                }
                if ( $print_col eq ')' ) {
                    $sql->{quote}{where_stmt} .= ")";
                    $sql->{print}{where_stmt} .= ")";
                    $unclosed--;
                    next WHERE;
                }
                if ( $count > 0 && $sql->{quote}{where_stmt} !~ /\(\z/ ) {
                    my $choices = [ $AND, $OR ];
                    unshift @$choices, undef if $opt->{sssc_mode};
                    print_select_statement( $info, $sql, $table );
                    # Choose
                    $AND_OR = choose(
                        $choices,
                        { %lyt_stmt_h }
                    );
                    if ( ! defined $AND_OR ) {
                        $sql->{quote}{where_args} = [];
                        $sql->{quote}{where_stmt} = " WHERE";
                        $sql->{print}{where_stmt} = " WHERE";
                        $count = 0;
                        $AND_OR = ' ';
                        next WHERE;
                    }
                    $AND_OR = ' ' . $AND_OR . ' ';
                }
                if ( $print_col eq '(' ) {
                    $sql->{quote}{where_stmt} .= $AND_OR . "(";
                    $sql->{print}{where_stmt} .= $AND_OR . "(";
                    $AND_OR = '';
                    $unclosed++;
                    next WHERE;
                }

                ( my $quote_col = $qt_columns->{$print_col} ) =~ s/\sAS\s\S+\z//;
                $sql->{quote}{where_stmt} .= $AND_OR . $quote_col;
                $sql->{print}{where_stmt} .= $AND_OR . $print_col;
                set_operator_sql( $info, $opt, $sql, 'where', $table, \@cols, $qt_columns, $quote_col );
                if ( ! $sql->{quote}{where_stmt} ) {
                    $sql->{quote}{where_args} = [];
                    $sql->{quote}{where_stmt} = " WHERE";
                    $sql->{print}{where_stmt} = " WHERE";
                    $count = 0;
                    $AND_OR = ' ';
                    next WHERE;
                }
                $count++;
            }
        }
        elsif( $custom eq $customize{'group_by'} ) {
            if ( $sql->{select_type} eq '*' || $sql->{select_type} eq 'chosen_cols' ) {
                reset_sql( $sql, $qt_columns );
            }
            my @cols = ( @$pr_columns );
            my $col_sep = ' ';
            $sql->{quote}{group_by_stmt} = " GROUP BY";
            $sql->{print}{group_by_stmt} = " GROUP BY";
            $sql->{quote}{group_by_cols} = [];
            $sql->{print}{group_by_cols} = [];
            $sql->{select_type} = 'group_by_cols';

            GROUP_BY: while ( 1 ) {
                my $choices = [ $info->{ok}, @cols ];
                unshift @$choices, undef if $opt->{sssc_mode};
                print_select_statement( $info, $sql, $table );
                # Choose
                my $print_col = choose(
                    $choices,
                    { %lyt_stmt_h }
                );
                if ( ! defined $print_col ) {
                    if ( @{$sql->{quote}{group_by_cols}} ) {
                        $sql->{quote}{group_by_stmt} = " GROUP BY";
                        $sql->{print}{group_by_stmt} = " GROUP BY";
                        $sql->{quote}{group_by_cols} = [];
                        $sql->{print}{group_by_cols} = [];
                        $col_sep = ' ';
                        delete $sql->{backup_in_hidd}{group_by_cols};
                        next GROUP_BY;
                    }
                    else {
                        $sql = clone( $backup_sql );
                        last GROUP_BY;
                    }
                }
                if ( $print_col eq $info->{ok} ) {
                    if ( $col_sep eq ' ' ) {
                        $sql->{quote}{group_by_stmt} = '';
                        $sql->{print}{group_by_stmt} = '';
                        if ( ! @{$sql->{quote}{aggr_cols}} ) {
                            $sql->{select_type} = '*';
                        }
                        delete $sql->{backup_in_hidd}{group_by_cols};
                    }
                    last GROUP_BY;
                }
                ( my $quote_col = $qt_columns->{$print_col} ) =~ s/\sAS\s\S+\z//;
                push @{$sql->{quote}{group_by_cols}}, $quote_col;
                push @{$sql->{print}{group_by_cols}}, $print_col;
                $sql->{quote}{group_by_stmt} .= $col_sep . $quote_col;
                $sql->{print}{group_by_stmt} .= $col_sep . $print_col;
                $col_sep = ', ';
            }
        }
        elsif( $custom eq $customize{'having'} ) {
            my @cols = ( @$pr_columns );
            my $AND_OR = ' ';
            $sql->{quote}{having_args} = [];
            $sql->{quote}{having_stmt} = " HAVING";
            $sql->{print}{having_stmt} = " HAVING";
            my $unclosed = 0;
            my $count = 0;

            HAVING: while ( 1 ) {
                my $choices = [ $info->{ok}, @{$info->{avail_aggregate}}, map( '@' . $_, @{$sql->{print}{aggr_cols}} ) ];
                if ( $opt->{h_parentheses} ) {
                    push @$choices, $unclosed ? ')' : '(';
                }
                unshift @$choices, undef if $opt->{sssc_mode};
                print_select_statement( $info, $sql, $table );
                # Choose
                my $aggr = choose(
                    $choices,
                    { %lyt_stmt_h }
                );
                if ( ! defined $aggr ) {
                    if ( $sql->{quote}{having_stmt} ne " HAVING" ) {
                        $sql->{quote}{having_args} = [];
                        $sql->{quote}{having_stmt} = " HAVING";
                        $sql->{print}{having_stmt} = " HAVING";
                        $count = 0;
                        $AND_OR = ' ';
                        next HAVING;
                    }
                    else {
                        $sql = clone( $backup_sql );
                        last HAVING;
                    }
                }
                if ( $aggr eq $info->{ok} ) {
                    if ( $count == 0 ) {
                        $sql->{quote}{having_stmt} = '';
                        $sql->{print}{having_stmt} = '';
                    }
                    last HAVING;
                }
                if ( $aggr eq ')' ) {
                    $sql->{quote}{having_stmt} .= ")";
                    $sql->{print}{having_stmt} .= ")";
                    $unclosed--;
                    next HAVING;
                }
                if ( $count > 0 && $sql->{quote}{having_stmt} !~ /\(\z/ ) {
                    my $choices = [ $AND, $OR ];
                    unshift @$choices, undef if $opt->{sssc_mode};
                    print_select_statement( $info, $sql, $table );
                    # Choose
                    $AND_OR = choose(
                        $choices,
                        { %lyt_stmt_h }
                    );
                    if ( ! defined $AND_OR ) {
                        $sql->{quote}{having_args} = [];
                        $sql->{quote}{having_stmt} = " HAVING";
                        $sql->{print}{having_stmt} = " HAVING";
                        $count = 0;
                        $AND_OR = ' ';
                        next HAVING;
                    }
                    $AND_OR = ' ' . $AND_OR . ' ';
                }
                if ( $aggr eq '(' ) {
                    $sql->{quote}{having_stmt} .= $AND_OR . "(";
                    $sql->{print}{having_stmt} .= $AND_OR . "(";
                    $AND_OR = '';
                    $unclosed++;
                    next HAVING;
                }
                my ( $print_col,  $quote_col );
                my ( $print_aggr, $quote_aggr);
                if ( ( any { '@' . $_ eq $aggr } @{$sql->{print}{aggr_cols}} ) ) {
                    ( $print_aggr = $aggr ) =~ s/^\@//;
                    $quote_aggr = $qt_columns->{$print_aggr};
                    $sql->{quote}{having_stmt} .= $AND_OR . $quote_aggr;
                    $sql->{print}{having_stmt} .= $AND_OR . $print_aggr;
                    $quote_col = $qt_columns->{$print_aggr};
                }
                elsif ( $aggr eq 'COUNT(*)' ) {
                    $print_col = '*';
                    $quote_col = '*';
                    $print_aggr = $aggr;
                    $quote_aggr = $aggr;
                    $sql->{quote}{having_stmt} .= $AND_OR . $quote_aggr;
                    $sql->{print}{having_stmt} .= $AND_OR . $print_aggr;
                }
                else {
                    $aggr =~ s/\(\S\)\z//;
                    $sql->{quote}{having_stmt} .= $AND_OR . $aggr . "(";
                    $sql->{print}{having_stmt} .= $AND_OR . $aggr . "(";
                    $quote_aggr                 =           $aggr . "(";
                    $print_aggr                 =           $aggr . "(";
                    my $choices = [ @cols ];
                    unshift @$choices, undef if $opt->{sssc_mode};
                    print_select_statement( $info, $sql, $table );
                    # Choose
                    $print_col = choose(
                        $choices,
                        { %lyt_stmt_h }
                    );
                    if ( ! defined $print_col ) {
                        $sql->{quote}{having_args} = [];
                        $sql->{quote}{having_stmt} = " HAVING";
                        $sql->{print}{having_stmt} = " HAVING";
                        $count = 0;
                        $AND_OR = ' ';
                        next HAVING;
                    }
                    ( $quote_col = $qt_columns->{$print_col} ) =~ s/\sAS\s\S+\z//;
                    $sql->{quote}{having_stmt} .= $quote_col . ")";
                    $sql->{print}{having_stmt} .= $print_col . ")";
                    $quote_aggr                .= $quote_col . ")";
                    $print_aggr                .= $print_col . ")";
                }
                set_operator_sql( $info, $opt, $sql, 'having', $table, \@cols, $qt_columns, $quote_aggr );
                if ( ! $sql->{quote}{having_stmt} ) {
                    $sql->{quote}{having_args} = [];
                    $sql->{quote}{having_stmt} = " HAVING";
                    $sql->{print}{having_stmt} = " HAVING";
                    $count = 0;
                    $AND_OR = ' ';
                    next HAVING;
                }
                $count++;
            }
        }
        elsif( $custom eq $customize{'order_by'} ) {
            my @functions = @{$info->{hidd_func_pr}}{@{$info->{keys_hidd_func_pr}}};
            my $f = join '|', map quotemeta, @functions;
            my @not_hidd = map { /^(?:$f)\((.*)\)\z/ ? $1 : () } @{$sql->{print}{aggr_cols}};
            my @cols =
                ( $sql->{select_type} eq '*' || $sql->{select_type} eq 'chosen_cols' )
                ? ( @$pr_columns, @{$sql->{print}{hidd}} )
                : ( @{$sql->{print}{group_by_cols}}, @{$sql->{print}{aggr_cols}}, @not_hidd );
            my $col_sep = ' ';
            $sql->{quote}{order_by_stmt} = " ORDER BY";
            $sql->{print}{order_by_stmt} = " ORDER BY";

            ORDER_BY: while ( 1 ) {
                my $choices = [ $info->{ok}, @cols ];
                unshift @$choices, undef if $opt->{sssc_mode};
                print_select_statement( $info, $sql, $table );
                # Choose
                my $print_col = choose(
                    $choices,
                    { %lyt_stmt_h }
                );
                if ( ! defined $print_col ) {
                    if ( $sql->{quote}{order_by_stmt} ne " ORDER BY" ) {
                        $sql->{quote}{order_by_args} = [];
                        $sql->{quote}{order_by_stmt} = " ORDER BY";
                        $sql->{print}{order_by_stmt} = " ORDER BY";
                        $col_sep = ' ';
                        next ORDER_BY;
                    }
                    else {
                        $sql = clone( $backup_sql );
                        last ORDER_BY;
                    }
                }
                if ( $print_col eq $info->{ok} ) {
                    if ( $col_sep eq ' ' ) {
                        $sql->{quote}{order_by_stmt} = '';
                        $sql->{print}{order_by_stmt} = '';
                    }
                    last ORDER_BY;
                }
                ( my $quote_col = $qt_columns->{$print_col} ) =~ s/\sAS\s\S+\z//;
                $sql->{quote}{order_by_stmt} .= $col_sep . $quote_col;
                $sql->{print}{order_by_stmt} .= $col_sep . $print_col;
                $choices = [ $ASC, $DESC ];
                unshift @$choices, undef if $opt->{sssc_mode};
                print_select_statement( $info, $sql, $table );
                # Choose
                my $direction = choose(
                    $choices,
                    { %lyt_stmt_h }
                );
                if ( ! defined $direction ){
                    $sql->{quote}{order_by_args} = [];
                    $sql->{quote}{order_by_stmt} = " ORDER BY";
                    $sql->{print}{order_by_stmt} = " ORDER BY";
                    $col_sep = ' ';
                    next ORDER_BY;
                }
                $sql->{quote}{order_by_stmt} .= ' ' . $direction;
                $sql->{print}{order_by_stmt} .= ' ' . $direction;
                $col_sep = ', ';
            }
        }
        elsif( $custom eq $customize{'limit'} ) {
            $sql->{quote}{limit_args} = [];
            $sql->{quote}{limit_stmt} = " LIMIT";
            $sql->{print}{limit_stmt} = " LIMIT";
            my $digits = 7;
            my ( $only_limit, $offset_and_limit ) = ( 'LIMIT', 'OFFSET-LIMIT' );

            LIMIT: while ( 1 ) {
                my $choices = [ $info->{ok}, $only_limit, $offset_and_limit ];
                unshift @$choices, undef if $opt->{sssc_mode};
                print_select_statement( $info, $sql, $table );
                # Choose
                my $choice = choose(
                    $choices,
                    { %lyt_stmt_h }
                );
                if ( ! defined $choice ) {
                    if ( @{$sql->{quote}{limit_args}} ) {
                        $sql->{quote}{limit_args} = [];
                        $sql->{quote}{limit_stmt} = " LIMIT";
                        $sql->{print}{limit_stmt} = " LIMIT";
                        next LIMIT;
                    }
                    else {
                        $sql = clone( $backup_sql );
                        last LIMIT;
                    }
                }
                if ( $choice eq $info->{ok} ) {
                    if ( ! @{$sql->{quote}{limit_args}} ) {
                        $sql->{quote}{limit_stmt} = '';
                        $sql->{quote}{limit_stmt} = '';
                    }
                    last LIMIT;
                }
                $sql->{quote}{limit_args} = [];
                $sql->{quote}{limit_stmt} = " LIMIT";
                $sql->{print}{limit_stmt} = " LIMIT";
                # Choose_a_number
                my $limit = choose_a_number( $info, $opt, $digits, '"LIMIT"' );
                next LIMIT if ! defined $limit || $limit eq '--';
                push @{$sql->{quote}{limit_args}}, $limit;
                $sql->{quote}{limit_stmt} .= ' ' . '?';
                $sql->{print}{limit_stmt} .= ' ' . insert_sep( $info, $opt, $limit );
                if ( $choice eq $offset_and_limit ) {
                    # Choose_a_number
                    my $offset = choose_a_number( $info, $opt, $digits, '"OFFSET"' );
                    if ( ! defined $offset || $offset eq '--' ) {
                        $sql->{quote}{limit_args} = [];
                        $sql->{quote}{limit_stmt} = " LIMIT";
                        $sql->{print}{limit_stmt} = " LIMIT";
                        next LIMIT;
                    }
                    push @{$sql->{quote}{limit_args}}, $offset;
                    $sql->{quote}{limit_stmt} .= " OFFSET " . '?';
                    $sql->{print}{limit_stmt} .= " OFFSET " . insert_sep( $info, $opt, $offset );
                }
            }
        }
        elsif ( $custom eq $customize{'hidden'} ) {
            my @functions = @{$info->{keys_hidd_func_pr}};
            my $stmt_key = '';
            if ( $sql->{select_type} eq '*' ) {
                @{$sql->{quote}{chosen_cols}} = map { $qt_columns->{$_} } @$pr_columns;
                @{$sql->{print}{chosen_cols}} = @$pr_columns;
                $stmt_key = 'chosen_cols';
            }
            elsif ( $sql->{select_type} eq 'chosen_cols' ) {
                $stmt_key = 'chosen_cols';
            }
            if ( $stmt_key eq 'chosen_cols' ) {
                if ( ! $sql->{backup_in_hidd}{print}{$stmt_key} ) {
                    @{$sql->{backup_in_hidd}{print}{$stmt_key}} = @{$sql->{print}{$stmt_key}};
                }
            }
            else {
                if ( @{$sql->{print}{aggr_cols}} && ! $sql->{backup_in_hidd}{print}{aggr_cols} ) {
                    @{$sql->{backup_in_hidd}{print}{'aggr_cols'}} = @{$sql->{print}{aggr_cols}};
                }
                if ( @{$sql->{print}{group_by_cols}} && ! $sql->{backup_in_hidd}{print}{group_by_cols} ) {
                    @{$sql->{backup_in_hidd}{print}{'group_by_cols'}} = @{$sql->{print}{group_by_cols}};
                }
            }
            my $changed = 0;

            HIDDEN: while ( 1 ) {
                my @cols = $stmt_key eq 'chosen_cols'
                    ? ( @{$sql->{print}{$stmt_key}}                                  )
                    : ( @{$sql->{print}{aggr_cols}}, @{$sql->{print}{group_by_cols}} );
                my @pre = ( undef, $info->{_confirm} );
                my $choices = [ @pre, map( "- $_", @cols ) ];
                print_select_statement( $info, $sql, $table );
                # Choose
                my $i = choose(
                    $choices,
                    { %{$info->{lyt_stmt_v}}, index => 1 }
                );
                if ( ! defined $i ) {
                    $sql = clone( $backup_sql );
                    last HIDDEN;
                }
                my $print_col = $choices->[$i];
                if ( ! defined $print_col ) {
                    $sql = clone( $backup_sql );
                    last HIDDEN;
                }
                if ( $print_col eq $info->{_confirm} ) {
                    if ( ! $changed ) {
                        $sql = clone( $backup_sql );
                        last HIDDEN;
                    }
                    $sql->{select_type} = $stmt_key if $sql->{select_type} eq '*';
                    last HIDDEN;
                }
                $print_col =~ s/^\-\s//;
                $i -= @pre;
                if ( $stmt_key ne 'chosen_cols' ) {
                    if ( $i - @{$sql->{print}{aggr_cols}} >= 0 ) {
                        $i -= @{$sql->{print}{aggr_cols}};
                        $stmt_key = 'group_by_cols';
                    }
                    else {
                        $stmt_key = 'aggr_cols';
                    }
                }
                if ( $sql->{print}{$stmt_key}[$i] ne $sql->{backup_in_hidd}{print}{$stmt_key}[$i] ) {
                    if ( $stmt_key ne 'aggr_cols' ) {
                        my $i = first_index { $sql->{print}{$stmt_key}[$i] eq $_ } @{$sql->{print}{hidd}};
                        splice( @{$sql->{print}{hidd}}, $i, 1 );
                    }
                    $sql->{print}{$stmt_key}[$i] = $sql->{backup_in_hidd}{print}{$stmt_key}[$i];
                    $sql->{quote}{$stmt_key}[$i] = $qt_columns->{$sql->{backup_in_hidd}{print}{$stmt_key}[$i]};
                    $changed++;
                    next HIDDEN;
                }
                print_select_statement( $info, $sql, $table );
                # Choose
                my $function = choose(
                    [ undef, map( "  $_", @functions ) ],
                    { %{$info->{lyt_stmt_v}} }
                );
                if ( ! defined $function ) {
                    next HIDDEN;
                }
                $function =~ s/^\s\s//;
                ( my $quote_col = $qt_columns->{$print_col} ) =~ s/\sAS\s\S+\z//;
                print_select_statement( $info, $sql, $table );
                my $new = App::DBBrowser::DB->new( $info );
                my ( $quote_hidd, $print_hidd ) = $new->col_functions( $function, $quote_col, $print_col, $info );
                if ( ! defined $quote_hidd ) {
                    next HIDDEN;
                }
                $print_hidd = unambiguous_key( $print_hidd, $pr_columns );
                if ( $stmt_key eq 'group_by_cols' ) {
                    $sql->{quote}{$stmt_key}[$i] = $quote_hidd;
                    $sql->{quote}{group_by_stmt} = " GROUP BY " . join( ', ', @{$sql->{quote}{$stmt_key}} );
                    $sql->{print}{group_by_stmt} = " GROUP BY " . join( ', ', @{$sql->{print}{$stmt_key}} );
                }
                $sql->{quote}{$stmt_key}[$i] = $quote_hidd . ' AS ' . $dbh->quote_identifier( $print_hidd );
                $sql->{print}{$stmt_key}[$i] = $print_hidd;
                $qt_columns->{$print_hidd} = $quote_hidd;
                if ( $stmt_key ne 'aggr_cols' ) { # WHERE: aggregate functions are not allowed
                    push @{$sql->{print}{hidd}}, $print_hidd;
                }
                $changed++;
                next HIDDEN;
            }
        }
        elsif( $custom eq $customize{'print_table'} ) {
            my ( $default_cols_sql, $from_stmt ) = $select_from_stmt =~ /^SELECT\s(.*?)(\sFROM\s.*)\z/;
            my $cols_sql;
            if ( $sql->{select_type} eq '*' ) {
                $cols_sql = ' ' . $default_cols_sql;
            }
            elsif ( $sql->{select_type} eq 'chosen_cols' ) {
                $cols_sql = ' ' . join( ', ', @{$sql->{quote}{chosen_cols}} );
            }
            elsif ( @{$sql->{quote}{aggr_cols}} || @{$sql->{quote}{group_by_cols}} ) {
                $cols_sql = ' ' . join( ', ', @{$sql->{quote}{group_by_cols}}, @{$sql->{quote}{aggr_cols}} );
            }
            else { #
                $cols_sql = ' ' . $default_cols_sql;
            }
            my $select .= "SELECT" . $sql->{quote}{distinct_stmt} . $cols_sql . $from_stmt;
            $select .= $sql->{quote}{where_stmt};
            $select .= $sql->{quote}{group_by_stmt};
            $select .= $sql->{quote}{having_stmt};
            $select .= $sql->{quote}{order_by_stmt};
            $select .= $sql->{quote}{limit_stmt};
            my @arguments = ( @{$sql->{quote}{where_args}}, @{$sql->{quote}{having_args}}, @{$sql->{quote}{limit_args}} );
            $info->{auto_limit} = 0;
            if ( ! $sql->{quote}{limit_stmt} && $opt->{max_rows} ) {
                $select .= " LIMIT ?";
                push @arguments, $opt->{max_rows};
                $info->{auto_limit} = 1;
            }
            say 'Computing: ...' if $opt->{progress_bar};
            my $sth = $dbh->prepare( $select );
            $sth->execute( @arguments );
            my $col_names = $sth->{NAME};
            my $all_arrayref = $sth->fetchall_arrayref;
            unshift @$all_arrayref, $col_names;
            local $| = 1;
            print CLEAR_SCREEN;
            return $all_arrayref;
        }
        else {
            die "$custom: no such value in the hash \%customize";
        }
    }
    return;
}

sub unambiguous_key {
    my ( $new_key, $keys ) = @_;
    while ( any { $new_key eq $_ } @$keys ) {
        $new_key .= '_';
    }
    return $new_key;
}

sub set_operator_sql {
    my ( $info, $opt, $sql, $clause, $table, $cols, $qt_columns, $quote_col ) = @_;
    my ( $stmt, $args );
    my %lyt_stmt_h = %{$info->{lyt_stmt_h}};
    if ( $clause eq 'where' ) {
        $stmt = 'where_stmt';
        $args = 'where_args';
    }
    elsif ( $clause eq 'having' ) {
        $stmt = 'having_stmt';
        $args = 'having_args';
    }
    my $choices = [ @{$opt->{operators}} ];
    unshift @$choices, undef if $opt->{sssc_mode};
    print_select_statement( $info, $sql, $table );
    # Choose
    my $operator = choose(
        $choices,
        { %lyt_stmt_h }
    );
    if ( ! defined $operator ) {
        $sql->{quote}{$args} = [];
        $sql->{quote}{$stmt} = '';
        $sql->{print}{$stmt} = '';
        return;
    }
    $operator =~ s/^\s+|\s+\z//g;
    if ( $operator !~ /\s%?col%?\z/ ) {
        if ( $operator !~ /REGEXP\z/ ) {
            $sql->{quote}{$stmt} .= ' ' . $operator;
            $sql->{print}{$stmt} .= ' ' . $operator;
        }
        if ( $operator =~ /NULL\z/ ) {
            # do nothing
        }
        elsif ( $operator =~ /^(?:NOT\s)?IN\z/ ) {
            my $col_sep = '';
            $sql->{quote}{$stmt} .= '(';
            $sql->{print}{$stmt} .= '(';

            IN: while ( 1 ) {
                my $status = print_select_statement( $info, $sql, $table, 1 );
                # Readline
                my $value = local_readline( $info, { status => $status, prompt => 'Value: ' } );
                if ( ! defined $value ) {
                    $sql->{quote}{$args} = [];
                    $sql->{quote}{$stmt} = '';
                    $sql->{print}{$stmt} = '';
                    return;
                }
                if ( $value eq '' ) {
                    if ( $col_sep eq ' ' ) {
                        $sql->{quote}{$args} = [];
                        $sql->{quote}{$stmt} = '';
                        $sql->{print}{$stmt} = '';
                        return;
                    }
                    $sql->{quote}{$stmt} .= ')';
                    $sql->{print}{$stmt} .= ')';
                    last IN;
                }
                $sql->{quote}{$stmt} .= $col_sep . '?';
                $sql->{print}{$stmt} .= $col_sep . $value;
                push @{$sql->{quote}{$args}}, $value;
                $col_sep = ',';
            }
        }
        elsif ( $operator =~ /^(?:NOT\s)?BETWEEN\z/ ) {
            my $status1 = print_select_statement( $info, $sql, $table, 1 );
            # Readline
            my $value_1 = local_readline( $info, { status => $status1, prompt => 'Value: ' } );
            if ( ! defined $value_1 ) {
                $sql->{quote}{$args} = [];
                $sql->{quote}{$stmt} = '';
                $sql->{print}{$stmt} = '';
                return;
            }
            $sql->{quote}{$stmt} .= ' ' . '?' .      ' AND';
            $sql->{print}{$stmt} .= ' ' . $value_1 . ' AND';
            push @{$sql->{quote}{$args}}, $value_1;
            my $status2 = print_select_statement( $info, $sql, $table, 1 );
            # Readline
            my $value_2 = local_readline( $info, { status => $status2, prompt => 'Value: ' } );
            if ( ! defined $value_2 ) {
                $sql->{quote}{$args} = [];
                $sql->{quote}{$stmt} = '';
                $sql->{print}{$stmt} = '';
                return;
            }
            $sql->{quote}{$stmt} .= ' ' . '?';
            $sql->{print}{$stmt} .= ' ' . $value_2;
            push @{$sql->{quote}{$args}}, $value_2;
        }
        elsif ( $operator =~ /REGEXP\z/ ) {
            my $case_sensitive = $opt->{regexp_case};
            $sql->{print}{$stmt} .= ' ' . $operator;
            my $status = print_select_statement( $info, $sql, $table, 1 );
            # Readline
            my $value = local_readline( $info, { status => $status, prompt => 'Pattern: ' } );
            if ( ! defined $value ) {
                $sql->{quote}{$args} = [];
                $sql->{quote}{$stmt} = '';
                $sql->{print}{$stmt} = '';
                return;
            }
            $value = '^$' if ! length $value;
            if ( $info->{db_driver} eq 'SQLite' ) {
                $value = qr/$value/i if ! $case_sensitive;
                $value = qr/$value/  if   $case_sensitive;
            }
            $sql->{quote}{$stmt} =~ s/.*\K\s\Q$quote_col\E//;
            my $new = App::DBBrowser::DB->new( $info );
            $sql->{quote}{$stmt} .= $new->sql_regexp( $quote_col, $operator =~ /^NOT/ ? 1 : 0, $case_sensitive );
            $sql->{print}{$stmt} .= ' ' . "'$value'";
            push @{$sql->{quote}{$args}}, $value;
        }
        else {
            my $status = print_select_statement( $info, $sql, $table, 1 );
            my $prompt = $operator =~ /LIKE\z/ ? 'Pattern: ' : 'Value: ';
            # Readline
            my $value = local_readline( $info, { status => $status, prompt => $prompt } );
            if ( ! defined $value ) {
                $sql->{quote}{$args} = [];
                $sql->{quote}{$stmt} = '';
                $sql->{print}{$stmt} = '';
                return;
            }
            $sql->{quote}{$stmt} .= ' ' . '?';
            $sql->{print}{$stmt} .= ' ' . "'$value'";
            push @{$sql->{quote}{$args}}, $value;
        }
    }
    elsif ( $operator =~ /\s%?col%?\z/ ) {
        my $arg;
        if ( $operator =~ /^(.+)\s(%?col%?)\z/ ) {
            $operator = $1;
            $arg = $2;
        }
        $operator =~ s/^\s+|\s+\z//g;
        $sql->{quote}{$stmt} .= ' ' . $operator;
        $sql->{print}{$stmt} .= ' ' . $operator;
        my $choices = [ @$cols ];
        unshift @$choices, undef if $opt->{sssc_mode};
        print_select_statement( $info, $sql, $table );
        # Choose
        my $print_col = choose(
            $choices,
            { %lyt_stmt_h, prompt => "$operator:" }
        );
        if ( ! defined $print_col ) {
            $sql->{quote}{$stmt} = '';
            $sql->{print}{$stmt} = '';
            return;
        }
        ( my $quote_col = $qt_columns->{$print_col} ) =~ s/\sAS\s\S+\z//;
        my ( @qt_args, @pr_args );
        if ( $arg =~ /^(%)col/ ) {
            push @qt_args, "'$1'";
            push @pr_args, "'$1'";
        }
        push @qt_args, $quote_col;
        push @pr_args, $print_col;
        if ( $arg =~ /col(%)\z/ ) {
            push @qt_args, "'$1'";
            push @pr_args, "'$1'";
        }
        if ( $operator =~ /LIKE\z/ ) {
            my $new = App::DBBrowser::DB->new( $info );
            $sql->{quote}{$stmt} .= ' ' . $new->concatenate( \@qt_args );
            $sql->{print}{$stmt} .= ' ' . join( '+', @pr_args );
        }
        else {
            $sql->{quote}{$stmt} .= ' ' . $quote_col;
            $sql->{print}{$stmt} .= ' ' . $print_col;
        }
    }
    return;
}


sub options {
    my ( $info, $opt ) = @_;
    my $menus = [
        [ 'db_defaults',    "- DB Defaults" ],
        [ 'db_drivers',     "- DB Drivers" ],
        [ 'db_login_once',  "- DB Login" ],
        [ '_env_dbi',       "- ENV DBI" ],
        [ '_expand',        "- Expand" ],
        [ 'lock_stmt',      "- Lock" ],
        [ 'max_rows',       "- Max Rows" ],
        [ 'metadata',       "- Metadata" ],
        [ 'mouse',          "- Mouse Mode" ],
        [ 'min_col_width',  "- Colwidth" ],
        [ 'operators',      "- Operators" ],
        [ '_parentheses',   "- Parentheses" ],
        [ 'progress_bar',   "- ProgressBar" ],
        [ 'regexp_case',    "- Regexp Case" ],
        [ 'sssc_mode',      "- Sssc Mode" ],
        [ 'tab_width',      "- Tabwidth" ],
        [ 'undef',          "- Undef" ],
    ];
    my $sub_menus = {
        _expand      => [
            [ 'keep_db_choice',     "- Choose Database" ],
            [ 'keep_schema_choice', "- Choose Schema" ],
            [ 'keep_table_choice',  "- Choose Table" ],
            [ 'table_expand',       "- Print  Table" ], ],
        _parentheses => [
            [ 'w_parentheses', "- Parentheses in WHERE" ],
            [ 'h_parentheses', "- Parentheses in HAVING TO" ], ],
        _env_dbi     => [
            [ 'env_dbi_user', "- Use DBI_USER" ],
            [ 'env_dbi_pass', "- Use DBI_PASS" ], ],
    };
    my $path = '  Path';
    my @pre = ( undef, $info->{_continue}, $info->{_help}, $path );
    my @real = map( $_->[1], @$menus );
    my $choices = [ @pre, @real ];

    OPTION: while ( 1 ) {
        # Choose
        my $idx = choose(
            $choices,
            { %{$info->{lyt_3}}, index => 1, undef => $info->{_exit} }
        );
        exit if ! defined $idx;
        my $key;
        if ( $idx < @pre ) {
            $key = $pre[$idx];
        }
        else {
            $idx -= @pre;
            $key = $menus->[$idx][0];
            die if $key !~ /^_/ && ! exists $opt->{$key};
        }
        if ( ! defined $key ) {
            if ( $info->{write_config} ) {
                write_config_file( $opt, $info->{config_file} );
                delete $info->{write_config};
            }
            exit();
        }
        elsif ( $key eq $info->{_continue} ) {
            if ( $info->{write_config} ) {
                write_config_file( $opt, $info->{config_file} );
                delete $info->{write_config};
            }
            return $opt;
        }
        elsif ( $key eq $info->{_help} ) {
            pod2usage( { -exitval => 'NOEXIT', -verbose => 2 } );
        }
        elsif ( $key eq $path ) {
            my $bin     = 'bin';
            my $app_dir = 'app dir';
            my $path = {
                $bin     => catfile( $RealBin, $RealScript ),
                $app_dir => $info->{app_dir},
            };
            my $keys = [ $bin, $app_dir ];
            my $len_key = 7;
            print_hash( $info, $path, $keys, $len_key, ( term_size() )[0] );
        }
        elsif ( $key eq 'tab_width' ) {
            my $digits = 3;
            my $prompt = 'Tab width';
            opt_number_range( $info, $opt, $key, $prompt, $digits );
        }
        elsif ( $key eq 'min_col_width' ) {
            my $digits = 3;
            my $prompt = 'Minimum Column width';
            opt_number_range( $info, $opt, $key, $prompt, $digits );
        }
        elsif ( $key eq 'undef' ) {
            my $prompt = 'Print replacement for undefined table vales';
            opt_readline( $info, $opt, $key, $prompt );
        }
        elsif ( $key eq 'progress_bar' ) {
            my $digits = 7;
            my $prompt = '"Threshold ProgressBar"';
            opt_number_range( $info, $opt, $key, $prompt, $digits );
        }
        elsif ( $key eq 'max_rows' ) {
            my $digits = 7;
            my $prompt = '"Max rows"';
            opt_number_range( $info, $opt, $key, $prompt, $digits );
        }
        elsif ( $key eq 'lock_stmt' ) {
            my $list = [ 'Lk0', 'Lk1' ];
            my $prompt = 'Keep statement';
            opt_choose_index( $info, $opt, $key, $prompt, $list );
        }
        elsif ( $key eq 'metadata' ) {
            my $list = $info->{yes_no};
            my $prompt = 'Enable Metadata';
            opt_choose_index( $info, $opt, $key, $prompt, $list );
        }
        elsif ( $key eq 'regexp_case' ) {
            my $list = $info->{yes_no};
            my $prompt = 'REGEXP case sensitiv';
            opt_choose_index( $info, $opt, $key, $prompt, $list );
        }
        elsif ( $key eq '_parentheses' ) {
            my $sub_menu = $sub_menus->{$key};
            my $two_val = [ 'NO', 'YES' ];
            multi_options( $info, $opt, $sub_menu, $two_val );
        }
        elsif ( $key eq 'db_login_once' ) {
            my $list = [ 'per-DB', 'once' ];
            my $prompt = 'Ask for credentials';
            opt_choose_index( $info, $opt, $key, $prompt, $list );
        }
        elsif ( $key eq '_env_dbi' ) {
            my $sub_menu = $sub_menus->{$key};
            my $two_val = [ 'NO', 'YES' ];
            multi_options( $info, $opt, $sub_menu, $two_val );
        }
        elsif ( $key eq 'db_defaults' ) {
            database_setting( $info, $opt );
        }
        elsif ( $key eq 'sssc_mode' ) {
            my $list = [ 'simple', 'compat' ];
            my $prompt = 'Sssc mode';
            opt_choose_index( $info, $opt, $key, $prompt, $list );
        }
        elsif ( $key eq 'operators' ) {
            my $available = $info->{avail_operators};
            opt_choose_a_list( $info, $opt, $key, $available );
        }
        elsif ( $key eq 'db_drivers' ) {
            my $available = $info->{avail_db_drivers};
            opt_choose_a_list( $info, $opt, $key, $available );
        }
        elsif ( $key eq 'mouse' ) {
            my $max = 4;
            my $prompt = 'Mouse mode';
            opt_number( $info, $opt, $key, $prompt, $max );
        }
        elsif ( $key eq '_expand' ) {
            my $two_val = [ 'Simple', 'Enchanted' ];
            my $sub_menu = $sub_menus->{$key};
            multi_options( $info, $opt, $sub_menu, $two_val );
        }
        else { die "Unknown option: $key" }
    }
}


sub multi_options {
    my ( $info, $opt, $sub_menu, $values ) = @_;
    my $count;
    my $tmp = {};
    my $longest = 0;
    my $menu_hash = {};
    my @keys = ();
    for my $sub ( @$sub_menu ) {
        my ( $key, $prompt ) = @$sub;
        $menu_hash->{$key} = $prompt;
        push @keys, $key;
        my $gcs = Unicode::GCString->new( $prompt );
        my $length = $gcs->columns();
        $longest = $length if $length > $longest;
        $tmp->{$key} = $opt->{$key};
    }
    while ( 1 ) {
        my $choices = [ undef ];
        for my $key ( @keys ) {
            my $current = $values->[$tmp->{$key}];
            push @$choices, sprintf "%-*s [%s]", $longest, $menu_hash->{$key}, $current;
        }
        push @$choices, $info->{_confirm};
        # Choose
        my $idx = choose(
            $choices,
            { %{$info->{lyt_3}}, prompt => 'Choose:', index => 1 }
        );
        return if ! defined $idx;
        my $choice = $choices->[$idx];
        return if ! defined $choice;
        if ( $choice eq $info->{_confirm} ) {
            if ( $count ) {
                for my $key ( @keys ) {
                    $opt->{$key} = $tmp->{$key};
                }
                $info->{write_config}++;
            }
            return;
        }
        my $key = $keys[$idx-1];
        $tmp->{$key}++;
        $tmp->{$key} = 0 if $tmp->{$key} == @$values;
        $count++;
    }
}

sub opt_choose_index {
    my ( $info, $opt, $key, $prompt, $list ) = @_;
    my $yn = 0;
    if ( $list->[0] eq 'YES' && $list->[1] eq 'NO' ) {
        $yn = 1;
    }
    if ( $yn ) {
        if ( $opt->{$key} == 0 ) {
            $opt->{$key} = 1;
        } elsif ( $opt->{$key} == 1 ) {
            $opt->{$key} = 0;
        }
    }
    my $current = $list->[$opt->{$key}];
    # Choose
    my $idx = choose(
        [ undef, @$list ],
        { %{$info->{lyt_1}}, prompt => $prompt . ' [' . $current . ']:', index => 1 }
    );
    return if ! defined $idx;
    return if $idx == 0;
    $idx--;
    $opt->{$key} = $idx;
    if ( $yn ) {
        if ( $opt->{$key} == 0 ) {
            $opt->{$key} = 1;
        } elsif ( $opt->{$key} == 1 ) {
            $opt->{$key} = 0;
        }
    }
    $info->{write_config}++;
    return;
}

sub opt_choose_a_list {
    my ( $info, $opt, $key, $available ) = @_;
    my $current = $opt->{$key};
    # Choose_list
    my $list = choose_list( $info, $current, $available );
    return if ! defined $list;
    $opt->{$key} = $list;
    $info->{write_config}++;
    return;
}

sub opt_number {
    my ( $info, $opt, $key, $prompt, $max ) = @_;
    my $current = $opt->{$key};
    # Choose
    my $choice = choose(
        [ undef, 0 .. $max ],
        { %{$info->{lyt_1}}, prompt => $prompt . ' [' . $current . ']:', justify => 1 }
    );
    return if ! defined $choice;
    $opt->{$key} = $choice;
    $info->{write_config}++;
    return;
}

sub opt_number_range {
    my ( $info, $opt, $key, $prompt, $digits ) = @_;
    my $current = $opt->{$key};
    $current = insert_sep( $info, $opt, $current );
    # Choose_a_number
    my $choice = choose_a_number( $info, $opt, $digits, $prompt, $current );
    return if ! defined $choice;
    $opt->{$key} = $choice eq '--' ? undef : $choice;
    $info->{write_config}++;
    return;
}

sub opt_readline {
    my ( $info, $opt, $key, $prompt ) = @_;
    my $current = $opt->{$key};
    $prompt .= ' ["' . $current . '"]: ';
    # Readline
    my $choice = local_readline( $info, { prompt => $prompt } );
    return if ! defined $choice;
    $opt->{$key} = $choice;
    $info->{write_config}++;
    return;
}


sub database_setting {
    my ( $info, $opt, $db ) = @_;
    my $db_driver;
    if ( ! defined $db ) {
        if ( @{$opt->{db_drivers}} == 1 ) {
            $db_driver = $opt->{db_drivers}[0];
        }
        else {
            # Choose
            $db_driver = choose(
                [ undef, @{$opt->{db_drivers}} ],
                { %{$info->{lyt_1}} }
            );
            return if ! defined $db_driver;
        }
    }
    else {
        $db_driver = $info->{db_driver};
    }
    my $section;
    if ( ! defined $db ) {
        $section = $db_driver;
    }
    else {
        $section = $db_driver . '_' . $db;
        for my $key ( keys %{$opt->{$db_driver}} ) {
            $opt->{$section}{$key} //= $opt->{$db_driver}{$key};
        }
    }
    my $orig = clone( $opt );
    my $menus = {
        SQLite => [
            [ 'sqlite_unicode',             "- Unicode" ],
            [ 'sqlite_see_if_its_a_number', "- See if its a number" ],
            [ '_binary_filter',             "- Binary Filter" ],
        ],
        mysql => [
            [ 'host',              "- Host" ],
            [ 'port',              "- Port" ],
            [ 'mysql_enable_utf8', "- Enable utf8" ],
            [ '_binary_filter',    "- Binary Filter" ],
        ],
        Pg => [
            [ 'host',           "- Host" ],
            [ 'port',           "- Port" ],
            [ 'pg_enable_utf8', "- Enable utf8" ],
            [ '_binary_filter', "- Binary Filter" ],
        ],
    };
    my $prompt;
    if ( defined $db ) {
        $prompt = 'DB: "' . ( $db_driver eq 'SQLite' ? basename( $db ) : $db ) . '"';
    }
    else {
        $prompt = 'Driver: ' . $db_driver;
    }
    my @pre = ( undef, $info->{_confirm} );
    my @real = map { $_->[1] } @{$menus->{$db_driver}};
    my $choices = [ @pre, @real ];

    DB_OPTION: while ( 1 ) {
        # Choose
        my $idx = choose(
            $choices,
            { %{$info->{lyt_3}}, index => 1, prompt => $prompt  }
        );
        exit if ! defined $idx;
        my $key;
        if ( $idx < @pre ) {
            $key = $pre[$idx];
        }
        else {
            $idx -= @pre;
            $key = $menus->{$db_driver}[$idx][0];
            die if ! exists $opt->{$db_driver}{$key};
        }
        if ( ! defined $key ) {
            if ( $info->{write_config} ) {
                $opt = clone( $orig );
            }
            return;
        }
        if ( $key eq $info->{_confirm} ) {
            if ( $info->{write_config} ) {
                write_config_file( $opt, $info->{config_file} );
                delete $info->{write_config};
                return 1;
            }
            return;
        }
        if ( $db_driver eq "SQLite" ) {
            if ( $key eq 'sqlite_unicode' ) {
                my $list = $info->{yes_no};
                my $prompt = 'Unicode';
                db_opt_choose_index( $info, $opt, $section, $key, $prompt, $list );
            }
            elsif ( $key eq 'sqlite_see_if_its_a_number' ) {
                my $list = $info->{yes_no};
                my $prompt = 'See if its a number';
                db_opt_choose_index( $info, $opt, $section, $key, $prompt, $list );
            }
            elsif ( $key eq '_binary_filter' ) {
                my $list = $info->{yes_no};
                my $prompt = 'Enable Binary Filter';
                db_opt_choose_index( $info, $opt, $section, $key, $prompt, $list );
            }
            else { die "Unknown key: $key" }
        }
        elsif ( $db_driver eq "mysql" ) {
            if ( $key eq 'mysql_enable_utf8' ) {
                my $list = $info->{yes_no};
                my $prompt = 'Enable utf8';
                db_opt_choose_index( $info, $opt, $section, $key, $prompt, $list );
            }
            elsif ( $key eq 'host' ) {
                my $prompt = 'Host';
                db_opt_readline( $info, $opt, $section, $key, $prompt );
            }
            elsif ( $key eq 'port' ) {
                my $prompt = 'Port';
                db_opt_readline( $info, $opt, $section, $key, $prompt );
            }
            elsif ( $key eq '_binary_filter' ) {
                my $list = $info->{yes_no};
                my $prompt = 'Enable Binary Filter';
                db_opt_choose_index( $info, $opt, $section, $key, $prompt, $list );
            }
            else { die "Unknown key: $key" }
        }
        elsif ( $db_driver eq "Pg" ) {
            if ( $key eq 'pg_enable_utf8' ) {
                my $prompt = 'Enable utf8';
                my $list = [ @{$info->{yes_no}}, 'AUTO' ];
                db_opt_choose_index( $info, $opt, $section, $key, $prompt, $list );
                $opt->{$section}{$key} = -1 if $opt->{$section}{$key} == 2;
            }
            elsif ( $key eq 'host' ) {
                my $prompt = 'Host';
                db_opt_readline( $info, $opt, $section, $key, $prompt );
            }
            elsif ( $key eq 'port' ) {
                my $prompt = 'Port';
                db_opt_readline( $info, $opt, $section, $key, $prompt );
            }
            elsif ( $key eq '_binary_filter' ) {
                my $list = $info->{yes_no};
                my $prompt = 'Enable Binary Filter';
                db_opt_choose_index( $info, $opt, $section, $key, $prompt, $list );
            }
            else { die "Unknown key: $key" }
        }
    }
}


sub print_hash {
    my ( $info, $hash, $keys, $len_key, $maxcols ) = @_;
    my $s_tab = $len_key + length( ' : ' );
    my $lf = Text::LineFold->new(
        Charset       => 'utf-8',
        ColMax        => $maxcols,
        Newline       => "\n",
        OutputCharset => '_UNICODE_',
        Urgent        => 'FORCE',
    );
    my @vals = ();
    for my $key ( @$keys ) {
        my $pr_key = sprintf "%*.*s : ", $len_key, $len_key, $key;
        my $text = $lf->fold( '' , ' ' x $s_tab, $pr_key . $hash->{$key} );
        $text =~ s/\R+\z//;
        for my $val ( split /\R+/, $text ) {
            push @vals, $val;
        }
    }
    choose(
        [ @vals ],
        { %{$info->{lyt_3}} }
    );
}


sub db_opt_choose_index {
    my ( $info, $opt, $section, $key, $prompt, $list ) = @_;
    my $yn = 0;
    if ( $list->[0] eq 'YES' && $list->[1] eq 'NO' ) {
        $yn = 1;
    }
    if ( $yn ) {
        if ( $opt->{$section}{$key} == 0 ) {
            $opt->{$section}{$key} = 1;
        } elsif ( $opt->{$section}{$key} == 1 ) {
            $opt->{$section}{$key} = 0;
        }
    }
    my $current = $list->[$opt->{$section}{$key}];
    # Choose
    my $idx = choose(
        [ undef, @$list ],
        { %{$info->{lyt_1}}, prompt => $prompt . ' [' . $current . ']:', index => 1 }
    );
    return if ! defined $idx;
    return if $idx == 0;
    $idx--;
    $opt->{$section}{$key} = $idx;
    if ( $yn ) {
        if ( $opt->{$section}{$key} == 0 ) {
            $opt->{$section}{$key} = 1;
        } elsif ( $opt->{$section}{$key} == 1 ) {
            $opt->{$section}{$key} = 0;
        }
    }
    $info->{write_config}++;
    return;
}


sub db_opt_readline {
    my ( $info, $opt, $section, $key, $prompt ) = @_;
    my $current = $opt->{$section}{$key};
    $prompt .= ' ["' . $current . '"]: ';
    # Readline
    my $choice = local_readline( $info, { prompt => $prompt } );
    return if ! defined $choice;
    $opt->{$section}{$key} = $choice;
    $info->{write_config}++;
    return;
}


sub local_readline {
    my ( $info, $args ) = @_;
    my $str = '';
    local $| = 1;
    local_readline_print( $info, $args, $str );
    ReadMode 'cbreak';
    while ( 1 ) {
        my $key = ReadKey;
        return if ! defined $key;
        if ( $key eq "\cD" ) {
            print "\n";
            return;
        }
        elsif ( $key eq "\n" or $key eq "\r" ) {
            print "\n";
            return $str;
        }
        elsif ( ord( $key ) == BSPACE || $key eq "\cH" ) {
            $str =~ s/.\z// if $str;
            local_readline_print( $info, $args, $str );
            next;
        }
        elsif ( $key !~ /^\p{Print}\z/ ) {
            local_readline_print( $info, $args, $str );
            next;
        }
        $str .= $key;
        local_readline_print( $info, $args, $str );
    }
    ReadMode 0;
    return $str;
}

sub local_readline_print {
    my ( $info, $args, $str ) = @_;
    print CLEAR_SCREEN;
    if ( defined $args->{status} ) {
        my $line_fold = Text::LineFold->new( %{$info->{line_fold}}, ColMax => ( term_size() )[0] - 2 );
        print $line_fold->fold( '', ' ' x $info->{stmt_init_tab}, $args->{status} );
    }
    print $args->{prompt} . ( $args->{no_echo} ? '' : $str );
}


sub choose_a_number {
    my ( $info, $opt, $digits, $name, $current ) = @_;
    my $tab = '  -  ';
    my $gcs_tab = Unicode::GCString->new( $tab );
    my $length_tab = $gcs_tab->columns;
    my $longest = $digits;
    my $thsd_sep = $opt->{thsd_sep};
    $longest += int( ( $digits - 1 ) / 3 ) if $thsd_sep ne '';
    my @choices_range = ();
    for my $di ( 0 .. $digits - 1 ) {
        my $begin = 1 . '0' x $di;
        $begin = 0 if $di == 0;
        $begin = insert_sep( $info, $opt, $begin );
        ( my $end = $begin ) =~ s/^[01]/9/;
        unshift @choices_range, sprintf " %*s%s%*s", $longest, $begin, $tab, $longest, $end;
    }
    my $confirm = sprintf "%-*s", $longest * 2 + $length_tab, $info->{confirm};
    my $back    = sprintf "%-*s", $longest * 2 + $length_tab, $info->{back};
    my ( $term_width ) = term_size();
    my $gcs_longest_range = Unicode::GCString->new( $choices_range[0] );
    if ( $gcs_longest_range->columns > $term_width ) {
        @choices_range = ();
        for my $di ( 0 .. $digits - 1 ) {
            my $begin = 1 . '0' x $di;
            $begin = 0 if $di == 0;
            $begin = insert_sep( $info, $opt, $begin );
            unshift @choices_range, sprintf "%*s", $longest, $begin;
        }
        $confirm = $info->{confirm};
        $back    = $info->{back};
    }
    my $reset = 'reset';
    my %numbers;
    my $result;

    NUMBER: while ( 1 ) {
        my $new_result = $result // '--';
        my $prompt = '';
        if ( @_ == 5 ) {
            $prompt .= sprintf "%s%*s\n",   'Current ' . $name . ': ', $longest, $current // '--';
            $prompt .= sprintf "%s%*s\n\n", '    New ' . $name . ': ', $longest, $new_result;
        }
        else {
            $prompt = sprintf "%s%*s\n\n", $name . ': ', $longest, $new_result;
        }
        # Choose
        my $range = choose(
            [ undef, @choices_range, $confirm ],
            { %{$info->{lyt_3}}, prompt => $prompt, justify => 1, undef => $back }
        );
        return if ! defined $range;
        if ( $range eq $confirm ) {
            return '--' if ! defined $result;
            $result =~ s/\Q$thsd_sep\E//g if $thsd_sep ne '';
            return $result;
        }
        my $zeros = ( split /\s*-\s*/, $range )[0];
        $zeros =~ s/^\s*\d//;
        ( my $zeros_no_sep = $zeros ) =~ s/\Q$thsd_sep\E//g if $thsd_sep ne '';
        my $count_zeros = length $zeros_no_sep;
        my @choices = $count_zeros ? map( $_ . $zeros, 1 .. 9 ) : ( 0 .. 9 );
        # Choose
        my $number = choose(
            [ undef, @choices, $reset ],
            { %{$info->{lyt_1}}, prompt => $prompt }
        );
        next if ! defined $number;
        if ( $number eq $reset ) {
            delete $numbers{$count_zeros};
        }
        else {
            $number =~ s/\Q$thsd_sep\E//g if $thsd_sep ne '';
            $numbers{$count_zeros} = $number;
        }
        $result = sum( @numbers{keys %numbers} );
        $result = insert_sep( $info, $opt, $result );
    }
}


sub choose_list {
    my ( $info, $current, $available ) = @_;
    my $new = [];
    my $key_cur = 'Current > ';
    my $key_new = '    New > ';
    my $gcs_cur = Unicode::GCString->new( $key_cur );
    my $gcs_new = Unicode::GCString->new( $key_new );
    my $length_key = $gcs_cur->columns > $gcs_new->columns ? $gcs_cur->columns : $gcs_new->columns;
    while ( 1 ) {
        my $prompt = $key_cur . join( ', ', map { "\"$_\"" } @$current ) . "\n";
        $prompt   .= $key_new . join( ', ', map { "\"$_\"" } @$new )     . "\n\n";
        $prompt   .= 'Choose:';
        # Choose
        my $choice = choose(
            [ undef, $info->{_confirm}, map( "- $_", @$available ) ],
            { %{$info->{lyt_3}}, prompt => $prompt, lf => [0,$length_key] }
        );
        if ( ! defined $choice ) {
            if ( @$new ) {
                $new = [];
                next;
            }
            else {
                return;
            }
        }
        if ( $choice eq $info->{_confirm} ) {
            return $new if @$new;
            return;
        }
        $choice =~ s/^-\s//;
        push @$new, $choice;
    }
}


sub insert_sep {
    my ( $info, $opt, $number ) = @_; # $info
    return if ! defined $number;
    my $separator = $opt->{thsd_sep};
    return $number if ! $separator;
    $number =~ s/(\d)(?=(?:\d{3})+\b)/$1$separator/g;
    return $number;
}


sub write_config_file {
    my ( $opt, $file ) = @_;
    my $tmp = {};
    for my $section ( sort keys %$opt ) {
        if ( ref( $opt->{$section} ) eq 'HASH' ) {
            for my $key ( keys %{$opt->{$section}} ) {
                $tmp->{$section}{$key} = $opt->{$section}{$key};
            }
        }
        else {
            my $key = $section;
            my $section = 'aa_generic';
            $tmp->{$section}{$key} = $opt->{$key};
        }
    }
    write_json( $file, $tmp );
}


sub read_config_file {
    my ( $opt, $file ) = @_;
    my $tmp = read_json( $file );
    for my $section ( keys %$tmp ) {
        for my $key ( keys %{$tmp->{$section}} ) {
            if ( $section eq 'aa_generic' ) {
                $opt->{$key} = $tmp->{$section}{$key};
            }
            else {
                $opt->{$section}{$key} = $tmp->{$section}{$key};
            }
        }
    }
    return $opt;
}


sub write_json {
    my ( $file, $h_ref ) = @_;
    my $json = JSON::XS->new->pretty->encode( $h_ref );
    open my $fh, '>', encode( 'locale_fs', $file ) or die $!;
    print $fh $json;
    close $fh;
}


sub read_json {
    my ( $file ) = @_;
    return {} if ! -f encode( 'locale_fs', $file );
    open my $fh, '<', encode( 'locale_fs', $file ) or die $!;
    my $json = do { local $/; <$fh> };
    close $fh;
    my $h_ref = {};
    $h_ref = JSON::XS->new->pretty->decode( $json ) if $json;
    return $h_ref;
}


{ # required if perl < 5.14

package App::DBBrowser::DB;

use warnings;
use strict;
use 5.10.1;

our $VERSION = '0.018';

use Encode       qw( encode decode );
use File::Find   qw( find );
use Scalar::Util qw( looks_like_number );

use DBI;
use Encode::Locale;
use Term::Choose qw( choose );


sub new {
    my ( $class, $info ) = @_;
    return $class if ref $class eq 'App::DBBrowser::DB';
    return bless $info, $class;
}


sub get_db_handle {
    my ( $self, $opt, $db, $user, $passwd ) = @_;
    my $db_driver = $info->{db_driver};
    return if ! defined $db && $db_driver eq 'SQLite';
    my $db_key = $db_driver . '_' . $db;
    my $db_arg = {};
    for my $option ( @{$self->{$db_driver}{options}} ) {
        next if $option =~ /^_/;
        $db_arg->{$option} = $opt->{$db_key}{$option} // $opt->{$db_driver}{$option};
    }
    my $dsn = 'dbi:' . $db_driver . ':dbname=' . $db;
    $dsn .= ';host=' . $db_arg->{host} if $db_arg->{host};
    $dsn .= ';port=' . $db_arg->{port} if $db_arg->{port};
    delete $db_arg->{host};
    delete $db_arg->{port};
    my $dbh = DBI->connect( $dsn, $user, $passwd, {
        PrintError => 0,
        RaiseError => 1,
        AutoCommit => 1,
        ShowErrorStatement => 1,
        %$db_arg,
    } ) or die DBI->errstr;
    if ( $db_driver eq 'SQLite' ) {
        $dbh->sqlite_create_function( 'regexp', 2, sub {
                my ( $regex, $string ) = @_;
                $string //= '';
                return $string =~ m/$regex/ism;
            }
        );
        $dbh->sqlite_create_function( 'truncate', 2, sub {
                my ( $number, $places ) = @_;
                return if ! defined $number;
                return $number if ! looks_like_number( $number );
                return sprintf( "%.*f", $places, int( $number * 10 ** $places ) / 10 ** $places );
            }
        );
        $dbh->sqlite_create_function( 'bit_length', 1, sub {
                my ( $string ) = @_;
                use bytes;
                return length( $string );
            }
        );
        $dbh->sqlite_create_function( 'char_length', 1, sub {
                my ( $string ) = @_;
                return length( $string );
            }
        );
    }
    return $dbh;
}


sub available_databases {
    my ( $self, $dbh, $metadata, @dirs ) = @_;
    my $databases = [];
    if ( $self->{db_driver} eq 'SQLite' ) {
        say 'Searching...';
        for my $dir ( @dirs ) {
            find( {
                wanted     => sub {
                    my $file = $File::Find::name;
                    return if ! -f $file;
                    return if ! -s $file;
                    return if ! -r $file;
                    #say $file;
                    if ( ! eval {
                        open my $fh, '<:raw', $file or die "$file: $!";
                        defined( read $fh, my $string, 13 ) or die "$file: $!";
                        close $fh;
                        push @$databases, decode( 'locale_fs', $file ) if $string eq 'SQLite format';
                        1 }
                    ) {
                        utf8::decode( $@ );
                        print $@;
                    }
                },
                no_chdir   => 1,
            },
            encode( 'locale_fs', $dir ) );
        }
        say 'Ended searching';
    }
    elsif( $self->{db_driver} eq 'Pg' ) {
        my $regexp = [];
        my $stmt = "SELECT datname FROM pg_database";
        if ( ! $metadata ) {
            $regexp = regexp_system( $self, 'database' );
            $stmt .= " WHERE " . join( " AND ", ( "datname !~ ?" ) x @$regexp ) if @$regexp;
        }
        $stmt .= " ORDER BY datname";
        $databases = $dbh->selectcol_arrayref( $stmt, {}, @$regexp );
    }
    elsif( $self->{db_driver} eq 'mysql' ) {
        my $regexp = [];
        my $stmt = "SELECT schema_name FROM information_schema.schemata";
        if ( ! $metadata ) {
            $regexp = regexp_system( $self, 'database' );
            $stmt .= " WHERE " . join( " AND ", ( "schema_name NOT REGEXP ?" ) x @$regexp ) if @$regexp;
        }
        $stmt .= " ORDER BY schema_name";
        $databases = $dbh->selectcol_arrayref( $stmt, {}, @$regexp );
    }
    return $databases;
}


sub info_database {
    my ( $self ) = @_;
    return                      if $self->{db_driver} eq 'SQLite';
    return 'information_schema' if $self->{db_driver} eq 'mysql';
    return 'postgres'           if $self->{db_driver} eq 'Pg';
}


sub regexp_system {
    my ( $self, $level ) = @_;
    if ( $self->{db_driver} eq 'SQLite' ) {
        return                if $level eq 'database';
        return                if $level eq 'schema';
        return [ '^sqlite_' ] if $level eq 'table';
    }
    elsif ( $self->{db_driver} eq 'mysql' ) {
        return [ '^mysql$', '^information_schema$', '^performance_schema$' ] if $level eq 'database';
        return                                                               if $level eq 'schema';
        return                                                               if $level eq 'table';
    }
    elsif ( $self->{db_driver} eq 'Pg' ) {
        return [ '^postgres$', '^template0$', '^template1$' ] if $level eq 'database';
        return [ '^pg_', '^information_schema$' ]             if $level eq 'schema';
        return                                                if $level eq 'table';
    }
    else {
        return;
    }
}


sub get_schema_names {
    my ( $self, $dbh, $db, $metadata ) = @_;
    if ( $self->{db_driver} eq 'SQLite' ) {
        return [ 'main' ];
    }
    elsif ( $self->{db_driver} eq 'Pg' ) {
        my $regexp = [];
        my $stmt = "SELECT schema_name FROM information_schema.schemata";
        if ( ! $metadata ) {
            $regexp = regexp_system( $self, 'schema' );
            $stmt .= " WHERE " . join( " AND ", ( "schema_name !~ ?" ) x @$regexp ) if @$regexp;
        }
        $stmt .= " ORDER BY schema_name";
        my $schemas = $dbh->selectcol_arrayref( $stmt, {}, @$regexp );
        return $schemas;
    }

    else {
        return [ $db ];
    }
}


sub get_table_names {
    my ( $self, $dbh, $schema, $metadata ) = @_;
    my $tables = [];
    if ( $self->{db_driver} eq 'SQLite' ) {
        my $regexp = [];
        my $stmt = "SELECT name FROM sqlite_master WHERE type = 'table'";
        if ( ! $metadata ) {
            $regexp = regexp_system( $self, 'table' );
            $stmt .= " AND " . join( " AND ", ( "name NOT REGEXP ?" ) x @$regexp ) if @$regexp;
        }
        $stmt .= " ORDER BY name";
        $tables = $dbh->selectcol_arrayref( $stmt, {}, @$regexp );
        push @$tables, 'sqlite_master' if $metadata;
    }
    else {
        my $stmt = "SELECT table_name FROM information_schema.tables
                       WHERE table_schema = ?
                       ORDER BY table_name";
                        # AND table_type = 'BASE TABLE'
        $tables = $dbh->selectcol_arrayref( $stmt, {}, ( $schema ) );
    }
    return $tables;
}


sub column_names_and_types {
    my ( $self, $dbh, $db, $schema, $data ) = @_;
    if ( $self->{db_driver} eq 'SQLite' ) {
        for my $table ( @{$data->{$db}{$schema}{tables}} ) {
            my $sth = $dbh->prepare( "SELECT * FROM " . $dbh->quote_identifier( undef, undef, $table ) );
            $data->{$db}{$schema}{col_names}{$table} = $sth->{NAME};
            $data->{$db}{$schema}{col_types}{$table} = $sth->{TYPE};
        }
    }
    else {
        my $stmt;
        if ( $self->{db_driver} eq 'mysql' ) {
            $stmt = "SELECT table_name, column_name, column_type
                        FROM information_schema.columns
                        WHERE table_schema = ?";
        }
        else {
            $stmt = "SELECT table_name, column_name, data_type
                        FROM information_schema.columns
                        WHERE table_schema = ?";
        }
        my $sth = $dbh->prepare( $stmt );
        $sth->execute( $schema );
        while ( my $row = $sth->fetchrow_arrayref() ) {
            my ( $table, $col_name, $col_type ) = @$row;
            push @{$data->{$db}{$schema}{col_names}{$table}}, $col_name;
            push @{$data->{$db}{$schema}{col_types}{$table}}, $col_type;
        }
    }
    return $data;
}


sub primary_and_foreign_keys {
    my ( $self, $dbh, $db, $schema, $data ) = @_;
    my $pk_cols = {};
    my $fks     = {};
    for my $table ( @{$data->{$db}{$schema}{tables}} ) {
        if ( $self->{db_driver} eq 'SQLite' ) {
            for my $c ( @{$dbh->selectall_arrayref( "pragma foreign_key_list( $table )" )} ) {
                $fks->{$table}{$c->[0]}{foreign_key_col}  [$c->[1]] = $c->[3];
                $fks->{$table}{$c->[0]}{reference_key_col}[$c->[1]] = $c->[4];
                $fks->{$table}{$c->[0]}{reference_table} = $c->[2];
            }
        }
        elsif ( $self->{db_driver} eq 'mysql' ) {
            my $stmt = "SELECT constraint_name, table_name, column_name, referenced_table_name,
                               referenced_column_name, position_in_unique_constraint
                           FROM information_schema.key_column_usage
                           WHERE table_schema = ? AND table_name = ? AND referenced_table_name IS NOT NULL";
            my $sth = $dbh->prepare( $stmt );
            $sth->execute( $schema, $table );
            while ( my $row = $sth->fetchrow_hashref ) {
                my $fk_name = $row->{constraint_name};
                my $pos     = $row->{position_in_unique_constraint} - 1;
                $fks->{$table}{$fk_name}{foreign_key_col}  [$pos] = $row->{column_name};
                $fks->{$table}{$fk_name}{reference_key_col}[$pos] = $row->{referenced_column_name};
                if ( ! $fks->{$table}{$fk_name}{reference_table} ) {
                    $fks->{$table}{$fk_name}{reference_table} = $row->{referenced_table_name};
                }
            }
        }
        else {
            my $sth = $dbh->foreign_key_info( undef, undef, undef, undef, $schema, $table );
            if ( defined $sth ) {
                while ( my $row = $sth->fetchrow_hashref ) {
                    my $fk_name = $row->{FK_NAME};
                    push @{$fks->{$table}{$fk_name}{foreign_key_col  }}, $row->{FK_COLUMN_NAME};
                    push @{$fks->{$table}{$fk_name}{reference_key_col}}, $row->{UK_COLUMN_NAME};
                    if ( ! $fks->{$table}{$fk_name}{reference_table} ) {
                        $fks->{$table}{$fk_name}{reference_table} = $row->{UK_TABLE_NAME};
                    }
                }
            }
        }
        $pk_cols->{$table} = [ $dbh->primary_key( undef, $schema, $table ) ];
    }
    return $pk_cols, $fks;
}


sub sql_regexp {
    my ( $self, $quote_col, $not_regexp, $case_sensitive ) = @_;
    if ( $self->{db_driver} eq 'SQLite' ) {
        if ( $not_regexp ) {
            return ' '. $quote_col . ' NOT REGEXP ?';
        }
        else {
            return ' '. $quote_col . ' REGEXP ?';
        }
    }
    elsif ( $self->{db_driver} eq 'mysql' ) {
        if ( $not_regexp ) {
            return ' '. $quote_col . ' NOT REGEXP ?'        if ! $case_sensitive;
            return ' '. $quote_col . ' NOT REGEXP BINARY ?' if   $case_sensitive;
        }
        else {
            return ' '. $quote_col . ' REGEXP ?'            if ! $case_sensitive;
            return ' '. $quote_col . ' REGEXP BINARY ?'     if   $case_sensitive;
        }
    }
    elsif ( $self->{db_driver} eq 'Pg' ) {
        if ( $not_regexp ) {
            return ' '. $quote_col . '::text' . ' !~* ?' if ! $case_sensitive;
            return ' '. $quote_col . '::text' . ' !~ ?'  if   $case_sensitive;
        }
        else {
            return ' '. $quote_col . '::text' . ' ~* ?'  if ! $case_sensitive;
            return ' '. $quote_col . '::text' . ' ~ ?'   if   $case_sensitive;
        }
    }
    elsif ( $self->{db_driver} eq 'oracle' ) {
        if ( $not_regexp ) {
            return ' NOT REGEXP_LIKE(' . $quote_col . ',?,\'i\')' if ! $case_sensitive;
            return ' NOT REGEXP_LIKE(' . $quote_col . ',?)'       if   $case_sensitive;
        }
        else {
            return ' REGEXP_LIKE(' . $quote_col . ',?,\'i\')'     if ! $case_sensitive;
            return ' REGEXP_LIKE(' . $quote_col . ',?)'           if   $case_sensitive;
        }
    }
    die 'No entry for "' . $self->{db_driver} . '"!';
}


sub concatenate {
    my ( $self, $arg ) = @_;
    return 'concat(' . join( ',', @$arg ) . ')' if $self->{db_driver} eq 'mysql';
    return join( ' || ', @$arg );
}


sub col_functions {
    my ( $self, $func, $quote_col, $print_col, $info ) = @_;
    my $db_driver = $self->{db_driver};
    my ( $quote_f, $print_f );
    $print_f = $info->{hidd_func_pr}{$func} . '(' . $print_col . ')';
    if ( $func =~ /^Epoch_to_Date(?:Time)?\z/ ) {
        my $prompt = "$print_f\nInterval:";
        my ( $microseconds, $milliseconds, $seconds ) = (
            '  ****************   Micro-Second',
            '  *************      Milli-Second',
            '  **********               Second' );
        my $choices = [ undef, $microseconds, $milliseconds, $seconds ];
        # Choose
        my $interval = choose(
            $choices,
            { %{$info->{lyt_stmt_v}}, prompt => $prompt }
        );
        return if ! defined $interval;
        my $div = $interval eq $microseconds ? 1000000 :
                  $interval eq $milliseconds ? 1000 : 1;
        if ( $func eq 'Epoch_to_DateTime' ) {
            $quote_f = "FROM_UNIXTIME($quote_col/$div,'%Y-%m-%d %H:%i:%s')"    if $db_driver eq 'mysql';
            $quote_f = "(TO_TIMESTAMP(${quote_col}::bigint/$div))::timestamp"  if $db_driver eq 'Pg';
            $quote_f = "DATETIME($quote_col/$div,'unixepoch','localtime')"     if $db_driver eq 'SQLite';
        }
        else {
            # mysql: FROM_UNIXTIME doesn't work with negative timestamps
            $quote_f = "FROM_UNIXTIME($quote_col/$div,'%Y-%m-%d')"       if $db_driver eq 'mysql';
            $quote_f = "(TO_TIMESTAMP(${quote_col}::bigint/$div))::date" if $db_driver eq 'Pg';
            $quote_f = "DATE($quote_col/$div,'unixepoch','localtime')"   if $db_driver eq 'SQLite';
        }
    }
    elsif ( $func eq 'Truncate' ) {
        my $prompt = "TRUNC $print_col\nDecimal places:";
        my $choices = [ undef, 0 .. 9 ];
        my $precision = choose( $choices, { %{$info->{lyt_stmt_h}}, prompt => $prompt } );
        return if ! defined $precision;
        if ( $db_driver eq 'Pg' ) {
            $quote_f = "TRUNC($quote_col,$precision)";
        }
        else {
            $quote_f = "TRUNCATE($quote_col,$precision)";
        }
    }
    elsif ( $func eq 'Bit_Length' ) {
        $quote_f = "BIT_LENGTH($quote_col)";
    }
    elsif ( $func eq 'Char_Length' ) {
        $quote_f = "CHAR_LENGTH($quote_col)";
    }
    return $quote_f, $print_f;
}


1;

}


__END__



=pod

=encoding UTF-8

=head1 NAME

C<db-browser> - Search and read in SQLite/MySQL/PostgreSQL databases.

=head1 VERSION

Version 0.018

=cut

=head1 SYNOPSIS

=head2 SQLite/MySQL/PostgreSQL

    db-browser

    db-browser -h|--help

When the C<db-browser> is called with the argument C<-h|--help> it shows a menu - see L</Options>.

=head2 SQLite

    db-browser [-s|--search] [directories to be searched]

If no directories are passed the home directory is searched for SQLite databases.

C<db-browser> called with C<-s|--search> causes a new search of SQLite databases instead of using the cached data.

=head1 DESCRIPTION

Search and read in SQLite/MySQL/PostgreSQL databases.

To be able to browse database-, schema- and table-lists and the content of tables one needs to have the database
privileges required for fetching the respective data.

The C<db-browser> expects an existing home directory with read and write permissions for the user of C<db-browser>.

It is recommended to work with an UTF encoding. Non mappable characters might break the output.

It is needed a terminal with a width of at least 40 print columns.

=head1 USAGE

The best way to find out how C<db-browser> works is calling C<db-browser>.

To be able to use all the features of the C<db-browser> some basic SQL knowledge is required.

=head3 Keys to move around

=over

=item *

the C<Arrow> keys (or C<hjkl>) to move up and down respective to move right and left.

=item *

the C<PageUp> key (or C<Ctrl-B>) to go back one page, the C<PageDown> key (or C<Ctrl-F>) to go forward one page.

=item *

the C<Home> key (or C<Ctrl-A>) to jump to the beginning of the menu, the C<End> key (or C<Ctrl-E>) to jump to the end
of the menu.

=item *

the C<Enter/Return> key to confirm a chosen menu item.

=back

With the option I<mouse mode> enabled it can be used the mouse with the left mouse key to navigate through the menus.

The C<q> key goes back (C<Ctrl-D> instead of C<q> if prompted for a string) in the menu hierarchy.

=head2 OPTIONS

=head3 HELP

Show this Info.

=head3 Path

Shows the path of the running C<db-browser> and the application directory.

=head3 Tabwidth

Set the number of spaces between columns.

=head3 Colwidth

Set the width the columns should have at least when printed.

=head3 Undef

Set the string that will be shown on the screen instead of an undefined field.

=head3 Max Rows

Set the maximum number of fetched table rows. This can be overwritten by setting a SQL LIMIT statement.

The fetched table rows are kept in memory.

To disable the automatic limit set I<max rows> to 0.

=head3 ProgressBar

Set the progress bar threshold. If the number of fields (rows x columns) is higher than the threshold a progress bar is
shown while preparing the data for the output.

=head3 DB Drivers

Choose the needed database drivers.

=head3 Operators

Choose the needed operators.

=head3 Parentheses

Enable parentheses in WHERE and/or HAVING TO clauses.

=head3 Sssc Mode

With the I<Sssc> mode "compat" enabled back-arrows are offered in the SQL menus entries. In the "simple" mode it can
be used the C<q> key instead of the back-arrows.

To reset a SQL "sub-statement" (e.g WHERE) re-enter into the respective menu entry and choose C<'- OK -'>.

=head3 Expand

Set the behavior of different menus.

For the menus "Choose Database", "Choose Schema" and "Choose Table" setting I<Expand> to "Enchanted" means: save the
menu position.

For "Print Table" "Enchanted" means: enable printing the chosen table row.

=head3 Mouse Mode

Set the I<mouse mode> (see L<Term::Choose/mouse>).

=head3 Lock

Set the default I<lock> value:

- Lk0: Reset the SQL-statement after each "PrintTable".

- Lk1: Reset the SQL-statement only when a table is selected.

=head3 Metadata

If enabled system tables/schemas/databases are appended to the respective list.

=head3 Regexp Case

If enabled REGEXP will match case sensitive.

With MySQL the sensitive match is achieved by enabling the BINARY operator.

=head3 DB Login

Determine when C<db-browser> asks for the login data:

- per-DB: log in data is asked once per database.

- once: log in data is asked only once and then used for all connections.

This option has no meaning if the SQLite driver is in use.

=head3 ENV DBI

- use C<DBI_USER> as username for all database logins.

- use C<DBI_PASS> as password for all database logins.

This option has no meaning if the SQLite driver is in use.

=head3 DB Defaults

Set Database defaults:

=head4 See if it's a number

If set to "YES" L<DBD::SQLite> tries to see if the bind values are number or not, and does not quote if they are
numbers.

See L<DBD::SQLite> for details. This is a SQLite-only option.

=head4 Enable utf8

If enabled the utf8 flag will be turned on for character data coming from the database.

For a more driver specific explanation see the documentation of the respective Perl DBI driver.

With the C<Pg> driver in use it is available apart the "YES/NO" choices also a "AUTO" choice. If the L<DBD::Pg> version
is less than 3.0.0 "AUTO" is mapped to "YES". For the meaning of "AUTO" look in
L<pg_enable_utf8|https://metacpan.org/pod/DBD::Pg#pg_enable_utf8-integer> for the value -1.

=head4 "Binary Filter"

Print "BNRY" instead of arbitrary binary data.

If the data matches the repexp C</[\x00-\x08\x0B-\x0C\x0E-\x1F]/> it is considered arbitrary binary data.

Printing arbitrary binary data could break the output.

Database defaults can be overwritten for each Database with the "Database settings".

=head1 HINTS

The following hints refer to the SQL menu - the menu which opens after a table was selected.

If C<AGGREGATE> or/and C<GROUP BY> is set, the C<SELECT> statement is automatically formed.

Changing the lock mode I<Lk[01]> will also reset the entire SQL.

=head3 Scalar functions

There is a hidden menu entry: selecting the prompt "Customize:" makes available some scalar functions:

=head4 Epoch_to_DateTime

=head4 Epoch_to_Date

=head4 Truncate

With SQLite the function C<TRUNCATE> returns stringified values:

- when comparing in C<WHERE> or C<HAVING TO> clauses with numbers take the non-truncated (original) value for the
comparison if C<sqlite_see_if_its_a_number> is enabled (default).

- also to get a numeric comparison in an C<ORDER BY> clause use the non-truncated (original) values for the ordering.

=head4 Bit_Length

=head4 Char_Length

To remove a chosen scalar function from a column select the column with the function a second time.

=head1 AUTHOR

Matthäus Kiem <cuer2s@gmail.com>

=head1 CREDITS

Thanks to the L<Perl-Community.de|http://www.perl-community.de> and the people form
L<stackoverflow|http://stackoverflow.com> for the help.

=head1 LICENSE AND COPYRIGHT

Copyright 2012-2014 Matthäus Kiem.

This program is free software; you can redistribute it and/or modify it under the same terms as Perl 5.10.0. For
details, see the full text of the licenses in the file LICENSE.

=cut
