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