Revert my previous changes (rev 1722 reverted back to rev 1721)
[dbsrgits/SQL-Translator.git] / lib / SQL / Translator / Producer / GraphViz.pm
CommitLineData
14d7eb56 1package SQL::Translator::Producer::GraphViz;
2
44659089 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
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
f2b20a77 31 my $trans = SQL::Translator->new(
d491c962 32 from => 'MySQL', # or your db of choice
f2b20a77 33 to => 'GraphViz',
d491c962 34 producer_args => {
f2b20a77 35 out_file => 'schema.png',
36 bgcolor => 'lightgoldenrodyellow',
d491c962 37 show_constraints => 1,
f2b20a77 38 show_datatypes => 1,
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
da810c37 49the L<GraphViz> module). It's nifty--you should try it!
d491c962 50
51=head1 PRODUCER ARGS
52
da810c37 53All L<GraphViz> constructor attributes are accepted and passed
54through to L<GraphViz/new>. The following defaults are assumed
55for some attributes:
d491c962 56
da810c37 57 layout => 'dot',
58 overlap => 'false',
d491c962 59
da810c37 60 node => {
61 shape => 'record',
62 style => 'filled',
63 fillcolor => 'white',
64 },
d491c962 65
da810c37 66 # in inches
67 width => 8.5,
68 height => 11,
d491c962 69
da810c37 70See the documentation of L<GraphViz/new> for more info on these
71and other attributes.
d491c962 72
da810c37 73In addition this producer accepts the following arguments:
d491c962 74
da810c37 75=over 4
d491c962 76
da810c37 77=item * skip_tables
d491c962 78
da810c37 79An arrayref or a comma-separated list of table names that should be
80skipped. Note that a skipped table node may still appear if another
81table has foreign key constraints pointing to the skipped table. If
82this happens no table field/index information will be included.
d491c962 83
da810c37 84=item * skip_tables_like
d491c962 85
da810c37 86An arrayref or a comma-separated list of regular expressions matching
87table names that should be skipped.
d491c962 88
da810c37 89=item * cluster
027cebc7 90
f2b20a77 91Clustering of tables allows you to group and box tables according to
92function or domain or whatever criteria you choose. The syntax for
93clustering tables is:
94
95 cluster => 'cluster1=table1,table2;cluster2=table3,table4'
96
97Or pass it as an arrayref like so:
98
99 cluster => [ 'cluster1=table1,table2', 'cluster2=table3,table4' ]
100
101Or like so:
102
103 cluster => [
104 { name => 'cluster1', tables => [ 'table1', 'table2' ] },
105 { name => 'cluster2', tables => [ 'table3', 'table4' ] },
106 ]
027cebc7 107
da810c37 108=item * out_file
027cebc7 109
da810c37 110The name of the file where the resulting GraphViz output will be
111written. Alternatively an open filehandle can be supplied. If
112undefined (the default) - the result is returned as a string.
027cebc7 113
da810c37 114=item * output_type (DEFAULT: 'png')
027cebc7 115
da810c37 116This determines which
117L<output method|GraphViz/as_canon,_as_text,_as_gif_etc._methods>
118will be invoked to generate the graph: C<png> translates to
119C<as_png>, C<ps> to C<as_ps> and so on.
027cebc7 120
da810c37 121=item * fontname
027cebc7 122
da810c37 123This sets the global font name (or full path to font file) for
124node, edge, and graph labels
027cebc7 125
da810c37 126=item * fontsize
027cebc7 127
da810c37 128This sets the global font size for node and edge labels (note that
129arbitrarily large sizes may be ignored due to page size or graph size
130constraints)
027cebc7 131
d491c962 132=item * show_fields (DEFAULT: true)
133
da810c37 134If set to a true value, the names of the colums in a table will
d491c962 135be displayed in each table's node
136
137=item * show_fk_only
138
da810c37 139If set to a true value, only columns which are foreign keys
d491c962 140will be displayed in each table's node
141
142=item * show_datatypes
143
da810c37 144If set to a true value, the datatype of each column will be
d491c962 145displayed next to each column's name; this option will have no
da810c37 146effect if the value of C<show_fields> is set to false
147
148=item * friendly_ints
149
150If set to a true value, each integer type field will be displayed
151as a tinyint, smallint, integer or bigint depending on the field's
152associated size parameter. This only applies for the C<integer>
153type (and not the C<int> type, which is always assumed to be a
15432-bit integer); this option will have no effect if the value of
155C<show_fields> is set to false
156
157=item * friendly_ints_extended
158
159If set to a true value, the friendly ints displayed will take into
160account the non-standard types, 'tinyint' and 'mediumint' (which,
161as far as I am aware, is only implemented in MySQL)
d491c962 162
9d93edab 163=item * show_sizes
d491c962 164
da810c37 165If set to a true value, the size (in bytes) of each CHAR and
d491c962 166VARCHAR column will be displayed in parentheses next to the
167column's name; this option will have no effect if the value of
da810c37 168C<show_fields> is set to false
d491c962 169
170=item * show_constraints
171
da810c37 172If set to a true value, a field's constraints (i.e., its
d491c962 173primary-key-ness, its foreign-key-ness and/or its uniqueness)
174will appear as a comma-separated list in brackets next to the
175field's name; this option will have no effect if the value of
da810c37 176C<show_fields> is set to false
d491c962 177
da810c37 178=item * show_indexes
179
180If set to a true value, each record will also show the indexes
181set on each table. It describes the index types along with
182which columns are included in the index.
d491c962 183
da810c37 184=item * show_index_names (DEFAULT: true)
185
186If C<show_indexes> is set to a true value, then the value of this
187parameter determines whether or not to print names of indexes.
188if C<show_index_names> is false, then a list of indexed columns
189will appear below the field list. Otherwise, it will be a list
190prefixed with the name of each index.
d491c962 191
192=item * natural_join
193
da810c37 194If set to a true value, L<SQL::Translator::Schema/make_natural_joins>
195will be called before generating the graph.
d491c962 196
197=item * join_pk_only
198
da810c37 199The value of this option will be passed as the value of the
200like-named argument to L<SQL::Translator::Schema/make_natural_joins>;
201implies C<< natural_join => 1 >>
d491c962 202
203=item * skip_fields
204
da810c37 205The value of this option will be passed as the value of the
206like-named argument to L<SQL::Translator::Schema/make_natural_joins>;
207implies C<< natural_join => 1 >>
d491c962 208
da810c37 209=back
44435b87 210
da810c37 211=head2 DEPRECATED ARGS
818e0d98 212
da810c37 213=over 4
818e0d98 214
da810c37 215=item * node_shape
44435b87 216
da810c37 217Deprecated, use node => { shape => ... } instead
b7478526 218
da810c37 219=item * add_color
b7478526 220
da810c37 221Deprecated, use bgcolor => 'lightgoldenrodyellow' instead
d6d6b205 222
da810c37 223If set to a true value, the graphic will have a background
224color of 'lightgoldenrodyellow'; otherwise the default
225white background will be used
d6d6b205 226
da810c37 227=item * nodeattrs
b7478526 228
da810c37 229Deprecated, use node => { ... } instead
b7478526 230
da810c37 231=item * edgeattrs
d44b5f54 232
da810c37 233Deprecated, use edge => { ... } instead
234
235=item * graphattrs
236
237Deprecated, use graph => { ... } instead
d44b5f54 238
d491c962 239=back
240
241=cut
242
da810c37 243use warnings;
14d7eb56 244use strict;
245use GraphViz;
997f14b2 246use SQL::Translator::Schema::Constants;
14d7eb56 247use SQL::Translator::Utils qw(debug);
d8324f08 248use Scalar::Util qw/openhandle/;
14d7eb56 249
250use vars qw[ $VERSION $DEBUG ];
11ad2df9 251$VERSION = '1.59';
14d7eb56 252$DEBUG = 0 unless defined $DEBUG;
253
14d7eb56 254sub produce {
997f14b2 255 my $t = shift;
256 my $schema = $t->schema;
14d7eb56 257 my $args = $t->producer_args;
258 local $DEBUG = $t->debug;
14d7eb56 259
da810c37 260 # translate legacy {node|edge|graph}attrs to just {node|edge|graph}
261 for my $argtype (qw/node edge graph/) {
262 my $old_arg = $argtype . 'attrs';
349ec4b8 263
264 my %arglist = (map
265 { %{ $_ || {} } }
266 ( delete $args->{$old_arg}, delete $args->{$argtype} )
267 );
268
269 $args->{$argtype} = \%arglist if keys %arglist;
da810c37 270 }
997f14b2 271
da810c37 272 # explode font settings
273 for (qw/fontsize fontname/) {
274 if (defined $args->{$_}) {
275 $args->{node}{$_} ||= $args->{$_};
276 $args->{edge}{$_} ||= $args->{$_};
277 $args->{graph}{$_} ||= $args->{$_};
278 }
279 }
14d7eb56 280
da810c37 281 # legacy add_color setting, trumped by bgcolor if set
282 $args->{bgcolor} ||= 'lightgoldenrodyellow' if $args->{add_color};
283
284 # legacy node_shape setting, defaults to 'record', trumped by {node}{shape}
285 $args->{node}{shape} ||= ( $args->{node_shape} || 'record' );
286
287 # maintain defaults
288 $args->{layout} ||= 'dot';
289 $args->{output_type} ||= 'png';
290 $args->{overlap} ||= 'false';
291 $args->{node}{style} ||= 'filled';
292 $args->{node}{fillcolor} ||= 'white';
293
294 $args->{show_fields} = 1 if not exists $args->{show_fields};
295 $args->{show_index_names} = 1 if not exists $args->{show_index_names};
296 $args->{width} = 8.5 if not defined $args->{width};
297 $args->{height} = 11 if not defined $args->{height};
298 for ( $args->{height}, $args->{width} ) {
299 $_ = 0 unless $_ =~ /^\d+(?:.\d+)?$/;
e36752ea 300 $_ = 0 if $_ < 0;
301 }
302
da810c37 303 # so split won't warn
304 $args->{$_} ||= '' for qw/skip_fields skip_tables skip_tables_like cluster/;
771479d7 305
da810c37 306 my %skip_fields = map { s/^\s+|\s+$//g; length $_ ? ($_, 1) : () }
307 split ( /,/, $args->{skip_fields} );
771479d7 308
da810c37 309 my %skip_tables = map { $_, 1 } (
310 ref $args->{skip_tables} eq 'ARRAY'
311 ? @{$args->{skip_tables}}
312 : split (/\s*,\s*/, $args->{skip_tables})
313 );
771479d7 314
da810c37 315 my @skip_tables_like = map { qr/$_/ } (
316 ref $args->{skip_tables_like} eq 'ARRAY'
317 ? @{$args->{skip_tables_like}}
318 : split (/\s*,\s*/, $args->{skip_tables_like})
319 );
771479d7 320
da810c37 321 # join_pk_only/skip_fields implies natural_join
322 $args->{natural_join} = 1
323 if ($args->{join_pk_only} or scalar keys %skip_fields);
771479d7 324
da810c37 325 # usually we do not want direction when using natural join
326 $args->{directed} = ($args->{natural_join} ? 0 : 1)
327 if not exists $args->{directed};
771479d7 328
da810c37 329 $schema->make_natural_joins(
330 join_pk_only => $args->{join_pk_only},
331 skip_fields => $args->{skip_fields},
332 ) if $args->{natural_join};
e36752ea 333
771479d7 334 my %cluster;
335 if ( defined $args->{'cluster'} ) {
336 my @clusters;
337 if ( ref $args->{'cluster'} eq 'ARRAY' ) {
338 @clusters = @{ $args->{'cluster'} };
339 }
340 else {
341 @clusters = split /\s*;\s*/, $args->{'cluster'};
342 }
343
344 for my $c ( @clusters ) {
345 my ( $cluster_name, @cluster_tables );
346 if ( ref $c eq 'HASH' ) {
347 $cluster_name = $c->{'name'} || $c->{'cluster_name'};
348 @cluster_tables = @{ $c->{'tables'} || [] };
349 }
350 else {
351 my ( $name, $tables ) = split /\s*=\s*/, $c;
352 $cluster_name = $name;
353 @cluster_tables = split /\s*,\s*/, $tables;
354 }
355
356 for my $table ( @cluster_tables ) {
357 $cluster{ $table } = $cluster_name;
358 }
359 }
360 }
361
d8324f08 362 #
363 # Create a blank GraphViz object and see if we can produce the output type.
364 #
da810c37 365 my $gv = GraphViz->new( %$args )
366 or die sprintf ("Can't create GraphViz object: %s\n",
367 $@ || 'reason unknown'
368 );
d8324f08 369
da810c37 370 my $output_method = "as_$args->{output_type}";
371
372 # the generators are AUTOLOADed so can't use ->can ($output_method)
d8324f08 373 eval { $gv->$output_method };
da810c37 374 die "Invalid output type: '$args->{output_type}'" if $@;
d8324f08 375
da810c37 376 #
377 # Process tables definitions, create nodes
378 #
14d7eb56 379 my %nj_registry; # for locations of fields for natural joins
380 my @fk_registry; # for locations of fields for foreign keys
381
44435b87 382 TABLE:
997f14b2 383 for my $table ( $schema->get_tables ) {
44435b87 384
da810c37 385 my $table_name = $table->name;
386 if ( @skip_tables_like or keys %skip_tables ) {
387 next TABLE if $skip_tables{ $table_name };
388 for my $regex ( @skip_tables_like ) {
389 next TABLE if $table_name =~ $regex;
390 }
44435b87 391 }
392
da810c37 393 my @fields = $table->get_fields;
394 if ( $args->{show_fk_only} ) {
997f14b2 395 @fields = grep { $_->is_foreign_key } @fields;
14d7eb56 396 }
14d7eb56 397
6264cdc8 398 my $field_str = '';
da810c37 399 if ($args->{show_fields}) {
818e0d98 400 my @fmt_fields;
da810c37 401 for my $field (@fields) {
402
403 my $field_info;
404 if ($args->{show_datatypes}) {
405
406 my $field_type = $field->data_type;
407 my $size = $field->size;
408
409 if ( $args->{friendly_ints} && $size && (lc ($field_type) eq 'integer') ) {
410 # Automatically translate to int2, int4, int8
411 # Type (Bits) Max. Signed/Unsigned Length
412 # tinyint* (8) 128 3
413 # 255 3
414 # smallint (16) 32767 5
415 # 65535 5
416 # mediumint* (24) 8388607 7
417 # 16777215 8
418 # int (32) 2147483647 10
419 # 4294967295 11
420 # bigint (64) 9223372036854775807 19
421 # 18446744073709551615 20
422 #
423 # * tinyint and mediumint are nonstandard extensions which are
424 # only available under MySQL (to my knowledge)
425 if ($size <= 3 and $args->{friendly_ints_extended}) {
426 $field_type = 'tinyint';
427 }
428 elsif ($size <= 5) {
429 $field_type = 'smallint';
430 }
431 elsif ($size <= 8 and $args->{friendly_ints_extended}) {
432 $field_type = 'mediumint';
433 }
434 elsif ($size <= 11) {
435 $field_type = 'integer';
436 }
437 else {
438 $field_type = 'bigint';
439 }
b7478526 440 }
b7478526 441
da810c37 442 $field_info = $field_type;
443 if ($args->{show_sizes} && $size && ($field_type =~ /^ (?: NUMERIC | DECIMAL | (VAR)?CHAR2? ) $/ix ) ) {
444 $field_info .= '(' . $size . ')';
818e0d98 445 }
da810c37 446 }
447
448 my $constraints;
449 if ($args->{show_constraints}) {
450 my @constraints;
451 push(@constraints, 'PK') if $field->is_primary_key;
452 push(@constraints, 'FK') if $field->is_foreign_key;
453 push(@constraints, 'U') if $field->is_unique;
454 push(@constraints, 'N') if $field->is_nullable;
455
456 $constraints = join (',', @constraints);
457 }
458
459 # construct the field line from all info gathered so far
460 push @fmt_fields, join (' ',
461 '-',
462 $field->name,
463 $field_info || (),
464 $constraints ? "[$constraints]" : (),
465 );
818e0d98 466 }
6264cdc8 467
818e0d98 468 # join field lines with graphviz formatting
da810c37 469 $field_str = join ('\l', @fmt_fields) . '\l';
470
b7478526 471 }
472
6264cdc8 473 my $index_str = '';
da810c37 474 if ($args->{show_indexes}) {
475
476 my @fmt_indexes;
477 for my $index ($table->get_indices) {
478 next unless $index->is_valid;
479
480 push @fmt_indexes, join (' ',
481 '*',
482 $args->{show_index_names}
483 ? $index->name . ':'
484 : ()
485 ,
486 join (', ', $index->fields),
487 ($index->type eq 'UNIQUE') ? '[U]' : (),
488 );
489 }
6264cdc8 490
da810c37 491 # join index lines with graphviz formatting (if any indexes at all)
492 $index_str = join ('\l', @fmt_indexes) . '\l' if @fmt_indexes;
6264cdc8 493 }
494
6264cdc8 495 my $name_str = $table_name . '\n';
496
497 # escape spaces
498 for ($name_str, $field_str, $index_str) {
da810c37 499 $_ =~ s/ /\\ /g;
b7478526 500 }
6264cdc8 501
da810c37 502 my $node_args;
503
6264cdc8 504 # only the 'record' type supports nice formatting
da810c37 505 if ($args->{node}{shape} eq 'record') {
506
818e0d98 507 # the necessity to supply shape => 'record' is a graphviz bug
da810c37 508 $node_args = {
509 shape => 'record',
510 label => sprintf ('{%s}',
511 join ('|',
512 $name_str,
513 $field_str || (),
514 $index_str || (),
6264cdc8 515 ),
da810c37 516 ),
517 };
6264cdc8 518 }
519 else {
520 my $sep = sprintf ('%s\n',
da810c37 521 '-' x ( (length $table_name) + 2)
6264cdc8 522 );
523
da810c37 524 $node_args = {
525 label => join ($sep,
526 $name_str,
527 $field_str || (),
528 $index_str || (),
529 ),
530 };
531 }
532
533 if (my $cluster_name = $cluster{$table_name} ) {
534 $node_args->{cluster} = $cluster_name;
6264cdc8 535 }
536
30e85fe9 537 $gv->add_node(qq["$table_name"], %$node_args);
da810c37 538
e36752ea 539 debug("Processing table '$table_name'");
540
997f14b2 541 debug("Fields = ", join(', ', map { $_->name } @fields));
14d7eb56 542
543 for my $f ( @fields ) {
997f14b2 544 my $name = $f->name or next;
545 my $is_pk = $f->is_primary_key;
546 my $is_unique = $f->is_unique;
14d7eb56 547
548 #
549 # Decide if we should skip this field.
550 #
da810c37 551 if ( $args->{natural_join} ) {
997f14b2 552 next unless $is_pk || $f->is_foreign_key;
14d7eb56 553 }
554
555 my $constraints = $f->{'constraints'};
556
da810c37 557 if ( $args->{natural_join} && !$skip_fields{ $name } ) {
14d7eb56 558 push @{ $nj_registry{ $name } }, $table_name;
559 }
997f14b2 560 }
561
da810c37 562 unless ( $args->{natural_join} ) {
997f14b2 563 for my $c ( $table->get_constraints ) {
564 next unless $c->type eq FOREIGN_KEY;
565 my $fk_table = $c->reference_table or next;
566
567 for my $field_name ( $c->fields ) {
568 for my $fk_field ( $c->reference_fields ) {
569 next unless defined $schema->get_table( $fk_table );
da810c37 570
571 # a condition is optional if at least one fk is nullable
572 push @fk_registry, [
573 $table_name,
574 $fk_table,
575 scalar (grep { $_->is_nullable } ($c->fields))
576 ];
14d7eb56 577 }
578 }
579 }
580 }
581 }
582
583 #
da810c37 584 # Process relationships, create edges
14d7eb56 585 #
da810c37 586 my (@table_bunches, %optional_constraints);
587 if ( $args->{natural_join} ) {
14d7eb56 588 for my $field_name ( keys %nj_registry ) {
589 my @table_names = @{ $nj_registry{ $field_name } || [] } or next;
590 next if scalar @table_names == 1;
591 push @table_bunches, [ @table_names ];
592 }
593 }
594 else {
da810c37 595 for my $i (0 .. $#fk_registry) {
596 my $fk = $fk_registry[$i];
597 push @table_bunches, [$fk->[0], $fk->[1]];
598 $optional_constraints{$i} = $fk->[2];
599 }
14d7eb56 600 }
601
602 my %done;
da810c37 603 for my $bi (0 .. $#table_bunches) {
604 my @tables = @{$table_bunches[$bi]};
14d7eb56 605
606 for my $i ( 0 .. $#tables ) {
607 my $table1 = $tables[ $i ];
771479d7 608 for my $j ( 1 .. $#tables ) {
d51a158e 609 next if $i == $j;
14d7eb56 610 my $table2 = $tables[ $j ];
14d7eb56 611 next if $done{ $table1 }{ $table2 };
f2b20a77 612 debug("Adding edge '$table2' -> '$table1'");
da810c37 613 $gv->add_edge(
f2b20a77 614 qq["$table2"],
615 qq["$table1"],
da810c37 616 arrowhead => $optional_constraints{$bi} ? 'empty' : 'normal',
617 );
14d7eb56 618 $done{ $table1 }{ $table2 } = 1;
14d7eb56 619 }
620 }
621 }
622
623 #
da810c37 624 # Print the image
14d7eb56 625 #
da810c37 626 if ( my $out = $args->{out_file} ) {
627 if (openhandle ($out)) {
628 print $out $gv->$output_method;
818e0d98 629 }
630 else {
da810c37 631 open my $fh, '>', $out or die "Can't write '$out': $!\n";
818e0d98 632 binmode $fh;
633 print $fh $gv->$output_method;
634 close $fh;
635 }
14d7eb56 636 }
637 else {
818e0d98 638 return $gv->$output_method;
14d7eb56 639 }
640}
641
6421;
643
977651a5 644# -------------------------------------------------------------------
645
14d7eb56 646=pod
647
14d7eb56 648=head1 AUTHOR
649
11ad2df9 650Ken Youens-Clark E<lt>kclark@cpan.orgE<gt>
651
652Jonathan Yu E<lt>frequency@cpan.orgE<gt>
d44b5f54 653
d491c962 654=head1 SEE ALSO
655
11ad2df9 656SQL::Translator, GraphViz
d491c962 657
14d7eb56 658=cut