DBIx-AutoUpgrade-NativeStrings
view release on metacpan or search on metacpan
lib/DBIx/AutoUpgrade/NativeStrings.pm view on Meta::CPAN
die "$class->new(): invalid options : " . join " / ", @invalid_options if @invalid_options;
# make sure that Encode::Locale is loaded if needed
require Encode::Locale if $self->{native} eq 'locale';
# return object
bless $self, $class;
}
sub inject_callbacks {
my ($self, $dbh, @invalid_args) = @_;
# check input args
$dbh->isa('DBI::db') or die '->inject_callbacks() : arg is not a DBI database handle';
!@invalid_args or die '->inject_callbacks() : too many args';
# coderef to be installed as common callback for all methods. This is a closure on $self.
my $upgrade_string_args = sub {
# NOTES: - here there is no unpacking of @_ because DBI callbacks must work directly on @_
# - $_ is the name of the DBI method
# for calls to bind_param() with an explicit bind type, some types should be left untouched (for ex. SQL_BLOB)
return if $_ eq 'bind_param' && $_[3] && !$self->{bind_type_is_string}->($_[3]);
# vars to be used in the loop
my $do_upgrade = $self->{native} eq 'default' ? sub {utf8::upgrade($_[0])}
: sub {$_[0] = decode($self->{native}, $_[0], $self->{decode_check})};
my $dbi_method = $_;
my $sql = !ref($_[1]) && $_[1]; # for $dbh methods, SQL is in this position; otherwise undef
lib/DBIx/AutoUpgrade/NativeStrings.pm view on Meta::CPAN
$debug->($i, "upgrading string in array arg [$i] ($val)");
$do_upgrade->($val);
}
}
}
return; # must end with an empty return (see L<DBI> documentation)
};
# now inject the callback for $dbh methods and for $sth methods
my $parent_callbacks = $dbh->{Callbacks} //= {};
my $child_callbacks = $parent_callbacks->{ChildCallbacks} //= {};
inject_callback($parent_callbacks, $_ => $upgrade_string_args) for @{$self->{dbh_methods}};
inject_callback($child_callbacks, $_ => $upgrade_string_args) for @{$self->{sth_methods}};
}
sub inject_callback {
my ($hash, $key, $coderef) = @_;
# in case a previous callback was already installed, we replace it with a sub that combines both
my $previous_cb = $hash->{$key};
my $new_cb = $previous_cb ? sub {&$coderef; &$previous_cb} : $coderef;
lib/DBIx/AutoUpgrade/NativeStrings.pm view on Meta::CPAN
=head1 SYNOPSIS
use utf8;
use DBI;
use DBIx::AutoUpgrade::NativeStrings;
use Encode;
my $injector = DBIx::AutoUpgrade::NativeStrings->new(native => 'cp1252');
my $dbh = DBI->connect(@dbi_connection_params);
$injector->inject_callbacks($dbh);
# these strings are semantically equal, but have different internal representations
my $str_utf8 = "il était une bergère, elle vendait ses Åufs en Â¥, ça paie 5¾ â° de mieux quâen â¬",
my $str_native = decode('cp1252', $str_utf8, Encode::LEAVE_SRC);
# Oracle example : check if strings passed to the database are equal
my $sql = "SELECT CASE WHEN ?=? THEN 'EQ' ELSE 'NE' END FROM DUAL";
my ($result) = $dbh->selectrow_array($sql, {}, $str_native, $str_utf8); # returns 'EQ'
lib/DBIx/AutoUpgrade/NativeStrings.pm view on Meta::CPAN
upgraded if needed, like arguments to other method calls; if the coderef returns false, the C<$value> is left intact.
The default coderef returns true when the C<$bind_type> is one of the DBI constants
C<SQL_CHAR>, C<SQL_VARCHAR>, C<SQL_LONGVARCHAR>, C<SQL_WLONGVARCHAR>, C<SQL_WVARCHAR>, C<SQL_WCHAR> or C<SQL_CLOB>.
=back
=head2 inject_callbacks
$injector->inject_callbacks($dbh);
Injects callbacks into the given database handle.
If that handle already has callbacks for the same methods, the system will arrange for those
other callbacks to be called I<after> all string arguments have been upgraded to utf8.
=head1 ARCHITECTURAL NOTES
=head2 Object-orientedness
Although I'm a big fan of L<Moose> and its variants, the present module is implemented
in POPO (Plain Old Perl Object) : since the object model is extremely simple, there was
no ground for using a sophisticated object system.
=head2 Strings are modified in-place
String arguments to DBI methods are modified I<in-place>.
It is unlikely that this would affect your client program, but
if it does, you need to make your own string copies before passing them to the DBI methods.
=head2 Possible redundancies
L<DBI> does not precisely document which of its public methods call each other.
For example, one would think that C<execute()> internally calls C<bind_param()>, but this does
not seem to be the case. So, to be on the safe side, callbacks installed here make no assumptions
about string transformations performed by other callbacks. There might be some redundancies,
but it does no harm since strings are never upgraded twice.
=head2 Caveats
The C<bind_param_inout()> method is not covered -- the client program must do the proper updates
if that method is used to send strings to the database.
=head1 AUTHOR
Laurent Dami, E<lt>dami at cpan.orgE<gt>
t/autoupgrade_nativestrings.t view on Meta::CPAN
my ($downgrade, $upgrade)
= $encoding eq 'default' ? (sub {my $str = shift; utf8::downgrade($str); $str},
sub {my $str = shift; utf8::upgrade($str) ; $str})
: (sub {my $str = shift; encode($encoding, $str)},
sub {my $str = shift; decode($encoding, $str)});
# connect to the database
my $dbh = DBI->connect(@opt{qw/data_source user passwd connect_attr/}) or die $DBI::errstr;
note "testing $encoding encoding on DBD driver $dbh->{Driver}{Name}";
# first check how tests behave without the callbacks. This proves that semantically equivalent strings
# are considered not equal by some drivers, and CP1252 strings are never considered equal to their UTF8
# equivalent, because they need an explicit decode()
my $expected_without_callbacks
= $encoding eq 'cp1252' || !$driver_autoupgrades_latin1{$dbh->{Driver}{Name}} ? 'NE' : 'EQ';
run_tests($dbh, $encoding, without_callbacks => $expected_without_callbacks, $str, $downgrade, $upgrade);
# now inject callbacks and test again, this time expecting 'EQ' results.
my @debug_msgs;
my $injector = DBIx::AutoUpgrade::NativeStrings->new(
native => $encoding,
debug => sub {push @debug_msgs, join "", @_},
);
$injector->inject_callbacks($dbh);
run_tests($dbh, $encoding, with_callbacks => 'EQ', $str, $downgrade, $upgrade);
# check a few simple things on debug messages
like $debug_msgs[0], qr/^triggering 'selectrow_array'.*?\[SELECT.*?in main at.*?line \d+/, "first debug msg";
like $debug_msgs[-1], qr/^triggering 'execute_array'/, "last debug msg";
}
sub run_tests {
my ($dbh, $encoding, $have_callbacks, $expected, $str, $downgrade, $upgrade) = @_;
my $context = "$encoding, $have_callbacks: expecting $expected";
my $str_native = $downgrade->($str);
my $str_utf8 = $upgrade->($str_native);
my ($sth, $result);
my $maybe_from_dual = sub {my $sql = shift; $sql .= " FROM DUAL" if $dbh->{Driver}{Name} eq 'Oracle'; $sql};
my $sql = $maybe_from_dual->("SELECT CASE WHEN ?=? THEN 'EQ' ELSE 'NE' END CMP_RESULT");
# testing dbh methods -- direct select from dbh
($result) = $dbh->selectrow_array(clonestr($sql), {}, clonestr($str_native, $str_utf8));
is $result, $expected, "[$context] (selectrow_array)";
t/autoupgrade_nativestrings.t view on Meta::CPAN
is $result, $expected, "[$context] (prepare / execute)";
$sth = $dbh->prepare(clonestr($sql));
$sth->bind_param(1, clonestr($str_native));
$sth->bind_param(2, clonestr($str_utf8));
$sth->execute;
($result) = $sth->fetchrow_array;
is $result, $expected, "[$context] (prepare / bind_param / execute)";
# testing interpolated strings without bind values -- native and utf8.
# Note: eval is needed because the result of interpolation without the callbacks may yield invalid SQL
my $sql1 = $downgrade->($maybe_from_dual->("SELECT CASE WHEN '$str'=? THEN 'EQ' ELSE 'NE' END"));
($result) = eval {$dbh->selectrow_array(clonestr($sql1), {}, clonestr($str_utf8))};
is $result, $expected, "[$context] (interpolated native string)" if $result;
my $sql2 = $upgrade->($sql1);
($result) = eval {$dbh->selectrow_array(clonestr($sql2), {}, clonestr($str_native))};
is $result, $expected, "[$context] (interpolated utf8 string)" if $result;
# if there is a table we can write into, test the 'do' method and a roundtrip to the database
if (!($opt{TABLE} && $opt{KEY_COL} && $opt{VAL_COL})) {
( run in 2.643 seconds using v1.01-cache-2.11-cpan-9b1e4054eb1 )