Downgrade global version - highest version in 9002 on cpan is 1.58 - thus go with...
[dbsrgits/SQL-Translator.git] / lib / SQL / Translator / Producer / GraphViz.pm
CommitLineData
14d7eb56 1package SQL::Translator::Producer::GraphViz;
2
3# -------------------------------------------------------------------
6264cdc8 4# Copyright (C) 2002-2009 SQLFairy Authors
14d7eb56 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
d491c962 21=pod
22
23=head1 NAME
24
25SQL::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,
9d93edab 39 show_sizes => 1
d491c962 40 }
41 ) or die SQL::Translator->error;
42
43 $trans->translate or die $trans->error;
44
45=head1 DESCRIPTION
46
47Creates a graph of a schema using the amazing graphviz
48(see http://www.graphviz.org/) application (via
49the GraphViz module). It's nifty--you should try it!
50
51=head1 PRODUCER ARGS
52
53=over 4
54
55=item * out_file
56
d8324f08 57The name of the file where the resulting GraphViz output will be
58written. Alternatively an open filehandle can be supplied. If
59undefined (the default) - the result is returned as a string.
d491c962 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
4d5d28cb 181=item * show_index_names
d6d6b205 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.
d8324f08 185if show_index_names is false, then a list of indexed columns
d6d6b205 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;
997f14b2 209use SQL::Translator::Schema::Constants;
14d7eb56 210use SQL::Translator::Utils qw(debug);
d8324f08 211use Scalar::Util qw/openhandle/;
14d7eb56 212
213use vars qw[ $VERSION $DEBUG ];
4ab3763d 214$VERSION = '1.59';
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
14d7eb56 239sub produce {
997f14b2 240 my $t = shift;
241 my $schema = $t->schema;
14d7eb56 242 my $args = $t->producer_args;
243 local $DEBUG = $t->debug;
14d7eb56 244
59cbb03f 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;
027cebc7 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'} || {};
59cbb03f 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'};
b7478526 265 my $show_indexes = $args->{'show_indexes'};
4d5d28cb 266 my $show_index_names = defined $args->{'show_index_names'} ? $args->{'show_index_names'} : 1;
b7478526 267 my $friendly_ints = $args->{'friendly_ints'};
d44b5f54 268 my $friendly_ints_ex = $args->{'friendly_ints_extended'};
37ce746e 269 my $show_constraints = $args->{'show_constraints'};
59cbb03f 270 my $join_pk_only = $args->{'join_pk_only'};
9d93edab 271 my $skip_fields = $args->{'skip_fields'} || '';
272 my %skip = map { s/^\s+|\s+$//g; length $_ ? ($_, 1) : () }
59cbb03f 273 split ( /,/, $skip_fields );
274 $natural_join ||= $join_pk_only;
14d7eb56 275
997f14b2 276 $schema->make_natural_joins(
277 join_pk_only => $join_pk_only,
278 skip_fields => $args->{'skip_fields'},
279 ) if $natural_join;
280
14d7eb56 281 die "Invalid layout '$layout'" unless VALID_LAYOUT->{ $layout };
14d7eb56 282 die "Invalid node shape'$node_shape'"
283 unless VALID_NODE_SHAPE->{ $node_shape };
284
e36752ea 285 for ( $height, $width ) {
286 $_ = 0 unless $_ =~ /^\d+(.\d)?$/;
287 $_ = 0 if $_ < 0;
288 }
289
e36752ea 290 my %args = (
14d7eb56 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',
027cebc7 298 fillcolor => 'white',
299 },
e36752ea 300 );
301 $args{'width'} = $width if $width;
302 $args{'height'} = $height if $height;
027cebc7 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 }
e36752ea 332
d8324f08 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
14d7eb56 343
344 my %nj_registry; # for locations of fields for natural joins
345 my @fk_registry; # for locations of fields for foreign keys
346
997f14b2 347 for my $table ( $schema->get_tables ) {
997f14b2 348 my @fields = $table->get_fields;
349 if ( $show_fk_only ) {
350 @fields = grep { $_->is_foreign_key } @fields;
14d7eb56 351 }
14d7eb56 352
6264cdc8 353 my $field_str = '';
b7478526 354 if ($show_fields) {
6264cdc8 355
356 my @fmt_fields;
b7478526 357 foreach my $field (@fields) {
6264cdc8 358
359 my $field_type;
b7478526 360 if ($show_datatypes) {
6264cdc8 361
362 $field_type = $field->data_type;
b7478526 363
364 # For the integer type, transform into different types based on
365 # requested size, if a size is given.
6264cdc8 366 if ($field->size and $friendly_ints and (lc $field_type) eq 'integer') {
b7478526 367 # Automatically translate to int2, int4, int8
d44b5f54 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
d8324f08 376 # 4294967295 11
d44b5f54 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;
6264cdc8 383 if ($size <= 3 and $friendly_ints_ex) {
384 $field_type = 'tinyint',
b7478526 385 }
6264cdc8 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';
b7478526 394 }
d44b5f54 395 else {
6264cdc8 396 $field_type = 'bigint';
b7478526 397 }
398 }
399
6264cdc8 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 . ')';
b7478526 408 }
409 }
410
6264cdc8 411 my $constraints;
b7478526 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;
6264cdc8 417
418 $constraints = join (',', @constraints);
b7478526 419 }
6264cdc8 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
b7478526 429 }
6264cdc8 430
431 # join field lines with graphviz formatting
432 $field_str = join ('\l', @fmt_fields) . '\l';
b7478526 433 }
434
6264cdc8 435 my $index_str = '';
b7478526 436 if ($show_indexes) {
6264cdc8 437
438 my @fmt_indexes;
b7478526 439 foreach my $index ($table->get_indices) {
440 next unless $index->is_valid;
441
6264cdc8 442 push @fmt_indexes, join (' ',
443 '*',
4d5d28cb 444 $show_index_names ? $index->name . ':' : (),
6264cdc8 445 join (', ', $index->fields),
446 ($index->type eq 'UNIQUE') ? '[U]' : (),
447 );
d6d6b205 448 }
6264cdc8 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;
b7478526 460 }
6264cdc8 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
e36752ea 493 debug("Processing table '$table_name'");
494
997f14b2 495 debug("Fields = ", join(', ', map { $_->name } @fields));
14d7eb56 496
497 for my $f ( @fields ) {
997f14b2 498 my $name = $f->name or next;
499 my $is_pk = $f->is_primary_key;
500 my $is_unique = $f->is_unique;
14d7eb56 501
502 #
503 # Decide if we should skip this field.
504 #
505 if ( $natural_join ) {
997f14b2 506 next unless $is_pk || $f->is_foreign_key;
14d7eb56 507 }
508
509 my $constraints = $f->{'constraints'};
510
511 if ( $natural_join && !$skip{ $name } ) {
512 push @{ $nj_registry{ $name } }, $table_name;
513 }
997f14b2 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 );
14d7eb56 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 ) {
d51a158e 553 next if $i == $j;
14d7eb56 554 my $table2 = $tables[ $j ];
14d7eb56 555 next if $done{ $table1 }{ $table2 };
13811af0 556 $gv->add_edge( $table2, $table1 );
14d7eb56 557 $done{ $table1 }{ $table2 } = 1;
558 $done{ $table2 }{ $table1 } = 1;
559 }
560 }
561 }
562
563 #
564 # Print the image.
565 #
14d7eb56 566 if ( $out_file ) {
d8324f08 567 if (openhandle ($out_file)) {
568 print $out_file $gv->$output_method;
569 }
570 else {
14d7eb56 571 open my $fh, ">$out_file" or die "Can't write '$out_file': $!\n";
28c31cbf 572 binmode $fh;
14d7eb56 573 print $fh $gv->$output_method;
574 close $fh;
d8324f08 575 }
14d7eb56 576 }
577 else {
d8324f08 578 return $gv->$output_method;
14d7eb56 579 }
580}
581
5821;
583
977651a5 584# -------------------------------------------------------------------
585
14d7eb56 586=pod
587
14d7eb56 588=head1 AUTHOR
589
590Ken Y. Clark E<lt>kclark@cpan.orgE<gt>
591
d44b5f54 592=head2 CONTRIBUTORS
593
594Jonathan Yu E<lt>frequency@cpan.orgE<gt>
595
d491c962 596=head1 SEE ALSO
597
598SQL::Translator, GraphViz
599
14d7eb56 600=cut