René Nyffenegger's collection of things on the web
René Nyffenegger on Oracle - Most wanted - Feedback
 

Creating an ERD out of SQL create table statements

The following perl script parses a file containing create table statements in SQL and creates an ERD using dot and Parse::RecDescent.
#   create_erd.pl
#
#   Copyright (C) 2004 René Nyffenegger
#
#   This source code is provided 'as-is', without any express or implied
#   warranty. In no event will the authors be held liable for any damages
#   arising from the use of this software.
#
#   Permission is granted to anyone to use this software for any purpose,
#   including commercial applications, and to alter it and redistribute it
#   freely, subject to the following restrictions:
#
#   1. The origin of this source code must not be misrepresented; you must not
#      claim that you wrote the original source code. If you use this source code
#      in a product, an acknowledgment in the product documentation would be
#      appreciated but is not required.
#
#   2. Altered source versions must be plainly marked as such, and must not be
#      misrepresented as being the original source code.
#
#   3. This notice may not be removed or altered from any source distribution.
#
#   René Nyffenegger rene.nyffenegger@adp-gmbh.ch
#
################################################################################

use strict;
use warnings;

use Parse::RecDescent;
use Data::Dumper;

my $out_file_type = 'png';

$::RD_ERRORS = 1; # Make sure the parser dies when it encounters an error
$::RD_WARN   = 1; # Enable warnings. This will warn on unused rules &c.
$::RD_HINT   = 1; # Give out hints to help fix problems.
#$::RD_TRACE  = 1; # Trace of parser

my $create_db_file = $ARGV[0];

open F, $create_db_file or die "could not open $create_db_file";

my @f = <F>;

my $create_table_grammar = q {

  create_table_stmts : create_table_stmt(s?)
   { @item[1..$#item] }

  create_table_stmt  : /create/i /table/i table_name '(' rel_props ')' /;?/
                       { {tab_nam => $item{table_name}, cols => $item{rel_props} } }

  table_name         : identifier
                       {$item[1]}

  rel_props          : columns                                        #relational properties (according Oracle documentation)
                       {$item[1]}

  columns            : column ',' columns
                       { if ($item[1]) { unshift @{$item[3]}, $item[1]} else {print "xxx\n"}; $item[3] }
                     | column
                       { [ $item[1] ] }

  column             : out_of_line_constr
                       { { col_nam=>'constraint' } } 
                     | identifier reference_clause
                       { $return = {col_nam=> $item{identifier}} ;  @{$return}{keys %{$item{reference_clause}}} =  values %{$item{reference_clause}}  } 
                     | identifier data_type primary_key(?) not_null(?) default(?)
                       { {col_nam=>$item{identifier}, type=>$item{data_type}} }

  out_of_line_constr : named_const constraint
                     | constraint

  named_const        : /constraint/i identifier

  constraint         : /check/i           paranthesis
                     | /unique/i          paranthesis
                     | /primary/i /key/i  paranthesis
                     | /foreighn/i /key/i paranthesis

  paranthesis        : '(' in_paranthesis ')'

  in_paranthesis     : ( /[^()]+/ | paranthesis)(s)
                     
  reference_clause   : /references/i identifier not_null(?)
                       { { refd_table=>$item[2] } }
                     | data_type /references/i identifier not_null(?)
                       { { type=>$item[1], refd_table=>$item[3] } }
                     
  default            : /default/i sql_string

  sql_string         : /'([^']|'')*'/

  primary_key        : /primary/i /key/i

  not_null           : /not/i /null/i

  data_type          : dt_ident precision(?)
                       {$item[1]. $item[2][0] || "" } 

  precision          : '(' number ')'
                        {$item[1].$item[2].$item[3]}
                     | '(' number ',' number ')'
                        {$item[1].$item[2].$item[3].$item[4].$item[5]}

  dt_ident           : /number/i         {$item[1]} 
                     | /int +identity/i  {$item[1]} 
                     | /int/i            {$item[1]} 
                     | /decimal/i        {$item[1]} 
                     | /smallint/i       {$item[1]} 
                     | /integer/i        {$item[1]} 
                     | /long raw/i       {$item[1]} 
                     | /long/i           {$item[1]} 
                     | /varchar2/i       {$item[1]} 
                     | /varchar/i        {$item[1]} 
                     | /char/i           {$item[1]} 
                     | /raw/i            {$item[1]} 
                     | /date/i           {$item[1]} 
                     | /smalldatetime/i  {$item[1]} 
                     | /blob/i           {$item[1]} 
                     | /clob/i           {$item[1]} 
                     | /nclob/i          {$item[1]} 
                     | /bit/i            {$item[1]} 


  number             : /\d+/
                      { $item[1] }
                      

  identifier         : m([A-Za-z_]\w*)
                      {$item[1]}
};

my $parser=Parse::RecDescent->new($create_table_grammar);

my $in_comment=0;

my $l_temp;
LINE:
foreach my $l (@f) {
  my $len = length $l;
  $l_temp="";

  my $first_quote =0;  # only set when already $in_comment
  my $asterik     =0;
  my $slash       =0;
  my $in_string   =0;
  my $hyphen      =0;

  for (my $i=0; $i<$len; $i++) {

    my $c = substr($l, $i, 1); 

    if ($in_comment) {
      if ($c eq "*") {
        $asterik     = 1;
      }
      elsif ($c eq "/") {
        if ($asterik) {
          $asterik     = 0;
          $slash       = 0;
          $in_comment  = 0;
        }
      }
      else {
        $asterik = 0;
      }
    }
    else {
      if ($in_string) {
        if ($c eq "'") {

          if ($first_quote) {
            $first_quote = 0;          
            $l_temp .= $c;
          }
          else {
            $first_quote = 1;
            $l_temp .= $c;
          }
        }
      }
      elsif ($c eq "/") {
        if ($slash) {
          $l_temp .= "/";
        }
        else {
          $slash = 1;
        }
      }
      elsif ($c eq "*") {
        if ($slash) {
          $in_comment = 1;
        }
        else {
          $l_temp .= $c;
        }
      }
      elsif ($c eq "-") {
        if ($hyphen) {
          $l_temp .= "\n";
          next LINE;
        }
        else {
          $hyphen = 1;
        }
      }
      elsif ($hyphen) {
        $hyphen = 0;
        $l_temp .= "-$c";
      }
      elsif ($first_quote) {
        $in_string   = 0;
        $first_quote = 0;
        $l_temp .= $c;
      }
      else {
        $l_temp .= $c;
      }
    }
  }
}
continue {
  $l = $l_temp;
}

# print join " ", @f;

my $result = $parser->create_table_stmts(join " ", @f) or die "could'n parse $create_db_file";

# print "result: \n";

#print Dumper($result);

#exit 0;


my %tables;
open DOT, "|dot -T$out_file_type > erd.$out_file_type";
print DOT "digraph erd {\n";

#print DOT 'page="11.69,8.27"' . "\n";
my $id_cnt=0;
my @refs;
foreach my $table (@$result) {

  my $table_name = uc $table->{tab_nam};
  
  print DOT uc $table_name;
  print DOT '[ shape="record" ';
  print DOT ' label = "{';
  print DOT uc $table_name . "|";

  foreach my $col (@{$table->{cols}}) {
    print DOT "|";
    if (exists $col->{refd_table} and uc $col->{refd_table} ne $table_name) {
      my $id_ref = "i" . ++$id_cnt;
      print DOT "<$id_ref> ";
      push @refs, {from_tbl=>$table_name, from_id=>$id_ref, to_tbl=>uc ($col->{refd_table})};
    }
    print DOT $col->{col_nam};
  }

 # print DOT join "|", map {$_->{col_nam}} @{$table->{cols}};
  print DOT '}"';
  print DOT "];\n";
}

foreach my $ref (@refs) {
  print DOT $ref->{from_tbl} . ":" . $ref->{from_id} . " -> " . $ref->{to_tbl} . ";\n";
}

print DOT "}";


It seems as though graphviz crashes with self referencing tables.

Links

Create table syntax for Oracle.