Changed show_index_name to show_index_names to make it better match the other options
[dbsrgits/SQL-Translator.git] / lib / SQL / Translator / Producer / GraphViz.pm
1 package SQL::Translator::Producer::GraphViz;
2
3 # -------------------------------------------------------------------
4 # $Id$
5 # -------------------------------------------------------------------
6 # Copyright (C) 2002-2009 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_names
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 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 =item * friendly_ints_extended
198
199 if set to a true value, the friendly ints displayed will take into
200 account the non-standard types, 'tinyint' and 'mediumint' (which,
201 as far as I am aware, is only implemented in MySQL)
202
203 =back
204
205 =cut
206
207 use strict;
208 use GraphViz;
209 use SQL::Translator::Schema::Constants;
210 use SQL::Translator::Utils qw(debug);
211
212 use vars qw[ $VERSION $DEBUG ];
213 $VERSION = sprintf "%d.%02d", q$Revision$ =~ /(\d+)\.(\d+)/;
214 $DEBUG   = 0 unless defined $DEBUG;
215
216 use constant VALID_LAYOUT => {
217     dot   => 1, 
218     neato => 1, 
219     twopi => 1,
220 };
221
222 use constant VALID_NODE_SHAPE => {
223     record        => 1, 
224     plaintext     => 1, 
225     ellipse       => 1, 
226     circle        => 1, 
227     egg           => 1, 
228     triangle      => 1, 
229     box           => 1, 
230     diamond       => 1, 
231     trapezium     => 1, 
232     parallelogram => 1, 
233     house         => 1, 
234     hexagon       => 1, 
235     octagon       => 1, 
236 };
237
238 use constant VALID_OUTPUT => {
239     canon => 1, 
240     text  => 1, 
241     ps    => 1, 
242     hpgl  => 1,
243     pcl   => 1, 
244     mif   => 1, 
245     pic   => 1, 
246     gd    => 1, 
247     gd2   => 1, 
248     gif   => 1, 
249     jpeg  => 1,
250     png   => 1, 
251     wbmp  => 1, 
252     cmap  => 1, 
253     ismap => 1, 
254     imap  => 1, 
255     vrml  => 1,
256     vtx   => 1, 
257     mp    => 1, 
258     fig   => 1, 
259     svg   => 1, 
260     plain => 1,
261 };
262
263 sub produce {
264     my $t          = shift;
265     my $schema     = $t->schema;
266     my $args       = $t->producer_args;
267     local $DEBUG   = $t->debug;
268
269     my $out_file         = $args->{'out_file'}    || '';
270     my $layout           = $args->{'layout'}      || 'dot';
271     my $node_shape       = $args->{'node_shape'}  || 'record';
272     my $output_type      = $args->{'output_type'} || 'png';
273     my $width            = defined $args->{'width'} 
274                            ? $args->{'width'} : 8.5;
275     my $height           = defined $args->{'height'}
276                            ? $args->{'height'} : 11;
277     my $fontsize         = $args->{'fontsize'};
278     my $fontname         = $args->{'fontname'};
279     my $edgeattrs        = $args->{'edgeattrs'} || {};
280     my $graphattrs       = $args->{'graphattrs'} || {};
281     my $nodeattrs        = $args->{'nodeattrs'} || {};
282     my $show_fields      = defined $args->{'show_fields'} 
283                            ? $args->{'show_fields'} : 1;
284     my $add_color        = $args->{'add_color'};
285     my $natural_join     = $args->{'natural_join'};
286     my $show_fk_only     = $args->{'show_fk_only'};
287     my $show_datatypes   = $args->{'show_datatypes'};
288     my $show_sizes       = $args->{'show_sizes'};
289     my $show_indexes     = $args->{'show_indexes'};
290     my $show_index_names = defined $args->{'show_index_names'} ? $args->{'show_index_names'} : 1;
291     my $friendly_ints    = $args->{'friendly_ints'};
292     my $friendly_ints_ex = $args->{'friendly_ints_extended'};
293     my $show_constraints = $args->{'show_constraints'};
294     my $join_pk_only     = $args->{'join_pk_only'};
295     my $skip_fields      = $args->{'skip_fields'} || '';
296     my %skip             = map { s/^\s+|\s+$//g; length $_ ? ($_, 1) : () }
297                            split ( /,/, $skip_fields );
298     $natural_join      ||= $join_pk_only;
299
300     $schema->make_natural_joins(
301         join_pk_only => $join_pk_only,
302         skip_fields  => $args->{'skip_fields'},
303     ) if $natural_join;
304
305     die "Invalid layout '$layout'" unless VALID_LAYOUT->{ $layout };
306     die "Invalid output type: '$output_type'"
307         unless VALID_OUTPUT->{ $output_type };
308     die "Invalid node shape'$node_shape'" 
309         unless VALID_NODE_SHAPE->{ $node_shape };
310
311     for ( $height, $width ) {
312         $_ = 0 unless $_ =~ /^\d+(.\d)?$/;
313         $_ = 0 if $_ < 0;
314     }
315
316     #
317     # Create GraphViz and see if we can produce the output type.
318     #
319     my %args = (
320         directed      => $natural_join ? 0 : 1,
321         layout        => $layout,
322         no_overlap    => 1,
323         bgcolor       => $add_color ? 'lightgoldenrodyellow' : 'white',
324         node          => { 
325             shape     => $node_shape, 
326             style     => 'filled', 
327             fillcolor => 'white',
328         },
329     );
330     $args{'width'}  = $width  if $width;
331     $args{'height'} = $height if $height;
332     # set fontsize for edge and node labels if specified
333     if ($fontsize) {
334         $args{'node'}->{'fontsize'} = $fontsize;
335         $args{'edge'} = {} unless $args{'edge'};
336         $args{'edge'}->{'fontsize'} = $fontsize;        
337     }
338     # set the font name globally for node, edge, and graph labels if
339     # specified (use node, edge, or graph attributes for individual
340     # font specification)
341     if ($fontname) {
342         $args{'node'}->{'fontname'} = $fontname;
343         $args{'edge'} = {} unless $args{'edge'};
344         $args{'edge'}->{'fontname'} = $fontname;        
345         $args{'graph'} = {} unless $args{'graph'};
346         $args{'graph'}->{'fontname'} = $fontname;        
347     }
348     # set additional node, edge, and graph attributes; these may
349     # possibly override ones set before
350     while (my ($key,$val) = each %$nodeattrs) {
351         $args{'node'}->{$key} = $val;
352     }
353     $args{'edge'} = {} if %$edgeattrs && !$args{'edge'};
354     while (my ($key,$val) = each %$edgeattrs) {
355         $args{'edge'}->{$key} = $val;
356     }
357     $args{'graph'} = {} if %$edgeattrs && !$args{'graph'};
358     while (my ($key,$val) = each %$graphattrs) {
359         $args{'graph'}->{$key} = $val;
360     }
361
362     my $gv =  GraphViz->new( %args ) or die "Can't create GraphViz object\n";
363
364     my %nj_registry; # for locations of fields for natural joins
365     my @fk_registry; # for locations of fields for foreign keys
366
367     for my $table ( $schema->get_tables ) {
368         my @fields     = $table->get_fields;
369         if ( $show_fk_only ) {
370             @fields = grep { $_->is_foreign_key } @fields;
371         }
372
373         my $field_str = '';
374         if ($show_fields) {
375
376           my @fmt_fields;
377           foreach my $field (@fields) {
378
379             my $field_type;
380             if ($show_datatypes) {
381
382               $field_type = $field->data_type;
383
384               # For the integer type, transform into different types based on
385               # requested size, if a size is given.
386               if ($field->size and $friendly_ints and (lc $field_type) eq 'integer') {
387                 # Automatically translate to int2, int4, int8
388                 # Type (Bits)     Max. Signed/Unsigned    Length
389                 # tinyint* (8)    128                     3
390                 #                 255                     3
391                 # smallint (16)   32767                   5
392                 #                 65535                   5
393                 # mediumint* (24) 8388607                 7
394                 #                 16777215                8
395                 # int (32)        2147483647              10
396                 #                 4294967295              10
397                 # bigint (64)     9223372036854775807     19
398                 #                 18446744073709551615    20
399                 #
400                 # * tinyint and mediumint are nonstandard extensions which are
401                 #   only available under MySQL (to my knowledge)
402                 my $size = $field->size;
403                 if ($size <= 3 and $friendly_ints_ex) {
404                   $field_type = 'tinyint',
405                 }
406                 elsif ($size <= 5) {
407                   $field_type = 'smallint';
408                 }
409                 elsif ($size <= 8 and $friendly_ints_ex) {
410                   $field_type = 'mediumint';
411                 }
412                 elsif ($size <= 11) {
413                   $field_type = 'integer';
414                 }
415                 else {
416                   $field_type = 'bigint';
417                 }
418               }
419
420               if (
421                 $show_sizes
422                   and
423                 $field->size
424                   and
425                 ($field_type =~ /^(var)?char2?$/ or $field_type eq 'numeric' or $field_type eq 'decimal')
426               ) {
427                 $field_type .= '(' . $field->size . ')';
428               }
429             }
430
431             my $constraints;
432             if ($show_constraints) {
433               my @constraints;
434               push(@constraints, 'PK') if $field->is_primary_key;
435               push(@constraints, 'FK') if $field->is_foreign_key;
436               push(@constraints, 'U')  if $field->is_unique;
437
438               $constraints = join (',', @constraints);
439             }
440
441             # construct the field line from all info gathered so far
442             push @fmt_fields, join (' ',
443               '-',
444               $field->name,
445               $field_type || (),
446               $constraints ? "[$constraints]" : (),
447             );
448
449           }
450
451           # join field lines with graphviz formatting
452           $field_str = join ('\l', @fmt_fields) . '\l';
453         }
454
455         my $index_str = '';
456         if ($show_indexes) {
457
458           my @fmt_indexes;
459           foreach my $index ($table->get_indices) {
460             next unless $index->is_valid;
461
462             push @fmt_indexes, join (' ',
463               '*',
464               $show_index_names ? $index->name . ':' : (),
465               join (', ', $index->fields),
466               ($index->type eq 'UNIQUE') ? '[U]' : (),
467             );
468           }
469
470           # join index lines with graphviz formatting (if any indexes at all)
471           $index_str = join ('\l', @fmt_indexes) . '\l' if @fmt_indexes;
472         }
473
474         my $table_name = $table->name;
475         my $name_str = $table_name . '\n';
476
477         # escape spaces
478         for ($name_str, $field_str, $index_str) {
479           $_ =~ s/ /\\ /g;
480         }
481
482
483         # only the 'record' type supports nice formatting
484         if ($node_shape eq 'record') {
485
486             # the necessity to supply shape => 'record' is a graphviz bug 
487             $gv->add_node( $table_name,
488               shape => 'record',
489               label => sprintf ('{%s}',
490                 join ('|',
491                   $name_str,
492                   $field_str || (),
493                   $index_str || (),
494                 ),
495               ),
496             );
497         }
498         else {
499             my $sep = sprintf ('%s\n',
500                 '-' x ( (length $table_name) + 2)
501             );
502
503             $gv->add_node( $table_name,
504                 label => join ($sep,
505                     $name_str,
506                     $field_str || (),
507                     $index_str || (),
508                 ),
509             );
510         }
511
512
513         debug("Processing table '$table_name'");
514
515         debug("Fields = ", join(', ', map { $_->name } @fields));
516
517         for my $f ( @fields ) {
518             my $name      = $f->name or next;
519             my $is_pk     = $f->is_primary_key;
520             my $is_unique = $f->is_unique;
521
522             #
523             # Decide if we should skip this field.
524             #
525             if ( $natural_join ) {
526                 next unless $is_pk || $f->is_foreign_key;
527             }
528
529             my $constraints = $f->{'constraints'};
530
531             if ( $natural_join && !$skip{ $name } ) {
532                 push @{ $nj_registry{ $name } }, $table_name;
533             }
534         }
535
536         unless ( $natural_join ) {
537             for my $c ( $table->get_constraints ) {
538                 next unless $c->type eq FOREIGN_KEY;
539                 my $fk_table = $c->reference_table or next;
540
541                 for my $field_name ( $c->fields ) {
542                     for my $fk_field ( $c->reference_fields ) {
543                         next unless defined $schema->get_table( $fk_table );
544                         push @fk_registry, [ $table_name, $fk_table ];
545                     }
546                 }
547             }
548         }
549     }
550
551     #
552     # Make the connections.
553     #
554     my @table_bunches;
555     if ( $natural_join ) {
556         for my $field_name ( keys %nj_registry ) {
557             my @table_names = @{ $nj_registry{ $field_name } || [] } or next;
558             next if scalar @table_names == 1;
559             push @table_bunches, [ @table_names ];
560         }
561     }
562     else {
563         @table_bunches = @fk_registry;
564     }
565
566     my %done;
567     for my $bunch ( @table_bunches ) {
568         my @tables = @$bunch;
569
570         for my $i ( 0 .. $#tables ) {
571             my $table1 = $tables[ $i ];
572             for my $j ( 0 .. $#tables ) {
573                 next if $i == $j;
574                 my $table2 = $tables[ $j ];
575                 next if $done{ $table1 }{ $table2 };
576                 $gv->add_edge( $table2, $table1 );
577                 $done{ $table1 }{ $table2 } = 1;
578                 $done{ $table2 }{ $table1 } = 1;
579             }
580         }
581     }
582
583     #
584     # Print the image.
585     #
586     my $output_method = "as_$output_type";
587     if ( $out_file ) {
588         open my $fh, ">$out_file" or die "Can't write '$out_file': $!\n";
589         binmode $fh;
590         print $fh $gv->$output_method;
591         close $fh;
592     }
593     else {
594         return $gv->$output_method;
595     }
596 }
597
598 1;
599
600 # -------------------------------------------------------------------
601
602 =pod
603
604 =head1 AUTHOR
605
606 Ken Y. Clark E<lt>kclark@cpan.orgE<gt>
607
608 =head2 CONTRIBUTORS
609
610 Jonathan Yu E<lt>frequency@cpan.orgE<gt>
611
612 =head1 SEE ALSO
613
614 SQL::Translator, GraphViz
615
616 =cut