File Coverage

blib/lib/Class/Inflate.pm
Criterion Covered Total %
statement 510 583 87.4
branch 124 204 60.7
condition 48 83 57.8
subroutine 33 35 94.2
pod 0 16 0.0
total 715 921 77.6


line stmt bran cond sub pod time code
1             package Class::Inflate;
2              
3 2     2   2443 use 5.006;
  2         7  
  2         248  
4 2     2   12 use strict;
  2         4  
  2         72  
5 2     2   11 use warnings;
  2         8  
  2         210  
6              
7             require Exporter;
8              
9             our @ISA = qw(Exporter);
10             our @EXPORT_OK = qw(inflate commit obliterate);
11             our @EXPORT = ('inflate'); # @EXPORT_OK;
12             our $VERSION = '0.07';
13             $::OBJECT = undef;
14              
15 2     2   3030 use Devel::Messenger qw(note);
  2         5683  
  2         890  
16              
17             # Preloaded methods go here.
18              
19             sub import {
20             # allows 'use' syntax
21 2     2   162 my ($class, @args) = @_;
22 2 50       18 @args and $class->make(@args);
23             }
24              
25             sub make {
26             # sets up glue to database based on 'use' statement
27 2     2 0 7 my ($generator_class, @args) = @_;
28 2         85 my $target_class = $generator_class->find_target_class;
29 2         8 my %persist = ();
30 2         9 while (@args) {
31 6         13 my ($table, $config) = splice(@args, 0, 2);
32 6         21 $persist{$table} = $config;
33             }
34             # $generator_class->_install_method($target_class, $sub_name, $code);
35 2         6 foreach my $export (@EXPORT) {
36 2         12 my $generate_export = '_' . $export;
37 2         13 $generator_class->$generate_export($target_class, \%persist, $export);
38             }
39             }
40              
41             sub find_target_class {
42             # determines where to export generated methods
43 2     2 0 7 my $class;
44 2         13 my $c = 0;
45 2         5 while (1) {
46 6         46 $class = (caller($c++))[0];
47 4         34 last unless ($class->isa('Class::Inflate') and
48 6 100 66     78 &{$class->can('ima_generator')});
49             }
50 2         8 return $class;
51             }
52              
53             sub ima_generator {
54             # a subclass may redefine this to return 0, if it wishes
55             # to allow methods added to itself
56 4     4 0 28 1;
57             }
58              
59             sub _install_method {
60             # exports method to target class
61 2     2   6 my $generator_class = shift;
62 2         4 my $target_class = shift;
63 2         4 my $accessor = shift;
64 2         4 my $code = shift;
65 2     2   14 no strict 'refs';
  2         5  
  2         21406  
66 2 50       4 unless (defined *{"$target_class\::$accessor"}{CODE}) {
  2         18  
67 2         18 note \7, "building accessor '$target_class\::$accessor'\n";
68 2         12 return *{"$target_class\::$accessor"} = $code;
  2         561  
69             }
70 0         0 return;
71             }
72              
73             sub _inflate {
74 2     2   5 my $generator_class = shift;
75 2         161 my $target_class = shift;
76 2         5 my $persist = shift;
77 2         3 my $sub_name = shift;
78             $generator_class->_install_method($target_class, $sub_name, sub {
79 3     3   136957 my $self = shift;
80 3 50       27 my $class = ref($self) ? ref($self) : $self;
81 3         9 my $dbh = shift;
82 3   33     38 my $filter = shift || $self;
83 3         35 my @sql = inflation_sql($class, $filter, $persist);
84 3         9 my @records = (); # in object format
85 3         6 my @data = (); # in table/field format
86 3         7 my %rows_fetched = ();
87 3         8 my %awaiting_join = ();
88 3         9 foreach my $sql (@sql) {
89 9         37 my @r = fetchrows($class, $dbh, $sql->{query}, $sql->{bind});
90 9         32 $rows_fetched{$sql->{table}} = \@r;
91 9 100 100     47 if (exists($persist->{$sql->{table}}{join}) and keys %{$persist->{$sql->{table}}{join}}) {
  7         38  
92 6         9 foreach my $table (keys %{$persist->{$sql->{table}}{join}}) {
  6         23  
93 6         29 note \3, "want to join $sql->{table} to $table\n";
94 6 100 66     435 if (@data and $rows_fetched{$table}) {
95 2         7 join_records($class, $persist, $table, \@data, $sql->{table}, \@r);
96             } else {
97 4   100     25 $awaiting_join{$table} ||= [];
98 4         7 push @{$awaiting_join{$table}}, $sql->{table};
  4         11  
99 4         19 note \3, " will wait till we have $table data\n";
100             }
101             }
102             } else {
103             # this is the master/parent table
104 3         8429 note \3, "populating dataset from $sql->{table}\n";
105 3         256 $rows_fetched{$sql->{table}} = @r;
106 3         10 @data = map { { $sql->{table} => [$_] } } splice @r;
  8         30  
107 3         22 note \3, "placed " . scalar(@data) . ' of ' . scalar(@r) . " records in dataset\n";
108             }
109 9 100 100     522 if (@data and $awaiting_join{$sql->{table}}) {
110 3         6 my $join_awaiting_records;
111             # need to recursively call this foreach loop
112             $join_awaiting_records = sub {
113 7     7   14 my $waiting = shift;
114 7 100       25 return unless exists($awaiting_join{$waiting});
115 3         15 note \3, "now we have $waiting data\n";
116 3         201 while (@{$awaiting_join{$waiting}}) {
  7         29  
117 4         7 my $table = shift @{$awaiting_join{$waiting}};
  4         10  
118 4         16 join_records($class, $persist, $waiting, \@data, $table, $rows_fetched{$table});
119 4         19 $join_awaiting_records->($table);
120             }
121 3         25 };
122 3         11 $join_awaiting_records->($sql->{table});
123             }
124             }
125             # TODO remove Dumper
126             #use Data::Dumper;
127             #note "dataset:\n" . Dumper(\@data) . "\n";
128 3 50       25 my $inflated_object = inflated_object($class, $persist, \@data, [ref($self) ? $self : ()]);
129 3         13 while (my $record = $inflated_object->($dbh)) {
130 8         93 push @records, $record;
131             }
132 3 50       161 return @records if wantarray;
133 0         0 return \@records;
134 2         28 });
135             }
136              
137             sub inflation_sql {
138 3     3 0 9 my $class = shift;
139 3         46 my $filter = shift;
140 3         10 my $persist = shift;
141 3 50       20 die "inflate filter must be a HASH\n" unless (UNIVERSAL::isa($filter, 'HASH'));
142 3         8 my @sql = ();
143 3         7 my $inflate = [];
144 3         27 foreach my $table (keys %$persist) {
145 9         18 push @$inflate, keys %{$persist->{$table}{methods}};
  9         60  
146             }
147 3         19 my $method_tables = method_tables($class, $persist);
148 3         24 my $inflation_fields = inflation_fields($class, $inflate, $persist, $method_tables);
149 3         17 my $filter_values = filter_values($class, $filter, $persist, $method_tables);
150 3         12 my %join_fields = (); # fields we must select because we will use them to match records
151             # newer code
152 3         13 foreach my $table (keys %$inflation_fields) {
153 9         38 note \5, "building query for table $table\n";
154 9         745 my $table_filter = add_filter_defaults($class, $filter_values, $persist, $table);
155 9         20 my @tables = ();
156 9         15 my @fields = ();
157 9         13 my @filter = ();
158 9         11 my @bind = ();
159 9         17 my $alias = '';
160 9 100       24 my $has_filter = exists($filter_values->{$table}) ? 1 : 0;
161 9         20 my $has_external_filter = scalar(keys %$filter_values) > $has_filter;
162 9 100       22 if ($has_external_filter) {
163             # see if we can join to the external tables
164 6         8 my $matched = 0;
165 6         8 my $need_join = 0;
166             # add current table and fields
167 6         14 $alias = 'a';
168 6         16 my %table_alias = ( $table => $alias );
169 6         20 push @tables, $table . ' ' . $table_alias{$table};
170 6         9 push @fields, map { $table_alias{$table} . '.' . $_ } @{$inflation_fields->{$table}};
  13         40  
  6         14  
171 6         21 foreach my $external (keys %$filter_values) {
172 6 50       18 next if ($table eq $external);
173             # add external table
174 6         21 $table_alias{$external} = ++$alias;
175             # see if all the fields in the filter join to fields in our table
176             #foreach my $field (@{$filter_values->{$external}{fields}}) {
177             # if (exists($persist->{$table}{join}{$external}) and exists($persist->{$table}{join}{$external}{$field})) {
178             # my $my_field_name = $persist->{$table}{join}{$external}{$field};
179             # # TODO get value right now?
180             # $matched++;
181             # } else {
182             # $need_join++;
183             # }
184             #}
185             }
186             # add join (field = field) to filter
187 6         20 my @aliases = keys %table_alias;
188 6         14 foreach my $external (@aliases) {
189 12 100       39 next if ($external eq $table);
190 6         40 note \6, "will need to join from $table to $external\n";
191 6 50       476 my @pkey = exists($persist->{$table}{key}) ? @{$persist->{$table}{key}} : ();
  6         58  
192             # iterate through tables we know how to join to
193 6         16 my @partial_forward_path = ();
194 6 100       25 my $next_path = join_path($persist, $table, $external, \@partial_forward_path, exists($persist->{$table}{join}) ? keys %{$persist->{$table}{join}} : ());
  5         22  
195 6         13 my @paths = ();
196 6         21 my ($path, $join) = run_path_iterator($next_path, $persist->{$table}, 0);
197 6 100       28 push @paths, $path if @$path;
198             # if @path, but not all primary key fields are used in join, check for additional paths and possibly use multiple paths
199 6   100     29 while (@$path and grep { !$join->{$_} } @pkey) {
  6         32  
200 2         9 note \6, "checking for additional join paths\n";
201 2         169 ($path, $join) = run_path_iterator($next_path, $persist->{$table}, 0);
202 2 50       14 push @paths, $path if @$path;
203             }
204 6         10 my $static_from = $table;
205 6         9 my $middle_join = 0;
206 6 100 66     42 if (!@paths and exists($persist->{$external}{join})) {
207 2         7 note \6, "checking for reverse join definition\n";
208 2         138 my @partial_reverse_path = ();
209 2         4 my $reverse_path = join_path($persist, $external, $table, \@partial_reverse_path, keys %{$persist->{$external}{join}});
  2         10  
210 2         8 ($path, $join) = run_path_iterator($reverse_path, $persist->{$table}, -1);
211 2 100       10 push @paths, $path if @$path;
212             # if @path, but not all primary key fields are used in join, check for additional paths and possibly use multiple paths
213 2   66     14 while (@$path and grep { !$join->{$_} } @pkey) {
  1         7  
214 1         4 note \6, "checking for additional join paths\n";
215 1         103 ($path, $join) = run_path_iterator($reverse_path, $persist->{$table}, -1);
216 1 50       8 push @paths, $path if @$path;
217             }
218 2         4 $static_from = $external;
219 2 100       26 if (!@paths) {
220             # check for a common table both ends know how to join to, if there is no direct join path
221 1         5 my $next_path = meet_in_the_middle($table, $external, \@partial_forward_path, \@partial_reverse_path);
222 1         6 my ($path, $join) = run_path_iterator($next_path, $persist->{$table}, 0);
223 1 50       6 push @paths, $path if @$path;
224             # if @path, but not all primary key fields are used in join, check for additional paths and possibly use multiple paths
225 1   66     10 while (@$path and grep { !$join->{$_} } @pkey) {
  2         10  
226 1         8 note \6, "checking for additional join paths\n";
227 1         88 ($path, $join) = run_path_iterator($next_path, $persist->{$table}, 0);
228 1 50       8 push @paths, $path if @$path;
229             }
230 1 50       5 if (@paths) {
231 1         2 $static_from = $table;
232 1         28 $middle_join = 1;
233             }
234             }
235             }
236 6         15 foreach my $path (@paths) {
237             #my $from = $static_from;
238 6         15 my @from_table = $static_from;
239 6 100       15 if ($middle_join) {
240 1         4 @from_table = ($table, $external);
241 1         6 note \7, "will look for join columns between $table and $external, forwards and backwards\n";
242             } else {
243 5         28 note \7, "will look for join columns between $table and $external, forwards only\n";
244             }
245 6         505 foreach my $from (@from_table) {
246 7         21 my @path = @$path;
247 7 100 100     34 if ($middle_join and $from eq $external) {
248 1         6 note \6, "looking for reverse join columns ($external to $table)\n";
249 1         79 @path = reverse @path;
250 1         2 shift @path; # remove $external from list
251             } else {
252 6         29 note \6, "looking for join columns ($table to $external)\n";
253             }
254 7         469 note \6, "path: " . join(' -> ', $from, @path) . "\n";
255 7         514 foreach my $step (@path) {
256             # add external table
257 8 100       23 my $t = ($step eq $table) ? $external : $step;
258 8 100       28 $table_alias{$t} = ++$alias unless (exists($table_alias{$t}));
259 8 100       15 push @tables, $t . ' ' . $table_alias{$t} unless (grep { $_ eq ($t . ' ' . $table_alias{$t}) } @tables);
  11         64  
260 8   100     38 $join_fields{$step} ||= [];
261 8 100       33 foreach my $field (exists($persist->{$from}{join}{$step}) ? keys %{$persist->{$from}{join}{$step}} : ()) {
  7         28  
262 7         23 my $join_from = $table_alias{$from} . '.' . $persist->{$from}{join}{$step}{$field};
263 7         18 my $join_to = $table_alias{$step} . '.' . $field;
264 7         9 my $seen = 0;
265 7         15 foreach my $filter_item (@filter) {
266 1 50 33     18 if ($filter_item eq ($join_from . ' = ' . $join_to) or $filter_item eq ($join_to . ' = ' . $join_from)) {
267 0         0 $seen = 1;
268 0         0 last;
269             }
270             }
271 7 50       21 if ($seen) {
272 0         0 note \6, ' skipping ' . $from . '.' . $persist->{$from}{join}{$step}{$field} . ' = ' . $step . '.' . $field . "\n";
273 0         0 next;
274             }
275 7         53 note \6, ' joining ' . $from . '.' . $persist->{$from}{join}{$step}{$field} . ' = ' . $step . '.' . $field . "\n";
276 7         665 push @filter, $table_alias{$from} . '.' . $persist->{$from}{join}{$step}{$field} . ' = ' . $table_alias{$step} . '.' . $field;
277 7         12 push @{$join_fields{$from}}, $persist->{$from}{join}{$step}{$field};
  7         25  
278 7         14 push @{$join_fields{$step}}, $field;
  7         25  
279             }
280 8         40 $from = $step;
281             }
282             }
283             }
284 6 50       123 unless (@paths) {
285 0         0 note \6, "no path from $table to $external found\n";
286 0         0 delete $table_alias{$external};
287             }
288             }
289 6         19 foreach my $external (keys %table_alias) {
290             # add external table conditions to filter
291 13 100       42 if (exists($filter_values->{$external})) {
292 6     6   47 my $next_filter = expand_bind(sub { $table_alias{$external} . '.' . shift }, $filter_values->{$external}{fields}, $filter_values->{$external}{values});
  6         20  
293 6         17 while (my ($f, $b) = $next_filter->()) {
294 6         9 push @filter, $f;
295 6         25 push @bind, @$b;
296             }
297             }
298             }
299             #if ($need_join) {
300             # # TODO join in SQL, select from both, remove other single table SQL
301             #} else {
302             # # TODO build SQL using our fieldnames for filter parameters
303             #}
304             } else {
305             # build SQL statement for this table - no joins necessary
306 3         6 push @tables, $table;
307 3         5 push @fields, @{$inflation_fields->{$table}};
  3         9  
308 3 50       13 if (exists($filter_values->{$table})) {
309 3     3   22 my $next_filter = expand_bind(sub { shift }, $filter_values->{$table}{fields}, $filter_values->{$table}{values});
  3         8  
310 3         14 while (my ($f, $b) = $next_filter->()) {
311 3         6 push @filter, $f;
312 3         12 push @bind, @$b;
313             }
314             }
315             }
316 9 50       38 if (keys %$table_filter) {
317 0 0       0 my $prefix = $alias ? 'a.' : '';
318 0     0   0 my $next_filter = expand_bind(sub { $prefix . shift }, $table_filter->{fields}, $table_filter->{values});
  0         0  
319 0         0 while (my ($f, $b) = $next_filter->()) {
320 0         0 push @filter, $f;
321 0         0 push @bind, @$b;
322             }
323             }
324 9 50       21 next unless @filter;
325 9         175 push @sql, {
326             'bind' => \@bind,
327             'table' => $table,
328             'tables' => \@tables,
329             'fields' => \@fields,
330             'filter' => \@filter,
331             };
332             }
333 3         13 foreach my $sql (@sql) {
334 9         578 my ($table, $alias) = split(/\s+/, $sql->{tables}[0]);
335 9 100       26 $alias .= $alias ? '.' : '';
336 9 50       35 if (exists($join_fields{$table})) {
337 9         13 my %current = map { $_ => 1 } @{$sql->{fields}};
  21         78  
  9         21  
338 9         17 foreach my $field (@{$join_fields{$table}}) {
  9         22  
339 14 50       59 unless (exists($current{$alias.$field})) {
340 0         0 push @{$sql->{fields}}, $alias.$field;
  0         0  
341 0         0 $current{$alias.$field}++;
342 0         0 note \6, "adding $table.$field to selection\n";
343             }
344             }
345             }
346 9         14 my $query = 'SELECT ' . join(', ', @{$sql->{fields}});
  9         34  
347 9         16 $query .= ' FROM ' . join(', ', @{$sql->{tables}});
  9         28  
348 9         13 $query .= ' WHERE ' . join(' AND ', @{$sql->{filter}});
  9         27  
349 9         24 $sql->{query} = $query;
350 9 50       21 note \5, 'built query: ' . $query . " -> " . join(', ', map { defined($_) ? $_ : '' } @{$sql->{bind}}) . "\n";
  9         63  
  9         18  
351             }
352 3 50       302 return @sql if wantarray;
353 0         0 return \@sql;
354             }
355              
356             sub expand_bind {
357             # returns an iterator which returns a filter "$field = ?" and bind value
358 9     9 0 15 my $transform = shift;
359 9         13 my $fields = shift;
360 9         12 my $values = shift;
361 9         11 my $c = 0;
362             return sub {
363 18 100   18   138 return if ($c >= @$fields);
364 9         22 my $field = $transform->($fields->[$c]);
365 9         23 my $value = $values->[$c++];
366 9         18 my $operator = '=';
367 9         13 my $placeholder = '?';
368 9 50       83 if (UNIVERSAL::isa($value, 'ARRAY')) {
369 0 0       0 if (@$value == 0) {
    0          
370 0         0 push @$value, undef;
371             } elsif (@$value > 1) {
372 0         0 $operator = 'IN';
373 0         0 $placeholder = '(' . join(', ', map { '?' } @$value) . ')';
  0         0  
374             }
375             } else {
376 9         22 $value = [$value];
377             }
378 9         73 return ($field . ' ' . $operator . ' ' . $placeholder, $value);
379             }
380 9         56 }
381              
382             sub join_path {
383             # determine possible paths to join two tables together
384 8     8 0 11 my $persist = shift;
385 8         13 my $launch = shift;
386 8         11 my $target = shift;
387 8   50     23 my $iterators = shift || [];
388 8         17 my $spacing = '';
389 8         36 note \7, "creating iterator from $launch to $target\n";
390 8         597 push @$iterators, [member_of($spacing, $target, @_), []];
391 8         16 my $c = 0;
392             return sub {
393 11     11   64 while ($c < @$iterators) {
394 15         36 my ($element, $match) = $iterators->[$c][0]->();
395 15         21 $spacing = ' ' x @{$iterators->[$c][1]};
  15         47  
396 15 100       39 unless (defined($element)) {
397 8         48 note \7, $spacing . "iterator $c is exhausted\n";
398 8         627 $c++;
399 8         26 next;
400             }
401 7 100       17 if ($match) {
402 5         19 note \7, $spacing . "iterator $c found a join path: " . join(' -> ', @{$iterators->[$c][1]}, $element) . "\n";
  5         29  
403 5         390 return @{$iterators->[$c][1]}, $element;
  5         25  
404             } else {
405 2 50       3 if (grep { /^$element$/ } @{$iterators->[$c][1]}) {
  0         0  
  2         8  
406 0         0 note \7, $spacing . "search loop detected: " . join(' -> ', @{$iterators->[$c][1]}, $element) . "\n";
  0         0  
407             } else {
408 2         4 $spacing .= ' ';
409 2         10 note \7, $spacing . "creating iterator from $element to $target\n";
410 2         135 push @$iterators, [member_of($spacing, $target, keys %{$persist->{$element}{join}}), [@{$iterators->[$c][1]}, $element]];
  2         9  
  2         11  
411             }
412             }
413             }
414 6         21 note \7, "all iterators exhausted\n";
415 6         439 return;
416 8         68 };
417             }
418              
419             sub member_of {
420             # iterator to say if an element matches the target
421 10   100 10 0 51 my $spacing = shift || '';
422 10         36 my $target = shift;
423 10         21 my @possible = @_;
424 10 100       37 note \7, $spacing . " determining if " . join(' or ', map { "'$_'" } @possible) . " knows how to join to '$target'\n" if @possible;
  7         43  
425             return sub {
426 15     15   43 while (my $element = shift(@possible)) {
427 7         29 return ($element, ($element eq $target));
428             }
429 8         17 return;
430             }
431 10         612 }
432              
433             sub run_path_iterator {
434             # kicks the iterator to find the next path. Returns the path, and fields it will use for the join
435 13     13 0 20 my $iterator = shift;
436 13         16 my $table_instructions = shift;
437 13   100     60 my $element = shift || 0; # expects 0 for forward, -1 for reverse path
438 13         28 my @path = $iterator->();
439 13 100       52 return (\@path, {}) unless @path;
440 6 100 66     134 my %join = map { $_ => 1 } (exists($table_instructions->{join}) and exists($table_instructions->{join}{$path[$element]})) ? values %{$table_instructions->{join}{$path[$element]}} : ();
  5         21  
  5         17  
441 6         28 return (\@path, \%join);
442             }
443              
444             sub meet_in_the_middle {
445 1     1 0 3 my $launch = shift;
446 1         2 my $target = shift;
447 1         2 my $forward = shift;
448 1         3 my $reverse = shift;
449 1         3 my @queue = ();
450 1         6 foreach my $fpath (@$forward) {
451 2 100       4 next unless @{$fpath->[1]};
  2         9  
452 1         4 foreach my $rpath (@$reverse) {
453 2 100       2 next unless @{$rpath->[1]};
  2         9  
454 1         2 push @queue, [[@{$fpath->[1]}], [reverse @{$rpath->[1]}]];
  1         5  
  1         7  
455             }
456             }
457 1         2 my %returned = ();
458             return sub {
459 2     2   8 while (my $queue = shift(@queue)) {
460 1         3 my ($fqueue, $rqueue) = @$queue;
461 1         1 my $c = 0;
462 1         3 foreach my $element (@$rqueue) {
463 1         2 $c++;
464 1 50       6 if ($element eq $fqueue->[-1]) {
465 1         10 note \7, "found a meet-in-the-middle join at '$element': " . join(' -> ', $launch, @$fqueue) . ' | ' . join(' <- ', @$rqueue, $target) . "\n";
466 1 50       100 if ($returned{$element}) {
467 0         0 note \7, " (ignorning because we have already found a join through '$element')\n";
468 0         0 next;
469             }
470 1         5 my @path = (@$fqueue, splice(@$rqueue, $c), $target);
471 1         8 note \7, " which makes a join of: " . join(' -> ', $launch, @path) . "\n";
472 1         76 my %seen = ();
473 1         3 my @multiple = grep { $seen{$_}++ } @path;
  2         7  
474 1 50       5 if (@multiple) {
475 0         0 note \7, " (ignoring because join goes through " . join(' and ', @multiple) . " more than once)\n";
476 0         0 next;
477             }
478 1         3 $returned{$element}++;
479 1         6 return @path;
480             }
481             }
482             }
483 1         3 return;
484             }
485 1         12 }
486              
487             sub inflation_fields {
488 3     3 0 7 my $class = shift;
489 3         6 my $inflate = shift;
490 3         5 my $persist = shift;
491 3   33     30 my $method_tables = shift || method_tables($class, $persist);
492 3         7 my %fields = ();
493 3         12 foreach my $method (@$inflate) {
494 15 50       43 unless (exists($method_tables->{$method})) {
495 0         0 warn "ignoring unknown method '$method' for inflation\n";
496 0         0 next;
497             }
498 15         23 my $table = $method_tables->{$method};
499 15 100       39 next if (exists($fields{$table})); # already did this table
500 9         18 my $methods = $persist->{$table}{methods};
501 9         14 my @fields = ();
502             # figure out which fields to select based on method names
503 9         22 foreach my $field (values %$methods) {
504 15 100       30 if (ref($field)) {
505 3 50 33     31 if (ref($field) eq 'HASH' and exists($field->{fields})) {
506 3 50       15 push @fields, ref($field->{fields}) eq 'ARRAY' ? @{$field->{fields}} : $field->{fields};
  3         20  
507             }
508             } else {
509 12         41 push @fields, $field;
510             }
511             }
512 9 50       73 next unless @fields;
513             # select any fields needed for joins
514 9 100       27 if (exists($persist->{$table}{join})) {
515 6         14 my %selected = map { $_ => 1 } @fields;
  6         22  
516 6         10 foreach my $parent (keys %{$persist->{$table}{join}}) {
  6         21  
517 6         11 foreach my $field (values %{$persist->{$table}{join}{$parent}}) {
  6         20  
518 6 50       18 unless (exists($selected{$field})) {
519 6         13 push @fields, $field;
520 6         35 $selected{$field}++;
521             }
522             }
523             }
524             }
525 9         40 note \6, "will select from table $table\n";
526 9         739 note \6, "will select fields " . join(', ', @fields) . "\n";
527 9         613 $fields{$table} = \@fields;
528             }
529 3         9 return \%fields;
530             }
531              
532             sub filter_values {
533             # returns the table name, field names and bind values for any method name
534 3     3 0 8 my $class = shift;
535 3         4 my $filter = shift;
536 3         9 my $persist = shift;
537 3   33     12 my $method_tables = shift || method_tables($class, $persist);
538 3         9 my %values = ();
539 3         12 foreach my $method (keys %$filter) {
540 3 50       24 if (UNIVERSAL::can($filter, $method)) {
541 3         11 my $value = $filter->$method();
542 3 50 33     48 if (!defined($value) or !length($value)) {
543             # undefined values of an object do not count as filter parameters
544 0         0 next;
545             }
546             }
547             # TODO skip warning if filter is an object, rather than a HASH
548 3 50       11 unless (exists($method_tables->{$method})) {
549 0         0 warn "ignoring unknown filter field '$method'\n";
550 0         0 next;
551             }
552 3         8 my $table = $method_tables->{$method};
553 3         10 my $methods = $persist->{$table}{methods};
554 3         28 note \6, "filtering on $method\n";
555 3         315 my $field = $methods->{$method};
556 3         7 my @field = ();
557 3         6 my @value = ();
558 3         12 local $::OBJECT = $filter;
559 3 50       11 if (ref($field)) {
560 0 0 0     0 if (ref($field) eq 'HASH' and exists($field->{fields})) {
561 0 0       0 push @field, ref($field->{fields}) eq 'ARRAY' ? @{$field->{fields}} : $field->{fields};
  0         0  
562 0 0   0   0 my $deflate = exists($field->{deflate}) ? $field->{deflate} : sub { @_ };
  0         0  
563 0 0       0 if (UNIVERSAL::can($filter, $method)) {
564 0         0 push @value, $deflate->($filter->$method());
565             } else {
566 0         0 push @value, $deflate->($filter->{$method});
567             }
568             }
569             } else {
570 3         5 push @field, $field;
571 3 50       15 if (UNIVERSAL::can($filter, $method)) {
572 3         11 push @value, $filter->$method();
573             } else {
574 0         0 push @value, $filter->{$method};
575             }
576             }
577 3 50       24 unless (@field == @value) {
578 0         0 warn "filter for $method specified " . scalar(@field) . " fields, but " . scalar(@value) . " values\n";
579             }
580 3         13 for (my $i = 0; $i < @field; $i++) {
581 3         19 note \6, " ($table.$field[$i] = $value[$i])\n";
582             }
583 3   50     253 $values{$table} ||= { 'fields' => [], 'values' => [] };
584 3         5 push @{$values{$table}{fields}}, @field;
  3         10  
585 3         6 push @{$values{$table}{values}}, @value;
  3         15  
586             }
587 3         12 return \%values;
588             }
589              
590             sub add_filter_defaults {
591             # add values from table filter hash, for fields which have not been set
592 9     9 0 15 my $class = shift;
593 9         16 my $filter_values = shift;
594 9         13 my $persist = shift;
595 9         12 my $table = shift;
596 9         15 my %new = ();
597 9 50 33     40 if (exists($persist->{$table}{filter}) and keys(%{$persist->{$table}{filter}})) {
  0         0  
598 0         0 note \6, "adding default filter values\n";
599 0         0 my %seen = ();
600 0 0       0 %seen = map { $_ => 1 } @{$filter_values->{$table}{fields}} if (exists($filter_values->{$table}));
  0         0  
  0         0  
601 0         0 my $default = $persist->{$table}{filter};
602 0         0 foreach my $field (keys %$default) {
603 0 0       0 unless (exists($seen{$field})) {
604 0         0 push @{$new{fields}}, $field;
  0         0  
605 0         0 push @{$new{values}}, $default->{$field};
  0         0  
606 0         0 note \6, " ($table.$field = $default->{$field})\n";
607             }
608             }
609             }
610 9         27 return \%new;
611             }
612              
613             sub fetchrows {
614 9     9 0 15 my $class = shift;
615 9         163 my $dbh = shift;
616 9         13 my $query = shift;
617 9         12 my $bind = shift;
618 9         14 my @records = ();
619 9 50 33     103 if ($dbh and my $sth = $dbh->prepare($query)) {
620 9 50       1765 note \2, $sth->{Statement} . ' -> ' . join(', ', map { defined($_) ? $_ : '' } @$bind) . "\n";
  9         58  
621 9 50       1622 if ($sth->execute(@$bind)) {
622 9         276 while (my $record = $sth->fetchrow_hashref('NAME_lc')) {
623 50         734 push @records, $record;
624             }
625             }
626             }
627 9         49 note \2, "fetched " . scalar(@records) . " records\n";
628 9 50       889 return @records if wantarray;
629 0         0 return \@records;
630             }
631              
632             sub join_records {
633 6     6 0 10 my $class = shift;
634 6         6 my $persist = shift;
635 6         10 my $parent = shift; # table name
636 6         9 my $data = shift; # master dataset
637 6         8 my $child = shift; # table name
638 6         7 my $records = shift; # records
639 6   50     24 my $join = $persist->{$child}{join}{$parent} || return; # can't join without instructions
640 6         15 my %parent = (); # keyed off join identifier
641 6         11 my $children = 0; # count matches
642 6         9 my %joined = ();
643 6         34 note \5, "joining " . scalar(@$records) . " $child to matching $parent records\n";
644 6         571 foreach my $d (@$data) {
645 16         28 my @identifier = ();
646 16         40 foreach my $field (sort keys %$join) {
647 16         60 push @identifier, $field, $d->{$parent}[0]->{lc($field)};
648             }
649 16         43 my $identifier = join(':', @identifier);
650             #note \7, " building parent identifier $identifier\n";
651 16         17 push @{$parent{$identifier}}, $d;
  16         53  
652             }
653 6         12 foreach my $r (@$records) {
654 42         56 my @identifier = ();
655 42         76 foreach my $field (sort keys %$join) {
656 42         113 push @identifier, $field, $r->{lc($join->{$field})};
657             }
658 42 50       71 my $identifier = join(':', map { defined($_) ? $_ : '' } @identifier);
  84         209  
659             #note \7, " building child identifier $identifier\n";
660 42 50       98 if (exists($parent{$identifier})) {
661             #note \7, " joining on $identifier\n";
662 42         40 foreach my $d (@{$parent{$identifier}}) {
  42         69  
663             # TODO do not push child record onto data record more than once (in multiple join scenario)
664 120         119 push @{$d->{$child}}, $r;
  120         185  
665 120         126 $children++;
666 120         220 $joined{$identifier}++;
667             }
668             }
669             }
670 6         50 note \5, "joined $children $child to " . scalar(keys %joined) . " $parent records\n";
671 6         589 return $children;
672             }
673              
674             sub inflated_object {
675             # returns iterator to turn table/field data into an object
676 3     3 0 6 my $class = shift;
677 3         7 my $persist = shift;
678 3         5 my $data = shift; # ARRAY ref we shift from
679 3         8 my $objects = shift; # should only ever contain zero or one objects
680 3         4 my $c = 0;
681             return sub {
682 11 100   11   35 return unless @$data;
683 8         12 my ($dbh) = @_;
684 8         12 my $d = shift(@$data);
685 8   66     55 my $object = $objects->[$c++] ||= $class->new();
686 8         51 local $::OBJECT = $object;
687 8         14 my @postinflate = ();
688 8         26 foreach my $table (keys %$d) {
689 24         113 my $records = $d->{$table};
690 24         153 note "[$c] inflating object $class with " . scalar(@$records) . " records from $table\n";
691 24         1721 my $methods = $persist->{$table}{methods};
692 24         60 foreach my $method (keys %$methods) {
693 40         219 note \6, "inflating method $method\n";
694 40         2894 my $field = $methods->{$method};
695 40 100       87 if (ref($field)) {
696 8 50 33     60 if (ref($field) eq 'HASH' and exists($field->{fields})) {
    0          
697 8         14 my @field = ();
698 8 50       25 push @field, ref($field->{fields}) eq 'ARRAY' ? @{$field->{fields}} : $field->{fields};
  8         21  
699 8 50       33 if (UNIVERSAL::can($object, $method)) {
700 8         15 my @values = ();
701 8         16 foreach my $record (@$records) {
702 96 50       6264 if (exists($field->{inflate})) {
703 0         0 push @values, $field->{inflate}->(@{$record}{@field});
  0         0  
704             } else {
705 96         119 push @values, @{$record}{@field};
  96         486  
706             }
707 96         144 foreach my $field (@field) {
708 96 50       492 note \6, " ($field = " . (defined($record->{$field}) ? $record->{$field} : '') . ")\n";
709             }
710             }
711 8 50 33     658 if ($field->{forceref} or ($field->{wantref} and @values > 1)) {
      33        
712 8         105 $object->$method(\@values);
713             } else {
714 0         0 $object->$method(@values);
715             }
716 8 50       91 if (exists($field->{postinflate})) {
717 0         0 push @postinflate, $field->{postinflate};
718             }
719             } else {
720             # TODO some warning - can't run method on object
721             }
722             } elsif (ref($field) eq 'HASH') {
723 0 0       0 if (exists($field->{inflate})) {
724 0         0 push my @values, $field->{inflate}->();
725 0         0 foreach my $value (@values) {
726 0         0 note \6, " ( = $value)\n";
727             }
728 0         0 $object->$method(@values);
729             }
730 0 0       0 if (exists($field->{postinflate})) {
731 0         0 push @postinflate, $field->{postinflate};
732             }
733             }
734             } else {
735 32 50       138 if (UNIVERSAL::can($object, $method)) {
736 32         46 my @values = ();
737 32         57 foreach my $record (@$records) {
738 48         1169 push @values, $record->{$field};
739 48 50       230 note \6, " ($field = " . (defined($record->{$field}) ? $record->{$field} : 'undef') . ")\n";
740             }
741 32         2328 $object->$method(@values);
742             } else {
743             # TODO some warning - can't run method on object
744             }
745             }
746             }
747             }
748 8 50       59 note \6, "running postinflate hooks\n" if @postinflate;
749 8         19 foreach my $code (@postinflate) {
750 0         0 $code->($dbh);
751             }
752 8         63 return $object;
753 3         40 };
754             }
755              
756             sub method_tables {
757 3     3 0 8 my $class = shift;
758 3         11 my $persist = shift;
759 3         9 my %table = ();
760 3         11 foreach my $table (keys %$persist) {
761 9         13 foreach my $method (keys %{$persist->{$table}{methods}}) {
  9         26  
762 15         44 $table{$method} = $table;
763             }
764             }
765 3         91 return \%table;
766             }
767              
768             1;
769             __END__