File Coverage

blib/lib/DBIx/RoboQuery/ResultSet.pm
Criterion Covered Total %
statement 142 146 97.2
branch 40 54 74.0
condition 14 23 60.8
subroutine 18 21 85.7
pod 14 14 100.0
total 228 258 88.3


line stmt bran cond sub pod time code
1             # vim: set ts=2 sts=2 sw=2 expandtab smarttab:
2             #
3             # This file is part of DBIx-RoboQuery
4             #
5             # This software is copyright (c) 2010 by Randy Stauner.
6             #
7             # This is free software; you can redistribute it and/or modify it under
8             # the same terms as the Perl 5 programming language system itself.
9             #
10 8     8   37 use strict;
  8         15  
  8         291  
11 8     8   39 use warnings;
  8         19  
  8         418  
12              
13             package DBIx::RoboQuery::ResultSet;
14             {
15             $DBIx::RoboQuery::ResultSet::VERSION = '0.032';
16             }
17             BEGIN {
18 8     8   165 $DBIx::RoboQuery::ResultSet::AUTHORITY = 'cpan:RWSTAUNER';
19             }
20             # ABSTRACT: Configure the results to get what you want
21              
22 8     8   43 use Carp qw(croak carp);
  8         10  
  8         460  
23 8     8   6340 use Timer::Simple;
  8         15184  
  8         21450  
24              
25              
26             sub new {
27 34     34 1 4958 my $class = shift;
28 34         52 my $query = shift;
29 34 100       134 my %opts = ref($_[0]) eq 'HASH' ? %{$_[0]} : @_;
  12         54  
30 34         179 my $self = {
31             row_count => -1,
32             times => {},
33             query => $query,
34             default_slice => {},
35              
36             # Process the template in case it changes anything (like query.key_columns)
37             # so that everything will get passed to the ResultSet.
38             sql => $query->sql(),
39             };
40              
41 34         115 bless $self, $class;
42              
43 34         118 foreach my $var ( $self->_pass_through_args() ){
44             # allow options to be specified directly
45 306 100       848 if( exists($opts{$var}) ){
    100          
46 36         144 $self->{$var} = $opts{$var};
47             }
48             # or look for them on the query object
49             elsif( exists($query->{$var}) ){
50 74         198 $self->{$var} = $query->{$var};
51             }
52             }
53              
54 34         146 DBIx::RoboQuery::Util::_ensure_arrayrefs($self);
55              
56 34   50     379 $self->{hash_key_name} ||=
      33        
57             ($self->{dbh} && $self->{dbh}{FetchHashKeyName})
58             || 'NAME_lc';
59              
60 34         172 return $self;
61             }
62              
63              
64             sub array {
65 2     2 1 31 my ($self, @args) = @_;
66              
67             # default to an array of hashrefs if no arguments are given
68 2 100       9 @args = $self->{default_slice}
69             unless @args;
70              
71 2 50       14 $self->execute() if !$self->{executed};
72              
73 2 50       9 croak('Columns unknown. Was this a SELECT?')
74             unless $self->{all_columns};
75              
76 2         5 my @tr_args = ();
77 2 50       16 if( @args ){
78             # if the slice is empty, fill it with the non-drop_columns
79 2         4 my $slice = $args[0];
80 2 100 66     28 if( ref($slice) eq 'HASH' and !keys(%$slice) ){
    50 33        
81 1         5 $slice->{$_} = 1 for $self->columns;
82             }
83             elsif( ref($slice) eq 'ARRAY' and !@$slice ){
84 1         2 my @col = @{$self->{all_columns}};
  1         4  
85 1         3 my %drop = map { $_ => 1 } @{ $self->{drop_columns} };
  0         0  
  1         4  
86             # turn [] into a list of (possibly non-contiguous) indexes: [1,2,4,5]
87 1         4 push(@$slice, grep { !$drop{ $col[$_] } } 0 .. $#col);
  3         8  
88             # set the first (only) element to an arrayref of column names
89 1         6 @tr_args = ( [@col[@$slice]] );
90             }
91             }
92              
93 2         6 my $tr = $self->{transformations};
94 2         8 my $t = Timer::Simple->new;
95              
96 2         22 my $rows = $self->{sth}->fetchall_arrayref(@args);
97 2 50       124 $rows = [map { $tr->call(@tr_args, $_) } @$rows]
  6         38342  
98             if $tr;
99              
100             # include transformations in the time for consistency with hash()
101 2         4994 $self->{times}{fetch} = $t->stop;
102 2         216 $self->{row_count} = @$rows;
103              
104 2         18 return $rows;
105             }
106              
107             # convenience method for subclasses
108              
109             sub _arrayref_args {
110 68     68   94 my ($self) = @_;
111 68         243 return $self->{query}->_arrayref_args;
112             }
113              
114              
115 0     0 1 0 sub bound_params { $_[0]->query->bound_params }
116 0     0 1 0 sub bound_values { $_[0]->query->bound_values }
117              
118              
119             sub columns {
120 7     7 1 13 my ($self) = @_;
121 7 50       30 croak('Columns not known until after the statement has executed')
122             unless $self->{executed};
123 7         8 return @{ $self->{columns} };
  7         40  
124             }
125              
126              
127             sub drop_columns {
128 7     7 1 2161 return @{$_[0]->{drop_columns}};
  7         36  
129             }
130              
131              
132             sub execute {
133 16     16 1 169 my ($self, @params) = @_;
134              
135             # the sql attribute is cached from $query->sql in the constructor
136 16         30 my $sql = $self->{sql};
137              
138 16         111 my $t = Timer::Simple->new;
139 16 50       2944 my $sth = $self->{sth} = $self->{dbh}->prepare($sql)
140             or croak $self->{dbh}->errstr;
141 16         1096 $self->{times}{prepare} = $t->stop;
142              
143             # call bind_param() regardless of @params b/c bind_param can specify a type
144 16 100       867 if( my $bind = $self->{bind_params} ){
145 1         2 local $_;
146 1         9 $sth->bind_param(@$_) for @$bind;
147             }
148              
149 16         361 $t->restart;
150 16 50       796 $self->{executed} = $sth->execute(@params)
151             or croak $sth->errstr;
152 16         877 $self->{times}{execute} = $t->stop;
153              
154 16 100       788 if( my $columns = $sth->{ $self->{hash_key_name} } ){
155             # save the full order for later (but break the reference)
156 15         47 $self->{all_columns} = [@$columns];
157              
158             # cache groups of column names
159 15         61 foreach my $set (
160             # preserve the order of non-dropped columns
161             [qw( columns all_columns drop_columns )],
162             # make the list of non-key columns available separately
163             [qw( non_key_columns columns key_columns )],
164             ){
165 30         51 my ($make, $from, $minus) = @$set;
166 30         33 my %other = map { ($_ => 1) } @{ $self->{ $minus } };
  29         81  
  30         63  
167 30         45 $self->{ $make } = [ grep { !$other{$_} } @{ $self->{ $from } } ];
  120         310  
  30         54  
168             }
169              
170 15 50       63 if( my $transformations = $self->{transformations} ){
171 15         113 foreach my $groups (
172             [key => $self->{key_columns}],
173             [non_key => $self->{non_key_columns}],
174             # aliases
175             [key_columns => {in => 'key'}],
176             [non_key_columns => {in => 'non_key'}],
177             ){
178 60         3061 $transformations->group(@$groups);
179             }
180             # set all the columns so we can use group exclusions
181 15         832 $transformations->fields(@$columns);
182             }
183             }
184              
185             # FIXME: check $sth->errstr (or someting: see DBI)
186             # to make sure we got all records without error
187              
188 16         598 return $self->{executed};
189             }
190              
191              
192             sub hash {
193 6     6 1 725 my ($self) = @_;
194 6 100       48 $self->execute() if !$self->{executed};
195             # TODO: care if this is called more than once?
196 6         13 my $sth = $self->{sth};
197              
198 6 100       10 my @key_columns = @{ $self->{key_columns} }
  6         52  
199             or croak('Cannot use hash() with an empty key_columns attribute');
200              
201             # We could just return $sth->fetchall_hashref(\@key_columns) if there are
202             # no preferences but we can't slice out the dropped columns that way.
203              
204 5         8 my @drop_columns = @{ $self->{drop_columns} };
  5         11  
205 5         9 my @columns = (@key_columns, @{ $self->{non_key_columns} });
  5         13  
206              
207             # we have to save the dropped columns so we can send them to preference()
208 5         10 my ($root, $dropped) = ({}, {});
209              
210             # NOTE: It seemed to me more powerful to transform the data upon fetch
211             # rather than upon storage in the tree: it gives you the option of
212             # pre-transforming the keys to adjust the way the tree is built
213             # and lets you know what to expect in the preference rules.
214             # Plus it was easier to implement.
215             # I can't think of a reason to want transform the key columns in the record
216             # but not the tree (ex: {A => {B => {k1 => 'a', k2 => 'b'}}})
217             # If you want un-adultered data for preferences you can select the column
218             # again with an alias and then drop it.
219              
220 5         8 my $tr = $self->{transformations};
221              
222             # we only increase the row count for new (not overridden) hashes
223 5         8 my $count = 0;
224 5         44 my $t = Timer::Simple->new;
225              
226             # check for preferences once... if there are none, do the quick version
227 5 100 66     33 if( !$self->{preferences} || !@{$self->{preferences}} ){
228             # we can't honor drop_columns with fetchall_hashref(), so fake it
229 3         21 while( my $row = $sth->fetchrow_hashref() ){
230 15 50       1217 $row = $tr->call($row) if $tr;
231 15         29648 my $hash = $root;
232 15   100     227 $hash = ($hash->{ $row->{$_} } ||= {}) for @key_columns;
233 15 100       66 ++$count unless keys %$hash;
234 15         179 @$hash{@columns} = @$row{@columns};
235             }
236             }
237             else {
238 2         13 while( my $row = $sth->fetchrow_hashref() ){
239 12 50       655 $row = $tr->call($row) if $tr;
240 12         956 my ($hash, $drop) = ($root, $dropped);
241             # traverse hash tree to get to {key1 => {key2 => {record}}}
242 12         23 foreach ( @key_columns ){
243 24   100     83 $hash = ($hash->{ $row->{$_} } ||= {});
244 24   100     93 $drop = ($drop->{ $row->{$_} } ||= {});
245             }
246             # if there's already a record there (not an empty hash)
247             # (a few benchmarks suggest keys() may be faster than exists())
248 12 100       31 if( keys %$hash ){
249 4         23 $row = $self->preference({%$drop, %$hash}, $row);
250             }
251             else {
252 8         9 ++$count;
253             }
254 12         39 @$drop{@drop_columns} = @$row{@drop_columns};
255 12         116 @$hash{@columns} = @$row{@columns};
256             }
257             }
258 5         498 $self->{times}{fetch} = $t->stop;
259 5         343 $self->{row_count} = $count;
260 5         41 return $root;
261             }
262              
263              
264             sub key_columns {
265 8     8 1 1766 my ($self) = @_;
266 8         10 return @{$self->{key_columns}};
  8         45  
267             }
268              
269              
270             sub non_key_columns {
271 4     4 1 8 my ($self) = @_;
272 4 50       15 croak('Columns not known until after the statement has executed')
273             unless $self->{executed};
274             # An empty array should mean that the rest are key or drop columns.
275             # If not defined, there's a problem.
276 4 50       10 croak('Columns unknown. Was this a SELECT?')
277             unless $self->{non_key_columns};
278 4         5 return @{$self->{non_key_columns}};
  4         26  
279             }
280              
281             # convenience method: args allowed in the constructor
282              
283             sub _pass_through_args {
284             (
285 34     34   164 $_[0]->_arrayref_args,
286             qw(
287             dbh
288             default_slice
289             hash_key_name
290             preferences
291             transformations
292             ));
293             }
294              
295              
296             sub preference {
297 19     19 1 11681 my ($self, @records) = @_;
298 19         43 my $rules = $self->{preferences};
299              
300             # return last record if there are no preferences
301 19 50 33     102 return $records[-1]
302             if !$rules || !@$rules;
303              
304 19         42 foreach my $rule ( @$rules ){
305 33         84 my $template = "[% IF $rule %]1[% ELSE %]0[% END %]";
306             # reverse records so that if any are equal the last one in wins
307 33         54 foreach my $record ( reverse @records ){
308 72         284 my $found = $self->{query}->_process_template(\$template, $record);
309 70 100       305 return $record if $found;
310             }
311             }
312             # last record is DBI compatibile plus it is often the newest record
313 2         11 return $records[-1];
314             }
315              
316              
317             sub query {
318 0     0 1 0 return $_[0]->{query};
319             }
320              
321              
322             sub row_count {
323 3     3 1 1807 return $_[0]->{row_count};
324             }
325              
326              
327             sub times {
328 3     3 1 1678 my ($self) = @_;
329 3         7 my %times = %{ $self->{times} };
  3         19  
330 3         12 $times{total} = $times{prepare} + $times{execute} + $times{fetch};
331 3         11 return \%times;
332             }
333              
334             # The DBI objects clean up after themselves, so DESTROY not currently warranted
335              
336             1;
337              
338             __END__