- Fixes a bug where _apply_default_value is not found (due to it being part of SQL...
[dbsrgits/SQL-Translator.git] / lib / SQL / Translator / Producer / GraphViz.pm
CommitLineData
14d7eb56 1package SQL::Translator::Producer::GraphViz;
2
3# -------------------------------------------------------------------
f51d8e46 4# $Id: GraphViz.pm,v 1.14 2007-09-26 13:20:09 schiffbruechige Exp $
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
192as a tinyint, smallint, integer or bigint depending on the field's
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
d491c962 197=back
198
199=cut
200
14d7eb56 201use strict;
202use GraphViz;
203use Data::Dumper;
997f14b2 204use SQL::Translator::Schema::Constants;
14d7eb56 205use SQL::Translator::Utils qw(debug);
206
207use vars qw[ $VERSION $DEBUG ];
f51d8e46 208$VERSION = sprintf "%d.%02d", q$Revision: 1.14 $ =~ /(\d+)\.(\d+)/;
14d7eb56 209$DEBUG = 0 unless defined $DEBUG;
210
211use constant VALID_LAYOUT => {
212 dot => 1,
213 neato => 1,
214 twopi => 1,
215};
216
217use constant VALID_NODE_SHAPE => {
218 record => 1,
219 plaintext => 1,
220 ellipse => 1,
221 circle => 1,
222 egg => 1,
223 triangle => 1,
224 box => 1,
225 diamond => 1,
226 trapezium => 1,
227 parallelogram => 1,
228 house => 1,
229 hexagon => 1,
230 octagon => 1,
231};
232
233use constant VALID_OUTPUT => {
234 canon => 1,
235 text => 1,
236 ps => 1,
237 hpgl => 1,
238 pcl => 1,
239 mif => 1,
240 pic => 1,
241 gd => 1,
242 gd2 => 1,
243 gif => 1,
244 jpeg => 1,
245 png => 1,
246 wbmp => 1,
247 cmap => 1,
248 ismap => 1,
249 imap => 1,
250 vrml => 1,
251 vtx => 1,
252 mp => 1,
253 fig => 1,
254 svg => 1,
255 plain => 1,
256};
257
258sub produce {
997f14b2 259 my $t = shift;
260 my $schema = $t->schema;
14d7eb56 261 my $args = $t->producer_args;
262 local $DEBUG = $t->debug;
14d7eb56 263
59cbb03f 264 my $out_file = $args->{'out_file'} || '';
265 my $layout = $args->{'layout'} || 'dot';
266 my $node_shape = $args->{'node_shape'} || 'record';
267 my $output_type = $args->{'output_type'} || 'png';
268 my $width = defined $args->{'width'}
269 ? $args->{'width'} : 8.5;
270 my $height = defined $args->{'height'}
271 ? $args->{'height'} : 11;
027cebc7 272 my $fontsize = $args->{'fontsize'};
273 my $fontname = $args->{'fontname'};
274 my $edgeattrs = $args->{'edgeattrs'} || {};
275 my $graphattrs = $args->{'graphattrs'} || {};
276 my $nodeattrs = $args->{'nodeattrs'} || {};
59cbb03f 277 my $show_fields = defined $args->{'show_fields'}
278 ? $args->{'show_fields'} : 1;
279 my $add_color = $args->{'add_color'};
280 my $natural_join = $args->{'natural_join'};
281 my $show_fk_only = $args->{'show_fk_only'};
282 my $show_datatypes = $args->{'show_datatypes'};
283 my $show_sizes = $args->{'show_sizes'};
b7478526 284 my $show_indexes = $args->{'show_indexes'};
d6d6b205 285 my $show_index_name = $args->{'show_index_name'} || 1;
b7478526 286 my $friendly_ints = $args->{'friendly_ints'};
37ce746e 287 my $show_constraints = $args->{'show_constraints'};
59cbb03f 288 my $join_pk_only = $args->{'join_pk_only'};
9d93edab 289 my $skip_fields = $args->{'skip_fields'} || '';
290 my %skip = map { s/^\s+|\s+$//g; length $_ ? ($_, 1) : () }
59cbb03f 291 split ( /,/, $skip_fields );
292 $natural_join ||= $join_pk_only;
14d7eb56 293
997f14b2 294 $schema->make_natural_joins(
295 join_pk_only => $join_pk_only,
296 skip_fields => $args->{'skip_fields'},
297 ) if $natural_join;
298
14d7eb56 299 die "Invalid layout '$layout'" unless VALID_LAYOUT->{ $layout };
300 die "Invalid output type: '$output_type'"
301 unless VALID_OUTPUT->{ $output_type };
302 die "Invalid node shape'$node_shape'"
303 unless VALID_NODE_SHAPE->{ $node_shape };
304
e36752ea 305 for ( $height, $width ) {
306 $_ = 0 unless $_ =~ /^\d+(.\d)?$/;
307 $_ = 0 if $_ < 0;
308 }
309
14d7eb56 310 #
311 # Create GraphViz and see if we can produce the output type.
312 #
e36752ea 313 my %args = (
14d7eb56 314 directed => $natural_join ? 0 : 1,
315 layout => $layout,
316 no_overlap => 1,
317 bgcolor => $add_color ? 'lightgoldenrodyellow' : 'white',
318 node => {
319 shape => $node_shape,
320 style => 'filled',
027cebc7 321 fillcolor => 'white',
322 },
e36752ea 323 );
324 $args{'width'} = $width if $width;
325 $args{'height'} = $height if $height;
027cebc7 326 # set fontsize for edge and node labels if specified
327 if ($fontsize) {
328 $args{'node'}->{'fontsize'} = $fontsize;
329 $args{'edge'} = {} unless $args{'edge'};
330 $args{'edge'}->{'fontsize'} = $fontsize;
331 }
332 # set the font name globally for node, edge, and graph labels if
333 # specified (use node, edge, or graph attributes for individual
334 # font specification)
335 if ($fontname) {
336 $args{'node'}->{'fontname'} = $fontname;
337 $args{'edge'} = {} unless $args{'edge'};
338 $args{'edge'}->{'fontname'} = $fontname;
339 $args{'graph'} = {} unless $args{'graph'};
340 $args{'graph'}->{'fontname'} = $fontname;
341 }
342 # set additional node, edge, and graph attributes; these may
343 # possibly override ones set before
344 while (my ($key,$val) = each %$nodeattrs) {
345 $args{'node'}->{$key} = $val;
346 }
347 $args{'edge'} = {} if %$edgeattrs && !$args{'edge'};
348 while (my ($key,$val) = each %$edgeattrs) {
349 $args{'edge'}->{$key} = $val;
350 }
351 $args{'graph'} = {} if %$edgeattrs && !$args{'graph'};
352 while (my ($key,$val) = each %$graphattrs) {
353 $args{'graph'}->{$key} = $val;
354 }
e36752ea 355
356 my $gv = GraphViz->new( %args ) or die "Can't create GraphViz object\n";
14d7eb56 357
358 my %nj_registry; # for locations of fields for natural joins
359 my @fk_registry; # for locations of fields for foreign keys
360
997f14b2 361 for my $table ( $schema->get_tables ) {
362 my $table_name = $table->name;
363 my @fields = $table->get_fields;
364 if ( $show_fk_only ) {
365 @fields = grep { $_->is_foreign_key } @fields;
14d7eb56 366 }
14d7eb56 367
b7478526 368 my $label = '{' . $table_name;
369 if ($show_fields) {
370 my $field_str = '';
371 foreach my $field (@fields) {
372 $field_str .= '-\ ' . $field->name;
373 if ($show_datatypes) {
374 my $dt = lc($field->data_type);
375
376 # For the integer type, transform into different types based on
377 # requested size, if a size is given.
378 if ($friendly_ints && $dt eq 'integer' && $field->size) {
379 # Automatically translate to int2, int4, int8
380 # Type (Bits) Max. Signed Length
381 # tinyint (8) 128 3
382 # smallint (16) 32767 5
383 # int (32) 2147483647 10
384 # bigint (64) 9223372036854775807 19
385 if ($field->size > 10) {
386 $dt = 'bigint';
387 }
388 elsif ($field->size > 5) {
389 $dt = 'integer';
390 }
391 elsif ($field->size > 3) {
392 $dt = 'smallint';
393 }
394 else { # 8 bits
395 $dt = 'tinyint';
396 }
397 }
398
399 $field_str .= '\ ' . $dt;
400 if ($show_sizes && $field->size && ($dt =~ /^(var)?char2?$/ || $dt eq 'numeric' || $dt eq 'decimal')) {
401 $field_str .= '(' . $field->size . ')';
402 }
403 }
404
405 if ($show_constraints) {
406 my @constraints;
407 push(@constraints, 'PK') if $field->is_primary_key;
408 push(@constraints, 'FK') if $field->is_foreign_key;
409 push(@constraints, 'U') if $field->is_unique;
410 if (scalar(@constraints)) {
411 $field_str .= '\ [' . join(',\ ', @constraints) . ']';
412 }
413 }
414 $field_str .= '\l';
415 }
416 $label .= '|' . $field_str;
417 }
418
419 if ($show_indexes) {
420 my $index_str = '';
421 foreach my $index ($table->get_indices) {
422 next unless $index->is_valid;
423
d6d6b205 424 $index_str .= '*\ ';
425 if ($show_index_name) {
426 $index_str .= $index->name . ': ';
427 }
b7478526 428 $index_str .= join(', ', $index->fields);
429 if ($index->type eq 'UNIQUE') {
430 $index_str .= '\ [U]';
431 }
432 $index_str .= '\l';
433 }
d6d6b205 434 # Only add the last box if index_str is non-null
435 if (length $index_str) {
436 $label .= '|' . $index_str;
437 }
b7478526 438 }
439 $label .= '}';
f51d8e46 440# $gv->add_node( $table_name, label => $label );
b7478526 441# $gv->add_node( $table_name, label => $label, ($node_shape eq 'record' ? ( shape => $node_shape ) : ()) );
442 $gv->add_node( $table_name, label => $label, shape => $node_shape );
e36752ea 443 debug("Processing table '$table_name'");
444
997f14b2 445 debug("Fields = ", join(', ', map { $_->name } @fields));
14d7eb56 446
447 for my $f ( @fields ) {
997f14b2 448 my $name = $f->name or next;
449 my $is_pk = $f->is_primary_key;
450 my $is_unique = $f->is_unique;
14d7eb56 451
452 #
453 # Decide if we should skip this field.
454 #
455 if ( $natural_join ) {
997f14b2 456 next unless $is_pk || $f->is_foreign_key;
14d7eb56 457 }
458
459 my $constraints = $f->{'constraints'};
460
461 if ( $natural_join && !$skip{ $name } ) {
462 push @{ $nj_registry{ $name } }, $table_name;
463 }
997f14b2 464 }
465
466 unless ( $natural_join ) {
467 for my $c ( $table->get_constraints ) {
468 next unless $c->type eq FOREIGN_KEY;
469 my $fk_table = $c->reference_table or next;
470
471 for my $field_name ( $c->fields ) {
472 for my $fk_field ( $c->reference_fields ) {
473 next unless defined $schema->get_table( $fk_table );
14d7eb56 474 push @fk_registry, [ $table_name, $fk_table ];
475 }
476 }
477 }
478 }
479 }
480
481 #
482 # Make the connections.
483 #
484 my @table_bunches;
485 if ( $natural_join ) {
486 for my $field_name ( keys %nj_registry ) {
487 my @table_names = @{ $nj_registry{ $field_name } || [] } or next;
488 next if scalar @table_names == 1;
489 push @table_bunches, [ @table_names ];
490 }
491 }
492 else {
493 @table_bunches = @fk_registry;
494 }
495
496 my %done;
497 for my $bunch ( @table_bunches ) {
498 my @tables = @$bunch;
499
500 for my $i ( 0 .. $#tables ) {
501 my $table1 = $tables[ $i ];
502 for my $j ( 0 .. $#tables ) {
d51a158e 503 next if $i == $j;
14d7eb56 504 my $table2 = $tables[ $j ];
14d7eb56 505 next if $done{ $table1 }{ $table2 };
13811af0 506 $gv->add_edge( $table2, $table1 );
14d7eb56 507 $done{ $table1 }{ $table2 } = 1;
508 $done{ $table2 }{ $table1 } = 1;
509 }
510 }
511 }
512
513 #
514 # Print the image.
515 #
516 my $output_method = "as_$output_type";
517 if ( $out_file ) {
518 open my $fh, ">$out_file" or die "Can't write '$out_file': $!\n";
28c31cbf 519 binmode $fh;
14d7eb56 520 print $fh $gv->$output_method;
521 close $fh;
522 }
523 else {
524 return $gv->$output_method;
525 }
526}
527
5281;
529
977651a5 530# -------------------------------------------------------------------
531
14d7eb56 532=pod
533
14d7eb56 534=head1 AUTHOR
535
536Ken Y. Clark E<lt>kclark@cpan.orgE<gt>
537
d491c962 538=head1 SEE ALSO
539
540SQL::Translator, GraphViz
541
14d7eb56 542=cut