The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
#!/usr/bin/env perl -w

use strict;
use File::Temp ();
use Jifty::DBI::Handle;

=head1 VARIABLES

=head2 @supported_drivers

Array of all supported DBD drivers.

=cut

our @supported_drivers = Jifty::DBI::Handle->supported_drivers;

=head2 @available_drivers

Array that lists only drivers from supported list
that user has installed.

=cut

our @available_drivers = Jifty::DBI::Handle->available_drivers;

=head1 FUNCTIONS

=head2 get_handle

Returns new DB specific handle. Takes one argument DB C<$type>.
Other arguments uses to construct handle.

=cut

sub get_handle
{
        my $type = shift;
        my $class = 'Jifty::DBI::Handle::'. $type;
        eval "require $class";
        die $@ if $@;
        my $handle;
        $handle = $class->new( @_ );
        return $handle;
}

=head2 handle_to_driver

Returns driver name which gets from C<$handle> object argument.

=cut

sub handle_to_driver
{
        my $driver = ref($_[0]);
        $driver =~ s/^.*:://;
        return $driver;
}

=head2 connect_handle

Connects C<$handle> object to DB.

=cut

sub connect_handle
{
        my $call = "connect_". lc handle_to_driver( $_[0] );
        return unless defined &$call;
        goto &$call;
}

=head2 connect_handle_with_driver($handle, $driver)

Connects C<$handle> using driver C<$driver>; can use this to test the
magic that turns a C<Jifty::DBI::Handle> into a C<Jifty::DBI::Handle::Foo>
on C<Connect>.

=cut

sub connect_handle_with_driver
{
        my $call = "connect_". lc $_[1];
        return unless defined &$call;
        @_ = $_[0];
        goto &$call;
}


our $SQLITE_FILENAME;
sub connect_sqlite
{
        my $handle = shift;
        (undef, $SQLITE_FILENAME ) = File::Temp::tempfile();
        return $handle->connect(
                driver => 'SQLite',
                database => $SQLITE_FILENAME);
}

sub connect_mysql
{
        my $handle = shift;
        return $handle->connect(
                driver => 'mysql',
                database => $ENV{'JDBI_TEST_MYSQL'},
                user => $ENV{'JDBI_TEST_MYSQL_USER'} || 'root',
                password => $ENV{'JDBI_TEST_MYSQL_PASS'} || '',
        );
}

sub connect_pg
{
        my $handle = shift;
        return $handle->connect(
                driver => 'Pg',
                database => $ENV{'JDBI_TEST_PG'},
                user => $ENV{'JDBI_TEST_PG_USER'} || 'postgres',
                password => $ENV{'JDBI_TEST_PG_PASS'} || '',
        );
}

sub connect_oracle
{
        my $handle = shift;
        return $handle->Connect(
                driver   => 'Oracle',
#                database => $ENV{'JDBI_TEST_ORACLE'},
                user     => $ENV{'JDBI_TEST_ORACLE_USER'} || 'test',
                password => $ENV{'JDBI_TEST_RACLE_PASS'} || 'test',
        );
}

=head2 disconnect_handle

Disconnects C<$handle> object.

=cut

sub disconnect_handle
{
        my $call = "disconnect_". lc handle_to_driver( $_[0] );
        return unless defined &$call;
        goto &$call;
}

=head2 disconnect_handle_with_driver($handle, $driver)

Disconnects C<$handle> using driver C<$driver>.

=cut

sub disconnect_handle_with_driver
{
        my $call = "disconnect_". lc $_[1];
        return unless defined &$call;
        @_ = $_[0];
        goto &$call;
}

sub disconnect_sqlite
{
        my $handle = shift;
        $handle->disconnect;
        unlink $SQLITE_FILENAME;
}

sub disconnect_mysql
{
        my $handle = shift;
        $handle->disconnect;

        # XXX: is there something we should do here?
}

sub disconnect_pg
{
        my $handle = shift;
        $handle->disconnect;

        # XXX: is there something we should do here?
}

=head2 should_test $driver

Checks environment for C<JDBI_TEST_*> variables.
Returns true if specified DB back-end should be tested.
Takes one argument C<$driver> name.

=cut

sub should_test
{
        my $driver = shift;
        return 1 if lc $driver eq 'sqlite';
        my $env = 'JDBI_TEST_'. uc $driver;
        return $ENV{$env};
}

=head2 has_schema $class { $driver | $handle }

Returns method name if C<$class> has schema for C<$driver> or C<$handle>.
If second argument is handle object then checks also for DB version
specific schemas, for example for MySQL 4.1.23 this function will check
next methods in the C<$class>: C<schema_mysql_4_1_23>, C<schema_mysql_4_1>,
C<schema_mysql_4> and C<schema_mysql>, but if second argument is C<$driver>
name then checks only for C<schema_mysql>.

Returns empty value if couldn't find method.

=cut

sub has_schema
{
        my ($class, $driver) = @_;
        unless( UNIVERSAL::isa( $driver, 'Jifty::DBI::Handle' ) ) {
                my $method = 'schema_'. lc $driver;
                $method = '' unless UNIVERSAL::can( $class, $method );
                return $method;
        } else {
                my $ver = $driver->database_version;
                return has_schema( $class, handle_to_driver( $driver ) ) unless $ver;

                my $method = 'schema_'. lc handle_to_driver( $driver );
                $ver =~ s/-.*$//;
                my @nums = grep $_, map { int($_) } split /\./, $ver;
                while( @nums ) {
                        my $m = $method ."_". join '_', @nums;
                        return $m if( UNIVERSAL::can( $class, $m ) );
                        pop @nums;
                }
                return has_schema( $class, handle_to_driver( $driver ) );
        }
}

=head2 init_schema

Takes C<$class> and C<$handle> or C<$driver> and inits schema
by calling method C<has_schema> returns of the C<$class>.
Returns last C<DBI::st> on success or last return value of the
SimpleQuery method on error.

=cut

sub init_schema
{
        my ($class, $handle) = @_;
        my $call = has_schema( $class, $handle );
        diag( "using '$class\:\:$call' schema for ". handle_to_driver( $handle ) ) if $ENV{TEST_VERBOSE};
        my $schema = $class->$call();
        $schema = ref( $schema )? $schema : [$schema];
        my $ret;
        foreach my $query( @$schema ) {
                $ret = $handle->simple_query( $query );
                return $ret unless UNIVERSAL::isa( $ret, 'DBI::st' );
        }
        return $ret;
}

=head2 cleanup_schema

Takes C<$class> and C<$handle> and cleanup schema by calling
C<cleanup_schema_$driver> method of the C<$class> if method exists.
Always returns undef.

=cut

sub cleanup_schema
{
        my ($class, $handle) = @_;
        my $call = "cleanup_schema_". lc handle_to_driver( $handle );
        return unless UNIVERSAL::can( $class, $call );
        my $schema = $class->$call();
        $schema = ref( $schema )? $schema : [$schema];
        foreach my $query( @$schema ) {
                eval { $handle->simple_query( $query ) };
        }
}

=head2 init_data

Takes a class to get data from and the handle, calls C<init_data>
method in the class, result is used to create new records of that
class. First row is used for columns names.

Example:

    init_data('TestApp::User', $handle);

    ...

    package TestApp::User;
    sub init_data { return (
        ['name', 'email'],

        ['ruz', 'ruz@localhost'],
        ...
    ) }

=cut

sub init_data
{
        my ($class, $handle) = @_;
        my @data = $class->init_data();
        my @columns = @{ shift @data };
        my $count = 0;
        foreach my $values ( @data ) {
                my %args;
                for( my $i = 0; $i < @columns; $i++ ) {
                        $args{ $columns[$i] } = $values->[$i];
                }
                my $rec = $class->new( handle => $handle );
                my $id = $rec->create( %args );
                die "Couldn't create record" unless $id;
                $count++;
        }
        return $count;
}

=head2 drop_table_if_exists

Takes a table name and handle. Drops the table in the DB if it exists.
Returns nothing interesting, shouldn't die.

=cut

sub drop_table_if_exists {
    my ($table, $handle) = @_;
    my $d = handle_to_driver( $handle );
    if ( $d eq 'Pg' ) {
        my ($exists) = $handle->dbh->selectrow_array(
            "select 1 from pg_tables where tablename = ?", undef, $table
        );
        $handle->simple_query("DROP TABLE $table") if $exists;
    }
    else {
        local $@;
        eval { $handle->simple_query("DROP TABLE IF EXISTS $table") };
    }
    return;
}

1;