The best I can think of is sharing with you my script, that does exactly this: takes a list of column definitions and alters database table respectively. It can add, drop, alter (even rename) columns and change primary keys. Unfortunately it's PHP so re-coding would be necessary, but maybe you can find general idea useful.
I've used this script successfully for months now to upgrade various installations of my CMS.
Function accepts (as a second argument) an array of arrays, where each of the latter contains at position:
0 - Column name
1 - MySql column type (ex. "int" or "varchar(30)").
2 - whether columns is nullable (true for allow null, false for forbid)
3 - The default value for column (ie. "0").
4 - true, when column is part of primary key
5 - old name of a column (thus column of name in 5., if exists, is going to be renamed to column of name in 0.)
First parameter is table name, and third is whether the function should drop columns that exist in database table, but were skipped in provided array.
Sorry for the disgusting contract, but this function was never meant to be a part of public interface. :-)
Here goes the CreateOrUpdateTable function body (references explained afterwards):
function CreateOrUpdateTable($tablename, array $columns, $allowdropcolumn = false)
{
foreach($columns as &$column)
{
if ((!isset($column[0])) || (!preg_match('/^[a-zA-Z0-9_\-]+$/', $column[0])))
$column[0] = 'TableColumn' . array_search($column, $columns);
if ((!isset($column[1])) || (!preg_match('/^(int|date|datetime|decimal\([0-9]+,[0-9]+\)|varchar\([0-9]+\)|char\([0-9]+\)|text|tinyint)$/', $column[1])))
$column[1] = 'int';
if ((!isset($column[2])) || (!is_bool($column[2])))
$column[2] = ALLOW_NULL;
if ((!isset($column[3])) || (!is_string($column[3])))
$column[3] = (($column[2] == ALLOW_NULL || $column[1] === 'text') ? 'NULL' : ($column[1] == 'int' ? "'0'" : ($column[1] == 'tinyint' ? "'0'" : ($column[1] == 'decimal' ? "'0.00'" : ($column[1] == 'date' ? "'1900-01-01'" : ($column[1] == 'datetime' ? "'1900-01-01 00:00:00'" : "''"))))));
else
$column[3] = "'" . Uti::Sql($column[3]) . "'";
if ((!isset($column[4])) || (!is_bool($column[4])))
$column[4] = false;
}
unset($column);
if (!$this->TableExists($tablename))
{
$statements = array();
foreach ($columns as $column)
{
$statement = $this->ColumnCreationStatement($column);
if ($statement !== '')
$statements[] = $statement;
}
$this->Query("create table " . $tablename . "(" . implode(',', $statements) . ") ENGINE=InnoDB DEFAULT CHARSET=latin2");
}
else
{
$this->Select("show columns in " . $tablename);
$existing = $this->AllRows(null, 'Field');
$oldkeys = array(); $newkeys = array();
foreach ($existing as $e)
if ($e['Key'] === 'PRI')
$oldkeys[] = $e['Field'];
sort($oldkeys);
$oldkeys = implode(',', $oldkeys);
$lastcolumn = ''; // not 'FIRST' as we can extend existing table here providing only extending columns
foreach ($columns as $column)
{
if ($column[4])
$newkeys[] = $column[0];
$newtype = $column[1] . ($column[1] === 'int' ? '(11)' : ($column[1] === 'tinyint' ? '(4)' : ''));
$newnull = ($column[2] === ALLOW_NULL ? 'YES' : 'NO');
$newdefault = $column[3];
if (isset($existing[$column[0]]))
{
$oldtype = $existing[$column[0]]['Type'];
$oldnull = $existing[$column[0]]['Null'];
$olddefault = isset($existing[$column[0]]['Default']) ? "'" . Uti::Sql($existing[$column[0]]['Default']) . "'" : "NULL";
if (($oldtype != $newtype) || ($oldnull != $newnull) || ($olddefault != $newdefault))
{
$this->SaveToLog("Altering table [" . $tablename . "], column [" . $column[0] . "], changing: type [" .
$oldtype . "] => [" . $newtype . "] nullability [" . $oldnull . "] => [" . $newnull . "] default [" . $olddefault . "] => [" . $newdefault . "]", true);
$statement = $this->ColumnCreationStatement($column, false);
if ($statement !== '')
$this->Query("alter table " . $tablename . " change " . $column[0] . " " . $statement);
}
unset($existing[$column[0]]);
}
else if (isset($column[5]) && (Uti::AnyExists(array_keys($existing), $column[5]) !== false))
{
$oldcolumn = Uti::AnyExists(array_keys($existing), $column[5]);
$this->SaveToLog("Altering table [" . $tablename . "], column [" . $column[0] . "], renaming: name [" . $oldcolumn . "] => [" . $column[0] . "] " .
" type [" . $newtype . "] nullability [" . $newnull . "] default [" . $newdefault . "]", true);
$statement = $this->ColumnCreationStatement($column, false);
if ($statement !== '')
$this->Query("alter table " . $tablename . " change " . $oldcolumn . " " . $statement);
unset($existing[$oldcolumn]);
}
else
{
$this->SaveToLog("Altering table [" . $tablename . "], column [" . $column[0] . "], adding: name [" . $column[0] . "] " .
" type [" . $newtype . "] nullability [" . $newnull . "] default [" . $newdefault . "]", true);
$statement = $this->ColumnCreationStatement($column, false);
if ($statement !== '')
$this->Query("alter table " . $tablename . " add " . $statement . " " . $lastcolumn);
}
$lastcolumn = 'AFTER ' . $column[0];
}
if ($allowdropcolumn)
{
foreach ($existing as $e)
{
$this->SaveToLog("Altering table [" . $tablename . "], column [" . $e['Field'] . "], dropping", true);
$this->Query("alter table " . $tablename . " drop " . $e['Field']);
}
}
sort($newkeys);
$newkeys = implode(',',$newkeys);
if ($oldkeys != $newkeys)
{
$this->SaveToLog("Altering table [" . $tablename . "], changing keys [" . $oldkeys . "] => [" . $newkeys . "]", true);
if ($oldkeys !== '')
$this->Query("alter table " . $tablename . " drop primary key");
if ($newkeys !== '')
$this->Query("alter table " . $tablename . " add primary key (" . $newkeys . ")");
}
}
}
Following external functions require explanation:
ColumnCreationStatement provides alter/create table fragment:
private function ColumnCreationStatement(array $columninfo, $includekey = true)
{
$r = '';
if ((count($columninfo) > 0) && (preg_match('/^[a-zA-Z0-9_\-]+$/', $columninfo[0])))
{
$r .= $columninfo[0];
if ((count($columninfo) > 1) && (preg_match('/^(int|date|datetime|decimal\([0-9]+,[0-9]+\)|varchar\([0-9]+\)|char\([0-9]+\)|text|tinyint)$/', $columninfo[1])))
$r .= ' ' . $columninfo[1];
else
$r .= ' int';
if ((count($columninfo) > 2) && is_bool($columninfo[2]))
$r .= ($columninfo[2] === NOT_NULL ? ' not null' : ' null');
if ((count($columninfo) > 3) && is_string($columninfo[3]) && ($columninfo[3] !== '') && ($columninfo[1] !== 'text'))
$r .= " default " . $columninfo[3];
if ((count($columninfo) > 4) && is_bool($columninfo[4]) && $includekey)
$r .= ($columninfo[4] === true ? ', primary key(' . $columninfo[0] . ')' : '');
}
return $r;
}
TableExists simply verifies whether table is available in database (using show tables like).
Query executes MySql statement (and yes: returns no result ;])
Select and AllRows are shortcuts for returning rows as hashtables collection.
SaveToLog is - I guess - obvious. :-)
And Uti::AnyExists looks like this:
public static function AnyExists($haystack, $needles, $separator = ';')
{
if (!is_array($needles))
$needles = explode($separator, $needles);
foreach ($needles as $needle)
{
if (array_search($needle, $haystack) !== false)
return $needle;
}
return false;
}
I hope it all helps. In case of any questions please feel free to ask in comments. :-)