The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package DBIx::Table::TestDataGenerator::TableProbe::SQLite;
use Moo;
use Moo::Role;

use strict;
use warnings;

use Carp;

use DBI qw(:sql_types);
use List::MoreUtils qw ( any );

use DBIx::Table::TestDataGenerator;

use Readonly;
Readonly my $COMMA         => q{,};
Readonly my $QUESTION_MARK => q{?};

with 'DBIx::Table::TestDataGenerator::TableProbe';

sub column_names {
    my ($self) = @_;
    my $sql = <<"END_SQL";
PRAGMA table_info('${\$self->table}')
END_SQL
    my $sth = $self->dbh->prepare($sql);
    $sth->execute();

    my @columns;
    while ( my @row = $sth->fetchrow_array ) {
        push @columns, $row[1];
    }
    return \@columns;
}

sub num_roots {
    my ( $self, $pkey_col, $parent_pkey_col ) = @_;
    my $table = $self->table;

    #note: SQLiteQL ignores NULL when counting values!
    #Therefore we use Coalesce to first replace NULL values by 0
    my $sql = <<"END_SQL";
SELECT COUNT($parent_pkey_col)
FROM $table
WHERE $pkey_col = $parent_pkey_col OR $parent_pkey_col IS NULL
END_SQL
    my $sth = $self->dbh->prepare($sql);
    $sth->execute();
    return ( $sth->fetchrow_array() )[0];
}

sub seed {

    #do nothing, see documentation
    return;
}

sub random_record {
    my ( $self, $table, $colname_list ) = @_;
    my $sql = <<"END_SQL";
SELECT $colname_list
FROM $table
ORDER BY RANDOM() LIMIT 1
END_SQL
    return $self->dbh->selectrow_hashref($sql);
}

{
    my @num_types = qw(INTEGER REAL);
    my @chr_types = qw( TEXT );

    #TODO: no date data type in SQLite, to handle dates one needs to
    #parse columns of type TEXT
    my @date_types = qw();

    sub get_incrementor {
        my ( $self, $type, $max ) = @_;
        if ( any { $type eq $_ } @num_types ) {
            return sub { return ++$max };
        }
        if ( any { $type eq $_ } @chr_types ) {
            my $i      = 0;
            my $suffix = 'A' x $max;
            return sub {
                return $suffix . $i++;
                }
        }
        croak
            "I do not know how to increment unique constraint column of type $type";
    }
}

sub get_type_preference_for_incrementing {
    return [ 'INTEGER', 'REAL', 'TEXT' ];
}

sub unique_columns_with_max {
    my ( $self, $get_pkey_columns ) = @_;

    my $sql;
    my $dbh        = $self->dbh;
    my $table_name = $self->table;

    my %uniq_col_info;

    if ($get_pkey_columns) {

        #the name does not matter, 'pkey' is o.k. since there is
        #only one primary key
        my @pkey_col_names = $dbh->primary_key( undef, undef, $table_name );
        $uniq_col_info{'pkey'} = {} if @pkey_col_names > 0;

        for my $col (@pkey_col_names) {
            my $data_type = $self->_get_data_type( $col, $table_name );
            my $max_val = $self->_get_max( $data_type, $col, $table_name );

            $uniq_col_info{'pkey'}->{$data_type} ||= [];
            push @{ $uniq_col_info{'pkey'}->{$data_type} },
                [ $col, $max_val ];
            last;
        }

    }    # end of primary key handling
    else {

        #unique constraint handling
        my $sth = $self->dbh->prepare("PRAGMA index_list($table_name)");
        $sth->execute();
        while ( my @row = $sth->fetchrow_array() ) {
            my ( $index_name, $is_unique_index ) = ( $row[1], $row[2] );
            next unless $is_unique_index;
            $uniq_col_info{$index_name} = {};

            #determine column names in unique index
            my $sth1 = $self->dbh->prepare("PRAGMA index_info($index_name)");
            $sth1->execute();
            while ( my @row1 = $sth1->fetchrow_array() ) {
                my $col = $row1[2];
                my $data_type = $self->_get_data_type( $col, $table_name );
                my $max_val =
                    $self->_get_max( $data_type, $col, $table_name );

                $uniq_col_info{$index_name}->{$data_type} ||= [];
                push @{ $uniq_col_info{$index_name}->{$data_type} },
                    [ $col, $max_val ];
            }
        }
    }
    return \%uniq_col_info;
}

sub _get_max {
    my ( $self, $data_type, $col, $table_name ) = @_;
    my %max_expr = (
        'INTEGER' => "MAX($col)",
        'REAL'    => "MAX($col)",
        'TEXT'    => "MAX(LENGTH($col))",
    );
    my $max_sql = <<"END_SQL";
SELECT $max_expr{$data_type}
FROM $table_name
END_SQL
    my $max_sth = $self->dbh->prepare($max_sql);
    $max_sth->execute();

    return ( $max_sth->fetchrow_array() )[0];
}

{
    my %data_type = ();

    sub _get_data_type {
        my ( $self, $col_name, $table_name ) = @_;
        unless ( keys %data_type ) {
            my $sth = $self->dbh->prepare("PRAGMA table_info($table_name)");
            $sth->execute();
            while ( my @row = $sth->fetchrow_array() ) {
                $data_type{ $row[1] } = $row[2];
            }
        }
        return $data_type{$col_name};
    }
}

#returns a ref to an array of array refs, where each of the latter
#is the metadata information for a single column constrained by a
#foreign key constraint. The meaning of these elements is as
#follows:
#   0. fkey id
#   1. referencing column id in current fkey
#   2. referenced table name
#   3. referencing column name
#   4. referenced column name
#   5. ON UPDATE action
#   6. ON DELETE action
#   7. match (?), always has value "NONE"
#We are only concerned with columns 0, 2, 3 and 4 here.
sub _get_foreign_key_info {
    my ( $self, $table_name ) = @_;
    my @foreign_key_info;
    my $sth = $self->dbh->prepare("PRAGMA foreign_key_list($table_name)");
    $sth->execute();
    while ( my @row = $sth->fetchrow_array ) {
        push @foreign_key_info, \@row;
    }
    return \@foreign_key_info;
}

sub fkey_name_to_fkey_table {
    my ($self) = @_;
    my $table_name = $self->table;

    my %fkey_tables;

    my @foreign_key_info = @{ $self->_get_foreign_key_info($table_name) };

    #there is a record for each column of each foreign key constraint, so
    #there are duplicates in the following assignment, but this does not
    #hurt
    for my $col_info (@foreign_key_info) {
        $fkey_tables{ @{$col_info}[0] } = @{$col_info}[2];
    }
    return \%fkey_tables;
}

sub fkey_referenced_cols_to_referencing_cols {
    my ($self) = @_;
    my $table_name = $self->table;

    my @foreign_key_info = @{ $self->_get_foreign_key_info($table_name) };

    my %all_refcol_to_col_dict;

    for my $col_info (@foreign_key_info) {
        my ( $fkey, $ref_col, $cons_col ) = @{$col_info}[ 0, 4, 3 ];
        if ( !defined $all_refcol_to_col_dict{$fkey} ) {
            $all_refcol_to_col_dict{$fkey} = {};
        }
        ${ $all_refcol_to_col_dict{$fkey} }{$ref_col} = $cons_col;
    }
    return \%all_refcol_to_col_dict;
}

sub fkey_referenced_cols {
    my ( $self, $fkey_tables ) = @_;

    my @foreign_key_info = @{ $self->_get_foreign_key_info( $self->table ) };

    my %all_refcol_lists;

    foreach ( keys %{$fkey_tables} ) {
        my $fkey = $_;
        my @ref_col_list;

        for my $col_info (@foreign_key_info) {
            my ( $fkey1, $ref_col ) = @{$col_info}[ 0, 4 ];
            next unless $fkey1 eq $fkey;
            push @ref_col_list, $ref_col;
        }
        my @ref_cols = join ', ', @ref_col_list;
        $all_refcol_lists{$fkey} = \@ref_cols;
    }

    return \%all_refcol_lists;
}

sub get_self_reference {
    my ( $self, $fkey_tables, $pkey_col_name ) = @_;
    my $table_name = $self->table;

    my %all_refcol_to_col_dict =
        %{ $self->fkey_referenced_cols_to_referencing_cols() };

    my @self_ref_info;

    for my $fkey ( keys %all_refcol_to_col_dict ) {

        #ignore fkeys pointing to other tables than the target table
        next unless $fkey_tables->{$fkey} eq $table_name;

        #ignore fkeys involving more than one column
        my %dict = %{ $all_refcol_to_col_dict{$fkey} };
        next unless keys %dict == 1;

        #check that name of referenced column is name of primary key column
        if ( ( keys %dict )[0] eq $pkey_col_name ) {
            @self_ref_info = ( $fkey, ( values %dict )[0] );
        }
    }

    return \@self_ref_info;
}

sub selfref_tree {
    my ( $self, $key_col, $parent_refkey_col ) = @_;
    my $table_name = $self->table;
    my $sql        = <<"END_SQL";
SELECT t.$key_col, t1.$key_col
FROM $table_name t LEFT OUTER JOIN $table_name t1
ON t.$parent_refkey_col = t1.$key_col;

END_SQL

    my %tree;
    my $sth = $self->dbh->prepare($sql);
    $sth->execute();
    while ( my ( $id, $parent_id ) = $sth->fetchrow_array() ) {
        if ( defined $tree{$parent_id} ) {
            push @{ $tree{$parent_id} }, $id;
        }
        else {
            $tree{$parent_id} = [$id];
        }
    }
    return \%tree;
}

1;    # End of DBIx::Table::TestDataGenerator::TableProbe::SQLite

__END__

=pod

=head1 NAME

DBIx::Table::TestDataGenerator::TableProbe::SQLite - SQLite (meta)data provider

=head1 DESCRIPTION

This module impersonates the TableProbe role to provide SQLite support.

=head1 SUBROUTINES/METHODS

For general comments about the TableProbe role methods, see the documentation of L<TableProbe|DBIx::Table::TestDataGenerator::TableProbe>.

=head2 seed

The random number generation of SQLite does not provide a method to seed it (yet), so this method does nothing.

=head2 fkey_name_to_fkey_table

In the case of SQLite, the foreign key constraints do not have names, but they have integer ids starting at 0. We use these integers as foreign key names.

=head1 AUTHOR

Jos\x{00E9} Diaz Seng, C<< <josediazseng at gmx.de> >>

=head1 LICENSE AND COPYRIGHT

Copyright 2012 Jos\x{00E9} Diaz Seng.

This program is free software; you can redistribute it and/or modify it
under the terms of either: the GNU General Public License as published
by the Free Software Foundation; or the Artistic License.

See http://dev.perl.org/licenses/ for more information.