Wednesday, September 17, 2008

Documenting a MySQL database

Today; a quick script I whipped together to create a nice HTML page out of a

mysqldump --no-data
output. Use it from the command line, and give the mysqldump filename as argument. The output is sent to stdout.

bash# php document_mysql_dump.php my_db_dump.sql >my_db_documentation.html

Have fun!


<?
/**
 * MySQL table definition documenting script.
 * Converts mysqldump files to HTML documentation.
 *
 * Freely Available For Now
 *
 * Author: mrOrigo (mrorigo@gmail.com) 2008-09-17
 *
 */

if($argc != 2)
  die(
"ERROR: Need one argument; the MySQL Dump file\n");
?>
<html>
<head>
<style type="text/css">
body { font-family: Verdana, Arial; font-size: 0.75em;}
table {     font-size: 1.0em;}

table.table_columns {}
table.table_columns tr.columns { background: #eeeeee;}
table.table_columns tr.columns td.name { background: #dddddd;}
table.table_columns tr.columns td.extra { color: #555555; background: #dfdfdf;}

table.table_keys {}
table.table_keys tr.keys { background: #eeeeee;}
table.table_keys tr.keys td.name { background: #dddddd;}

table.table_constraints {}
table.table_constraints tr.constraints { background: #eeeeee;}
table.table_constraints tr.constraints td.name { background: #dddddd;}

table.table_extras {}
table.table_extras tr.extras { background: #eeeeee;}
table.table_extras tr.extras td.value { background: pink; }
</style>
</head>
<body>
<?
$file 
$argv[1];
$text file_get_contents($file);
if(!
$text)
  die(
"File not found: $argv[1]\n");

$nm preg_match_all("/CREATE TABLE `(.*?)` \(.(.*?) (ENGINE=.*?);/ms"$text$matches);

$tables = Array();
for(
$i=0$i<$nm$i++) {
  
$definition $matches[2][$i];

  
// "Cheat" the spaces in comments to be &nbsp;, for a few regexps to work
  
$definition preg_replace_callback("/COMMENT '(.*)'/im"
                      
create_function('$a',
                              
'return str_replace(" ", "&nbsp;", $a[0]);'),
                      
$definition);
  
$tables[] = Array("name" => $matches[1][$i],
            
"definition" => parseDefinition($definition),
            
"extra" => $matches[3][$i]);
}

foreach(
$tables as $table) {
  print 
"<p>";
  print 
"<a name='table_".$table["name"]."'>";
  print 
"<h3>Table \"".$table["name"]."\"</h3></a>";
  print 
"<b>Definition</b>";

  print 
"<table cellspacing='0' cellpadding='2' border='1' class='table_columns'>";
  print 
"<tr><th>Column name</th><th>Column type</th><th>Size/Options</th><th>Signed</th><th>Extra</th></tr>";
  foreach(
$table["definition"]["columns"] as $cname => $c)
    print 
"<tr class='columns'><td class='name'>$c[name]</td><td class='type'> $c[type]</td><td class='size'> ".($c["size"]?$c["size"]:"&nbsp;")."</td><td class='signed'> ".($c["signed"] ? $c["signed"] : "&nbsp;"). "</td><td class='extra'> $c[extra]</td></tr>";
  print 
"</table>";

  print 
"<b>Indices</b>";
  print 
"<table cellspacing='0' cellpadding='2' border='1' class='table_keys'>";
  print 
"<tr><th>Index name</th><th>Columns</th></tr>";
  foreach(
$table["definition"]["keys"] as $kname => $k)
    print 
"<tr class='keys'><td class='name'>$k[name]</td><td class='columns'>$k[columns]</td></tr>";
  print 
"</table>";

  if(
count($table["definition"]["constraints"]) > 0) {
    print 
"<b>Constraints</b>";
    print 
"<table cellspacing='0' cellpadding='2' border='1' class='table_constraints'>";
    print 
"<tr><th>Name</th><th>Column</th><th>Foreign Table</th><th>Foreign Column</th><th>Extra</th></tr>";
    foreach(
$table["definition"]["constraints"] as $kname => $k) {
      
$ftlink "<a href='#table_".$k["foreignTable"]."'>".$k["foreignTable"]."</a>";
      print 
"<tr class='constraints'><td class='name'>$k[name]</td><td class='column'>$k[column]</td><td class='foreign_table'>$ftlink</td><td class='foreign_column'>$k[foreignColumn]</td><td class='extra'>$k[extra]</td></tr>";
    }
    print 
"</table>";
  }
  print 
"<b>Table extras</b>";
  print 
"<table cellspacing='0' cellpadding='2' border='1' class='table_extras'>";
  
$extras parseTableExtras($table["extra"]);
  foreach(
$extras as $en => $ev)
    print 
"<tr class='extras'><td class='name'>$en</td><td class='value' colspan=3>$ev</td></tr>";
  print 
"</table>";
}


function 
parseTableExtras($extra)
{
  
$extra preg_replace_callback("/COMMENT='(.*)'/im"
                 
create_function('$a',
                         
'return str_replace(" ", "&nbsp;", $a[0]);'),
                 
$extra);
  
// Split out the extras
  
preg_match_all("/(ENGINE|AUTO_INCREMENT|DEFAULT CHARSET|COMMENT)=([a-zA-Z0-9_'&;!\?]*)/"$extra$matches);  

  
// Make into usable array
  
$extras = Array();
  for(
$i=0;$i<count($matches[0]);$i++)
    
$extras[$matches[1][$i]] = str_replace("&nbsp;"" "$matches[2][$i]);
  return 
$extras;
}

function 
parseDefinition($definition)
{
  
$mysql_types = Array("bit",          "bool",
               
"boolean",      "tinyint",
               
"smallint",      "mediumint",
               
"int",          "integer",
               
"bigint",      "serial",
               
"float",          "double",
               
"decimal",
               
"datetime",        "date",
               
"timestamp",       "time",  "year",
               
"blob",          "char",
               
"varchar",      "enum",
               
"set",          "binary",
               
"varbinary",      "tinyblob",
               
"tinytext",      "blob",
               
"text",          "mediumtext",
               
"mediumblob",      "longtext",
               
"longblob");

  
// Split into columns, keys and constraints
  
preg_match("/^(.*?)(PRIMARY?\ ?KEY.*?)(CONSTRAINT.*)/si"
         
$definition,
         
$matches);
  if(!
$matches[3])  // Some tables don't have constraints..
        
preg_match("/^(.*?)(PRIMARY?\ KEY.*)(CONSTRAINT.*)?/si"
           
$definition,
           
$matches);
  
$columns $matches[1];
  
$keys $matches[2];
  
$constraints $matches[3];

  
// Split the columns
  
preg_match_all("/(.*)\s+(".join("|",$mysql_types).")\(?([a-zA-Z0-9',]*?)\)?\s+(unsigned)?\s?([^,]*)/Si",
         
$columns,
         
$matches);
  
$_c=Array("names" => &$matches[1],
        
"types" => &$matches[2],
        
"sizes" => &$matches[3],
        
"signed" => &$matches[4],
        
"extra" => &$matches[5]);
  
// Transform columns into a more usable array
  
$columns = Array();
  for(
$i=0$i<count($_c); $i++) {
    
$n trim($_c["names"][$i], " `");
    if(
$n != "")
      
$columns[$n] = Array("type" => $_c["types"][$i],
               
"name" => $n,
               
"size" => $_c["sizes"][$i],
               
"signed"=> $_c["signed"][$i],
               
"extra" => $_c["extra"][$i]);
  }

  
// Parse and tidy up keys
  
preg_match_all("/(PRIMARY)?.*?(KEY).*?(.*)?(\(.*\))(.*)/"$keys$matches);
  
$keys = Array();
  for(
$i=0$i<count($matches[0]); $i++) {
    
$n trim($matches[3][$i]," (`)");
    if(
$n == "")
      
$n "PRIMARY";
    
$keys$n ] = Array("name" => $n,
            
"columns" => trim($matches[4][$i], " (`)"));
      };

  
// Parse and tidy up constraints
  
preg_match_all("/(CONSTRAINT)\s+(.*)\s+FOREIGN KEY \((.*)\)\s+REFERENCES\s+(.*)\s+\((.*)\)\s+([^,\)]*)/"$constraints$matches);
  
$constraints = Array();
  for(
$i=0$i<count($matches[0]); $i++) {
    
$n trim($matches[2][$i]," (`)");
    
$constraints$n ] = Array("name" => $n,
                   
"column" => trim($matches[3][$i]," (`)"),
                   
"foreignTable" => trim($matches[4][$i]," (`)"),
                   
"foreignColumn" => trim($matches[5][$i]," (`)"),
                   
"extra" => $matches[6][$i]);
  }

  return Array(
"columns" => $columns,
           
"keys" => $keys,
           
"constraints" => $constraints);
}

?>