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