1 package SQL::Translator::Producer::GraphViz;
7 SQL::Translator::Producer::GraphViz - GraphViz producer for SQL::Translator
13 my $trans = SQL::Translator->new(
14 from => 'MySQL', # or your db of choice
17 out_file => 'schema.png',
18 bgcolor => 'lightgoldenrodyellow',
19 show_constraints => 1,
23 ) or die SQL::Translator->error;
25 $trans->translate or die $trans->error;
29 Creates a graph of a schema using the amazing graphviz
30 (see http://www.graphviz.org/) application (via
31 the L<GraphViz> module). It's nifty--you should try it!
35 All L<GraphViz> constructor attributes are accepted and passed
36 through to L<GraphViz/new>. The following defaults are assumed
52 See the documentation of L<GraphViz/new> for more info on these
55 In addition this producer accepts the following arguments:
61 An arrayref or a comma-separated list of table names that should be
62 skipped. Note that a skipped table node may still appear if another
63 table has foreign key constraints pointing to the skipped table. If
64 this happens no table field/index information will be included.
66 =item * skip_tables_like
68 An arrayref or a comma-separated list of regular expressions matching
69 table names that should be skipped.
73 Clustering of tables allows you to group and box tables according to
74 function or domain or whatever criteria you choose. The syntax for
77 cluster => 'cluster1=table1,table2;cluster2=table3,table4'
79 Or pass it as an arrayref like so:
81 cluster => [ 'cluster1=table1,table2', 'cluster2=table3,table4' ]
86 { name => 'cluster1', tables => [ 'table1', 'table2' ] },
87 { name => 'cluster2', tables => [ 'table3', 'table4' ] },
92 The name of the file where the resulting GraphViz output will be
93 written. Alternatively an open filehandle can be supplied. If
94 undefined (the default) - the result is returned as a string.
96 =item * output_type (DEFAULT: 'png')
99 L<output method|GraphViz/as_canon,_as_text,_as_gif_etc._methods>
100 will be invoked to generate the graph: C<png> translates to
101 C<as_png>, C<ps> to C<as_ps> and so on.
105 This sets the global font name (or full path to font file) for
106 node, edge, and graph labels
110 This sets the global font size for node and edge labels (note that
111 arbitrarily large sizes may be ignored due to page size or graph size
114 =item * show_fields (DEFAULT: true)
116 If set to a true value, the names of the colums in a table will
117 be displayed in each table's node
121 If set to a true value, only columns which are foreign keys
122 will be displayed in each table's node
124 =item * show_datatypes
126 If set to a true value, the datatype of each column will be
127 displayed next to each column's name; this option will have no
128 effect if the value of C<show_fields> is set to false
130 =item * friendly_ints
132 If set to a true value, each integer type field will be displayed
133 as a tinyint, smallint, integer or bigint depending on the field's
134 associated size parameter. This only applies for the C<integer>
135 type (and not the C<int> type, which is always assumed to be a
136 32-bit integer); this option will have no effect if the value of
137 C<show_fields> is set to false
139 =item * friendly_ints_extended
141 If set to a true value, the friendly ints displayed will take into
142 account the non-standard types, 'tinyint' and 'mediumint' (which,
143 as far as I am aware, is only implemented in MySQL)
147 If set to a true value, the size (in bytes) of each CHAR and
148 VARCHAR column will be displayed in parentheses next to the
149 column's name; this option will have no effect if the value of
150 C<show_fields> is set to false
152 =item * show_constraints
154 If set to a true value, a field's constraints (i.e., its
155 primary-key-ness, its foreign-key-ness and/or its uniqueness)
156 will appear as a comma-separated list in brackets next to the
157 field's name; this option will have no effect if the value of
158 C<show_fields> is set to false
162 If set to a true value, each record will also show the indexes
163 set on each table. It describes the index types along with
164 which columns are included in the index.
166 =item * show_index_names (DEFAULT: true)
168 If C<show_indexes> is set to a true value, then the value of this
169 parameter determines whether or not to print names of indexes.
170 if C<show_index_names> is false, then a list of indexed columns
171 will appear below the field list. Otherwise, it will be a list
172 prefixed with the name of each index.
176 If set to a true value, L<SQL::Translator::Schema/make_natural_joins>
177 will be called before generating the graph.
181 The value of this option will be passed as the value of the
182 like-named argument to L<SQL::Translator::Schema/make_natural_joins>;
183 implies C<< natural_join => 1 >>
187 The value of this option will be passed as the value of the
188 like-named argument to L<SQL::Translator::Schema/make_natural_joins>;
189 implies C<< natural_join => 1 >>
193 =head2 DEPRECATED ARGS
199 Deprecated, use node => { shape => ... } instead
203 Deprecated, use bgcolor => 'lightgoldenrodyellow' instead
205 If set to a true value, the graphic will have a background
206 color of 'lightgoldenrodyellow'; otherwise the default
207 white background will be used
211 Deprecated, use node => { ... } instead
215 Deprecated, use edge => { ... } instead
219 Deprecated, use graph => { ... } instead
228 use SQL::Translator::Schema::Constants;
229 use SQL::Translator::Utils qw(debug);
230 use Scalar::Util qw/openhandle/;
233 our $VERSION = '1.59';
234 $DEBUG = 0 unless defined $DEBUG;
238 my $schema = $t->schema;
239 my $args = $t->producer_args;
240 local $DEBUG = $t->debug;
242 # translate legacy {node|edge|graph}attrs to just {node|edge|graph}
243 for my $argtype (qw/node edge graph/) {
244 my $old_arg = $argtype . 'attrs';
248 ( delete $args->{$old_arg}, delete $args->{$argtype} )
251 $args->{$argtype} = \%arglist if keys %arglist;
254 # explode font settings
255 for (qw/fontsize fontname/) {
256 if (defined $args->{$_}) {
257 $args->{node}{$_} ||= $args->{$_};
258 $args->{edge}{$_} ||= $args->{$_};
259 $args->{graph}{$_} ||= $args->{$_};
263 # legacy add_color setting, trumped by bgcolor if set
264 $args->{bgcolor} ||= 'lightgoldenrodyellow' if $args->{add_color};
266 # legacy node_shape setting, defaults to 'record', trumped by {node}{shape}
267 $args->{node}{shape} ||= ( $args->{node_shape} || 'record' );
270 $args->{layout} ||= 'dot';
271 $args->{output_type} ||= 'png';
272 $args->{overlap} ||= 'false';
273 $args->{node}{style} ||= 'filled';
274 $args->{node}{fillcolor} ||= 'white';
276 $args->{show_fields} = 1 if not exists $args->{show_fields};
277 $args->{show_index_names} = 1 if not exists $args->{show_index_names};
278 $args->{width} = 8.5 if not defined $args->{width};
279 $args->{height} = 11 if not defined $args->{height};
280 for ( $args->{height}, $args->{width} ) {
281 $_ = 0 unless $_ =~ /^\d+(?:.\d+)?$/;
285 # so split won't warn
286 $args->{$_} ||= '' for qw/skip_fields skip_tables skip_tables_like cluster/;
288 my %skip_fields = map { s/^\s+|\s+$//g; length $_ ? ($_, 1) : () }
289 split ( /,/, $args->{skip_fields} );
291 my %skip_tables = map { $_, 1 } (
292 ref $args->{skip_tables} eq 'ARRAY'
293 ? @{$args->{skip_tables}}
294 : split (/\s*,\s*/, $args->{skip_tables})
297 my @skip_tables_like = map { qr/$_/ } (
298 ref $args->{skip_tables_like} eq 'ARRAY'
299 ? @{$args->{skip_tables_like}}
300 : split (/\s*,\s*/, $args->{skip_tables_like})
303 # join_pk_only/skip_fields implies natural_join
304 $args->{natural_join} = 1
305 if ($args->{join_pk_only} or scalar keys %skip_fields);
307 # usually we do not want direction when using natural join
308 $args->{directed} = ($args->{natural_join} ? 0 : 1)
309 if not exists $args->{directed};
311 $schema->make_natural_joins(
312 join_pk_only => $args->{join_pk_only},
313 skip_fields => $args->{skip_fields},
314 ) if $args->{natural_join};
317 if ( defined $args->{'cluster'} ) {
319 if ( ref $args->{'cluster'} eq 'ARRAY' ) {
320 @clusters = @{ $args->{'cluster'} };
323 @clusters = split /\s*;\s*/, $args->{'cluster'};
326 for my $c ( @clusters ) {
327 my ( $cluster_name, @cluster_tables );
328 if ( ref $c eq 'HASH' ) {
329 $cluster_name = $c->{'name'} || $c->{'cluster_name'};
330 @cluster_tables = @{ $c->{'tables'} || [] };
333 my ( $name, $tables ) = split /\s*=\s*/, $c;
334 $cluster_name = $name;
335 @cluster_tables = split /\s*,\s*/, $tables;
338 for my $table ( @cluster_tables ) {
339 $cluster{ $table } = $cluster_name;
345 # Create a blank GraphViz object and see if we can produce the output type.
347 my $gv = GraphViz->new( %$args )
348 or die sprintf ("Can't create GraphViz object: %s\n",
349 $@ || 'reason unknown'
352 my $output_method = "as_$args->{output_type}";
354 # the generators are AUTOLOADed so can't use ->can ($output_method)
355 eval { $gv->$output_method };
356 die "Invalid output type: '$args->{output_type}'" if $@;
359 # Process tables definitions, create nodes
361 my %nj_registry; # for locations of fields for natural joins
362 my @fk_registry; # for locations of fields for foreign keys
365 for my $table ( $schema->get_tables ) {
367 my $table_name = $table->name;
368 if ( @skip_tables_like or keys %skip_tables ) {
369 next TABLE if $skip_tables{ $table_name };
370 for my $regex ( @skip_tables_like ) {
371 next TABLE if $table_name =~ $regex;
375 my @fields = $table->get_fields;
376 if ( $args->{show_fk_only} ) {
377 @fields = grep { $_->is_foreign_key } @fields;
381 if ($args->{show_fields}) {
383 for my $field (@fields) {
386 if ($args->{show_datatypes}) {
388 my $field_type = $field->data_type;
389 my $size = $field->size;
391 if ( $args->{friendly_ints} && $size && (lc ($field_type) eq 'integer') ) {
392 # Automatically translate to int2, int4, int8
393 # Type (Bits) Max. Signed/Unsigned Length
396 # smallint (16) 32767 5
398 # mediumint* (24) 8388607 7
400 # int (32) 2147483647 10
402 # bigint (64) 9223372036854775807 19
403 # 18446744073709551615 20
405 # * tinyint and mediumint are nonstandard extensions which are
406 # only available under MySQL (to my knowledge)
407 if ($size <= 3 and $args->{friendly_ints_extended}) {
408 $field_type = 'tinyint';
411 $field_type = 'smallint';
413 elsif ($size <= 8 and $args->{friendly_ints_extended}) {
414 $field_type = 'mediumint';
416 elsif ($size <= 11) {
417 $field_type = 'integer';
420 $field_type = 'bigint';
424 $field_info = $field_type;
425 if ($args->{show_sizes} && $size && ($field_type =~ /^ (?: NUMERIC | DECIMAL | (VAR)?CHAR2? ) $/ix ) ) {
426 $field_info .= '(' . $size . ')';
431 if ($args->{show_constraints}) {
433 push(@constraints, $field->is_auto_increment ? 'PA' : 'PK') if $field->is_primary_key;
434 push(@constraints, 'FK') if $field->is_foreign_key;
435 push(@constraints, 'U') if $field->is_unique;
436 push(@constraints, 'N') if $field->is_nullable;
438 $constraints = join (',', @constraints);
441 # construct the field line from all info gathered so far
442 push @fmt_fields, join (' ',
446 $constraints ? "[$constraints]" : (),
450 # join field lines with graphviz formatting
451 $field_str = join ('\l', @fmt_fields) . '\l';
456 if ($args->{show_indexes}) {
459 for my $index ($table->get_indices) {
460 next unless $index->is_valid;
462 push @fmt_indexes, join (' ',
464 $args->{show_index_names}
468 join (', ', $index->fields),
469 ($index->type eq 'UNIQUE') ? '[U]' : (),
473 # join index lines with graphviz formatting (if any indexes at all)
474 $index_str = join ('\l', @fmt_indexes) . '\l' if @fmt_indexes;
477 my $name_str = $table_name . '\n';
480 for ($name_str, $field_str, $index_str) {
486 # only the 'record' type supports nice formatting
487 if ($args->{node}{shape} eq 'record') {
489 # the necessity to supply shape => 'record' is a graphviz bug
492 label => sprintf ('{%s}',
502 my $sep = sprintf ('%s\n',
503 '-' x ( (length $table_name) + 2)
515 if (my $cluster_name = $cluster{$table_name} ) {
516 $node_args->{cluster} = $cluster_name;
519 $gv->add_node(qq["$table_name"], %$node_args);
521 debug("Processing table '$table_name'");
523 debug("Fields = ", join(', ', map { $_->name } @fields));
525 for my $f ( @fields ) {
526 my $name = $f->name or next;
527 my $is_pk = $f->is_primary_key;
528 my $is_unique = $f->is_unique;
531 # Decide if we should skip this field.
533 if ( $args->{natural_join} ) {
534 next unless $is_pk || $f->is_foreign_key;
537 my $constraints = $f->{'constraints'};
539 if ( $args->{natural_join} && !$skip_fields{ $name } ) {
540 push @{ $nj_registry{ $name } }, $table_name;
544 unless ( $args->{natural_join} ) {
545 for my $c ( $table->get_constraints ) {
546 next unless $c->type eq FOREIGN_KEY;
547 my $fk_table = $c->reference_table or next;
549 for my $field_name ( $c->fields ) {
550 for my $fk_field ( $c->reference_fields ) {
551 next unless defined $schema->get_table( $fk_table );
553 # a condition is optional if at least one fk is nullable
557 scalar (grep { $_->is_nullable } ($c->fields))
566 # Process relationships, create edges
568 my (@table_bunches, %optional_constraints);
569 if ( $args->{natural_join} ) {
570 for my $field_name ( keys %nj_registry ) {
571 my @table_names = @{ $nj_registry{ $field_name } || [] } or next;
572 next if scalar @table_names == 1;
573 push @table_bunches, [ @table_names ];
577 for my $i (0 .. $#fk_registry) {
578 my $fk = $fk_registry[$i];
579 push @table_bunches, [$fk->[0], $fk->[1]];
580 $optional_constraints{$i} = $fk->[2];
585 for my $bi (0 .. $#table_bunches) {
586 my @tables = @{$table_bunches[$bi]};
588 for my $i ( 0 .. $#tables ) {
589 my $table1 = $tables[ $i ];
590 for my $j ( 1 .. $#tables ) {
592 my $table2 = $tables[ $j ];
593 next if $done{ $table1 }{ $table2 };
594 debug("Adding edge '$table2' -> '$table1'");
598 arrowhead => $optional_constraints{$bi} ? 'empty' : 'normal',
600 $done{ $table1 }{ $table2 } = 1;
608 if ( my $out = $args->{out_file} ) {
609 if (openhandle ($out)) {
610 print $out $gv->$output_method;
613 open my $fh, '>', $out or die "Can't write '$out': $!\n";
615 print $fh $gv->$output_method;
620 return $gv->$output_method;
630 Ken Youens-Clark E<lt>kclark@cpan.orgE<gt>
632 Jonathan Yu E<lt>frequency@cpan.orgE<gt>
636 SQL::Translator, GraphViz