File Coverage

blib/lib/DBD/Sys/CompositeTable.pm
Criterion Covered Total %
statement 27 99 27.2
branch 1 34 2.9
condition 0 2 0.0
subroutine 7 10 70.0
pod 3 3 100.0
total 38 148 25.6


line stmt bran cond sub pod time code
1             package DBD::Sys::CompositeTable;
2              
3 4     4   18 use strict;
  4         7  
  4         116  
4 4     4   21 use warnings;
  4         7  
  4         103  
5 4     4   17 use vars qw(@ISA $VERSION);
  4         8  
  4         243  
6              
7             require SQL::Eval;
8             require DBI::DBD::SqlEngine;
9 4     4   19 use Scalar::Util qw(blessed weaken);
  4         6  
  4         198  
10 4     4   18 use Clone qw(clone);
  4         8  
  4         151  
11 4     4   19 use Carp qw(croak);
  4         7  
  4         4716  
12              
13             @ISA = qw(DBD::Sys::Table);
14             $VERSION = "0.102";
15              
16             =pod
17              
18             =head1 NAME
19              
20             DBD::Sys::CompositeTable - Table implementation to compose different sources into one table
21              
22             =head1 ISA
23              
24             DBD::Sys::CompositeTable
25             ISA DBD::Sys::Table
26             ISA DBI::DBD::SqlEngine::Table
27              
28             =head1 DESCRIPTION
29              
30             DBD::Sys::CompositeTable provides a table which composes the data from
31             several sources in one data table.
32              
33             While constructing this table, the columns of the embedded tables are
34             collected and a heading and a merge plan for the composed result table
35             is generated.
36              
37             Simplified example of table procs:
38              
39             $alltables = $dbh->selectall_hashref("select * from procs", "pid");
40              
41             # calls
42             # DBD::Sys::CompositeTable( [ 'DBD::Sys::Plugin::Any::Procs',
43             # 'DBD::Sys::Plugin::Win32::Procs' ],
44             # $attr );
45              
46             This will fetch the column names from both embedded tables and get (simplfied):
47              
48             # %colNames = (
49             # 'DBD::Sys::Plugin::Any::Procs' => [
50             # 'pid', 'ppid', 'uid', 'gid', 'cmndline', 'sess', 'priority', 'ttynum', 'start', 'run', 'status',
51             # ],
52             # 'DBD::Sys::Plugin::Win32::Procs' => [
53             # 'pid', 'ppid', 'uid', 'gid', 'cmndline', 'sess', 'priority', 'thread', 'start', 'run', 'status',
54             # ]
55             # );
56             # @colNames = (
57             # 'pid', 'ppid', 'uid', 'gid', 'cmndline', 'sess', 'priority', 'ttynum', 'start', 'run', 'status', 'threads',
58             # );
59             # %mergeCols = (
60             # 'DBD::Sys::Plugin::Any::Procs' => [
61             # 0 .. 10,
62             # ],
63             # 'DBD::Sys::Plugin::Win32::Procs' => [
64             # 7,
65             # ]
66             # );
67             # $primaryKey = 'pid';
68              
69             The merge phase in C finally does (let's assume running
70             in a cygwin environment, where Proc::ProcessTable and Win32::Process::Info
71             both are working):
72              
73             +-----+------+-----+-----+----------+------+----------+---------+-------+-----+---------+
74             | pid | ppid | uid | gid | cmndline | sess | priority | ttynum | start | run | status |
75             +-----+------+-----+-----+----------+------+----------+---------+-------+-----+---------+
76             | 0 | 0 | 0 | 0 | 'init' | 0 | 4 | | 0 | 999 | 'ioblk' |
77             | 100 | 0 | 200 | 20 | 'bash' | 1 | 8 | pty/1 | 10000 | 200 | 'wait' |
78             +-----+------+-----+-----+----------+------+----------+---------+-------+-----+---------+
79              
80             +-----+------+-----+-----+----------+------+----------+-------+-----+---------+---------+
81             | pid | ppid | uid | gid | cmndline | sess | priority | start | run | status | threads |
82             +-----+------+-----+-----+----------+------+----------+-------+-----+---------+---------+
83             | 782 | 241 | 501 | 501 | 'cygwin' | 0 | 4 | 0 | 999 | 'ioblk' | 2 |
84             | 100 | 0 | 501 | 501 | 'bash' | 1 | 8 | 10000 | 200 | 'wait' | 8 |
85             +-----+------+-----+-----+----------+------+----------+-------+-----+---------+---------+
86              
87             The resulting table would be:
88              
89             +-----+------+-----+-----+----------+------+----------+---------+-------+-----+---------+---------+
90             | pid | ppid | uid | gid | cmndline | sess | priority | ttynum | start | run | status | threads |
91             +-----+------+-----+-----+----------+------+----------+---------+-------+-----+---------+---------+
92             | 0 | 0 | 0 | 0 | 'init' | 0 | 4 | | 0 | 999 | 'ioblk' | |
93             | 100 | 0 | 200 | 20 | 'bash' | 1 | 8 | pty/1 | 10000 | 200 | 'wait' | 8 |
94             | 782 | 241 | 501 | 501 | 'cygwin' | 0 | 4 | | 0 | 999 | 'ioblk' | 8 |
95             +-----+------+-----+-----+----------+------+----------+---------+-------+-----+---------+---------+
96              
97             In the real world, it's a bit more complicated and especially the process
98             table is a bit larger, but it illustrates the most important points:
99              
100             =over 4
101              
102             =item *
103              
104             missing columns are attached right
105              
106             =item *
107              
108             missing rows are appended at the end of the first table (and are
109             constructed as good as possible from the data we have)
110              
111             =item *
112              
113             once existing data are neither verified nor overwritten (see the difference
114             in the cygwin uid (root => uid 0) and win32 uid (Administrator => uid 501).
115              
116             =back
117              
118             This is a fictive example - it's not verified how DBD::Sys behaves in
119             I! Maybe the user mapping works fine - maybe there will be no
120             problem at all. Maybe you will get duplicated lines for each process
121             with completely different data.
122              
123             This is an experimental feature. Use with caution!
124              
125             =head1 METHODS
126              
127             =head2 new
128              
129             sub new( $proto, $tableInfo, $attrs ) { ... }
130              
131             Creates a new composite table based on the tables in C<$tableInfo>,
132             analyses the result view and create a merge plan for extending rows and
133             appending rows.
134              
135             The order of the embedded tables is primarily influenced by the priority
136             of the table and secondarily by the alphabetic order of their package
137             names.
138              
139             In L example, C has
140             a priority of 100 and C has a priority
141             of 500. So C dominates.
142              
143             =cut
144              
145             my %compositedInfo;
146              
147             sub _pk_cmp_fail
148             {
149 0     0   0 my ( $pk, $epk ) = @_;
150 0 0       0 ref($pk) eq ref($epk)
    0          
    0          
151             or return
152             sprintf(
153             "Can't compare primary key type (%s) of '%s' with primary key type (%s) of '%s'",
154             ref($epk) ? "\\" . ref($epk) : "SCALAR", "%s",
155             ref($pk) ? "\\" . ref($pk) : "SCALAR", "%s"
156             );
157 0 0       0 if ( ref($pk) eq "" )
    0          
158             {
159 0 0       0 $pk eq $epk and return;
160             return
161 0         0 sprintf( "Primary key (%s) of '%s' differs from primary key (%s) of '%s'",
162             DBI::neat($epk), "%s", DBI::neat($pk), "%s" );
163             }
164             elsif ( ref($pk) eq "ARRAY" )
165             {
166 0 0       0 join( "\0", sort @$epk ) eq join( "\0", sort @$pk ) and return;
167             return
168 0         0 sprintf( "Primary key (%s) of '%s' differs from primary key (%s) of '%s'",
169             DBI::neat_list($epk), "%s", DBI::neat_list($pk), "%s" );
170             }
171              
172 0         0 croak "Invalid type for primary key: " . ref($pk);
173             }
174              
175             sub new
176             {
177 5     5 1 9 my ( $proto, $tableInfo, $attrs ) = @_;
178              
179 5 50       118 my @tableClasses =
180 5         25 sort { ( $a->get_priority() <=> $b->get_priority() ) || ( blessed($a) cmp blessed($b) ) }
181             @$tableInfo;
182              
183 5         50 my $compositeName = join( "-", @tableClasses );
184 5         9 my ( @embed, %allColNames, @allColNames, $allColIdx, %mergeCols, %enhanceCols, $primaryKey );
185 5         8 $allColIdx = 0;
186 5         10 foreach my $tblClass (@tableClasses)
187             {
188 5         25 my %embedAttrs = %$attrs;
189 5         32 my $embedded = $tblClass->new( \%embedAttrs );
190 0           push( @embed, $embedded );
191 0 0         next if ( defined( $compositedInfo{$compositeName} ) );
192              
193 0           my @embedColNames = $embedded->get_col_names();
194 0 0         if ($allColIdx)
195             {
196 0           my $embedPK = $embedded->get_primary_key();
197 0           my $pkFailure = _pk_cmp_fail( $primaryKey, $embedPK );
198 0 0         $pkFailure and croak( sprintf( $pkFailure, $tblClass, join( ", ", keys %mergeCols ) ) );
199 0           $mergeCols{$tblClass} = [];
200 0           foreach my $colIdx ( 0 .. $#embedColNames )
201             {
202 0           my $colName = $embedColNames[$colIdx];
203 0 0         if ( exists( $allColNames{$colName} ) )
204             {
205 0           $enhanceCols{$tblClass}->{ $allColNames{$colName} } = $colIdx;
206             }
207             else
208             {
209 0           push( @allColNames, $colName );
210 0           push( @{ $mergeCols{$tblClass} }, $colIdx );
  0            
211 0           $allColNames{$colName} = $allColIdx++;
212             }
213             }
214             }
215             else
216             {
217 0           %allColNames = map { $_ => $allColIdx++ } @embedColNames;
  0            
218 0           @allColNames = @embedColNames;
219 0           $mergeCols{$tblClass} = [ 0 .. $#embedColNames ];
220 0           $primaryKey = $embedded->get_primary_key();
221             }
222             }
223              
224 0 0         defined( $compositedInfo{$compositeName} )
225             or $compositedInfo{$compositeName} = {
226             col_names => \@allColNames,
227             primary_key => $primaryKey,
228             merge_cols => \%mergeCols,
229             enhance_cols => \%enhanceCols,
230             };
231              
232 0           $attrs->{meta} = {
233             composite_name => $compositeName,
234             embed => \@embed,
235             primary_key => $compositedInfo{$compositeName}->{primary_key},
236             merge_cols => $compositedInfo{$compositeName}->{merge_cols},
237             enhance_cols => $compositedInfo{$compositeName}->{enhance_cols},
238             };
239 0           $attrs->{col_names} = clone( $compositedInfo{$compositeName}->{col_names} );
240              
241 0           return $proto->SUPER::new($attrs);
242             }
243              
244             =head2 get_col_names
245              
246             This method is called during the construction phase of the table. It must be
247             a I method - the called context is the class name of the constructed
248             object.
249              
250             =cut
251              
252             sub get_col_names
253             {
254 0     0 1   return @{ $_[0]->{col_names} };
  0            
255             }
256              
257             =head2 collect_data
258              
259             Merges the collected data by the embedded tables into one composed list
260             of rows. This list of rows will be delivered to C when
261             C is called.
262              
263             The merge phase is demonstrated in the example in L.
264              
265             =cut
266              
267             sub collect_data
268             {
269 0     0 1   my $self = $_[0];
270 0           my %data;
271              
272 0           my $meta = $self->{meta};
273 0           my $compositeName = $meta->{composite_name};
274 0           my $rowOffset = 0;
275 0           my @primaryKeys =
276 0 0         ( ref $meta->{primary_key} ) ? @{ $meta->{primary_key} } : ( $meta->{primary_key} );
277 0           foreach my $embedded ( @{ $meta->{embed} } )
  0            
278             {
279 0           my @pkIdx = map { $embedded->column_num($_) } @primaryKeys;
  0            
280 0           my $mergeCols = $meta->{merge_cols}->{ blessed($embedded) };
281 0           my $enhanceCols = $meta->{enhance_cols}->{ blessed($embedded) };
282 0           my $nextRowOffset = $rowOffset + scalar(@$mergeCols);
283              
284 0           while ( my $row = $embedded->fetch_row() )
285             {
286 0           my $pks = join( "\0", map { DBI::neat($_) } @$row[@pkIdx] );
  0            
287 0           my $ref = \%data;
288 0   0       $ref = $ref->{$pks} ||= [];
289 0 0         if ( @{$ref} )
  0            
290             {
291 0 0         if ( scalar( @{$ref} ) == $nextRowOffset )
  0            
292             {
293 0           warn "primary key '"
294             . $meta->{primary_key}
295             . "' is not unique for "
296             . blessed($embedded);
297             }
298             else
299             {
300 0           push( @{$ref}, @$row[@$mergeCols] );
  0            
301             }
302             }
303             else
304             {
305 0 0         if ( 0 == $rowOffset )
306             {
307 0           @$ref = @$row;
308             }
309             else
310             {
311 0           my @entry = (undef) x $rowOffset;
312 0           @entry[ keys %{$enhanceCols} ] = @$row[ values %{$enhanceCols} ];
  0            
  0            
313 0           push( @entry, @$row[@$mergeCols] );
314 0           @$ref = @entry;
315             }
316             }
317             }
318              
319 0           $rowOffset = $nextRowOffset;
320             }
321              
322 0           my @data = values %data;
323              
324 0           return \@data;
325             }
326              
327             =head1 AUTHOR
328              
329             Jens Rehsack
330             CPAN ID: REHSACK
331             rehsack@cpan.org
332             http://search.cpan.org/~rehsack/
333              
334             =head1 COPYRIGHT
335              
336             This program is free software; you can redistribute
337             it and/or modify it under the same terms as Perl itself.
338              
339             The full text of the license can be found in the
340             LICENSE file included with this module.
341              
342             =head1 SUPPORT
343              
344             Free support can be requested via regular CPAN bug-tracking system. There is
345             no guaranteed reaction time or solution time, but it's always tried to give
346             accept or reject a reported ticket within a week. It depends on business load.
347             That doesn't mean that ticket via rt aren't handles as soon as possible,
348             that means that soon depends on how much I have to do.
349              
350             Business and commercial support should be acquired from the authors via
351             preferred freelancer agencies.
352              
353             =head1 SEE ALSO
354              
355             perl(1), L, L, L, L,
356             L.
357              
358             =cut
359              
360             1;