- Added a show_index_name parameter which determines whether index names should be...
[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 * show_index_name
182
183 if show_indexes is set to a true value, then the value of this
184 parameter determines whether or not to print names of indexes.
185 if show_index_name is false, then a list of indexed columns
186 will appear below the field list. otherwise, it will be a list
187 prefixed with the name of each index. it defaults to true.
188
189 =item * friendly_ints
190
191 if set to a true value, each integer type field will be displayed
192 as a tinyint, smallint, integer or bigint depending on the field's
193 associated size parameter. this only applies for the 'integer'
194 type (and not the lowercase 'int' type, which is assumed to be a
195 32-bit integer).
196
197 =back
198
199 =cut
200
201 use strict;
202 use GraphViz;
203 use Data::Dumper;
204 use SQL::Translator::Schema::Constants;
205 use SQL::Translator::Utils qw(debug);
206
207 use vars qw[ $VERSION $DEBUG ];
208 $VERSION = sprintf "%d.%02d", q$Revision: 1.14 $ =~ /(\d+)\.(\d+)/;
209 $DEBUG   = 0 unless defined $DEBUG;
210
211 use constant VALID_LAYOUT => {
212     dot   => 1, 
213     neato => 1, 
214     twopi => 1,
215 };
216
217 use constant VALID_NODE_SHAPE => {
218     record        => 1, 
219     plaintext     => 1, 
220     ellipse       => 1, 
221     circle        => 1, 
222     egg           => 1, 
223     triangle      => 1, 
224     box           => 1, 
225     diamond       => 1, 
226     trapezium     => 1, 
227     parallelogram => 1, 
228     house         => 1, 
229     hexagon       => 1, 
230     octagon       => 1, 
231 };
232
233 use constant VALID_OUTPUT => {
234     canon => 1, 
235     text  => 1, 
236     ps    => 1, 
237     hpgl  => 1,
238     pcl   => 1, 
239     mif   => 1, 
240     pic   => 1, 
241     gd    => 1, 
242     gd2   => 1, 
243     gif   => 1, 
244     jpeg  => 1,
245     png   => 1, 
246     wbmp  => 1, 
247     cmap  => 1, 
248     ismap => 1, 
249     imap  => 1, 
250     vrml  => 1,
251     vtx   => 1, 
252     mp    => 1, 
253     fig   => 1, 
254     svg   => 1, 
255     plain => 1,
256 };
257
258 sub produce {
259     my $t          = shift;
260     my $schema     = $t->schema;
261     my $args       = $t->producer_args;
262     local $DEBUG   = $t->debug;
263
264     my $out_file         = $args->{'out_file'}    || '';
265     my $layout           = $args->{'layout'}      || 'dot';
266     my $node_shape       = $args->{'node_shape'}  || 'record';
267     my $output_type      = $args->{'output_type'} || 'png';
268     my $width            = defined $args->{'width'} 
269                            ? $args->{'width'} : 8.5;
270     my $height           = defined $args->{'height'}
271                            ? $args->{'height'} : 11;
272     my $fontsize         = $args->{'fontsize'};
273     my $fontname         = $args->{'fontname'};
274     my $edgeattrs        = $args->{'edgeattrs'} || {};
275     my $graphattrs       = $args->{'graphattrs'} || {};
276     my $nodeattrs        = $args->{'nodeattrs'} || {};
277     my $show_fields      = defined $args->{'show_fields'} 
278                            ? $args->{'show_fields'} : 1;
279     my $add_color        = $args->{'add_color'};
280     my $natural_join     = $args->{'natural_join'};
281     my $show_fk_only     = $args->{'show_fk_only'};
282     my $show_datatypes   = $args->{'show_datatypes'};
283     my $show_sizes       = $args->{'show_sizes'};
284     my $show_indexes     = $args->{'show_indexes'};
285     my $show_index_name  = $args->{'show_index_name'} || 1;
286     my $friendly_ints    = $args->{'friendly_ints'};
287     my $show_constraints = $args->{'show_constraints'};
288     my $join_pk_only     = $args->{'join_pk_only'};
289     my $skip_fields      = $args->{'skip_fields'} || '';
290     my %skip             = map { s/^\s+|\s+$//g; length $_ ? ($_, 1) : () }
291                            split ( /,/, $skip_fields );
292     $natural_join      ||= $join_pk_only;
293
294     $schema->make_natural_joins(
295         join_pk_only => $join_pk_only,
296         skip_fields  => $args->{'skip_fields'},
297     ) if $natural_join;
298
299     die "Invalid layout '$layout'" unless VALID_LAYOUT->{ $layout };
300     die "Invalid output type: '$output_type'"
301         unless VALID_OUTPUT->{ $output_type };
302     die "Invalid node shape'$node_shape'" 
303         unless VALID_NODE_SHAPE->{ $node_shape };
304
305     for ( $height, $width ) {
306         $_ = 0 unless $_ =~ /^\d+(.\d)?$/;
307         $_ = 0 if $_ < 0;
308     }
309
310     #
311     # Create GraphViz and see if we can produce the output type.
312     #
313     my %args = (
314         directed      => $natural_join ? 0 : 1,
315         layout        => $layout,
316         no_overlap    => 1,
317         bgcolor       => $add_color ? 'lightgoldenrodyellow' : 'white',
318         node          => { 
319             shape     => $node_shape, 
320             style     => 'filled', 
321             fillcolor => 'white',
322         },
323     );
324     $args{'width'}  = $width  if $width;
325     $args{'height'} = $height if $height;
326     # set fontsize for edge and node labels if specified
327     if ($fontsize) {
328         $args{'node'}->{'fontsize'} = $fontsize;
329         $args{'edge'} = {} unless $args{'edge'};
330         $args{'edge'}->{'fontsize'} = $fontsize;        
331     }
332     # set the font name globally for node, edge, and graph labels if
333     # specified (use node, edge, or graph attributes for individual
334     # font specification)
335     if ($fontname) {
336         $args{'node'}->{'fontname'} = $fontname;
337         $args{'edge'} = {} unless $args{'edge'};
338         $args{'edge'}->{'fontname'} = $fontname;        
339         $args{'graph'} = {} unless $args{'graph'};
340         $args{'graph'}->{'fontname'} = $fontname;        
341     }
342     # set additional node, edge, and graph attributes; these may
343     # possibly override ones set before
344     while (my ($key,$val) = each %$nodeattrs) {
345         $args{'node'}->{$key} = $val;
346     }
347     $args{'edge'} = {} if %$edgeattrs && !$args{'edge'};
348     while (my ($key,$val) = each %$edgeattrs) {
349         $args{'edge'}->{$key} = $val;
350     }
351     $args{'graph'} = {} if %$edgeattrs && !$args{'graph'};
352     while (my ($key,$val) = each %$graphattrs) {
353         $args{'graph'}->{$key} = $val;
354     }
355
356     my $gv =  GraphViz->new( %args ) or die "Can't create GraphViz object\n";
357
358     my %nj_registry; # for locations of fields for natural joins
359     my @fk_registry; # for locations of fields for foreign keys
360
361     for my $table ( $schema->get_tables ) {
362         my $table_name = $table->name;
363         my @fields     = $table->get_fields;
364         if ( $show_fk_only ) {
365             @fields = grep { $_->is_foreign_key } @fields;
366         }
367
368         my $label = '{' . $table_name;
369         if ($show_fields) {
370           my $field_str = '';
371           foreach my $field (@fields) {
372             $field_str .= '-\ ' . $field->name;
373             if ($show_datatypes) {
374               my $dt = lc($field->data_type);
375
376               # For the integer type, transform into different types based on
377               # requested size, if a size is given.
378               if ($friendly_ints && $dt eq 'integer' && $field->size) {
379                 # Automatically translate to int2, int4, int8
380                 # Type (Bits)     Max. Signed           Length
381                 # tinyint (8)     128                   3
382                 # smallint (16)   32767                 5
383                 # int (32)        2147483647            10
384                 # bigint (64)     9223372036854775807   19
385                 if ($field->size > 10) {
386                   $dt = 'bigint';
387                 }
388                 elsif ($field->size > 5) {
389                   $dt = 'integer';
390                 }
391                 elsif ($field->size > 3) {
392                   $dt = 'smallint';
393                 }
394                 else { # 8 bits
395                   $dt = 'tinyint';
396                 }
397               }
398
399               $field_str .= '\ ' . $dt;
400               if ($show_sizes && $field->size && ($dt =~ /^(var)?char2?$/ || $dt eq 'numeric' || $dt eq 'decimal')) {
401                 $field_str .= '(' . $field->size . ')';
402               }
403             }
404
405             if ($show_constraints) {
406               my @constraints;
407               push(@constraints, 'PK') if $field->is_primary_key;
408               push(@constraints, 'FK') if $field->is_foreign_key;
409               push(@constraints, 'U')  if $field->is_unique;
410               if (scalar(@constraints)) {
411                 $field_str .= '\ [' . join(',\ ', @constraints) . ']';
412               }
413             }
414             $field_str .= '\l';
415           }
416           $label .= '|' . $field_str;
417         }
418
419         if ($show_indexes) {
420           my $index_str = '';
421           foreach my $index ($table->get_indices) {
422             next unless $index->is_valid;
423
424             $index_str .= '*\ ';
425             if ($show_index_name) {
426               $index_str .= $index->name . ': ';
427             }
428             $index_str .= join(', ', $index->fields);
429             if ($index->type eq 'UNIQUE') {
430               $index_str .= '\ [U]';
431             }
432             $index_str .= '\l';
433           }
434           # Only add the last box if index_str is non-null
435           if (length $index_str) {
436             $label .= '|' . $index_str;
437           }
438         }
439         $label .= '}';
440 #        $gv->add_node( $table_name, label => $label );
441 #        $gv->add_node( $table_name, label => $label, ($node_shape eq 'record' ? ( shape => $node_shape ) : ()) );
442         $gv->add_node( $table_name, label => $label, shape => $node_shape );
443         debug("Processing table '$table_name'");
444
445         debug("Fields = ", join(', ', map { $_->name } @fields));
446
447         for my $f ( @fields ) {
448             my $name      = $f->name or next;
449             my $is_pk     = $f->is_primary_key;
450             my $is_unique = $f->is_unique;
451
452             #
453             # Decide if we should skip this field.
454             #
455             if ( $natural_join ) {
456                 next unless $is_pk || $f->is_foreign_key;
457             }
458
459             my $constraints = $f->{'constraints'};
460
461             if ( $natural_join && !$skip{ $name } ) {
462                 push @{ $nj_registry{ $name } }, $table_name;
463             }
464         }
465
466         unless ( $natural_join ) {
467             for my $c ( $table->get_constraints ) {
468                 next unless $c->type eq FOREIGN_KEY;
469                 my $fk_table = $c->reference_table or next;
470
471                 for my $field_name ( $c->fields ) {
472                     for my $fk_field ( $c->reference_fields ) {
473                         next unless defined $schema->get_table( $fk_table );
474                         push @fk_registry, [ $table_name, $fk_table ];
475                     }
476                 }
477             }
478         }
479     }
480
481     #
482     # Make the connections.
483     #
484     my @table_bunches;
485     if ( $natural_join ) {
486         for my $field_name ( keys %nj_registry ) {
487             my @table_names = @{ $nj_registry{ $field_name } || [] } or next;
488             next if scalar @table_names == 1;
489             push @table_bunches, [ @table_names ];
490         }
491     }
492     else {
493         @table_bunches = @fk_registry;
494     }
495
496     my %done;
497     for my $bunch ( @table_bunches ) {
498         my @tables = @$bunch;
499
500         for my $i ( 0 .. $#tables ) {
501             my $table1 = $tables[ $i ];
502             for my $j ( 0 .. $#tables ) {
503                 next if $i == $j;
504                 my $table2 = $tables[ $j ];
505                 next if $done{ $table1 }{ $table2 };
506                 $gv->add_edge( $table2, $table1 );
507                 $done{ $table1 }{ $table2 } = 1;
508                 $done{ $table2 }{ $table1 } = 1;
509             }
510         }
511     }
512
513     #
514     # Print the image.
515     #
516     my $output_method = "as_$output_type";
517     if ( $out_file ) {
518         open my $fh, ">$out_file" or die "Can't write '$out_file': $!\n";
519         binmode $fh;
520         print $fh $gv->$output_method;
521         close $fh;
522     }
523     else {
524         return $gv->$output_method;
525     }
526 }
527
528 1;
529
530 # -------------------------------------------------------------------
531
532 =pod
533
534 =head1 AUTHOR
535
536 Ken Y. Clark E<lt>kclark@cpan.orgE<gt>
537
538 =head1 SEE ALSO
539
540 SQL::Translator, GraphViz
541
542 =cut