Remove all expansion $XX tags (isolated commit, easily revertable)
[dbsrgits/SQL-Translator.git] / lib / SQL / Translator / Producer / GraphViz.pm
1 package SQL::Translator::Producer::GraphViz;
2
3 # -------------------------------------------------------------------
4 # Copyright (C) 2002-2009 SQLFairy Authors
5 #
6 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License as
8 # published by the Free Software Foundation; version 2.
9 #
10 # This program is distributed in the hope that it will be useful, but
11 # WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13 # General Public License for more details.
14 #
15 # You should have received a copy of the GNU General Public License
16 # along with this program; if not, write to the Free Software
17 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
18 # 02111-1307  USA
19 # -------------------------------------------------------------------
20
21 =pod
22
23 =head1 NAME
24
25 SQL::Translator::Producer::GraphViz - GraphViz producer for SQL::Translator
26
27 =head1 SYNOPSIS
28
29   use SQL::Translator;
30
31   my $trans = new SQL::Translator(
32       from => 'MySQL',            # or your db of choice
33       to => 'GraphViz',
34       producer_args => {
35           out_file => 'schema.png',
36           add_color => 1,
37           show_constraints => 1,
38           show_datatypes => 1,
39           show_sizes => 1
40       }
41   ) or die SQL::Translator->error;
42
43   $trans->translate or die $trans->error;
44
45 =head1 DESCRIPTION
46
47 Creates a graph of a schema using the amazing graphviz
48 (see http://www.graphviz.org/) application (via
49 the GraphViz module).  It's nifty--you should try it!
50
51 =head1 PRODUCER ARGS
52
53 =over 4
54
55 =item * out_file
56
57 The name of the file where the resulting GraphViz output will be
58 written. Alternatively an open filehandle can be supplied. If
59 undefined (the default) - the result is returned as a string.
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_names 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 use Scalar::Util qw/openhandle/;
212
213 use vars qw[ $VERSION $DEBUG ];
214 $VERSION = '1.99';
215 $DEBUG   = 0 unless defined $DEBUG;
216
217 use constant VALID_LAYOUT => {
218     dot   => 1, 
219     neato => 1, 
220     twopi => 1,
221 };
222
223 use constant VALID_NODE_SHAPE => {
224     record        => 1, 
225     plaintext     => 1, 
226     ellipse       => 1, 
227     circle        => 1, 
228     egg           => 1, 
229     triangle      => 1, 
230     box           => 1, 
231     diamond       => 1, 
232     trapezium     => 1, 
233     parallelogram => 1, 
234     house         => 1, 
235     hexagon       => 1, 
236     octagon       => 1, 
237 };
238
239 sub produce {
240     my $t          = shift;
241     my $schema     = $t->schema;
242     my $args       = $t->producer_args;
243     local $DEBUG   = $t->debug;
244
245     my $out_file         = $args->{'out_file'}    || '';
246     my $layout           = $args->{'layout'}      || 'dot';
247     my $node_shape       = $args->{'node_shape'}  || 'record';
248     my $output_type      = $args->{'output_type'} || 'png';
249     my $width            = defined $args->{'width'} 
250                            ? $args->{'width'} : 8.5;
251     my $height           = defined $args->{'height'}
252                            ? $args->{'height'} : 11;
253     my $fontsize         = $args->{'fontsize'};
254     my $fontname         = $args->{'fontname'};
255     my $edgeattrs        = $args->{'edgeattrs'} || {};
256     my $graphattrs       = $args->{'graphattrs'} || {};
257     my $nodeattrs        = $args->{'nodeattrs'} || {};
258     my $show_fields      = defined $args->{'show_fields'} 
259                            ? $args->{'show_fields'} : 1;
260     my $add_color        = $args->{'add_color'};
261     my $natural_join     = $args->{'natural_join'};
262     my $show_fk_only     = $args->{'show_fk_only'};
263     my $show_datatypes   = $args->{'show_datatypes'};
264     my $show_sizes       = $args->{'show_sizes'};
265     my $show_indexes     = $args->{'show_indexes'};
266     my $show_index_names = defined $args->{'show_index_names'} ? $args->{'show_index_names'} : 1;
267     my $friendly_ints    = $args->{'friendly_ints'};
268     my $friendly_ints_ex = $args->{'friendly_ints_extended'};
269     my $show_constraints = $args->{'show_constraints'};
270     my $join_pk_only     = $args->{'join_pk_only'};
271     my $skip_fields      = $args->{'skip_fields'} || '';
272     my %skip             = map { s/^\s+|\s+$//g; length $_ ? ($_, 1) : () }
273                            split ( /,/, $skip_fields );
274     $natural_join      ||= $join_pk_only;
275
276     $schema->make_natural_joins(
277         join_pk_only => $join_pk_only,
278         skip_fields  => $args->{'skip_fields'},
279     ) if $natural_join;
280
281     die "Invalid layout '$layout'" unless VALID_LAYOUT->{ $layout };
282     die "Invalid node shape'$node_shape'" 
283         unless VALID_NODE_SHAPE->{ $node_shape };
284
285     for ( $height, $width ) {
286         $_ = 0 unless $_ =~ /^\d+(.\d)?$/;
287         $_ = 0 if $_ < 0;
288     }
289
290     my %args = (
291         directed      => $natural_join ? 0 : 1,
292         layout        => $layout,
293         no_overlap    => 1,
294         bgcolor       => $add_color ? 'lightgoldenrodyellow' : 'white',
295         node          => { 
296             shape     => $node_shape, 
297             style     => 'filled', 
298             fillcolor => 'white',
299         },
300     );
301     $args{'width'}  = $width  if $width;
302     $args{'height'} = $height if $height;
303     # set fontsize for edge and node labels if specified
304     if ($fontsize) {
305         $args{'node'}->{'fontsize'} = $fontsize;
306         $args{'edge'} = {} unless $args{'edge'};
307         $args{'edge'}->{'fontsize'} = $fontsize;        
308     }
309     # set the font name globally for node, edge, and graph labels if
310     # specified (use node, edge, or graph attributes for individual
311     # font specification)
312     if ($fontname) {
313         $args{'node'}->{'fontname'} = $fontname;
314         $args{'edge'} = {} unless $args{'edge'};
315         $args{'edge'}->{'fontname'} = $fontname;        
316         $args{'graph'} = {} unless $args{'graph'};
317         $args{'graph'}->{'fontname'} = $fontname;        
318     }
319     # set additional node, edge, and graph attributes; these may
320     # possibly override ones set before
321     while (my ($key,$val) = each %$nodeattrs) {
322         $args{'node'}->{$key} = $val;
323     }
324     $args{'edge'} = {} if %$edgeattrs && !$args{'edge'};
325     while (my ($key,$val) = each %$edgeattrs) {
326         $args{'edge'}->{$key} = $val;
327     }
328     $args{'graph'} = {} if %$edgeattrs && !$args{'graph'};
329     while (my ($key,$val) = each %$graphattrs) {
330         $args{'graph'}->{$key} = $val;
331     }
332
333     #
334     # Create a blank GraphViz object and see if we can produce the output type.
335     #
336     my $gv = GraphViz->new( %args ) or die "Can't create GraphViz object\n";
337     my $output_method = "as_$output_type";
338
339     # the generators are AUTOLOADed so can't use ->can ($output_method) 
340     eval { $gv->$output_method };
341     die "Invalid output type: '$output_type'" if $@;
342
343
344     my %nj_registry; # for locations of fields for natural joins
345     my @fk_registry; # for locations of fields for foreign keys
346
347     for my $table ( $schema->get_tables ) {
348         my @fields     = $table->get_fields;
349         if ( $show_fk_only ) {
350             @fields = grep { $_->is_foreign_key } @fields;
351         }
352
353         my $field_str = '';
354         if ($show_fields) {
355
356           my @fmt_fields;
357           foreach my $field (@fields) {
358
359             my $field_type;
360             if ($show_datatypes) {
361
362               $field_type = $field->data_type;
363
364               # For the integer type, transform into different types based on
365               # requested size, if a size is given.
366               if ($field->size and $friendly_ints and (lc $field_type) eq 'integer') {
367                 # Automatically translate to int2, int4, int8
368                 # Type (Bits)     Max. Signed/Unsigned    Length
369                 # tinyint* (8)    128                     3
370                 #                 255                     3
371                 # smallint (16)   32767                   5
372                 #                 65535                   5
373                 # mediumint* (24) 8388607                 7
374                 #                 16777215                8
375                 # int (32)        2147483647              10
376                 #                 4294967295              11
377                 # bigint (64)     9223372036854775807     19
378                 #                 18446744073709551615    20
379                 #
380                 # * tinyint and mediumint are nonstandard extensions which are
381                 #   only available under MySQL (to my knowledge)
382                 my $size = $field->size;
383                 if ($size <= 3 and $friendly_ints_ex) {
384                   $field_type = 'tinyint',
385                 }
386                 elsif ($size <= 5) {
387                   $field_type = 'smallint';
388                 }
389                 elsif ($size <= 8 and $friendly_ints_ex) {
390                   $field_type = 'mediumint';
391                 }
392                 elsif ($size <= 11) {
393                   $field_type = 'integer';
394                 }
395                 else {
396                   $field_type = 'bigint';
397                 }
398               }
399
400               if (
401                 $show_sizes
402                   and
403                 $field->size
404                   and
405                 ($field_type =~ /^(var)?char2?$/ or $field_type eq 'numeric' or $field_type eq 'decimal')
406               ) {
407                 $field_type .= '(' . $field->size . ')';
408               }
409             }
410
411             my $constraints;
412             if ($show_constraints) {
413               my @constraints;
414               push(@constraints, 'PK') if $field->is_primary_key;
415               push(@constraints, 'FK') if $field->is_foreign_key;
416               push(@constraints, 'U')  if $field->is_unique;
417
418               $constraints = join (',', @constraints);
419             }
420
421             # construct the field line from all info gathered so far
422             push @fmt_fields, join (' ',
423               '-',
424               $field->name,
425               $field_type || (),
426               $constraints ? "[$constraints]" : (),
427             );
428
429           }
430
431           # join field lines with graphviz formatting
432           $field_str = join ('\l', @fmt_fields) . '\l';
433         }
434
435         my $index_str = '';
436         if ($show_indexes) {
437
438           my @fmt_indexes;
439           foreach my $index ($table->get_indices) {
440             next unless $index->is_valid;
441
442             push @fmt_indexes, join (' ',
443               '*',
444               $show_index_names ? $index->name . ':' : (),
445               join (', ', $index->fields),
446               ($index->type eq 'UNIQUE') ? '[U]' : (),
447             );
448           }
449
450           # join index lines with graphviz formatting (if any indexes at all)
451           $index_str = join ('\l', @fmt_indexes) . '\l' if @fmt_indexes;
452         }
453
454         my $table_name = $table->name;
455         my $name_str = $table_name . '\n';
456
457         # escape spaces
458         for ($name_str, $field_str, $index_str) {
459           $_ =~ s/ /\\ /g;
460         }
461
462
463         # only the 'record' type supports nice formatting
464         if ($node_shape eq 'record') {
465
466             # the necessity to supply shape => 'record' is a graphviz bug 
467             $gv->add_node( $table_name,
468               shape => 'record',
469               label => sprintf ('{%s}',
470                 join ('|',
471                   $name_str,
472                   $field_str || (),
473                   $index_str || (),
474                 ),
475               ),
476             );
477         }
478         else {
479             my $sep = sprintf ('%s\n',
480                 '-' x ( (length $table_name) + 2)
481             );
482
483             $gv->add_node( $table_name,
484                 label => join ($sep,
485                     $name_str,
486                     $field_str || (),
487                     $index_str || (),
488                 ),
489             );
490         }
491
492
493         debug("Processing table '$table_name'");
494
495         debug("Fields = ", join(', ', map { $_->name } @fields));
496
497         for my $f ( @fields ) {
498             my $name      = $f->name or next;
499             my $is_pk     = $f->is_primary_key;
500             my $is_unique = $f->is_unique;
501
502             #
503             # Decide if we should skip this field.
504             #
505             if ( $natural_join ) {
506                 next unless $is_pk || $f->is_foreign_key;
507             }
508
509             my $constraints = $f->{'constraints'};
510
511             if ( $natural_join && !$skip{ $name } ) {
512                 push @{ $nj_registry{ $name } }, $table_name;
513             }
514         }
515
516         unless ( $natural_join ) {
517             for my $c ( $table->get_constraints ) {
518                 next unless $c->type eq FOREIGN_KEY;
519                 my $fk_table = $c->reference_table or next;
520
521                 for my $field_name ( $c->fields ) {
522                     for my $fk_field ( $c->reference_fields ) {
523                         next unless defined $schema->get_table( $fk_table );
524                         push @fk_registry, [ $table_name, $fk_table ];
525                     }
526                 }
527             }
528         }
529     }
530
531     #
532     # Make the connections.
533     #
534     my @table_bunches;
535     if ( $natural_join ) {
536         for my $field_name ( keys %nj_registry ) {
537             my @table_names = @{ $nj_registry{ $field_name } || [] } or next;
538             next if scalar @table_names == 1;
539             push @table_bunches, [ @table_names ];
540         }
541     }
542     else {
543         @table_bunches = @fk_registry;
544     }
545
546     my %done;
547     for my $bunch ( @table_bunches ) {
548         my @tables = @$bunch;
549
550         for my $i ( 0 .. $#tables ) {
551             my $table1 = $tables[ $i ];
552             for my $j ( 0 .. $#tables ) {
553                 next if $i == $j;
554                 my $table2 = $tables[ $j ];
555                 next if $done{ $table1 }{ $table2 };
556                 $gv->add_edge( $table2, $table1 );
557                 $done{ $table1 }{ $table2 } = 1;
558                 $done{ $table2 }{ $table1 } = 1;
559             }
560         }
561     }
562
563     #
564     # Print the image.
565     #
566     if ( $out_file ) {
567       if (openhandle ($out_file)) {
568         print $out_file $gv->$output_method;
569       }
570       else {
571         open my $fh, ">$out_file" or die "Can't write '$out_file': $!\n";
572         binmode $fh;
573         print $fh $gv->$output_method;
574         close $fh;
575       }
576     }
577     else {
578       return $gv->$output_method;
579     }
580 }
581
582 1;
583
584 # -------------------------------------------------------------------
585
586 =pod
587
588 =head1 AUTHOR
589
590 Ken Y. Clark E<lt>kclark@cpan.orgE<gt>
591
592 =head2 CONTRIBUTORS
593
594 Jonathan Yu E<lt>frequency@cpan.orgE<gt>
595
596 =head1 SEE ALSO
597
598 SQL::Translator, GraphViz
599
600 =cut