File Coverage

blib/lib/DBIx/AutoUpgrade/NativeStrings.pm
Criterion Covered Total %
statement 79 84 94.0
branch 23 34 67.6
condition 22 34 64.7
subroutine 13 15 86.6
pod 2 5 40.0
total 139 172 80.8


line stmt bran cond sub pod time code
1 1     1   20831 use 5.10.0;
  1         5  
2             package DBIx::AutoUpgrade::NativeStrings;
3 1     1   8 use utf8;
  1         3  
  1         10  
4 1     1   36 use strict;
  1         3  
  1         82  
5 1     1   9 use warnings;
  1         3  
  1         51  
6 1     1   7 use Scalar::Util qw/looks_like_number/;
  1         3  
  1         92  
7 1     1   10 use Encode qw/decode/;
  1         3  
  1         82  
8 1         1737 use DBI (),
9              
10 1     1   9 our $VERSION = 1.02;
  1         3  
11              
12             my @default_dbh_methods = qw/do
13             prepare
14             selectrow_array
15             selectrow_arrayref
16             selectrow_hashref
17             selectall_arrayref
18             selectall_array
19             selectall_hashref
20             selectcol_arrayref/;
21              
22             my @default_sth_methods = qw/bind_param
23             bind_param_array
24             execute
25             execute_array/;
26              
27             my @sql_string_types = (DBI::SQL_CHAR, DBI::SQL_VARCHAR, DBI::SQL_LONGVARCHAR, DBI::SQL_WLONGVARCHAR,
28             DBI::SQL_WVARCHAR, DBI::SQL_WCHAR, DBI::SQL_CLOB);
29              
30              
31             my %valid_options = (
32             # name expected reftype default value
33             # ==== ================ =============
34             native => ['NOREF' , 'default' ],
35             decode_check => ['NOREF' , ],
36             debug => ['CODE' , ],
37             dbh_methods => ['ARRAY' , \@default_dbh_methods ],
38             sth_methods => ['ARRAY' , \@default_sth_methods ],
39             bind_type_is_string => ['ARRAY' , \&default_bind_type_is_string],
40             );
41              
42              
43              
44             sub new {
45 3     3 1 292548 my ($class, %options) = @_;
46              
47             # build object internals, checking validity of input args and supplying default values
48 3         20 my $self = {};
49 3         27 while (my ($option, $details) = each %valid_options) {
50 18         63 my ($expected_reftype, $default_val) = @$details;
51 18         38 my $val = delete $options{$option};
52 18 50 100     69 !$val or (ref $val || 'NOREF') eq $expected_reftype
      66        
53             or die "$class->new(): '$option' should be a $expected_reftype";
54 18   100     70 $val //= $default_val;
55 18 100       82 $self->{$option} = $val if $val;
56             }
57              
58             # check that there are no remaining input args
59 3         12 my @invalid_options = keys %options;
60 3 50       16 die "$class->new(): invalid options : " . join " / ", @invalid_options if @invalid_options;
61              
62             # make sure that Encode::Locale is loaded if needed
63 3 50       11 require Encode::Locale if $self->{native} eq 'locale';
64              
65             # return object
66 3         16 bless $self, $class;
67             }
68              
69              
70             sub inject_callbacks {
71 3     3 1 25 my ($self, $dbh, @invalid_args) = @_;
72              
73             # check input args
74 3 50       32 $dbh->isa('DBI::db') or die '->inject_callbacks() : arg is not a DBI database handle';
75 3 50       15 !@invalid_args or die '->inject_callbacks() : too many args';
76              
77             # coderef to be installed as common callback for all methods. This is a closure on $self.
78             my $upgrade_string_args = sub {
79             # NOTES: - here there is no unpacking of @_ because DBI callbacks must work directly on @_
80             # - $_ is the name of the DBI method
81              
82             # for calls to bind_param() with an explicit bind type, some types should be left untouched (for ex. SQL_BLOB)
83 174 50 66 174   36207 return if $_ eq 'bind_param' && $_[3] && !$self->{bind_type_is_string}->($_[3]);
      33        
84              
85             # vars to be used in the loop
86 13         51 my $do_upgrade = $self->{native} eq 'default' ? sub {utf8::upgrade($_[0])}
87 174 100       866 : sub {$_[0] = decode($self->{native}, $_[0], $self->{decode_check})};
  26         124  
88 174         354 my $dbi_method = $_;
89 174   100     684 my $sql = !ref($_[1]) && $_[1]; # for $dbh methods, SQL is in this position; otherwise undef
90             my $debug = sub {
91 39 50       113 return if !$self->{debug}; # client wants no debugging
92              
93 39         97 my ($arg_pos, $end_msg) = @_;
94 39         101 my $start_msg = "triggering '$dbi_method' callback";
95 39 100 100     193 $start_msg .= " for [$sql]" if $sql and $arg_pos > 1;
96              
97             # try to find the 1st stack frame above DBI and DBIx
98 39         64 my $stack_level = 0;
99 39         342 while (my ($package, $file, $line) = caller $stack_level) {
100 78 100 50     461 $start_msg .= " in $package at $file line $line" and last if $package !~ /^DBI/;
101 39         223 $stack_level += 1;
102             }
103              
104 39         218 $self->{debug}->("$start_msg; $end_msg");
105 174         716 };
106              
107              
108             # loop over members of @_; start only at 1 because $_[0] is the DBI handle
109             ARG:
110 174         593 foreach my $i (1 .. $#_) {
111              
112             # if arg is undef or empty string or 0, there is nothing to do
113 420 50       1485 next ARG if !$_[$i];
114              
115             # if arg is a scalar and needs upgrading, do it
116 420 100       986 if (! ref $_[$i]) {
    100          
117 297 100       555 next ARG if dont_need_upgrade($_[$i]);
118 33         180 $debug->($i, "upgrading arg [$i] ($_[$i])");
119 33         245 $do_upgrade->($_[$i]);
120             }
121              
122             # if arg is an arrayref (used by the *_array methods), upgrade strings in that array
123             elsif (ref $_[$i] eq 'ARRAY') {
124 18         36 for my $val (grep {!dont_need_upgrade($_)} @{$_[$i]}) {
  36         78  
  18         49  
125 6         52 $debug->($i, "upgrading string in array arg [$i] ($val)");
126 6         56 $do_upgrade->($val);
127             }
128             }
129             }
130              
131 174         258364 return; # must end with an empty return (see L documentation)
132 3         51 };
133              
134             # now inject the callback for $dbh methods and for $sth methods
135 3   50     84 my $parent_callbacks = $dbh->{Callbacks} //= {};
136 3   50     31 my $child_callbacks = $parent_callbacks->{ChildCallbacks} //= {};
137 3         7 inject_callback($parent_callbacks, $_ => $upgrade_string_args) for @{$self->{dbh_methods}};
  3         25  
138 3         6 inject_callback($child_callbacks, $_ => $upgrade_string_args) for @{$self->{sth_methods}};
  3         14  
139             }
140              
141              
142             sub inject_callback {
143 39     39 0 79 my ($hash, $key, $coderef) = @_;
144              
145             # in case a previous callback was already installed, we replace it with a sub that combines both
146 39         64 my $previous_cb = $hash->{$key};
147 39 50   0   66 my $new_cb = $previous_cb ? sub {&$coderef; &$previous_cb} : $coderef;
  0         0  
  0         0  
148              
149 39         149 $hash->{$key} = $new_cb;
150             }
151              
152             sub dont_need_upgrade {
153 333     333 0 568 my $scalar = shift;
154             # no need to upgrade if ..
155 333   100     2606 return looks_like_number($scalar) # .. it's a number
156             || utf8::is_utf8($scalar) # .. it's already a utf8 string
157 1     1   10 || $scalar !~ /\P{ASCII}/; # .. it only contains ASCII chars
  1         4  
  1         21  
158             }
159              
160             sub default_bind_type_is_string {
161 0     0 0   my $bind_type = shift;
162              
163             # according to L, the bind type can be given either as a scalar or as a hashref with a TYPE key
164 0 0 0       $bind_type = $bind_type->{TYPE} if (ref $bind_type || '') eq 'HASH';
165              
166 0   0       return looks_like_number($bind_type) && grep {$bind_type == $_} @sql_string_types;
167             }
168              
169             1;
170              
171              
172             __END__