File Coverage

blib/lib/DBIx/SearchBuilder/Handle/MariaDB.pm
Criterion Covered Total %
statement 12 144 8.3
branch 0 62 0.0
condition 0 18 0.0
subroutine 4 24 16.6
pod 13 16 81.2
total 29 264 10.9


line stmt bran cond sub pod time code
1             package DBIx::SearchBuilder::Handle::MariaDB;
2              
3 1     1   1959 use strict;
  1         2  
  1         29  
4 1     1   4 use warnings;
  1         1  
  1         33  
5 1     1   4 use version;
  1         1  
  1         8  
6              
7 1     1   56 use base qw(DBIx::SearchBuilder::Handle);
  1         2  
  1         2622  
8              
9             =head1 NAME
10              
11             DBIx::SearchBuilder::Handle::MariaDB - A MariaDB specific Handle object
12              
13             =head1 SYNOPSIS
14              
15              
16             =head1 DESCRIPTION
17              
18             This module provides a subclass of DBIx::SearchBuilder::Handle that
19             compensates for some of the idiosyncrasies of MySQL.
20              
21             =head1 METHODS
22              
23             =head2 Insert
24              
25             Takes a table name as the first argument and assumes that the rest of the arguments are an array of key-value pairs to be inserted.
26              
27             If the insert succeeds, returns the id of the insert, otherwise, returns
28             a Class::ReturnValue object with the error reported.
29              
30             =cut
31              
32             sub Insert {
33 0     0 1   my $self = shift;
34              
35 0           my $sth = $self->SUPER::Insert(@_);
36 0 0         if (!$sth) {
37 0           return ($sth);
38             }
39              
40             # Follow the advice in the docs and use last_insert_id rather than
41             # {'mariadb_insertid'}.
42             #
43             # https://metacpan.org/dist/DBD-MariaDB/view/lib/DBD/MariaDB.pod#mariadb_insertid
44              
45 0           $self->{'id'} = $self->dbh->last_insert_id();
46              
47             # Docs say last_insert_id could still return undef, so keeping this code
48 0 0         unless ( $self->{'id'} ) {
49 0           $self->{'id'} = $self->FetchResult('SELECT LAST_INSERT_ID()');
50             }
51 0 0         warn "$self no row id returned on row creation" unless ($self->{'id'});
52              
53 0           return( $self->{'id'}); #Add Succeded. return the id
54             }
55              
56             =head2 KnowsBLOBs
57              
58             Returns 1 if the current database supports inserts of BLOBs automatically.
59             Returns undef if the current database must be informed of BLOBs for inserts.
60              
61             =cut
62              
63             sub KnowsBLOBs {
64 0     0 1   my $self = shift;
65 0           return(undef);
66             }
67              
68             =head2 BLOBParams FIELD_NAME FIELD_TYPE
69              
70             Returns a hash ref for the bind_param call to identify BLOB types used by
71             the current database for a particular column type.
72              
73             =cut
74              
75             sub BLOBParams {
76 0     0 1   my $self = shift;
77 0           my $field = shift;
78 0           my $type = shift;
79              
80 0 0         if ( $type =~ /^(blob|longblob)$/i ) {
81             # Don't assign to key 'value' as it is defined later.
82 0           return ( { TYPE => 'SQL_BLOB', } );
83             }
84             else {
85             # Normal handling for these, so no hashref
86 0           return;
87             }
88             }
89              
90              
91             =head2 SimpleUpdateFromSelect
92              
93             Customization of L.
94             Mysql doesn't support update with subqueries when those fetch data from
95             the table that is updated.
96              
97             =cut
98              
99             sub SimpleUpdateFromSelect {
100 0     0 1   my ($self, $table, $values, $query, @query_binds) = @_;
101              
102 0 0         return $self->SUPER::SimpleUpdateFromSelect(
103             $table, $values, $query, @query_binds
104             ) unless $query =~ /\b\Q$table\E\b/i;
105              
106 0           my $sth = $self->SimpleQuery( $query, @query_binds );
107 0 0         return $sth unless $sth;
108              
109 0           my (@binds, @columns);
110 0           for my $k (sort keys %$values) {
111 0           push @columns, $k;
112 0           push @binds, $values->{$k};
113             }
114              
115 0 0         $table = $self->QuoteName($table) if $self->{'QuoteTableNames'};
116 0           my $update_query = "UPDATE $table SET "
117             . join( ', ', map "$_ = ?", @columns )
118             .' WHERE ID IN ';
119              
120 0           return $self->SimpleMassChangeFromSelect(
121             $update_query, \@binds,
122             $query, @query_binds
123             );
124             }
125              
126              
127             sub DeleteFromSelect {
128 0     0 0   my ($self, $table, $query, @query_binds) = @_;
129              
130 0 0         return $self->SUPER::DeleteFromSelect(
131             $table, $query, @query_binds
132             ) unless $query =~ /\b\Q$table\E\b/i;
133              
134 0 0         $table = $self->QuoteName($table) if $self->{'QuoteTableNames'};
135 0           return $self->SimpleMassChangeFromSelect(
136             "DELETE FROM $table WHERE id IN ", [],
137             $query, @query_binds
138             );
139             }
140              
141             sub SimpleMassChangeFromSelect {
142 0     0 0   my ($self, $update_query, $update_binds, $search, @search_binds) = @_;
143              
144 0           my $sth = $self->SimpleQuery( $search, @search_binds );
145 0 0         return $sth unless $sth;
146              
147              
148             # tried TEMPORARY tables, much slower than fetching and delete
149             # also size of ENGINE=MEMORY is limitted by option, on disk
150             # tables more slower than in memory
151 0           my $res = 0;
152              
153 0           my @ids;
154 0           while ( my $id = ($sth->fetchrow_array)[0] ) {
155 0           push @ids, $id;
156 0 0         next if @ids < 1000;
157              
158 0           my $q = $update_query .'('. join( ',', ('?')x@ids ) .')';
159 0           my $sth = $self->SimpleQuery( $q, @$update_binds, splice @ids );
160 0 0         return $sth unless $sth;
161              
162 0           $res += $sth->rows;
163             }
164 0 0         if ( @ids ) {
165 0           my $q = $update_query .'('. join( ',', ('?')x@ids ) .')';
166 0           my $sth = $self->SimpleQuery( $q, @$update_binds, splice @ids );
167 0 0         return $sth unless $sth;
168              
169 0           $res += $sth->rows;
170             }
171 0 0         return $res == 0? '0E0': $res;
172             }
173              
174             =head2 DatabaseVersion
175              
176             Returns the MariaDB version, trimming off any -foo identifier
177              
178             =cut
179              
180             sub DatabaseVersion {
181 0     0 1   my $self = shift;
182 0           my $v = $self->SUPER::DatabaseVersion();
183              
184 0           $v =~ s/\-.*$//;
185 0           return ($v);
186             }
187              
188             =head2 CaseSensitive
189              
190             Returns undef, since MariaDB's searches are not case sensitive by default
191              
192             =cut
193              
194             sub CaseSensitive {
195 0     0 1   my $self = shift;
196 0           return(undef);
197             }
198              
199             sub DistinctQuery {
200 0     0 1   my $self = shift;
201 0           my $statementref = shift;
202 0           my $sb = shift;
203              
204 0 0         return $self->SUPER::DistinctQuery( $statementref, $sb, @_ )
205             if $sb->_OrderClause !~ /(?
206              
207 0 0         if ( substr($self->DatabaseVersion, 0, 1) == 4 ) {
208 0           local $sb->{'group_by'} = [{FIELD => 'id'}];
209              
210 0           my ($idx, @tmp, @specials) = (0, ());
211 0           foreach ( @{$sb->{'order_by'}} ) {
  0            
212 0 0 0       if ( !exists $_->{'ALIAS'} || ($_->{'ALIAS'}||'') eq "main" ) {
      0        
213 0           push @tmp, $_; next;
  0            
214             }
215              
216             push @specials,
217             ((($_->{'ORDER'}||'') =~ /^des/i)?'MAX':'MIN')
218 0 0 0       ."(". $_->{'ALIAS'} .".". $_->{'FIELD'} .")"
219             ." __special_sort_$idx";
220 0           push @tmp, { ALIAS => '', FIELD => "__special_sort_$idx", ORDER => $_->{'ORDER'} };
221 0           $idx++;
222             }
223              
224 0           local $sb->{'order_by'} = \@tmp;
225 0           $$statementref = "SELECT ". join( ", ", 'main.*', @specials ) ." FROM $$statementref";
226 0           $$statementref .= $sb->_GroupClause;
227 0           $$statementref .= $sb->_OrderClause;
228             } else {
229 0           local $sb->{'group_by'} = [{FIELD => 'id'}];
230             local $sb->{'order_by'} = [
231             map {
232             ($_->{'ALIAS'}||'') ne "main"
233 0 0 0       ? { %{$_}, FIELD => ((($_->{'ORDER'}||'') =~ /^des/i)?'MAX':'MIN') ."(".$_->{FIELD}.")" }
  0 0 0        
234             : $_
235             }
236 0           @{$sb->{'order_by'}}
  0            
237             ];
238 0           $$statementref = "SELECT main.* FROM $$statementref";
239 0           $$statementref .= $sb->_GroupClause;
240 0           $$statementref .= $sb->_OrderClause;
241             }
242             }
243              
244             sub Fields {
245 0     0 0   my $self = shift;
246 0           my $table = shift;
247              
248 0           my $cache = \%DBIx::SearchBuilder::Handle::FIELDS_IN_TABLE;
249 0 0         unless ( $cache->{ lc $table } ) {
250 0 0         my $sth = $self->dbh->column_info( undef, undef, $table, '%' )
251             or return ();
252 0           my $info = $sth->fetchall_arrayref({});
253 0           foreach my $e ( sort {$a->{'ORDINAL_POSITION'} <=> $b->{'ORDINAL_POSITION'}} @$info ) {
  0            
254 0   0       push @{ $cache->{ lc $e->{'TABLE_NAME'} } ||= [] }, lc $e->{'COLUMN_NAME'};
  0            
255             }
256             }
257 0 0         return @{ $cache->{ lc $table } || [] };
  0            
258             }
259              
260             =head2 SimpleDateTimeFunctions
261              
262             Returns hash reference with specific date time functions of this
263             database for L.
264              
265             =cut
266              
267             sub SimpleDateTimeFunctions {
268 0     0 1   my $self = shift;
269             return $self->{'_simple_date_time_functions'} ||= {
270 0   0       %{ $self->SUPER::SimpleDateTimeFunctions(@_) },
  0            
271             datetime => '?',
272             time => 'TIME(?)',
273              
274             hourly => "DATE_FORMAT(?, '%Y-%m-%d %H')",
275             hour => 'HOUR(?)',
276              
277             date => 'DATE(?)',
278             daily => 'DATE(?)',
279              
280             day => 'DAYOFMONTH(?)',
281             dayofmonth => 'DAYOFMONTH(?)',
282              
283             monthly => "DATE_FORMAT(?, '%Y-%m')",
284             month => 'MONTH(?)',
285              
286             annually => 'YEAR(?)',
287             year => 'YEAR(?)',
288              
289             dayofweek => "DAYOFWEEK(?) - 1", # 1-7, 1 - Sunday
290             dayofyear => "DAYOFYEAR(?)", # 1-366
291             weekofyear => "WEEK(?)", # skip mode argument, so it can be controlled in MariaDB config
292             };
293             }
294              
295              
296             =head2 ConvertTimezoneFunction
297              
298             Custom implementation of L.
299              
300             Use the following query to get list of timezones:
301              
302             SELECT Name FROM mysql.time_zone_name;
303              
304             See also details on how MariaDB works with mysql timezone tables
305              
306             https://mariadb.com/kb/en/time-zones/
307              
308             =cut
309              
310             sub ConvertTimezoneFunction {
311 0     0 1   my $self = shift;
312 0           my %args = (
313             From => 'UTC',
314             To => undef,
315             Field => '',
316             @_
317             );
318 0 0 0       return $args{'Field'} unless $args{From} && $args{'To'};
319 0 0         return $args{'Field'} if lc $args{From} eq lc $args{'To'};
320 0           my $dbh = $self->dbh;
321 0           $_ = $dbh->quote( $_ ) foreach @args{'From', 'To'};
322 0           return "CONVERT_TZ( $args{'Field'}, $args{'From'}, $args{'To'} )";
323             }
324              
325             sub _DateTimeIntervalFunction {
326 0     0     my $self = shift;
327 0           my %args = ( From => undef, To => undef, @_ );
328              
329 0           return "TIMESTAMPDIFF(SECOND, $args{'From'}, $args{'To'})";
330             }
331              
332              
333             =head2 QuoteName
334              
335             Quote table or column name to avoid reserved word errors.
336              
337             =cut
338              
339             # over-rides inherited method
340             sub QuoteName {
341 0     0 1   my ($self, $name) = @_;
342             # use dbi built in quoting if we have a connection,
343 0 0         if ($self->dbh) {
344 0           return $self->SUPER::QuoteName($name);
345             }
346              
347 0           return sprintf('`%s`', $name);
348             }
349              
350             sub DequoteName {
351 0     0 1   my ($self, $name) = @_;
352              
353             # If we have a handle, the base class can do it for us
354 0 0         if ($self->dbh) {
355 0           return $self->SUPER::DequoteName($name);
356             }
357              
358 0 0         if ($name =~ /^`(.*)`$/) {
359 0           return $1;
360             }
361 0           return $name;
362             }
363              
364             sub _ExtractBindValues {
365 0     0     my $self = shift;
366 0           my $value = shift;
367 0           return $self->SUPER::_ExtractBindValues( $value, '\\' );
368             }
369              
370             sub _IsMariaDB {
371 0     0     my $self = shift;
372              
373             # We override DatabaseVersion to chop off "-MariaDB-whatever", so
374             # call super here to get the original version
375 0           my $v = $self->SUPER::DatabaseVersion();
376              
377 0           return ($v =~ /mariadb/i);
378             }
379              
380             sub _RequireQuotedTables {
381 0     0     my $self = shift;
382              
383             # MariaDB version does not match mysql, and hasn't added new reserved words
384             # like "groups".
385 0           return 0;
386             }
387              
388             =head2 HasSupportForCombineSearchAndCount
389              
390             MariaDB 10.2+ supports this.
391              
392             =cut
393              
394             sub HasSupportForCombineSearchAndCount {
395 0     0 1   my $self = shift;
396 0           my ($version) = $self->DatabaseVersion =~ /^(\d+\.\d+)/;
397              
398 0 0         return (version->parse('v'.$version) >= version->parse('v10.2')) ? 1 : 0;
399             }
400              
401             sub CastAsDecimal {
402 0     0 1   my $self = shift;
403 0 0         my $field = shift or return;
404              
405             # CAST($field AS DECIMAL) rounds values to integers by default. It supports
406             # specific precisions like CAST($field AS DECIMAL(5,2)), but we don't know
407             # the precisions in advance. +0 works like other dbs.
408 0           return "($field+0)";
409             }
410              
411             1;