File Coverage

blib/lib/DBIx/NamedParams.pm
Criterion Covered Total %
statement 126 135 93.3
branch 28 38 73.6
condition 22 30 73.3
subroutine 24 25 96.0
pod 5 6 83.3
total 205 234 87.6


line stmt bran cond sub pod time code
1             package DBIx::NamedParams;
2              
3 3     3   281845 use 5.008001;
  3         88  
4 3     3   20 use strict;
  3         24  
  3         102  
5 3     3   17 use warnings;
  3         6  
  3         174  
6 3     3   640 use utf8;
  3         19  
  3         45  
7 3     3   2045 use Encode;
  3         34546  
  3         231  
8 3     3   25 use Carp qw( croak );
  3         7  
  3         218  
9 3     3   1485 use parent qw( DBI );
  3         934  
  3         18  
10 3     3   41794 use DBI::Const::GetInfoType;
  3         17926  
  3         515  
11 3     3   1626 use Log::Dispatch;
  3         780694  
  3         139  
12 3     3   29 use POSIX qw( strftime );
  3         8  
  3         62  
13 3     3   2020 use Scalar::Util qw( reftype );
  3         7  
  3         160  
14 3     3   1630 use Term::Encoding qw( term_encoding );
  3         1911  
  3         198  
15              
16 3     3   1450 use version 0.77; our $VERSION = version->declare("v0.0.11");
  3         6219  
  3         24  
17              
18             our $KeepBindingIfNoKey = 0;
19              
20             my $_package = __PACKAGE__;
21             my $_default_log_filename = $ENV{'HOME'} || $ENV{'USERPROFILE'} || $ENV{'TMP'} || '/tmp';
22             $_default_log_filename =~ s#\\#/#g;
23             $_default_log_filename .= '/DBIx-NamedParams.log';
24              
25             my %_SQL_TypeRefs = ();
26             my %_SQL_TypeInvs = ();
27             my $_SQL_Types = '';
28             my $_log = undef;
29              
30             sub import {
31 3     3   113 DBI->import();
32 3         12 *{DBI::db::driver_typename_map} = \&driver_typename_map;
33 3         8 *{DBI::db::prepare_ex} = \&prepare_ex;
34 3         9 *{DBI::st::mapped_params} = \&mapped_params;
35 3         5 *{DBI::st::bind_param_ex} = \&bind_param_ex;
36 3         9 _init();
37             }
38              
39             sub _init {
40 3     3   8 foreach ( @{ $DBI::EXPORT_TAGS{sql_types} } ) {
  3         18  
41 174         231 my $refFunc = \&{"DBI::$_"};
  174         442  
42 174 50       669 if (/^SQL_(.*)$/i) {
43 174         273 $_SQL_TypeRefs{$1} = &{$refFunc};
  174         524  
44 174         240 $_SQL_TypeInvs{ &{$refFunc} } = $1;
  174         631  
45             }
46             }
47 3         72 $_SQL_Types = all_sql_types();
48             }
49              
50             sub _thisFuncName {
51 29     29   89 ( caller(1) )[3] =~ /([^:]+)$/;
52 29         986 return $1;
53             }
54              
55             sub debug_log {
56 0   0 0 1 0 my $filename = shift || $_default_log_filename;
57 0         0 $_log = Log::Dispatch->new(
58             outputs => [
59             [ 'File',
60             min_level => 'debug',
61             filename => encode( term_encoding, $filename ),
62             binmode => ":utf8",
63             permissions => 0666,
64             newline => 1,
65             ],
66             ],
67             );
68 0         0 $_log->info( _thisFuncName(), strftime( "%Y-%m-%d %H:%M:%S", localtime ) );
69             }
70              
71             sub all_sql_types {
72             return wantarray
73 4 100   4 1 2767 ? sort( keys(%_SQL_TypeRefs) )
74             : join( "|", sort( keys(%_SQL_TypeRefs) ) );
75             }
76              
77             sub driver_typename_map {
78 1     1 1 68434 my $self = shift;
79             my %map = map {
80 1         28 my $datatype = $_->{'SQL_DATA_TYPE'} # MS SQL Server
81             || $_->{'SQL_DATATYPE'} # MySQL
82 5   66     516 || $_->{'DATA_TYPE'}; # SQLite
83 5   100     31 ( $_->{'TYPE_NAME'} || '' ) => $_SQL_TypeInvs{$datatype} || 'WVARCHAR';
      50        
84             } $self->type_info();
85 1 50       21 if ( $self->get_info( $GetInfoType{'SQL_DBMS_NAME'} ) eq 'Microsoft SQL Server' ) {
86 0         0 $map{'datetime'} = 'WVARCHAR';
87 0         0 $map{'smalldatetime'} = 'WVARCHAR';
88             }
89 1         2119 return %map;
90             }
91              
92             sub prepare_ex {
93 18     18 1 48309 my ( $self, $sqlex, $refHash ) = @_;
94 18   100     91 my $validHash = defined($refHash) && ( reftype($refHash) || '' ) eq 'HASH';
95 18 100       465 if ( $sqlex =~ /\:([\w]+)\+-($_SQL_Types)\b/ ) {
96 9 100       25 if ($validHash) {
97 4         185 $sqlex =~ s/\:([\w]+)\+-($_SQL_Types)\b/_parse_ex1($refHash,$1,$2);/ge;
  4         16  
98             } else {
99 5         79 croak("prepare_ex need a hash reference when SQL is variable length.");
100             }
101             }
102 13         33 my @params = ();
103 13         353 $sqlex =~ s/\:([\w]+)(?:\{(\d+)\})?-($_SQL_Types)\b/_parse_ex2(\@params,$1,$2,$3);/ge;
  22         61  
104 13 50       39 if ($_log) {
105 0         0 $_log->info( _thisFuncName(), 'sql_raw', "{{$sqlex}}" );
106             }
107 13 50       95 my $sth = $self->prepare($sqlex) or croak($DBI::errstr);
108 13         1131 $sth->mapped_params(@params);
109 13 100       30 if ($validHash) {
110 4         11 $sth->bind_param_ex($refHash);
111             }
112 13         112 return $sth;
113             }
114              
115             sub _parse_ex1 {
116 4     4   16 my ( $refHash, $name, $type ) = @_;
117 4         9 my $numOfArray = 0;
118 4 50       24 if ( ref( $refHash->{$name} ) eq 'ARRAY' ) {
119 4         6 $numOfArray = scalar( @{ $refHash->{$name} } );
  4         10  
120             } else {
121 0         0 croak("Must be array: ${name}");
122             }
123 4         32 return ":${name}{${numOfArray}}-${type}";
124             }
125              
126             sub _parse_ex2 {
127 22     22   37 my $params = shift;
128 22   50     65 my $name = shift || '';
129 22   100     72 my $repeat = shift || 0;
130 22   50     55 my $type = shift || '';
131 22         27 my $index = scalar( @{$params} ) + 1;
  22         34  
132              
133 22 50       51 if ($_log) {
134 0 0       0 $_log->info( _thisFuncName(), "[${index}]", "\"${name}\"",
135             ( !$repeat ) ? "scalar" : "array[$repeat]", $type );
136             }
137 22 100       51 if ( !$repeat ) { # scalar
138             push(
139 17         65 @{$params},
140             { Name => $name,
141 17         26 Type => $_SQL_TypeRefs{$type},
142             Array => -1,
143             }
144             );
145 17         80 return '?';
146             } else { # array
147 5         18 for ( my $i = 0; $i < $repeat; ++$i ) {
148             push(
149 16         57 @{$params},
150             { Name => $name,
151 16         24 Type => $_SQL_TypeRefs{$type},
152             Array => $i,
153             }
154             );
155             }
156 5         28 return substr( '?,' x $repeat, 0, -1 );
157             }
158             }
159              
160             sub mapped_params {
161 42     42 0 75 my $self = shift;
162 42         59 my $inner = tied( %{$self} );
  42         87  
163 42 100       91 if (@_) {
164 11         44 $inner->{$_package}{MappedParams} = [@_];
165             }
166 42 100       63 return @{ $inner->{$_package}{MappedParams} || [] };
  42         170  
167             }
168              
169             sub bind_param_ex {
170 3     3   4410 no warnings 'uninitialized';
  3         7  
  3         950  
171 34     34 1 8675 my ( $self, $refHash ) = @_;
172 34 100 100     242 if ( !defined($refHash) || ( reftype($refHash) || '' ) ne 'HASH' ) {
      100        
173 5         58 croak("bind_param_ex need a hash reference.");
174             }
175 29         67 my $thisFunc = _thisFuncName();
176 29         62 my $i = 0;
177 29         69 foreach my $param ( $self->mapped_params() ) {
178 77         113 ++$i;
179 77 100 100     222 if ( $KeepBindingIfNoKey && !exists( $refHash->{ $param->{'Name'} } ) ) {
180 14         21 next;
181             }
182 63         91 my $idx = $param->{'Array'};
183 63         118 my $value1 = $refHash->{ $param->{'Name'} };
184 63 100 66     186 my $value2
185             = ( $idx < 0 || ref($value1) ne 'ARRAY' )
186             ? $value1
187             : $value1->[$idx];
188 63         92 my $datatype = $param->{'Type'};
189 63 50       111 if ($_log) {
190 0         0 $_log->info( $thisFunc, "[$i]", "\"$value2\"", $_SQL_TypeInvs{$datatype} );
191             }
192 63 50       421 $self->bind_param( $i, $value2, { TYPE => $datatype } )
193             or croak($DBI::errstr);
194             }
195 29         107 return $self;
196             }
197              
198             1;
199             __END__
200              
201             =encoding utf-8
202              
203             =head1 NAME
204              
205             DBIx::NamedParams - use named parameters instead of '?'
206              
207             =head1 SYNOPSIS
208              
209             This module allows you to use named parameters as the placeholders instead of '?'.
210              
211             use DBIx::NamedParams;
212              
213             # Connect DB
214             my $dbh = DBI->connect( ... ) or die($DBI::errstr);
215              
216             # Bind scalar
217             # :<Name>-<Type>
218             my $sql_insert = qq{
219             INSERT INTO `Users` ( `Name`, `Status` ) VALUES ( :Name-VARCHAR, :State-INTEGER );
220             };
221             my $sth_insert = $dbh->prepare_ex( $sql_insert ) or die($DBI::errstr);
222             $sth_insert->bind_param_ex( { 'Name' => 'Rio', 'State' => 1, } ) or die($DBI::errstr);
223             my $rv = $sth_insert->execute() or die($DBI::errstr);
224              
225             # Bind fixed array
226             # :<Name>{Number}-<Type>
227             my $sql_select1 = qq{
228             SELECT `ID`, `Name`, `Status`
229             FROM `Users`
230             WHERE `Status` in ( :State{4}-INTEGER );
231             };
232             my $sth_select1 = $dbh->prepare_ex( $sql_select1 ) or die($DBI::errstr);
233             $sth_select1->bind_param_ex( { 'State' => [ 1,2,4,8 ], } ) or die($DBI::errstr);
234             my $rv = $sth_select1->execute() or die($DBI::errstr);
235              
236             # Bind variable array
237             # :<Name>+-<Type>
238             my $sql_select2 = qq{
239             SELECT `ID`, `Name`, `Status`
240             FROM `Users`
241             WHERE `Status` in ( :State+-INTEGER );
242             };
243             my $sth_select2 = $dbh->prepare_ex( $sql_select2, { 'State' => [ 1,2,4,8 ], } )
244             or die($DBI::errstr);
245             my $rv = $sth_select2->execute() or die($DBI::errstr);
246              
247             =head1 DESCRIPTION
248              
249             DBIx::NamedParams helps binding SQL parameters.
250              
251             =head1 FLAGS
252              
253             =head2 $DBIx::NamedParams::KeepBindingIfNoKey
254              
255             In C<bind_param_ex()>, this flag controls the behavior when the hash reference doesn't have the key
256             in the SQL statement.
257              
258             Defaults to false. The placeholders according to the missing keys are set to C<undef>.
259             All of the placeholders have to be set at once.
260              
261             Setting this to a true value, the placeholders according to the missing keys are kept.
262             You can set some placeholders at first, and set other placeholders later.
263             If you want to set a placeholder to null, you have to set C<undef> explicitly.
264              
265             =head1 METHODS
266              
267             =head2 DBIx::NamedParams Class Methods
268              
269             =head3 all_sql_types
270              
271             Returns the all SQL data types defined in L<DBI> .
272              
273             my @types = DBIx::NamedParams::all_sql_types();
274              
275             =head3 debug_log
276              
277             Writes the parsed SQL statement and the values at the parameter positions into the log file.
278             When omitting the filename, creates the log file in the home directory.
279              
280             DBIx::NamedParams::debug_log( '/tmp/testNamedParams.log' );
281              
282             =head2 Database Handle Methods
283              
284             =head3 driver_typename_map
285              
286             Returns the hash from the driver type names to the DBI typenames.
287              
288             my %map = $dbh->driver_typename_map();
289              
290             =head3 prepare_ex
291              
292             Prepares a statement for later execution by the database engine and returns a reference to a statement handle object.
293             When the SQL statement has the variable array C<:E<lt>NameE<gt>+-E<lt>TypeE<gt>>, the hash reference as the second argument is mandatory.
294             When the SQL statement doesn't have the variable array C<:E<lt>NameE<gt>+-E<lt>TypeE<gt>>, the hash reference as the second argument is optional.
295              
296             my $sth = $dbh->prepare_ex( $statement, $hashref ) or die($DBI::errstr);
297              
298             =head2 Statement Handle Methods
299              
300             =head3 bind_param_ex
301              
302             Binds each parameters at once according to the hash reference.
303             The hash reference should have the keys that are same names to the parameter names in the SQL statement.
304             When the hash reference doesn't have the key that is same to the parameter name, the parameter is not set.
305              
306             $sth->bind_param_ex( $hashref ) or die($DBI::errstr);
307              
308             =head1 SEE ALSO
309              
310             =head2 Similar modules
311              
312             L<Tao::DBI>
313              
314             L<DBIx::NamedBinding>
315              
316             L<SQL::NamedPlaceholder>
317              
318             =head2 DBD informations
319              
320             L<SQLite Keywords|https://www.sqlite.org/lang_keywords.html> explains how to quote the identifier.
321              
322             =head1 LICENSE
323              
324             Copyright (C) TakeAsh.
325              
326             This library is free software; you can redistribute it and/or modify
327             it under the same terms as Perl itself.
328              
329             =head1 AUTHOR
330              
331             L<TakeAsh|https://github.com/TakeAsh/>
332              
333             =cut
334