The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
#!perl -w
# vim:ts=8:sw=4

use DBI;
use DBD::Oracle qw(:ora_types SQLCS_NCHAR SQLCS_IMPLICIT ORA_OCI);
use strict;
use Test::More;

*BAILOUT = sub { die "@_\n" } unless defined &BAILOUT;

unshift @INC ,'t';
require 'nchar_test_lib.pl';

my @test_sets;
push @test_sets, [ "LONG",	0,		0 ];
push @test_sets, [ "LONG RAW",	ORA_LONGRAW,	0 ];
push @test_sets, [ "NCLOB",	ORA_CLOB,	0 ] unless ORA_OCI() < 9.0 or $ENV{DBD_ALL_TESTS};
push @test_sets, [ "CLOB",	ORA_CLOB,	0 ] ;
push @test_sets, [ "BLOB",	ORA_BLOB,	0 ] ;

my $tests_per_set = 96;
my $tests = @test_sets * $tests_per_set-1; 
#very odd little thing that took a while to figure out.
#Seems I now have 479 tests which is 9 more so 96 test then -1 to round it off

$| = 1;
my $dbuser = $ENV{ORACLE_USERID} || 'scott/tiger';
my $table = table();
my $use_utf8_data;	# set per test_set below
my %warnings;

my @skip_unicode;
push @skip_unicode, "Perl < 5.6 "          if $] < 5.006;
push @skip_unicode, "Oracle client < 9.0 " if ORA_OCI() < 9.0 and !$ENV{DBD_ALL_TESTS};

# Set size of test data (in 10KB units)
#	Minimum value 3 (else tests fail because of assumptions)
#	Normal  value 8 (to test old 64KB threshold well)
my $sz = 8;

my($p1, $p2, $tmp, @tmp);

#my $dbh = db_handle();


 $dbuser = $ENV{ORACLE_USERID} || 'scott/tiger';
  my $dsn = oracle_test_dsn();  
 my  $dbh = DBI->connect($dsn, $dbuser, '',{
                               PrintError => 0,
                       });

if ($dbh) {
    plan tests => $tests;
} else {
    plan skip_all => "Unable to connect to Oracle";
}

my $ora_server_version = $dbh->func("ora_server_version");
note("ora_server_version: @$ora_server_version\n");
show_db_charsets($dbh) if $dbh;

foreach (@test_sets) {
    my ($type_name, $type_num, $test_no_type) = @$_;
    $use_utf8_data = use_utf8_data($dbh,$type_name);
    note( qq(
    =========================================================================
    Running long test for $type_name ($type_num) use_utf8_data=$use_utf8_data
));
    run_long_tests($dbh, $type_name, $type_num);
    run_long_tests($dbh, $type_name, 0) if $test_no_type;
}

exit 0;

# end.


END {
    drop_table( $dbh ) if not $ENV{DBD_SKIP_TABLE_DROP};
    $dbh->disconnect if $dbh;
}


sub use_utf8_data
{
    my ( $dbh, $type_name ) = @_;
    if (   ($type_name =~ m/^CLOB/i  and db_ochar_is_utf($dbh) && client_ochar_is_utf8())
        or ($type_name =~ m/^NCLOB/i and db_nchar_is_utf($dbh) && client_nchar_is_utf8()) ) {
	return 1 unless @skip_unicode;
	warn "Skipping Unicode data tests: @skip_unicode\n" if !$warnings{use_utf8_data}++;
    }
    return 0;
}

sub run_long_tests
{
    my ($dbh, $type_name, $type_num) = @_;
    my ($sth);
    my $append_len;
    SKIP:
    { #it all

        # relationships between these lengths are important # e.g.
        my %long_data;
        my @long_data;
        $long_data[2] = ("2bcdefabcd"  x 1024) x ($sz-1);  # 70KB  > 64KB && < long_data1
        $long_data[1] = ("1234567890"  x 1024) x ($sz  );  # 80KB >> 64KB && > long_data2
        $long_data[0] = ("0\177x\0X"   x 2048) x (1    );  # 10KB  < 64KB

        if ( $use_utf8_data ) { # make $long_data0 be UTF8
            my $utf_x = "0\x{263A}xyX"; #lab: the ubiquitous smiley face
            $long_data[0] = ($utf_x x 2048) x (1    );        # 10KB  < 64KB
            if (length($long_data[0]) > 10240) {
                note "known bug in perl5.6.0 utf8 support, applying workaround\n";
                my $utf_z = "0\x{263A}xyZ" ;
                $long_data[0] = $utf_z;
                $long_data[0] .= $utf_z foreach (1..2047);
            }
            if ($type_name eq 'BLOB') {
                # convert string from utf-8 to byte encoding XXX
                $long_data[0] = pack "C*", (unpack "C*", $long_data[0]);
            }
        }
	my $be_utf8 = ($type_name eq  'BLOB') ? 0
		    : ($type_name eq  'CLOB') ? client_ochar_is_utf8()
		    : ($type_name eq 'NCLOB') ? client_nchar_is_utf8()
		    : 0; # XXX umm, what about LONGs?

        # special hack for long_data[0] since RAW types need pairs of HEX
        $long_data[0] = "00FF" x (length($long_data[0]) / 2) if $type_name =~ /RAW/i;

        my $len_data0 = length($long_data[0]);
        my $len_data1 = length($long_data[1]);
        my $len_data2 = length($long_data[2]);

        # warn if some of the key aspects of the data sizing are tampered with
        warn "long_data[0] is > 64KB: $len_data0\n"
                if $len_data0 > 65535;
        warn "long_data[1] is < 64KB: $len_data1\n"
                if $len_data1 < 65535;
        warn "long_data[2] is not smaller than $long_data[1] ($len_data2 > $len_data1)\n"
                if $len_data2 >= $len_data1;

        my $tdata = {
            cols => long_test_cols( $type_name ),
            rows => []
        };


        skip "Unable to create test table for '$type_name' data ($DBI::err)." ,$tests_per_set
            if (!create_table($dbh, $tdata, 1));
            # typically OCI 8 client talking to Oracle 7 database

        note("long_data[0] length $len_data0\n");
        note("long_data[1] length $len_data1\n");
        note("long_data[2] length $len_data2\n");

        note(" --- insert some $type_name data (ora_type $type_num)\n");
        my $sqlstr = "insert into $table values (?, ?, SYSDATE)" ;
        ok( $sth = $dbh->prepare( $sqlstr ), "prepare: $sqlstr" );
        my $bind_attr = { ora_type => $type_num };
	# The explicit SQLCS_IMPLICIT is needed in some odd cases
        $bind_attr->{ora_csform} = ($type_name =~ /^NCLOB/) ? SQLCS_NCHAR : SQLCS_IMPLICIT;

        $sth->bind_param(2, undef, $bind_attr )
		or die "$type_name: $DBI::errstr" if $type_num;

        ok($sth->execute(40, $long_data{40} = $long_data[0] ), "insert long data 40" );
        ok($sth->execute(41, $long_data{41} = $long_data[1] ), "insert long data 41" );
        ok($sth->execute(42, $long_data{42} = $long_data[2] ), "insert long data 42" );
        ok($sth->execute(43, $long_data{43} = undef), "insert long data undef 43" ); # NULL

        array_test($dbh);

        note(" --- fetch $type_name data back again -- truncated - LongTruncOk == 1\n");
        $dbh->{LongReadLen} = 20;
        $dbh->{LongTruncOk} =  1;
        note("LongReadLen $dbh->{LongReadLen}, LongTruncOk $dbh->{LongTruncOk}\n");

        # This behaviour isn't specified anywhere, sigh:
        my $out_len = $dbh->{LongReadLen};
        $out_len *= 2 if ($type_name =~ /RAW/i);

        $sqlstr = "select * from $table order by idx";
        ok($sth = $dbh->prepare($sqlstr), "prepare: $sqlstr" );
        $sth->trace(0);
        ok($sth->execute, "execute: $sqlstr" );
        ok($tmp = $sth->fetchall_arrayref, "fetch_arrayref for $sqlstr" );
        $sth->trace(0);
        SKIP: {
            if ($DBI::err && $DBI::errstr =~ /ORA-01801:/) {
                # ORA-01801: date format is too long for internal buffer
                skip " If you're using Oracle <= 8.1.7 then this error is probably\n"
                    ." due to an Oracle bug and not a DBD::Oracle problem.\n" , 5 ;
            }
            cmp_ok(@$tmp ,'==' ,4 ,'four rows' );
            #print "tmp->[0][1] = " .$tmp->[0][1] ."\n" ;
	    for my $i (0..2) {
		my $v = $tmp->[$i][1];
		cmp_ok_byte_nice($v, substr($long_data[$i],0,$out_len), "truncated to LongReadLen $out_len");
		if ($type_name eq 'BLOB') {
		    ok( !utf8::is_utf8($v), "BLOB non-UTF8");
		}
		else {
		    # allow result to have UTF8 flag even if source data didn't
		    # (not ideal but would need better test data)
		    ok( utf8::is_utf8($v) >= utf8::is_utf8($long_data[$i]),
			"$type_name UTF8 setting");
		}
	    }
            # use Data::Dumper; print Dumper($tmp->[3]);
            ok(!defined $tmp->[3][1], "last row undefined"); # NULL # known bug in DBD::Oracle <= 1.13
        }

        note(" --- fetch $type_name data back again -- truncated - LongTruncOk == 0\n");
        $dbh->{LongReadLen} = $len_data1 - 10; # so $long_data[0] fits but long_data[1] doesn't
        $dbh->{LongReadLen} = $dbh->{LongReadLen} / 2 if $type_name =~ /RAW/i;
        my $LongReadLen = $dbh->{LongReadLen};
        $dbh->{LongTruncOk} = 0;
        note("LongReadLen $dbh->{LongReadLen}, LongTruncOk $dbh->{LongTruncOk}\n");

        $sqlstr = "select * from $table order by idx";
        ok($sth = $dbh->prepare($sqlstr), "prepare $sqlstr" );
        ok($sth->execute, "execute $sqlstr" );
        ok($tmp = $sth->fetchrow_arrayref, "fetchrow_arrayref $sqlstr" );
        ok($tmp->[1] eq $long_data[0], "length tmp->[1] ".length($tmp->[1]) );

        {
            local $sth->{PrintError} = 0;
            ok(!defined $sth->fetchrow_arrayref,
                    "truncation error not triggered "
                    ."(LongReadLen $LongReadLen, data ".length($tmp->[1]||0).")");
            $tmp = $sth->err || 0;
            ok( ($tmp == 1406 || $tmp == 24345) ,"tmp==1406 || tmp==24345 tmp actually=$tmp" );
        }
	$sth->finish;

        note(" --- fetch $type_name data back again -- complete - LongTruncOk == 0\n");
        $dbh->{LongReadLen} = $len_data1 +1000;
        $dbh->{LongTruncOk} = 0;
        note("LongReadLen $dbh->{LongReadLen}, LongTruncOk $dbh->{LongTruncOk}\n");

        $sqlstr = "select * from $table order by idx";
        ok($sth = $dbh->prepare($sqlstr), "prepare: $sqlstr" );
        ok($sth->execute, "execute $sqlstr" );

	for my $i (0..2) {
	    ok($tmp = $sth->fetchrow_arrayref, "fetchrow_arrayref $sqlstr" );
	    ok($tmp->[1] eq $long_data[$i],
                cdif($tmp->[1],$long_data[$i], "Len ".length($tmp->[1])) );
	}
	$sth->finish;


        SKIP: {
            skip( "blob_read tests for LONGs - not currently supported", 15 )
                if ($type_name =~ /LONG/i) ;

            #$dbh->trace(4);
            note(" --- fetch $type_name data back again -- via blob_read\n\n");

            $dbh->{LongReadLen} = 1024 * 90;
            $dbh->{LongTruncOk} =  1;
            $sqlstr = "select idx, lng, dt from $table order by idx";
            ok($sth = $dbh->prepare($sqlstr) ,"prepare $sqlstr" );
            ok($sth->execute, "execute $sqlstr" );


	    note("fetch via fetchrow_arrayref\n");
            ok($tmp = $sth->fetchrow_arrayref, "fetchrow_arrayref 1: $sqlstr"  );
	    cmp_ok_byte_nice($tmp->[1], $long_data[0], "truncated to LongReadLen $out_len");

	    note("read via blob_read_all\n");
            cmp_ok(blob_read_all($sth, 1, \$p1, 4096) ,'==', length($long_data[0]),
	    	"blob_read_all = length(\$long_data[0])" );
            ok($p1 eq $long_data[0], cdif($p1, $long_data[0]) );
	    $sth->trace(0);


            ok($tmp = $sth->fetchrow_arrayref, "fetchrow_arrayref 2: $sqlstr" );
            cmp_ok(blob_read_all($sth, 1, \$p1, 12345) ,'==', length($long_data[1]),
	    	"blob_read_all = length(long_data[1])" );
            ok($p1 eq $long_data[1], cdif($p1, $long_data[1]) );


            ok($tmp = $sth->fetchrow_arrayref, "fetchrow_arrayref 3: $sqlstr"  );
            my $len = blob_read_all($sth, 1, \$p1, 34567);

	    cmp_ok($len,'==', length($long_data[2]), "length of long_data[2] = $len" );
	    cmp_ok_byte_nice($p1, $long_data[2], "3rd row via blob_read_all");

	    note("result is ".(utf8::is_utf8($p1) ? "UTF8" : "non-UTF8")."\n");
	    if ($be_utf8) {
	        ok( utf8::is_utf8($p1), "result should be utf8");
	    }
	    else {
	        ok( !utf8::is_utf8($p1), "result should not be utf8");
	    }
        } #skip


        SKIP: {
            skip( "ora_auto_lob tests for $type_name" ."s - not supported", 7+(13*3) )
                if not ( $type_name =~ /LOB/i );

            note(" --- testing ora_auto_lob to access $type_name LobLocator\n\n");
            my $data_fmt = "%03d foo!";

            $sqlstr = qq{
                    SELECT lng, idx FROM $table ORDER BY idx
                    FOR UPDATE -- needed so lob locator is writable
                };
            my $ll_sth = $dbh->prepare($sqlstr, { ora_auto_lob => 0 } );  # 0: get lob locator instead of lob contents
            ok($ll_sth ,"prepare $sqlstr" );

            ok($ll_sth->execute ,"execute $sqlstr" );
            while (my ($lob_locator, $idx) = $ll_sth->fetchrow_array) {
                note("$idx: ".DBI::neat($lob_locator)."\n");
                last if !defined($lob_locator) && $idx == 43;

                ok($lob_locator, '$lob_locator is true' );
                is(ref $lob_locator , 'OCILobLocatorPtr', '$lob_locator is a OCILobLocatorPtr' );
                ok( (ref $lob_locator and $$lob_locator), '$lob_locator deref ptr is true' ) ;
                
                # check ora_lob_chunk_size:
		my $chunk_size = $dbh->func($lob_locator, 'ora_lob_chunk_size');
		ok(!$DBI::err, "DBI::errstr");
		
                my $data = sprintf $data_fmt, $idx; #create a little data
                note("length of data to be written at offset 1: " .length($data) ."\n" );
                ok($dbh->func($lob_locator, 1, $data, 'ora_lob_write') ,"ora_lob_write" );
            }
	    is($ll_sth->rows, 4);

            note(" --- round again to check contents after $type_name write updates...\n");
	    ok($ll_sth->execute,"execute (again 1) $sqlstr" );
	    while (my ($lob_locator, $idx) = $ll_sth->fetchrow_array) {
		note("$idx locator: ".DBI::neat($lob_locator)."\n");
                next if !defined($lob_locator) && $idx == 43;
		diag("DBI::errstr=$DBI::errstr\n") if $DBI::err ;

		my $content = $dbh->func($lob_locator, 1, 20, 'ora_lob_read');
		diag("DBI::errstr=$DBI::errstr\n") if $DBI::err ;
		ok($content,"content is true" );
		note("$idx content: ".nice_string($content)."\n"); #.DBI::neat($content)."\n";
		cmp_ok(length($content) ,'==', 20 ,"lenth(content)" );

		# but prefix has been overwritten:
		my $data = sprintf $data_fmt, $idx;
		ok(substr($content,0,length($data)) eq $data ,"length(content)=length(data)" );

		# ora_lob_length agrees:
		my $len = $dbh->func($lob_locator, 'ora_lob_length');
		ok(!$DBI::err ,"DBI::errstr" );
		cmp_ok($len ,'==', length($long_data{$idx}) ,"length(long_data{idx}) = length of locator data" );

		# now trim the length
		$dbh->func($lob_locator, $idx, 'ora_lob_trim');
		ok(!$DBI::err, "DBI::errstr" );

		# and append some text
		SKIP: {
		    $append_len = 0;
		    skip( "ora_lob_append() not reliable in Oracle 8 (Oracle bug #886191)", 1 )
			if ORA_OCI() < 9 or $ora_server_version->[0] < 9;

		    my $append_data = "12345";
		    $append_len = length($append_data);
		    $dbh->func($lob_locator, $append_data, 'ora_lob_append');
		    ok(!$DBI::err ,"ora_lob_append DBI::errstr" );
		    # XXX ought to test data was actually appended
		}

	    } #while fetchrow
	    is($ll_sth->rows, 4);

            note(" --- round again to check the $type_name length...\n");
	    ok($ll_sth->execute ,"execute (again 2) $sqlstr" );
	    while (my ($lob_locator, $idx) = $ll_sth->fetchrow_array) {
	       note("$idx locator: ".DBI::neat($lob_locator)."\n");
               next if !defined($lob_locator) && $idx == 43;
	       my $len = $dbh->func($lob_locator, 'ora_lob_length');
	       #lab: possible logic error here w/resp. to len
	       ok(!$DBI::err ,"DBI::errstr" );
	       cmp_ok( $len ,'==', $idx + $append_len ,"len == idx+5" );
	    }
	    is($ll_sth->rows, 4);

        } #skip for LONG types

    } #skip it all (tests_per_set)

    $sth->finish if $sth;
    drop_table( $dbh )

} # end of run_long_tests



sub array_test {
    my ($dbh) = @_;
    return 0;	# XXX disabled
    eval {
	$dbh->{RaiseError}=1;
	$dbh->trace(0);
	my $sth = $dbh->prepare(qq{
	   UPDATE $table set idx=idx+1 RETURNING idx INTO ?
	});
	my ($a,$b);
	$a = [];
	$sth->bind_param_inout(1,\$a, 2);
	$sth->execute;
	note("a=$a\n");
	note("a=@$a\n");
    };
    die "RETURNING array: $@";
}


sub print_substrs
{
    my ($dbh,$len) = @_;
    my $tsql = "select substr(lng,1,$len),idx from $table order by idx" ;
    diag("-- prepare: $tsql\n") ;
    my $tsth = $dbh->prepare( $tsql );
    $tsth->execute();
    while ( my ( $d,$i ) = $tsth->fetchrow_array() )
    {
        last if not defined $d;
        diag("$i: $d\n");
    }
}

sub print_lengths
{
    my ($dbh) = @_;
    my $tsql = "select length(lng),idx from $table order by idx" ;
    diag("-- prepare: $tsql\n");
    my $tsth = $dbh->prepare( $tsql );
    $tsth->execute();
    while ( my ( $l,$i ) = $tsth->fetchrow_array() )
    {
        last if not defined $l;
        diag("$i: $l\n");
    }
}


sub blob_read_all {
    my ($sth, $field_idx, $blob_ref, $lump) = @_;

    $lump ||= 4096; # use benchmarks to get best value for you
    my $offset = 0;
    my @frags;
    while (1) {
	my $frag = $sth->blob_read($field_idx, $offset, $lump);
	last unless defined $frag;
	my $len = length $frag;
	last unless $len;
	push @frags, $frag;
	$offset += $len;
	#print "blob_read_all: offset $offset, len $len\n";
    }
    $$blob_ref = join "", @frags;
    return length($$blob_ref);
}

sub unc {
    my @str = @_;
    foreach (@str) { s/([\000-\037\177-\377])/ sprintf "\\%03o", ord($_) /eg; }
    return join "", @str unless wantarray;
    return @str;
}

sub cdif {
    my ($s1, $s2, $msg) = @_;
    $msg = ($msg) ? ", $msg" : "";
    my ($l1, $l2) = (length($s1), length($s2));
    return "Strings are identical$msg" if $s1 eq $s2;
    return "Strings are of different lengths ($l1 vs $l2)$msg" # check substr matches?
	if $l1 != $l2;
    my $i;
    for($i=0; $i < $l1; ++$i) {
	my ($c1,$c2) = (ord(substr($s1,$i,1)), ord(substr($s2,$i,1)));
	next if $c1 == $c2;
        return sprintf "Strings differ at position %d (\\%03o vs \\%03o)$msg",
		$i,$c1,$c2;
    }
    return "(cdif error $l1/$l2/$i)";
}


__END__