The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
#!/usr/bin/perl -w -I./t
#
# Test insertion into varchar columns using unicode and codepage chrs
# Must be a unicode build of DBD::ODBC
# Currently needs MS SQL Server
#
use open ':std', ':encoding(utf8)';
use Test::More;
use strict;
use Data::Dumper;

$| = 1;

use DBI qw(:utils);
use DBI::Const::GetInfoType;
my $has_test_nowarnings = 1;
eval "require Test::NoWarnings";
$has_test_nowarnings = undef if $@;

my $dbh;

BEGIN {
	if ($] < 5.008001) {
		plan skip_all => "Old Perl lacking unicode support";
	} elsif (!defined $ENV{DBI_DSN}) {
             plan skip_all => "DBI_DSN is undefined";
      }
}

END {
    # tidy up
    if ($dbh) {
        local $dbh->{PrintError} = 0;
        local $dbh->{PrintWarn} = 0;
        eval {
            $dbh->do(q/drop table PERL_DBD_TABLE1/);
        };
    }
}

# get the server, database and table collations
sub collations {
    my ($h, $table) = @_;

    # so we can use :: not meaning placeholders
    $h->{odbc_ignore_named_placeholders} = 1;

    # get database name to use later when finding collation for table
    my $database_name = $h->get_info($GetInfoType{SQL_DATABASE_NAME});
    diag "Database: ", $database_name;

    # now find out the collations
    # server collation:
    my $r = $h->selectrow_arrayref(
        q/SELECT CONVERT (varchar, SERVERPROPERTY('collation'))/);
    diag "Server collation: ", $r->[0], "\n";

    # database collation:
    $r = $h->selectrow_arrayref(
        q/SELECT CONVERT (varchar, DATABASEPROPERTYEX(?,'collation'))/,
        undef, $database_name);
    diag "Database collation: ", $r->[0];

    # now call sp_help to find out about our table
    # first result-set should be name, owner, type and create datetime
    # second result-set should be:
    #  column_name, type, computed, length, prec, scale, nullable, trimtrailingblanks,
    #  fixedlennullinsource, collation
    # third result-set is identity columns
    # fourth result-set is row guilded columns
    # there are other result-sets depending on the object
    # sp_help -> http://technet.microsoft.com/en-us/library/ms187335.aspx
    my $column_collation;
    diag "Calling sp_help for table:";
    my $s = $h->prepare(q/{call sp_help(?)}/);
    $s->execute($table);
    my $result_set = 1;
    do {
        my $rows = $s->fetchall_arrayref;
        if ($result_set <= 2) {
            foreach my $row (@{$rows}) {
                diag join(",", map {$_ ? $_ : 'undef'} @{$row});
            }
        }
        if ($result_set == 2) {
            foreach my $row (@{$rows}) {
                diag "column:", $row->[0], " collation:", $row->[9], "\n";
                $column_collation = $row->[9];
            }
        }
        $result_set++;
    } while $s->{odbc_more_results};

    # now using the last column collation from above find the codepage
    $r = $h->selectrow_arrayref(
        q/SELECT COLLATIONPROPERTY(?, 'CodePage')/,
        undef, $column_collation);
    diag "Code page for column collation: ", $r->[0];
}

# output various codepage information
sub code_page {
    eval {require Win32::API::More};
    if ($@) {
        diag("Win32::API::More not available");
        return;
    }
    Win32::API::More->Import("kernel32", "UINT GetConsoleOutputCP()");
    Win32::API::More->Import("kernel32", "UINT GetACP()");
    my $cp = GetConsoleOutputCP();
    diag "Current active console code page: $cp\n";
    $cp = GetACP();
    diag "active code page: $cp\n";
    1;
}

# given a string call diag to output the ord of each character
sub ords {
    my $str = shift;

    use bytes;

    diag "    ords of output string:";
    foreach my $s(split(//, $str)) {
        diag sprintf("%x", ord($s)), ",";
    }
}

# read back the length of the data inserted according to the db and the data
# inserted (although nothing is done with the latter right now).
# given a perl expected length and a db expected length check them
# given a hex string of bytes the data should look like when cast to a
# binary check the inserted data matches what we expect.
sub show_it {
    my ($h, $expected_perl_length, $expected_db_length, $hex) = @_;

    my $r = $h->selectall_arrayref(q/select len(a), a from PERL_DBD_TABLE1 order by b asc/);

    diag( Dumper($r));
    foreach my $row(@$r) {
        is($row->[0], shift @{$expected_db_length}, "db character length") or
            diag("dsc: " . data_string_desc($row->[0]));
        if (!is(length($row->[1]), shift @{$expected_perl_length},
                "expected perl length")) {
            diag(data_string_desc($row->[1]));
            ords($row->[1]);
        }
    }

    if ($hex) {
        foreach my $hex_val(@$hex) {
            $r = $h->selectrow_arrayref(q/select count(*) from PERL_DBD_TABLE1 where cast(a as varbinary(100)) = / . $hex_val);
            is($r->[0], 1, "hex comparison $hex_val");
        }
    }
    $h->do(q/delete from PERL_DBD_TABLE1/);
}

# insert the string into the database
# daig output info about the inserted data
sub execute {
    my ($s, @strings) = @_;

    diag "  INPUT:";
    foreach my $string(@strings) {
        #diag "    input string: $string";
        diag "    data_string_desc of input string: ", data_string_desc($string);
        diag "    ords of input string: ";
        foreach my $s(split(//, $string)) {
            diag sprintf("%x,", ord($s));
        }

        {
            diag "    bytes of input string: ";
            use bytes;
            foreach my $s(split(//, $string)) {
                diag sprintf("%x,", ord($s));
            }
        }
    }

    ok($s->execute(@strings), "execute");
}

$dbh = DBI->connect();
unless($dbh) {
   BAIL_OUT("Unable to connect to the database $DBI::errstr\nTests skipped.\n");
   exit 0;
}
my $driver_name = $dbh->get_info($GetInfoType{SQL_DRIVER_NAME});
diag "Driver: ", $driver_name;

$dbh->{RaiseError} = 1;
eval {local $dbh->{PrintWarn} =0; $dbh->{PrintError} = 0;$dbh->do(q/drop table PERL_DBD_TABLE1/)};

my $dbname = $dbh->get_info($GetInfoType{SQL_DBMS_NAME});
if ($dbname !~ /Microsoft SQL Server/i) {
    note "Not MS SQL Server";
    plan skip_all => "Not MS SQL Server";
    exit;
}

if (!$dbh->{odbc_has_unicode}) {
    note "Not a unicode build of DBD::ODBC";
    plan skip_all => "Not a unicode build of DBD::ODBC";
    exit 0;
}

if ($^O eq 'MSWin32') {
    if (!code_page()) {
        note "Win32::API not found";
    }
}

eval {
    $dbh->do(q/create table PERL_DBD_TABLE1 (b integer, a varchar(100) collate Latin1_General_CI_AS)/);
};
if ($@) {
    fail("Cannot create table with collation - $@");
    done_testing();
    exit 0;
}

collations($dbh, 'PERL_DBD_TABLE1');

my $sql = q/insert into PERL_DBD_TABLE1 (b, a) values(?, ?)/;

my $s;
# a simple unicode string
my $unicode = "\x{20ac}\x{a3}";
diag "Inserting a unicode euro, utf8 flag on:\n";
$s = $dbh->prepare($sql); # redo to ensure no sticky params
execute($s, 1, $unicode);
show_it($dbh, [2], [2], ['0x80a3']);

my $codepage;
# a simple codepage string
{
    use bytes;
    $codepage = chr(0xa3) . chr(0x80); # it is important this is different to $unicode
}
diag "Inserting a codepage/bytes string:\n";
$s = $dbh->prepare($sql); # redo to ensure no sticky params
execute($s, 1, $codepage);
show_it($dbh, [2], [2], ['0xa380']);

# inserting a mixture of unicode chrs and codepage chrs per row in same insert
# unicode first - checks we rebind the 2nd parameter as SQL_CHAR
diag "Inserting a unicode followed by codepage chrs:\n";
$s = $dbh->prepare($sql); # redo to ensure no sticky params
execute($s, 1, $unicode);
execute($s, 2, $codepage);
show_it($dbh, [2,2], [2,2], ['0x80a3', '0x80a3']);

# inserting a mixture of unicode chrs and codepage chrs per row in same insert
# codepage first - checks we rebind the 2nd parameter SQL_WCHAR
diag "Inserting codepage chrs followed by unicode:\n";
$s = $dbh->prepare($sql); # redo to ensure no sticky params
execute($s, 1, $codepage);
execute($s, 2, $unicode);
show_it($dbh, [2,2], [2,2], ['0xa380', '0x80a3']);

Test::NoWarnings::had_no_warnings() if ($has_test_nowarnings);
done_testing();