/**
* Creates a table with the given names and columns.
*
- * @param string $name Name of the table
- * @param array $columns Array of ColumnDef objects
- * for new table.
+ * @param string $tableName Name of the table
+ * @param array $def Table definition array listing fields and indexes.
*
* @return boolean success flag
*/
- public function createTable($name, $columns)
+ public function createTable($tableName, $def)
{
$statements = $this->buildCreateTable($tableName, $def);
return $this->runSqlSet($statements);
*/
public function buildCreateTable($name, $def)
{
+ $def = $this->validateDef($name, $def);
+ $def = $this->filterDef($def);
$sql = array();
foreach ($def['fields'] as $col => $colDef) {
$this->appendColumnDef($sql, $col, $colDef);
}
- // Primary and unique keys are constraints, so go within
+ // Primary, unique, and foreign keys are constraints, so go within
// the CREATE TABLE statement normally.
if (!empty($def['primary key'])) {
$this->appendPrimaryKeyDef($sql, $def['primary key']);
}
}
+ if (!empty($def['foreign keys'])) {
+ foreach ($def['foreign keys'] as $keyName => $keyDef) {
+ $this->appendForeignKeyDef($sql, $keyName, $keyDef);
+ }
+ }
+
// Multi-value indexes are advisory and for best portability
// should be created as separate statements.
$statements = array();
$this->endCreateTable($name, $def);
if (!empty($def['indexes'])) {
foreach ($def['indexes'] as $col => $colDef) {
- $this->appendCreateIndex($statements, $table, $col, $colDef);
+ $this->appendCreateIndex($statements, $name, $col, $colDef);
}
}
}
/**
- * Append an SQL fragment with a constraint definition for a primary
+ * Append an SQL fragment with a constraint definition for a unique
* key in a CREATE TABLE statement.
*
* @param array $sql
*/
function appendUniqueKeyDef(array &$sql, $name, array $def)
{
- $sql[] = "UNIQUE $key " . $this->buildIndexList($def);
+ $sql[] = "CONSTRAINT $name UNIQUE " . $this->buildIndexList($def);
+ }
+
+ /**
+ * Append an SQL fragment with a constraint definition for a foreign
+ * key in a CREATE TABLE statement.
+ *
+ * @param array $sql
+ * @param string $name
+ * @param array $def
+ */
+ function appendForeignKeyDef(array &$sql, $name, array $def)
+ {
+ if (count($def) != 2) {
+ throw new Exception("Invalid foreign key def for $name: " . var_export($def, true));
+ }
+ list($refTable, $map) = $def;
+ $srcCols = array_keys($map);
+ $refCols = array_values($map);
+ $sql[] = "CONSTRAINT $name FOREIGN KEY " .
+ $this->buildIndexList($srcCols) .
+ " REFERENCES " .
+ $this->quoteIdentifier($refTable) .
+ " " .
+ $this->buildIndexList($refCols);
}
/**
$statements[] = "CREATE INDEX $name ON $table " . $this->buildIndexList($def);
}
+ /**
+ * Append an SQL statement to drop an index from a table.
+ *
+ * @param array $statements
+ * @param string $table
+ * @param string $name
+ * @param array $def
+ */
+ function appendDropIndex(array &$statements, $table, $name)
+ {
+ $statements[] = "DROP INDEX $name ON " . $this->quoteIdentifier($table);
+ }
+
function buildIndexList(array $def)
{
// @fixme
- return '(' . implode(',', array_map(array($this, 'quoteIdentifier'), $def)) . ')';
+ return '(' . implode(',', array_map(array($this, 'buildIndexItem'), $def)) . ')';
+ }
+
+ function buildIndexItem($def)
+ {
+ if (is_array($def)) {
+ list($name, $size) = $def;
+ return $this->quoteIdentifier($name) . '(' . intval($size) . ')';
+ }
+ return $this->quoteIdentifier($def);
}
/**
{
$ok = true;
foreach ($statements as $sql) {
+ if (defined('DEBUG_INSTALLER')) {
+ echo "<tt>" . htmlspecialchars($sql) . "</tt><br/>\n";
+ }
$res = $this->conn->query($sql);
if (PEAR::isError($res)) {
* @return array of SQL statements
*/
- function buildEnsureTable($tableName, $def)
+ function buildEnsureTable($tableName, array $def)
{
try {
$old = $this->getTableDef($tableName);
- } catch (Exception $e) {
- // @fixme this is a terrible check :D
- if (preg_match('/no such table/', $e->getMessage())) {
- return $this->buildCreateTable($tableName, $def);
- } else {
- throw $e;
- }
+ } catch (SchemaTableMissingException $e) {
+ return $this->buildCreateTable($tableName, $def);
}
- $cur = array_keys($old['fields']);
- $new = array_keys($def['fields']);
-
- $toadd = array_diff($new, $cur);
- $todrop = array_diff($cur, $new);
- $same = array_intersect($new, $cur);
- $tomod = array();
+ // Filter the DB-independent table definition to match the current
+ // database engine's features and limitations.
+ $def = $this->validateDef($tableName, $def);
+ $def = $this->filterDef($def);
- // Find which fields have actually changed definition
- // in a way that we need to tweak them for this DB type.
- foreach ($same as $name) {
- $curCol = $old['fields'][$name];
- $newCol = $cur['fields'][$name];
-
- if (!$this->columnsEqual($curCol, $newCol)) {
- $tomod[] = $name;
- }
- }
+ $statements = array();
+ $fields = $this->diffArrays($old, $def, 'fields', array($this, 'columnsEqual'));
+ $uniques = $this->diffArrays($old, $def, 'unique keys');
+ $indexes = $this->diffArrays($old, $def, 'indexes');
+ $foreign = $this->diffArrays($old, $def, 'foreign keys');
- if (count($toadd) + count($todrop) + count($tomod) == 0) {
- // nothing to do
- return true;
+ // Drop any obsolete or modified indexes ahead...
+ foreach ($indexes['del'] + $indexes['mod'] as $indexName) {
+ $this->appendDropIndex($statements, $tableName, $indexName);
}
// For efficiency, we want this all in one
$phrase = array();
- foreach ($toadd as $columnName) {
+ foreach ($foreign['del'] + $foreign['mod'] as $keyName) {
+ $this->appendAlterDropForeign($phrase, $keyName);
+ }
+
+ foreach ($uniques['del'] + $uniques['mod'] as $keyName) {
+ $this->appendAlterDropUnique($phrase, $keyName);
+ }
+
+ foreach ($fields['add'] as $columnName) {
$this->appendAlterAddColumn($phrase, $columnName,
$def['fields'][$columnName]);
}
- foreach ($todrop as $columnName) {
+ foreach ($fields['mod'] as $columnName) {
$this->appendAlterModifyColumn($phrase, $columnName,
$old['fields'][$columnName],
$def['fields'][$columnName]);
}
- foreach ($tomod as $columnName) {
+ foreach ($fields['del'] as $columnName) {
$this->appendAlterDropColumn($phrase, $columnName);
}
- $sql = 'ALTER TABLE ' . $tableName . ' ' . implode(', ', $phrase);
+ foreach ($uniques['mod'] + $uniques['add'] as $keyName) {
+ $this->appendAlterAddUnique($phrase, $keyName, $def['unique keys'][$keyName]);
+ }
- $res = $this->conn->query($sql);
+ foreach ($foreign['mod'] + $foreign['add'] as $keyName) {
+ $this->appendAlterAddForeign($phrase, $keyName, $def['foreign keys'][$keyName]);
+ }
- if (PEAR::isError($res)) {
- throw new Exception($res->getMessage());
+ $this->appendAlterExtras($phrase, $tableName);
+
+ if (count($phrase) > 0) {
+ $sql = 'ALTER TABLE ' . $tableName . ' ' . implode(",\n", $phrase);
+ $statements[] = $sql;
}
- return true;
+ // Now create any indexes...
+ foreach ($indexes['mod'] + $indexes['add'] as $indexName) {
+ $this->appendCreateIndex($statements, $tableName, $indexName, $def['indexes'][$indexName]);
+ }
+
+ return $statements;
+ }
+
+ function diffArrays($oldDef, $newDef, $section, $compareCallback=null)
+ {
+ $old = isset($oldDef[$section]) ? $oldDef[$section] : array();
+ $new = isset($newDef[$section]) ? $newDef[$section] : array();
+
+ $oldKeys = array_keys($old);
+ $newKeys = array_keys($new);
+
+ $toadd = array_diff($newKeys, $oldKeys);
+ $todrop = array_diff($oldKeys, $newKeys);
+ $same = array_intersect($newKeys, $oldKeys);
+ $tomod = array();
+ $tokeep = array();
+
+ // Find which fields have actually changed definition
+ // in a way that we need to tweak them for this DB type.
+ foreach ($same as $name) {
+ if ($compareCallback) {
+ $same = call_user_func($compareCallback, $old[$name], $new[$name]);
+ } else {
+ $same = ($old[$name] == $new[$name]);
+ }
+ if ($same) {
+ $tokeep[] = $name;
+ continue;
+ }
+ $tomod[] = $name;
+ }
+ return array('add' => $toadd,
+ 'del' => $todrop,
+ 'mod' => $tomod,
+ 'keep' => $tokeep,
+ 'count' => count($toadd) + count($todrop) + count($tomod));
}
/**
$phrase[] = 'DROP COLUMN ' . $this->quoteIdentifier($columnName);
}
+ function appendAlterAddUnique(array &$phrase, $keyName, array $def)
+ {
+ $sql = array();
+ $sql[] = 'ADD';
+ $this->appendUniqueKeyDef($sql, $keyName, $def);
+ $phrase[] = implode(' ', $sql);
+ }
+
+ function appendAlterAddForeign(array &$phrase, $keyName, array $def)
+ {
+ $sql = array();
+ $sql[] = 'ADD';
+ $this->appendForeignKeyDef($sql, $keyName, $def);
+ $phrase[] = implode(' ', $sql);
+ }
+
+ function appendAlterDropUnique(array &$phrase, $keyName)
+ {
+ $phrase[] = 'DROP CONSTRAINT ' . $keyName;
+ }
+
+ function appendAlterDropForeign(array &$phrase, $keyName)
+ {
+ $phrase[] = 'DROP FOREIGN KEY ' . $keyName;
+ }
+
+ function appendAlterExtras(array &$phrase, $tableName)
+ {
+ // no-op
+ }
+
/**
* Quote a db/table/column identifier if necessary.
*
function quoteValue($val)
{
- if (is_int($val) || is_float($val) || is_double($val)) {
- return strval($val);
- } else {
- return '"' . $this->conn->escapeSimple($val) . '"';
- }
+ return $this->conn->quoteSmart($val);
}
/**
function typeAndSize($column)
{
- $type = $this->mapType($column);
+ //$type = $this->mapType($column);
+ $type = $column['type'];
+ if (isset($column['size'])) {
+ $type = $column['size'] . $type;
+ }
$lengths = array();
- if ($column['type'] == 'numeric') {
- if (isset($column['precision'])) {
- $lengths[] = $column['precision'];
- if (isset($column['scale'])) {
- $lengths[] = $column['scale'];
- }
+ if (isset($column['precision'])) {
+ $lengths[] = $column['precision'];
+ if (isset($column['scale'])) {
+ $lengths[] = $column['scale'];
}
} else if (isset($column['length'])) {
$lengths[] = $column['length'];
}
}
- /**
- * Map a native type back to an independent type + size
- *
- * @param string $type
- * @return array ($type, $size) -- $size may be null
- */
- protected function reverseMapType($type)
- {
- return array($type, null);
- }
-
/**
* Convert an old-style set of ColumnDef objects into the current
* Drupal-style schema definition array, for backwards compatibility
* with plugins written for 0.9.x.
*
* @param string $tableName
- * @param array $defs
+ * @param array $defs: array of ColumnDef objects
* @return array
*/
- function oldToNew($tableName, $defs)
+ protected function oldToNew($tableName, array $defs)
{
$table = array();
$prefixes = array(
'big',
);
foreach ($defs as $cd) {
- $cd->addToTableDef($table);
$column = array();
$column['type'] = $cd->type;
foreach ($prefixes as $prefix) {
if (!$cd->nullable) {
$column['not null'] = true;
}
- if ($cd->autoincrement) {
+ if ($cd->auto_increment) {
$column['type'] = 'serial';
}
if ($cd->default) {
return $table;
}
+ /**
+ * Filter the given table definition array to match features available
+ * in this database.
+ *
+ * This lets us strip out unsupported things like comments, foreign keys,
+ * or type variants that we wouldn't get back from getTableDef().
+ *
+ * @param array $tableDef
+ */
+ function filterDef(array $tableDef)
+ {
+ return $tableDef;
+ }
+
+ /**
+ * Validate a table definition array, checking for basic structure.
+ *
+ * If necessary, converts from an old-style array of ColumnDef objects.
+ *
+ * @param string $tableName
+ * @param array $def: table definition array
+ * @return array validated table definition array
+ *
+ * @throws Exception on wildly invalid input
+ */
+ function validateDef($tableName, array $def)
+ {
+ if (count($def) && $def[0] instanceof ColumnDef) {
+ $def = $this->oldToNew($tableName, $def);
+ }
+
+ // A few quick checks :D
+ if (!isset($def['fields'])) {
+ throw new Exception("Invalid table definition for $tableName: no fields.");
+ }
+
+ return $def;
+ }
+
function isNumericType($type)
{
$type = strtolower($type);