field spec. * @param string $table The table name. * @return array */ protected abstract function fieldList($table); /** * Returns a list of all tables in the database. * The table names will be in lower case. * @return array */ protected abstract function tableList(); /** * The table list, generated by the tableList() function. * Used by the requireTable() function. * @var array */ protected $tableList; /** * The field list, generated by the fieldList() function. * An array of maps of field name => field spec, indexed * by table name. * @var array */ protected $fieldList; /** * The index list for each table, generated by the indexList() function. * An map from table name to an array of index names. * @var array */ protected $indexList; /** * Large array structure that represents a schema update transaction */ protected $schemaUpdateTransaction; /** * Start a schema-updating transaction. * All calls to requireTable/Field/Index will keep track of the changes requested, but not actually do anything. * Once */ function beginSchemaUpdate() { $this->tableList = $this->tableList(); $this->indexList = null; $this->fieldList = null; $this->schemaUpdateTransaction = array(); } function endSchemaUpdate() { foreach($this->schemaUpdateTransaction as $tableName => $changes) { switch($changes['command']) { case 'create': $this->createTable($tableName, $changes['newFields'], $changes['newIndexes']); break; case 'alter': $this->alterTable($tableName, $changes['newFields'], $changes['newIndexes'], $changes['alteredFields'], $changes['alteredIndexes']); break; } } $this->schemaUpdateTransaction = null; } // Transactional schema altering functions - they don't do anyhting except for update schemaUpdateTransaction function transCreateTable($table) { $this->schemaUpdateTransaction[$table] = array('command' => 'create', 'newFields' => array(), 'newIndexes' => array()); } function transCreateField($table, $field, $schema) { $this->transInitTable($table); $this->schemaUpdateTransaction[$table]['newFields'][$field] = $schema; } function transCreateIndex($table, $index, $schema) { $this->transInitTable($table); $this->schemaUpdateTransaction[$table]['newIndexes'][$index] = $schema; } function transAlterField($table, $field, $schema) { $this->transInitTable($table); $this->schemaUpdateTransaction[$table]['alteredFields'][$field] = $schema; } function transAlterIndex($table, $index, $schema) { $this->transInitTable($table); $this->schemaUpdateTransaction[$table]['alteredIndexes'][$index] = $schema; } /** * Handler for the other transXXX methods - mark the given table as being altered * if it doesn't already exist */ protected function transInitTable($table) { if(!isset($this->schemaUpdateTransaction[$table])) { $this->schemaUpdateTransaction[$table] = array( 'command' => 'alter', 'newFields' => array(), 'newIndexes' => array(), 'alteredFields' => array(), 'alteredIndexes' => array(), ); } } /** * Generate the following table in the database, modifying whatever already exists * as necessary. * @param string $table The name of the table * @param string $fieldSchema A list of the fields to create, in the same form as DataObject::$db * @param string $indexSchema A list of indexes to create. The keys of the array are the names of the index. * The values of the array can be one of: * - true: Create a single column index on the field named the same as the index. * - array('fields' => array('A','B','C'), 'type' => 'index/unique/fulltext'): This gives you full * control over the index. */ function requireTable($table, $fieldSchema = null, $indexSchema = null) { if(!isset($this->tableList[strtolower($table)])) { $this->transCreateTable($table); Database::alteration_message("Table $table: created","created"); } else { $this->checkAndRepairTable($table); } // Create custom fields if($fieldSchema) { foreach($fieldSchema as $fieldName => $fieldSpec) { $fieldObj = eval(ViewableData::castingObjectCreator($fieldSpec)); $fieldObj->setTable($table); $fieldObj->requireField(); } } // Create custom indexes if($indexSchema) { foreach($indexSchema as $indexName => $indexDetails) { $this->requireIndex($table, $indexName, $indexDetails); } } } /** * If the given table exists, move it out of the way by renaming it to _obsolete_(tablename). * @param string $table The table name. */ function dontRequireTable($table) { if(!isset($this->tableList)) $this->tableList = $this->tableList(); if(isset($this->tableList[strtolower($table)])) { while($this->tableList[strtolower("_obsolete_{$table}$suffix")]) { $suffix = $suffix ? ($suffix+1) : 2; } $this->renameTable($table, "_obsolete_{$table}$suffix"); Database::alteration_message("Table $table: renamed to _obsolete_{$table}$suffix","obsolete"); } } /** * Generate the given index in the database, modifying whatever already exists as necessary. * @param string $table The table name. * @param string $index The index name. * @param string|boolean $spec The specification of the index. See requireTable() for more information. */ function requireIndex($table, $index, $spec) { $newTable = false; if($spec === true) { $spec = "($index)"; } $spec = ereg_replace(" *, *",",",$spec); if(!isset($this->tableList[strtolower($table)])) $newTable = true; if(!$newTable && !isset($this->indexList[$table])) { $this->indexList[$table] = $this->indexList($table); } if($newTable || !isset($this->indexList[$table][$index])) { $this->transCreateIndex($table, $index, $spec); Database::alteration_message("Index $table.$index: created as $spec","created"); } else if($this->indexList[$table][$index] != $spec) { $this->transAlterIndex($table, $index, $spec); Database::alteration_message("Index $table.$index: changed to $spec (from {$this->indexList[$table][$index]})","changed"); } } /** * Generate the given field on the table, modifying whatever already exists as necessary. * @param string $table The table name. * @param string $field The field name. * @param string $spec The field specification. */ function requireField($table, $field, $spec) { $newTable = false; Profiler::mark('requireField'); // Collations didn't come in until MySQL 4.1. Anything earlier will throw a syntax error if you try and use // collations. if(!$this->supportsCollations()) { $spec = eregi_replace(' *character set [^ ]+( collate [^ ]+)?( |$)','\\2',$spec); } if(!isset($this->tableList[strtolower($table)])) $newTable = true; if(!$newTable && !isset($this->fieldList[$table])) { $this->fieldList[$table] = $this->fieldList($table); } if($newTable || !isset($this->fieldList[$table][$field])) { Profiler::mark('createField'); $this->transCreateField($table, $field, $spec); Profiler::unmark('createField'); Database::alteration_message("Field $table.$field: created as $spec","created"); } else if($this->fieldList[$table][$field] != $spec) { Profiler::mark('alterField'); $this->transAlterField($table, $field, $spec); Profiler::unmark('alterField'); Database::alteration_message("Field $table.$field: changed to $spec (from {$this->fieldList[$table][$field]})","changed"); } Profiler::unmark('requireField'); } /** * Execute a complex manipulation on the database. * A manipulation is an array of insert / or update sequences. The keys of the array are table names, * and the values are map containing 'command' and 'fields'. Command should be 'insert' or 'update', * and fields should be a map of field names to field values, including quotes. The field value can * also be a SQL function or similar. * @param array $manipulation */ function manipulate($manipulation) { foreach($manipulation as $table => $writeInfo) { if(isset($writeInfo['fields']) && $writeInfo['fields']) { $fieldList = array(); foreach($writeInfo['fields'] as $fieldName => $fieldVal) { $fieldList[] = "`$fieldName` = $fieldVal"; } $fieldList = implode(", ", $fieldList); if(!isset($writeInfo['where']) && isset($writeInfo['id'])) { $writeInfo['where'] = "ID = $writeInfo[id]"; } switch($writeInfo['command']) { case "update": $sql = "update `$table` SET $fieldList where $writeInfo[where]"; $this->query($sql); // If numAffectedRecord = 0, then we want to run instert instead if(!$this->affectedRows()) { if(!isset($writeInfo['fields']['ID']) && isset($writeInfo['id'])) { $fieldList .= ", ID = $writeInfo[id]"; } $sql = "insert into `$table` SET $fieldList"; $this->query($sql, null); } break; case "insert": if(!isset($writeInfo['fields']['ID']) && isset($writeInfo['id'])) { $fieldList .= ", ID = $writeInfo[id]"; } $fieldList = Database::replace_with_null($fieldList); $sql = "insert into `$table` SET $fieldList"; $this->query($sql); break; default: $sql = null; user_error("Database::manipulate() Can't recognise command '$writeInfo[command]'", E_USER_ERROR); } } } } /** Replaces "''" with "null", recursively walks through the given array. * @param string $array Array where the replacement should happen */ static function replace_with_null(&$array) { $array = ereg_replace('= *\'\'', "= null", $array); if(is_array($array)) { foreach($array as $key => $value) { if(is_array($value)) { array_walk($array, array(Database, 'replace_with_null')); } } } return $array; } /** * Error handler for database errors. * All database errors will call this function to report the error. It isn't a static function; * it will be called on the object itself and as such can be overridden in a subclass. * @todo hook this into a more well-structured error handling system. * @param string $msg The error message. * @param int $errorLevel The level of the error to throw. */ function databaseError($msg, $errorLevel = E_USER_ERROR) { user_error("DATABASE ERROR: $msg", $errorLevel); } /** * Enable supression of database messages. */ function quiet() { Database::$supressOutput = true; } static function alteration_message($message,$type=""){ if(!Database::$supressOutput) { $color = ""; switch ($type){ case "created": $color = "green"; break; case "obsolete": $color = "red"; break; case "error": $color = "red"; break; case "deleted": $color = "red"; break; case "changed": $color = "blue"; break; case "repaired": $color = "blue"; break; default: $color=""; } echo "
" . Convert::raw2xml($k) . " | "; } $result .= "
---|
" . Convert::raw2xml($v) . " | "; } $result .= "