f72f5e12378e1e5ba3ef04f8436d13ae1471efe9
[dbsrgits/SQL-Translator.git] / lib / SQL / Translator / Producer / GraphViz.pm
1 package SQL::Translator::Producer::GraphViz;
2
3 # -------------------------------------------------------------------
4 # $Id: GraphViz.pm,v 1.14 2007-09-26 13:20:09 schiffbruechige Exp $
5 # -------------------------------------------------------------------
6 # Copyright (C) 2002-4 SQLFairy Authors
7 #
8 # This program is free software; you can redistribute it and/or
9 # modify it under the terms of the GNU General Public License as
10 # published by the Free Software Foundation; version 2.
11 #
12 # This program is distributed in the hope that it will be useful, but
13 # WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
15 # General Public License for more details.
16 #
17 # You should have received a copy of the GNU General Public License
18 # along with this program; if not, write to the Free Software
19 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
20 # 02111-1307  USA
21 # -------------------------------------------------------------------
22
23 =pod
24
25 =head1 NAME
26
27 SQL::Translator::Producer::GraphViz - GraphViz producer for SQL::Translator
28
29 =head1 SYNOPSIS
30
31   use SQL::Translator;
32
33   my $trans = new SQL::Translator(
34       from => 'MySQL',            # or your db of choice
35       to => 'GraphViz',
36       producer_args => {
37           out_file => 'schema.png',
38           add_color => 1,
39           show_constraints => 1,
40           show_datatypes => 1,
41           show_sizes => 1
42       }
43   ) or die SQL::Translator->error;
44
45   $trans->translate or die $trans->error;
46
47 =head1 DESCRIPTION
48
49 Creates a graph of a schema using the amazing graphviz
50 (see http://www.graphviz.org/) application (via
51 the GraphViz module).  It's nifty--you should try it!
52
53 =head1 PRODUCER ARGS
54
55 =over 4
56
57 =item * out_file
58
59 the name of the file where the graphviz graphic is to be written
60
61 =item * layout (DEFAULT: 'dot')
62
63 determines which layout algorithm GraphViz will use; possible
64 values are 'dot' (the default GraphViz layout for directed graph
65 layouts), 'neato' (for undirected graph layouts - spring model)
66 or 'twopi' (for undirected graph layouts - circular)
67
68 =item * node_shape (DEFAULT: 'record')
69
70 sets the node shape of each table in the graph; this can be
71 one of 'record', 'plaintext', 'ellipse', 'circle', 'egg',
72 'triangle', 'box', 'diamond', 'trapezium', 'parallelogram',
73 'house', 'hexagon', or 'octagon'
74
75 =item * output_type (DEFAULT: 'png')
76
77 sets the file type of the output graphic; possible values are
78 'ps', 'hpgl', 'pcl', 'mif', 'pic', 'gd', 'gd2', 'gif', 'jpeg',
79 'png', 'wbmp', 'cmap', 'ismap', 'imap', 'vrml', 'vtx', 'mp',
80 'fig', 'svg', 'canon', 'plain' or 'text' (see GraphViz for
81 details on each of these)
82
83 =item * width (DEFAULT: 8.5)
84
85 width (in inches) of the output graphic
86
87 =item * height (DEFAULT: 11)
88
89 height (in inches) of the output grahic
90
91 =item * fontsize
92
93 custom font size for node and edge labels (note that arbitrarily large
94 sizes may be ignored due to page size or graph size constraints)
95
96 =item * fontname
97
98 custom font name (or full path to font file) for node, edge, and graph
99 labels
100
101 =item * nodeattrs
102
103 reference to a hash of node attribute names and their values; these
104 may override general fontname or fontsize parameter
105
106 =item * edgeattrs
107
108 reference to a hash of edge attribute names and their values; these
109 may override general fontname or fontsize parameter
110
111 =item * graphattrs
112
113 reference to a hash of graph attribute names and their values; these
114 may override the general fontname parameter
115
116 =item * show_fields (DEFAULT: true)
117
118 if set to a true value, the names of the colums in a table will
119 be displayed in each table's node
120
121 =item * show_fk_only
122
123 if set to a true value, only columns which are foreign keys
124 will be displayed in each table's node
125
126 =item * show_datatypes
127
128 if set to a true value, the datatype of each column will be
129 displayed next to each column's name; this option will have no
130 effect if the value of show_fields is set to false
131
132 =item * show_sizes
133
134 if set to a true value, the size (in bytes) of each CHAR and
135 VARCHAR column will be displayed in parentheses next to the
136 column's name; this option will have no effect if the value of
137 show_fields is set to false
138
139 =item * show_constraints
140
141 if set to a true value, a field's constraints (i.e., its
142 primary-key-ness, its foreign-key-ness and/or its uniqueness)
143 will appear as a comma-separated list in brackets next to the
144 field's name; this option will have no effect if the value of
145 show_fields is set to false
146
147 =item * add_color
148
149 if set to a true value, the graphic will have a background
150 color of 'lightgoldenrodyellow'; otherwise the background
151 color will be white
152
153 =item * natural_join
154
155 if set to a true value, the make_natural_join method of
156 SQL::Translator::Schema will be called before generating the
157 graph; a true value for join_pk_only (see below) implies a
158 true value for this option
159
160 =item * join_pk_only
161
162 the value of this option will be passed as the value of the
163 like-named argument in the make_natural_join method (see
164 natural_join above) of SQL::Translator::Schema, if either the
165 value of this option or the natural_join option is set to true
166
167 =item * skip_fields
168
169 the value of this option will be passed as the value of the
170 like-named argument in the make_natural_join method (see
171 natural_join above) of SQL::Translator::Schema, if either
172 the natural_join or join_pk_only options has a true value
173
174 =item * show_indexes
175
176 if set to a true value, each record will also show the indexes
177 set on each table. it describes the index types along with
178 which columns are included in the index. this option requires
179 that show_fields is a true value as well
180
181 =item * friendly_ints
182
183 if set to a true value, each integer type field will be displayed
184 as a tinyint, smallint, integer or bigint depending on the field's
185 associated size parameter. this only applies for the 'integer'
186 type (and not the lowercase 'int' type, which is assumed to be a
187 32-bit integer).
188
189 =back
190
191 =cut
192
193 use strict;
194 use GraphViz;
195 use Data::Dumper;
196 use SQL::Translator::Schema::Constants;
197 use SQL::Translator::Utils qw(debug);
198
199 use vars qw[ $VERSION $DEBUG ];
200 $VERSION = sprintf "%d.%02d", q$Revision: 1.14 $ =~ /(\d+)\.(\d+)/;
201 $DEBUG   = 0 unless defined $DEBUG;
202
203 use constant VALID_LAYOUT => {
204     dot   => 1, 
205     neato => 1, 
206     twopi => 1,
207 };
208
209 use constant VALID_NODE_SHAPE => {
210     record        => 1, 
211     plaintext     => 1, 
212     ellipse       => 1, 
213     circle        => 1, 
214     egg           => 1, 
215     triangle      => 1, 
216     box           => 1, 
217     diamond       => 1, 
218     trapezium     => 1, 
219     parallelogram => 1, 
220     house         => 1, 
221     hexagon       => 1, 
222     octagon       => 1, 
223 };
224
225 use constant VALID_OUTPUT => {
226     canon => 1, 
227     text  => 1, 
228     ps    => 1, 
229     hpgl  => 1,
230     pcl   => 1, 
231     mif   => 1, 
232     pic   => 1, 
233     gd    => 1, 
234     gd2   => 1, 
235     gif   => 1, 
236     jpeg  => 1,
237     png   => 1, 
238     wbmp  => 1, 
239     cmap  => 1, 
240     ismap => 1, 
241     imap  => 1, 
242     vrml  => 1,
243     vtx   => 1, 
244     mp    => 1, 
245     fig   => 1, 
246     svg   => 1, 
247     plain => 1,
248 };
249
250 sub produce {
251     my $t          = shift;
252     my $schema     = $t->schema;
253     my $args       = $t->producer_args;
254     local $DEBUG   = $t->debug;
255
256     my $out_file         = $args->{'out_file'}    || '';
257     my $layout           = $args->{'layout'}      || 'dot';
258     my $node_shape       = $args->{'node_shape'}  || 'record';
259     my $output_type      = $args->{'output_type'} || 'png';
260     my $width            = defined $args->{'width'} 
261                            ? $args->{'width'} : 8.5;
262     my $height           = defined $args->{'height'}
263                            ? $args->{'height'} : 11;
264     my $fontsize         = $args->{'fontsize'};
265     my $fontname         = $args->{'fontname'};
266     my $edgeattrs        = $args->{'edgeattrs'} || {};
267     my $graphattrs       = $args->{'graphattrs'} || {};
268     my $nodeattrs        = $args->{'nodeattrs'} || {};
269     my $show_fields      = defined $args->{'show_fields'} 
270                            ? $args->{'show_fields'} : 1;
271     my $add_color        = $args->{'add_color'};
272     my $natural_join     = $args->{'natural_join'};
273     my $show_fk_only     = $args->{'show_fk_only'};
274     my $show_datatypes   = $args->{'show_datatypes'};
275     my $show_sizes       = $args->{'show_sizes'};
276     my $show_indexes     = $args->{'show_indexes'};
277     my $friendly_ints    = $args->{'friendly_ints'};
278     my $show_constraints = $args->{'show_constraints'};
279     my $join_pk_only     = $args->{'join_pk_only'};
280     my $skip_fields      = $args->{'skip_fields'} || '';
281     my %skip             = map { s/^\s+|\s+$//g; length $_ ? ($_, 1) : () }
282                            split ( /,/, $skip_fields );
283     $natural_join      ||= $join_pk_only;
284
285     $schema->make_natural_joins(
286         join_pk_only => $join_pk_only,
287         skip_fields  => $args->{'skip_fields'},
288     ) if $natural_join;
289
290     die "Invalid layout '$layout'" unless VALID_LAYOUT->{ $layout };
291     die "Invalid output type: '$output_type'"
292         unless VALID_OUTPUT->{ $output_type };
293     die "Invalid node shape'$node_shape'" 
294         unless VALID_NODE_SHAPE->{ $node_shape };
295
296     for ( $height, $width ) {
297         $_ = 0 unless $_ =~ /^\d+(.\d)?$/;
298         $_ = 0 if $_ < 0;
299     }
300
301     #
302     # Create GraphViz and see if we can produce the output type.
303     #
304     my %args = (
305         directed      => $natural_join ? 0 : 1,
306         layout        => $layout,
307         no_overlap    => 1,
308         bgcolor       => $add_color ? 'lightgoldenrodyellow' : 'white',
309         node          => { 
310             shape     => $node_shape, 
311             style     => 'filled', 
312             fillcolor => 'white',
313         },
314     );
315     $args{'width'}  = $width  if $width;
316     $args{'height'} = $height if $height;
317     # set fontsize for edge and node labels if specified
318     if ($fontsize) {
319         $args{'node'}->{'fontsize'} = $fontsize;
320         $args{'edge'} = {} unless $args{'edge'};
321         $args{'edge'}->{'fontsize'} = $fontsize;        
322     }
323     # set the font name globally for node, edge, and graph labels if
324     # specified (use node, edge, or graph attributes for individual
325     # font specification)
326     if ($fontname) {
327         $args{'node'}->{'fontname'} = $fontname;
328         $args{'edge'} = {} unless $args{'edge'};
329         $args{'edge'}->{'fontname'} = $fontname;        
330         $args{'graph'} = {} unless $args{'graph'};
331         $args{'graph'}->{'fontname'} = $fontname;        
332     }
333     # set additional node, edge, and graph attributes; these may
334     # possibly override ones set before
335     while (my ($key,$val) = each %$nodeattrs) {
336         $args{'node'}->{$key} = $val;
337     }
338     $args{'edge'} = {} if %$edgeattrs && !$args{'edge'};
339     while (my ($key,$val) = each %$edgeattrs) {
340         $args{'edge'}->{$key} = $val;
341     }
342     $args{'graph'} = {} if %$edgeattrs && !$args{'graph'};
343     while (my ($key,$val) = each %$graphattrs) {
344         $args{'graph'}->{$key} = $val;
345     }
346
347     my $gv =  GraphViz->new( %args ) or die "Can't create GraphViz object\n";
348
349     my %nj_registry; # for locations of fields for natural joins
350     my @fk_registry; # for locations of fields for foreign keys
351
352     for my $table ( $schema->get_tables ) {
353         my $table_name = $table->name;
354         my @fields     = $table->get_fields;
355         if ( $show_fk_only ) {
356             @fields = grep { $_->is_foreign_key } @fields;
357         }
358
359         my $label = '{' . $table_name;
360         if ($show_fields) {
361           my $field_str = '';
362           foreach my $field (@fields) {
363             $field_str .= '-\ ' . $field->name;
364             if ($show_datatypes) {
365               my $dt = lc($field->data_type);
366
367               # For the integer type, transform into different types based on
368               # requested size, if a size is given.
369               if ($friendly_ints && $dt eq 'integer' && $field->size) {
370                 # Automatically translate to int2, int4, int8
371                 # Type (Bits)     Max. Signed           Length
372                 # tinyint (8)     128                   3
373                 # smallint (16)   32767                 5
374                 # int (32)        2147483647            10
375                 # bigint (64)     9223372036854775807   19
376                 if ($field->size > 10) {
377                   $dt = 'bigint';
378                 }
379                 elsif ($field->size > 5) {
380                   $dt = 'integer';
381                 }
382                 elsif ($field->size > 3) {
383                   $dt = 'smallint';
384                 }
385                 else { # 8 bits
386                   $dt = 'tinyint';
387                 }
388               }
389
390               $field_str .= '\ ' . $dt;
391               if ($show_sizes && $field->size && ($dt =~ /^(var)?char2?$/ || $dt eq 'numeric' || $dt eq 'decimal')) {
392                 $field_str .= '(' . $field->size . ')';
393               }
394             }
395
396             if ($show_constraints) {
397               my @constraints;
398               push(@constraints, 'PK') if $field->is_primary_key;
399               push(@constraints, 'FK') if $field->is_foreign_key;
400               push(@constraints, 'U')  if $field->is_unique;
401               if (scalar(@constraints)) {
402                 $field_str .= '\ [' . join(',\ ', @constraints) . ']';
403               }
404             }
405             $field_str .= '\l';
406           }
407           $label .= '|' . $field_str;
408         }
409
410         if ($show_indexes) {
411           my $index_str = '';
412           foreach my $index ($table->get_indices) {
413             next unless $index->is_valid;
414
415             $index_str .= '*\ ' . $index->name . ': ';
416             $index_str .= join(', ', $index->fields);
417             if ($index->type eq 'UNIQUE') {
418               $index_str .= '\ [U]';
419             }
420             $index_str .= '\l';
421           }
422           $label .= '|' . $index_str;
423         }
424         $label .= '}';
425 #        $gv->add_node( $table_name, label => $label );
426 #        $gv->add_node( $table_name, label => $label, ($node_shape eq 'record' ? ( shape => $node_shape ) : ()) );
427         $gv->add_node( $table_name, label => $label, shape => $node_shape );
428         debug("Processing table '$table_name'");
429
430         debug("Fields = ", join(', ', map { $_->name } @fields));
431
432         for my $f ( @fields ) {
433             my $name      = $f->name or next;
434             my $is_pk     = $f->is_primary_key;
435             my $is_unique = $f->is_unique;
436
437             #
438             # Decide if we should skip this field.
439             #
440             if ( $natural_join ) {
441                 next unless $is_pk || $f->is_foreign_key;
442             }
443
444             my $constraints = $f->{'constraints'};
445
446             if ( $natural_join && !$skip{ $name } ) {
447                 push @{ $nj_registry{ $name } }, $table_name;
448             }
449         }
450
451         unless ( $natural_join ) {
452             for my $c ( $table->get_constraints ) {
453                 next unless $c->type eq FOREIGN_KEY;
454                 my $fk_table = $c->reference_table or next;
455
456                 for my $field_name ( $c->fields ) {
457                     for my $fk_field ( $c->reference_fields ) {
458                         next unless defined $schema->get_table( $fk_table );
459                         push @fk_registry, [ $table_name, $fk_table ];
460                     }
461                 }
462             }
463         }
464     }
465
466     #
467     # Make the connections.
468     #
469     my @table_bunches;
470     if ( $natural_join ) {
471         for my $field_name ( keys %nj_registry ) {
472             my @table_names = @{ $nj_registry{ $field_name } || [] } or next;
473             next if scalar @table_names == 1;
474             push @table_bunches, [ @table_names ];
475         }
476     }
477     else {
478         @table_bunches = @fk_registry;
479     }
480
481     my %done;
482     for my $bunch ( @table_bunches ) {
483         my @tables = @$bunch;
484
485         for my $i ( 0 .. $#tables ) {
486             my $table1 = $tables[ $i ];
487             for my $j ( 0 .. $#tables ) {
488                 next if $i == $j;
489                 my $table2 = $tables[ $j ];
490                 next if $done{ $table1 }{ $table2 };
491                 $gv->add_edge( $table2, $table1 );
492                 $done{ $table1 }{ $table2 } = 1;
493                 $done{ $table2 }{ $table1 } = 1;
494             }
495         }
496     }
497
498     #
499     # Print the image.
500     #
501     my $output_method = "as_$output_type";
502     if ( $out_file ) {
503         open my $fh, ">$out_file" or die "Can't write '$out_file': $!\n";
504         binmode $fh;
505         print $fh $gv->$output_method;
506         close $fh;
507     }
508     else {
509         return $gv->$output_method;
510     }
511 }
512
513 1;
514
515 # -------------------------------------------------------------------
516
517 =pod
518
519 =head1 AUTHOR
520
521 Ken Y. Clark E<lt>kclark@cpan.orgE<gt>
522
523 =head1 SEE ALSO
524
525 SQL::Translator, GraphViz
526
527 =cut