File Coverage

blib/lib/GraphViz/DBI/General.pm
Criterion Covered Total %
statement 18 89 20.2
branch 0 12 0.0
condition 0 5 0.0
subroutine 6 14 42.8
pod 7 7 100.0
total 31 127 24.4


line stmt bran cond sub pod time code
1             package GraphViz::DBI::General;
2             # doom@kzsu.stanford.edu
3             # 31 Mar 2005
4              
5             =head1 NAME
6              
7             GraphViz::DBI::General - graph table relationships from a DBI handle
8              
9             =head1 SYNOPSIS
10              
11             use GraphViz::DBI::General;
12              
13             my $gbdh = GraphViz::DBI::General->new($dbh);
14             $gbdh->schema('public'); # default used by Postgresql
15             $gbdh->catalog( undef );
16             open my $fh, ">", $diagram_file or die "Couldn't open $diagram_file: $!";
17             $gbdh->graph_tables->as_png($fh);
18              
19             =head1 DESCRIPTION
20              
21             This is a subclass of GraphViz::DBI. It can be used to generate
22             a graph of foreign key relationships between the tables of a
23             database, given a database handle (and perhaps a schema name
24             and/or a catalog name). It should work for any database with a
25             driver that supports the foreign_key_info method (such as
26             Postgresql, versions 7.3 and later).
27              
28             Note that foreign_key_info is labeled as "experimental" in the
29             DBI documentation: if it's behavior changes in the future,
30             it may cause problems for this code.
31              
32             =head2 Known Compatibility
33              
34             Currently this module has been tested with:
35              
36             Version
37             -----------|--------
38             Postgresql | 8.0.1
39             DBI | 1.48
40             DBD::Pg | 1.40
41             Linux | 2.4.20
42              
43             If you're so inclined, please do report your experiences with
44             using it in other environments. Future versions will include a
45             summary of this information.
46              
47             Please send the output of the version_report method along with your
48             reports:
49              
50             my $gbdh = GraphViz::DBI::General->new($dbh);
51             print $gbdh->version_report;
52              
53             =head2 Schema and Catalog settings
54              
55             The settings you will most likely want to use with the postgresql
56             database are as indicated in the SYNOPSIS above:
57              
58             $gbdh->schema('public');
59             $gbdh->catalog( undef );
60              
61             You might, however have a different schema name you need to work
62             with (your login name is a common choice on many systems).
63              
64             In postgresql there is not concept of the "catalog", so it's set to
65             undef, this may be different for your own database.
66              
67             For some databases you might not need any schema or catalog setting,
68             and both should be undef.
69              
70              
71             =head2 MOTIVATION
72              
73             This module was inspired by GraphViz::DBI, which generates a
74             graph of table relationships given only a DBI handle.
75             Unfortunately, however, it relies on a naming convention to find
76             foreign key relationships, and has no concept of schemas (or
77             catalogs), which makes it unusable with a database such as
78             Postgresql (for Postgresql, it draws tables for the entire
79             pg_catalog and information_schema, generating huge output
80             graphs).
81              
82             GraphViz::DBI::General behaves exactly like GraphViz::DBI, except
83             that it restricts it's scope to a given catalog and schema (if
84             these are applicable), and also uses the DBI method
85             foreign_key_info to find foreign keys rather than relying on some
86             arbitrary naming convention.
87              
88             In theory, this makes GraphViz::DBI::General more general, and it
89             should be usable with any database with a fully-featured DBD
90             driver; but in fact, I'm not certain how widespread foreign_key_info
91             support is (or will be). At the very least GraphViz::DBI::General
92             works with Postgresql, which the original GraphViz::DBI definitely
93             does not.
94              
95             =head2 METHODS
96              
97             GraphViz::DBI::General provides:
98              
99             o Methods for specifying the schema and catalog (when applicable).
100             o A get_tables method with scope restricted by schema & catalog.
101             o A version of the is_foreign_key method that does not rely on naming conventions.
102              
103             See L and L for documentation of the other
104             available methods.
105              
106             In detail, the methods provided by GraphViz::DBI::General are:
107              
108             =over
109              
110             =cut
111              
112 1     1   10117 use 5.006;
  1         89  
  1         189  
113 1     1   8 use strict;
  1         2  
  1         59  
114 1     1   7 use warnings;
  1         7  
  1         37  
115 1     1   7 use Carp;
  1         2  
  1         92  
116 1     1   1462 use Data::Dumper;
  1         38554  
  1         108  
117              
118             our $VERSION = '0.1';
119              
120 1     1   11 use base 'GraphViz::DBI';
  1         2  
  1         1442  
121              
122             sub _init {
123 0     0     my $self = shift;
124 0           my $dbh = shift;
125 0           my %args = @_;
126 0           $self->{schema} = $args{schema};
127 0           $self->{catalog} = $args{catalog};
128 0           $self->SUPER::_init( $dbh );
129             }
130              
131             =item set_schema - set the schema attribute (only required
132             for some databases, e.g. typically 'public' for Postgresql)
133              
134             =cut
135              
136             sub set_schema {
137 0     0 1   my ($self, $schema) = @_;
138 0           $self->{schema} = $schema;
139             }
140              
141             =item set_catalog - set the catalog attribute (not needed if the
142             database in use does not support the feature, e.g. Postgresql
143             does not).
144              
145             =cut
146              
147             sub set_catalog {
148 0     0 1   my ($self, $catalog) = @_;
149 0           $self->{catalog} = $catalog;
150             }
151              
152             =item get_schema - returns the value of the schema attribute.
153              
154             =cut
155              
156             sub get_schema {
157 0     0 1   my $self = shift;
158 0           $self->{schema};
159             }
160              
161             =item get_catalog - returns the value of the catalog attribute.
162              
163             =cut
164              
165             sub get_catalog {
166 0     0 1   my $self = shift;
167 0           $self->{catalog};
168             }
169              
170              
171             =item get_tables - determines a listing of all tables (for the
172             current schema and/or catalog, which should be specified if
173             applicable to the database in use). Returns the list, and saves a
174             reference to it as the attribute "tables".
175              
176             =cut
177              
178             sub get_tables {
179 0     0 1   my $self = shift;
180 0           my $schema = $self->get_schema;
181 0           my $catalog = $self->get_catalog;
182 0           my @tables = $self->get_dbh->tables( $catalog, $schema, undef, undef);
183 0           local $_;
184 0           foreach (@tables) {
185 0 0         s/^$schema\.// if $schema; # Needed to work with postgresql.
186 0 0         s/^$catalog\.// if $catalog; # Possibly needed for JDBC, etc. (TODO Check that this works.)
187             }
188 0   0       $self->{tables} ||= \@tables;
189 0           return @tables;
190             }
191              
192             =item is_foreign_key - given two args "table" and "field" determines if table.field
193             is a foreign key for some other table, and if so returns the name of the
194             table, otherwise a false value ('').
195             This version should override the one in GraphViz::DBI.
196              
197             =cut
198              
199             sub is_foreign_key {
200 0     0 1   my ($self, $fk_table_candidate, $fk_field_candidate) = @_;
201 0           my $schema = $self->get_schema;
202 0           my $catalog = $self->get_catalog;
203              
204 0           my ($dbh, $sth, $aref);
205 0           $dbh = $self->get_dbh;
206              
207             # Use the foreign_key_info DBI method to look up *all* fk fields in the candidate table:
208              
209 0           my $fk_catalog = $catalog;
210 0           my $fk_schema = $schema;
211 0           my $fk_table = $fk_table_candidate;
212              
213 0 0         if ( $sth =
214             $dbh->foreign_key_info( undef, undef, undef,
215             $fk_catalog, $fk_schema, $fk_table )
216             ) {
217              
218 0           while ($aref = $sth->fetchrow_arrayref) {
219              
220 0           my $pktable_schem = $aref->[1]; # TODO have no use for this?
221 0           my $pktable_name = $aref->[2];
222 0           my $pkcolumn_name = $aref->[3];
223              
224 0           my $fktable_name = $aref->[6];
225 0           my $fkcolumn_name = $aref->[7];
226              
227             # if the key from foreign_key_info has a column name
228             # that matches the one we're looking up, then we've got one
229              
230 0 0 0       if ( $fkcolumn_name eq $fk_field_candidate
231             && $fktable_name eq $fk_table_candidate ) {
232 0           return $pktable_name;
233             }
234             }
235             }
236 0           return '';
237             }
238              
239             =item version_report - report on the versions of different
240             software packages in use by this module.
241              
242             =cut
243              
244             sub version_report {
245 0     0 1   my $self = shift;
246 0           my $dbh = $self->get_dbh;
247              
248 0           my ($sql_dbms_name, $sql_dbms_ver, $os_name, $os_version, @piece);
249 0           my ($dbd_name, $dbd_version, @dbd_name_candidates);
250 0           my ($report, @results);
251              
252 0           $sql_dbms_name = $dbh->get_info( 17 ); # SQL_DBMS_NAME
253 0           $sql_dbms_ver = $dbh->get_info( 18 ); # SQL_DBMS_VER
254              
255             # Get the OS name, and version if possible
256             # TODO - look for standard solution to this (CPAN?)
257 0           @piece = split /\s+/, `uname -a`;
258 0 0         if (@piece) {
259 0           $os_name = $piece[0];
260 0           $os_version = $piece[2];
261             } else { # if "uname" isn't available
262 0           $os_name = $^O;
263 0           $os_version = '???';
264             }
265              
266             # Hash to relate the DBD::* form of a database name with
267             # the more official form reported by the DBI "get_info" method.
268 0           my %dbd = (
269             PostgreSQL => 'Pg',
270             MySQL => 'mysql',
271             Mysql => 'mysql',
272             msql => 'mSQL',
273             oracle => 'Oracle',
274             );
275              
276             # TODO - Does this need more entires? With luck only "DBD::Pg"
277             # will have the problem of "SQL_DBMS_NAME" being different from the DBD name.
278             # (Naming in general is a mess in the postgres/Pg/postgresql/PostgreSQL
279             # world... postmaster? psql?)
280              
281 0           @dbd_name_candidates = ('DBD::' . $dbd{$sql_dbms_name} ,
282             'DBD::' . $sql_dbms_name ,
283             $sql_dbms_name);
284 0           for my $candidate (@dbd_name_candidates) {
285 0 0         if ( $dbd_version = eval('$' . $candidate . '::VERSION') ) {
286 0           $dbd_name = $candidate;
287 0           last;
288             }
289             }
290              
291             @results = (
292 0           {$sql_dbms_name => $sql_dbms_ver},
293             {'DBI' => $DBI::VERSION},
294             {"DBD::$dbd_name" => $dbd_version},
295             {'GraphViz' => $GraphViz::VERSION},
296             {'GraphViz::DBI' => $GraphViz::DBI::VERSION},
297             {$os_name => $os_version},
298             );
299              
300 0           $report = "Software in use with GraphViz::DBI::General v. $GraphViz::DBI::General::VERSION:\n";
301              
302 0           $report .= " | Version \n";
303 0           $report .= "----------------|----------------\n";
304              
305 0           foreach my $rec (@results) {
306 0           my $label = ( keys( %{ $rec } ) )[0];
  0            
307 0           my $value = $rec->{ $label };
308 0           $report .= sprintf("%15s | %-12s\n", $label, $value);
309             }
310              
311 0           return $report;
312             }
313              
314             1;
315              
316             __END__