The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
# -*-perl-*-

# $Id: 30_dbi.t,v 3.5 2003/05/10 19:22:41 lachoy Exp $

# Note that this is a good way to see if certain databases support the
# type checking methods of the DBI -- in fact, we might want to add
# some date/datetime items in the table as well to see what happens

use strict;
use Data::Dumper qw( Dumper );

use constant NUM_TESTS       => 62;
use constant TEST_TABLE_NAME => 'spops_test';

my $SPOPS_CLASS = 'DBITest';
my @ID_LIST     = ( 42, 1792, 1588 );

my ( $db, $do_end );

END {
    cleanup( $db, TEST_TABLE_NAME ) if ( $do_end );
 }

# Table definition, just for reference
# CREATE TABLE foo (
#    spops_id    int not null primary key,
#    spops_name  char(20),
#    spops_goop  char(20) not null,
#    spops_num   int default 2
# )

{
    # Grab our DBI routines and be sure we're supposed to run.

    do "t/dbi_config.pl";

    my $config = test_dbi_run();

    $do_end++;

    require Test::More;
    Test::More->import( tests => NUM_TESTS );

    # Ensure we can get to SPOPS::Initialize
    require_ok( 'SPOPS::Initialize' );

    my $driver_name = $config->{DBI_driver};
    my $spops_dbi_driver = get_spops_driver( $config, $driver_name );

    # Create the class using SPOPS::Initialize

    my $spops_config = {
        tester => {
           class        => $SPOPS_CLASS,
           isa          => [ $spops_dbi_driver, 'SPOPS::DBI' ],
           field        => [ qw/ spops_id spops_name spops_goop spops_num / ],
           id_field     => 'spops_id',
           skip_undef   => [ 'spops_num' ],
           sql_defaults => [ 'spops_num' ],
           base_table   => TEST_TABLE_NAME,
           table_name   => TEST_TABLE_NAME,
        },
    };
    my $class_init_list = eval {
        SPOPS::Initialize->process({ config => $spops_config })
    };
    ok( ! $@, 'Initialize process run' );
    is( $class_init_list->[0], $SPOPS_CLASS, 'Initialize class' );

    check_dbd_compliance( $config, $driver_name, $SPOPS_CLASS );

    # Check that class was initialized ok

    is( scalar @{ $SPOPS_CLASS->field_list },
        scalar @{ $spops_config->{tester}{field} },
        "Class initialize set 'field_list' property" );
    is( $SPOPS_CLASS->table_name, TEST_TABLE_NAME,
        "Class initialize set 'table_name' property" );

    # Create a database handle and create our testing table

    $db = get_db_handle( $config );
    create_table( $db, 'simple', TEST_TABLE_NAME );
    my $sql_data_types = get_sql_types( $db, TEST_TABLE_NAME, $driver_name );

    # See whether we get back the right information for various
    # configuration items

    {
        my $base_id_field = $SPOPS_CLASS->id_field;
        my ( $id_field ) = $SPOPS_CLASS->id_field_select;
        is( $id_field, TEST_TABLE_NAME . ".$base_id_field", "ID field for SELECT" );
        my ( $nq_id_field ) = $SPOPS_CLASS->id_field_select({ noqualify => 1 });
        is( $nq_id_field, $base_id_field, "ID field for SELECT (not qualified)" );

        my $id_for_clause = 45;
        my $data_type = $sql_data_types->{ $base_id_field };
        my $quoted = $db->quote( $id_for_clause, $data_type );
        my $id_clause = $SPOPS_CLASS->id_clause( $id_for_clause, undef,
                                                 { db => $db } );
        is( $id_clause, TEST_TABLE_NAME . ".$base_id_field = $quoted", "ID clause" );
        my $nq_id_clause = $SPOPS_CLASS->id_clause( $id_for_clause, 'noqualify',
                                                    { db => $db } );
        #warn "Datatype: [$data_type]; Clauses: [$id_clause] [$nq_id_clause]\n";
        is( $nq_id_clause, "$base_id_field = $quoted", "ID clause" );
    }

    # Create an object

    {
        my $obj = eval { $SPOPS_CLASS->new({ spops_name => 'MyProject',
                                             spops_goop => 'oopie doop',
                                             spops_num  => 241,
                                             spops_id   => 42 } ) };
        ok( ! $@, 'Create object' );

        # Save the object

        eval { $obj->save({ is_add => 1, db => $db, skip_cache => 1 }) };
        ok( ! $@, 'Save object (create)' );
        if ( $@ ) {
            warn "Error saving object: $@\n", Dumper( SPOPS::Error->get ), "\n";
        }
    }

    # Try to fetch an object with an empty ID

    {
        my $obj = eval { $SPOPS_CLASS->fetch( '', { db => $db, skip_cache => 1 }) };
        ok( ! $@, 'Fetch object (empty ID)' );
        is( $obj, undef, 'Fetched object with empty ID is undef' );

        my $obj_u = eval { $SPOPS_CLASS->fetch( undef, { db => $db, skip_cache => 1 }) };
        ok( ! $@, 'Fetch object (undef ID)' );
        is( $obj, undef, 'Fetched object with undef ID is undef' );
    }

    # Fetch an object, then update it

    {
        my $obj = eval { $SPOPS_CLASS->fetch( 42, { db => $db, skip_cache => 1 } ) };
        ok( ! $@, 'Fetch object (perform)' );
        if ( $@ ) {
            warn "Cannot fetch object: $@\n", Dumper( SPOPS::Error->get ), "\n";
        }

        ok( $obj->{spops_name} eq 'MyProject', 'Fetch object (correct data)' );

        $obj->{spops_name} = 'TheirProject';
        $obj->{spops_goop} = 'over there';
        eval { $obj->save({ db => $db, skip_cache => 1 }) };
        ok( ! $@, 'Save object (update)' );
        if ( $@ ) {
            warn "Cannot update object: $@\n", Dumper( SPOPS::Error->get ), "\n";
        }

        my $new_obj = eval { $SPOPS_CLASS->fetch( 42, { db => $db, skip_cache => 1 } ) };
        ok( $new_obj->{spops_name} eq $obj->{spops_name}, 'Fetch object (after update)' );
    }

    # Fetch an object then clone it and save it

    {
        my $obj     = eval { $SPOPS_CLASS->fetch( 42, { db => $db, skip_cache => 1 } ) };
        my $new_obj = eval { $obj->clone({ spops_name => 'YourProject',
                                           spops_goop => 'this n that',
                                           spops_id   => 1792 } ) };
        ok( ! $@, 'Clone object (perform)' );
        ok( $new_obj->{spops_name} ne $obj->{spops_name}, 'Clone object (correct data)');

        eval { $new_obj->save( { is_add => 1, db => $db, skip_cache => 1 } ) };
        ok( ! $@, 'Save object (create, after clone)' );
        if ( $@ ) {
            warn "Cannot save object: $@\n", Dumper( SPOPS::Error->get ), "\n";

        }
    }

    # Create another object, but this time don't define the spops_num
    # field and see if the default comes through. Also pass along an
    # 'insert_alter' statement and see if it worked.

    {
        my $obj = $SPOPS_CLASS->new({ spops_id   => 1588,
                                      spops_goop => 'here we go!',
                                      spops_name => 'AnotherProject' });
        eval { $obj->save({ is_add => 1, db => $db, skip_cache => 1,
                            insert_alter => { spops_goop => "'added -- %s'" } }) };
        ok( ! $@, 'Insert object with default data unspecified' );
        is( $obj->{spops_num}, 2, 'Fetch object (correct data with default)' );

        my $redo_obj = eval { $SPOPS_CLASS->fetch( $obj->id,
                                                   { db => $db, skip_cache => 1 } ) };
        is( $redo_obj->{spops_goop}, "added -- here we go!", 'Insert alter' );
    }

    # Fetch one of the above objects, update by hand one of the values
    # in the table and then refetch that field to see if it works

    {
        my $obj = eval { $SPOPS_CLASS->fetch( 42, { db => $db,
                                                    skip_cache => 1 } ) };
        my $orig = $obj->{spops_name};
        my $new_set  = "Changed for refetch";
        my $sql = 'UPDATE ' . TEST_TABLE_NAME .
                  '   SET spops_name = ? ' .
                  ' WHERE ' . $obj->id_clause( undef, undef, { db => $db }) ;
        my ( $sth );
        eval {
            $sth = $db->prepare( $sql );
            $sth->execute( $new_set );
        };
        ok( ! $@, 'Update for refetch' );
        my $new_return = $obj->refetch( 'spops_name', { db => $db } );
        is( $obj->{spops_name}, $new_set, 'Refetched field match' );
        is( $new_return, $new_set, 'Refetched and returned field match' );
    }

    # Fetch one of the above objects, then update only one field

    {
        my $obj = eval { $SPOPS_CLASS->fetch( 42, { db => $db,
                                                    skip_cache => 1 } ) };
        my $changed = 'One field update';
        $obj->{spops_name} = $changed;
        my $rv = eval { $obj->field_update( 'spops_name', { db => $db } ) };
        diag( "[RV $rv] $@" ) if ( $@ );
        ok( $rv && ! $@, 'Field update (single) execution' );
        is( $obj->{spops_name}, $changed, 'Field update (single) internal match' );
        my $redo_obj = eval { $SPOPS_CLASS->fetch( 42, { db => $db,
                                                         skip_cache => 1 } ) };
        is( $redo_obj->{spops_name}, $obj->{spops_name}, 'Field update (single) external match' );
    }

    # Now try to do a field update with multiple fields
    {
        my $obj = eval { $SPOPS_CLASS->fetch( 42, { db => $db,
                                                    skip_cache => 1 } ) };
        my $changed_text = 'Multi field update';
        my $changed_num  = 1066;
        my $rv = eval { $obj->field_update({ spops_name => $changed_text,
                                             spops_num  => $changed_num },
                                          { db => $db } ) };
        warn $@ if ( $@ );
        ok( $rv && ! $@, 'Field update (multiple) execution' );
        is( $obj->{spops_name}, $changed_text, 'Field update (multiple) internal match' );
        is( $obj->{spops_num}, $changed_num, 'Field update (multiple) internal match' );
        my $redo_obj = eval { $SPOPS_CLASS->fetch( 42, { db => $db,
                                                         skip_cache => 1 } ) };
        is( $redo_obj->{spops_name}, $obj->{spops_name}, 'Field update (multiple) external match' );
        is( $redo_obj->{spops_num}, $obj->{spops_num}, 'Field update (multiple) external match' );
    }

    # Now do a field update with multiple objects (class-level call)
    {
        my $changed_text = 'Multi field update';
        my $rv = eval { $SPOPS_CLASS->field_update( { spops_name => $changed_text },
                                                    { db         => $db,
                                                      where      => 'spops_num > 0' } ) };
        warn $@ if ( $@ );
        ok( $rv, 'Field update (multiple object) execution' );
        my $obj_list = eval { $SPOPS_CLASS->fetch_group({ db => $db, skip_cache => 1 }) };
        warn $@ if ( $@ );
        is( $obj_list->[0]->{spops_name}, $changed_text, 'Field update (multiple object 1) match' );
        is( $obj_list->[1]->{spops_name}, $changed_text, 'Field update (multiple object 2) match' );
        is( $obj_list->[2]->{spops_name}, $changed_text, 'Field update (multiple object 3) match' );
    }

    # Try a class-level field update where we match a single object

    {
        my $changed = 'Class level update';
        my $rv = eval { $SPOPS_CLASS->field_update( { spops_name => $changed },
                                                    { where => 'spops_num = 1066',
                                                      db    => $db } ) };
        warn $@ if ( $@ );
        ok( $rv, 'Field update (multiple object) execution with one match' );
        my $obj_list = eval { $SPOPS_CLASS->fetch_group({ where => 'spops_num = ?',
                                                          value => [ 1066 ],
                                                          db    => $db,
                                                          skip_cache => 1 }) };
        is( $obj_list->[0]{spops_name}, $changed, 'Field update (single object) match' );
    }

    # Try a field update with multiple objects (class-level call)
    # where nothing matches

    {
        my $changed_text = 'Multi field update';
        my $rv = eval { $SPOPS_CLASS->field_update( { spops_name => $changed_text },
                                                    { db    => $db,
                                                      where => 'spops_num < 0' } ) };
        warn $@ if ( $@ );
        is( int( $rv ), 0, 'Field update (multiple object) execution with no match' );
    }

    # Fetch the three objects in the db and be sure we got them all

    {
        my $obj_list = eval { $SPOPS_CLASS->fetch_group({ db => $db, skip_cache => 1 } ) };
        ok( ! $@, 'Fetch group' );
        if ( $@ ) {
            warn "Cannot retrieve objects: $@\n", Dumper( SPOPS::Error->get ), "\n";
        }

        ok( ref $obj_list eq 'ARRAY' && scalar @{ $obj_list } == 3, 'Fetch group (return check)' );
    }

    # Fetch a count of the objects in the database

    {
        my $obj_count = eval { $SPOPS_CLASS->fetch_count({ db => $db }) };
        ok( ! $@, 'Fetch count execution' );
        is( $obj_count, 3, 'Fetch count value' );

        my $skip_obj_count = eval {
            $SPOPS_CLASS->fetch_count({ db            => $db,
                                        skip_security => 1 })
        };
        ok( ! $@, 'Fetch count execution (security skipped)' );
        is( $skip_obj_count, 3, 'Fetch count value (security_skipped)' );
    }

    # Create an iterator and run through the objects

    {
        my $iter = eval { $SPOPS_CLASS->fetch_iterator({ db         => $db,
                                                         skip_cache => 1 }) };
        ok( ! $@, 'Fetch iterator execution' );
        ok( $iter->isa( 'SPOPS::Iterator::DBI' ), 'Iterator returned (fetch_iterator)' );
        my $count = 0;
        $count++ while ( my $obj = $iter->get_next );
        is( $count, 3, 'Iterator fetch count (fetch_iterator)' );
    }

    # Create an iterator from the object IDs then run through them

    {
        my $iter = SPOPS::Iterator::DBI->new({ id_list => \@ID_LIST,
                                               class   => $SPOPS_CLASS,
                                               db      => $db });
        ok( $iter->isa( 'SPOPS::Iterator::DBI' ), 'Iterator returned (ID list)' );
        my $count = 0;
        $count++ while ( my $obj = $iter->get_next );
        is( $count, 3, 'Iterator fetch count (ID list)' );
    }

    # Define an object but pass in a per-object 'no_insert' definition
    {
        my $obj = $SPOPS_CLASS->new({ spops_id => 4001, spops_name => 'FOO!',
                                      spops_goop => 'OOF!', spops_num => 4001 });
        eval { $obj->save({ is_add => 1,
                            no_insert => [ 'spops_name' ],
                            db => $db, skip_cache => 1 }) };
        ok( ! $@, 'Insert object with "no_insert" field specified' );
        isnt( $obj->{spops_name}, 'FOO!', 'Data reset for no_insert field in object' );
        my $new_obj = eval { $SPOPS_CLASS->fetch( 4001, { db => $db,
                                                          skip_cache => 1 }) };
        ok( ! $@, 'Refetch no_insert object' );
        isnt( $new_obj->{spops_name}, 'FOO!', 'Fetched data proper data for no_insert field' );
    }

    # Fetch an object for updating, change a field and ensure it
    # didn't change

    {
        my $obj = eval { $SPOPS_CLASS->fetch( 4001,
                                              { db => $db, skip_cache => 1 }) };
        ok( ! $@, "Fetch object for no_update" );
        my $old_value = $obj->{spops_num};
        $obj->{spops_num} = 5555;
        eval { $obj->save({ no_update => [ 'spops_num' ],
                            db => $db, skip_cache => 1 }) };
        ok( ! $@, 'Update object with "no_update" field specified' );
        my $new_obj = eval { $SPOPS_CLASS->fetch( 4001,
                                                  { db => $db, skip_cache => 1 }) };
        ok( ! $@, 'Refetch no_update object' );
        is( $new_obj->{spops_num}, $old_value, 'Old value not overwritten for no_update field' );
    }

# Future testing ideas:
#  - security
#  - timestamp checking
#  - fetch_group using 'where'

}