mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 14:05:37 +02:00
d8e9af8af8
Database abstraction broken up into controller, connector, query builder, and schema manager, each independently configurable via YAML / Injector Creation of new DBQueryGenerator for database specific generation of SQL Support for parameterised queries, move of code base to use these over escaped conditions Refactor of SQLQuery into separate query classes for each of INSERT UPDATE DELETE and SELECT Support for PDO Installation process upgraded to use new ORM SS_DatabaseException created to handle database errors, maintaining details of raw sql and parameter details for user code designed interested in that data. Renamed DB static methods to conform correctly to naming conventions (e.g. DB::getConn -> DB::get_conn) 3.2 upgrade docs Performance Optimisation and simplification of code to use more concise API API Ability for database adapters to register extensions to ConfigureFromEnv.php
570 lines
18 KiB
PHP
570 lines
18 KiB
PHP
<?php
|
|
|
|
/**
|
|
* Represents schema management object for MySQL
|
|
*
|
|
* @package framework
|
|
* @subpackage model
|
|
*/
|
|
class MySQLSchemaManager extends DBSchemaManager {
|
|
|
|
/**
|
|
* Identifier for this schema, used for configuring schema-specific table
|
|
* creation options
|
|
*/
|
|
const ID = 'MySQL';
|
|
|
|
public function createTable($table, $fields = null, $indexes = null, $options = null, $advancedOptions = null) {
|
|
$fieldSchemas = $indexSchemas = "";
|
|
|
|
if (!empty($options[self::ID])) {
|
|
$addOptions = $options[self::ID];
|
|
} elseif (!empty($options[get_class($this)])) {
|
|
Deprecation::notice(
|
|
'3.2',
|
|
'Use MySQLSchemaManager::ID for referencing mysql-specific table creation options'
|
|
);
|
|
$addOptions = $options[get_class($this)];
|
|
} elseif (!empty($options[get_parent_class($this)])) {
|
|
Deprecation::notice(
|
|
'3.2',
|
|
'Use MySQLSchemaManager::ID for referencing mysql-specific table creation options'
|
|
);
|
|
$addOptions = $options[get_parent_class($this)];
|
|
} else {
|
|
$addOptions = "ENGINE=InnoDB";
|
|
}
|
|
|
|
if (!isset($fields['ID'])) {
|
|
$fields['ID'] = "int(11) not null auto_increment";
|
|
}
|
|
if ($fields) {
|
|
foreach ($fields as $k => $v)
|
|
$fieldSchemas .= "\"$k\" $v,\n";
|
|
}
|
|
if ($indexes) {
|
|
foreach ($indexes as $k => $v) {
|
|
$indexSchemas .= $this->getIndexSqlDefinition($k, $v) . ",\n";
|
|
}
|
|
}
|
|
|
|
// Switch to "CREATE TEMPORARY TABLE" for temporary tables
|
|
$temporary = empty($options['temporary'])
|
|
? ""
|
|
: "TEMPORARY";
|
|
|
|
$this->query("CREATE $temporary TABLE \"$table\" (
|
|
$fieldSchemas
|
|
$indexSchemas
|
|
primary key (ID)
|
|
) {$addOptions}");
|
|
|
|
return $table;
|
|
}
|
|
|
|
public function alterTable($tableName, $newFields = null, $newIndexes = null, $alteredFields = null,
|
|
$alteredIndexes = null, $alteredOptions = null, $advancedOptions = null
|
|
) {
|
|
if ($this->isView($tableName)) {
|
|
$this->alterationMessage(
|
|
sprintf("Table %s not changed as it is a view", $tableName),
|
|
"changed"
|
|
);
|
|
return;
|
|
}
|
|
$alterList = array();
|
|
|
|
if ($newFields) {
|
|
foreach ($newFields as $k => $v) {
|
|
$alterList[] .= "ADD \"$k\" $v";
|
|
}
|
|
}
|
|
if ($newIndexes) {
|
|
foreach ($newIndexes as $k => $v) {
|
|
$alterList[] .= "ADD " . $this->getIndexSqlDefinition($k, $v);
|
|
}
|
|
}
|
|
if ($alteredFields) {
|
|
foreach ($alteredFields as $k => $v) {
|
|
$alterList[] .= "CHANGE \"$k\" \"$k\" $v";
|
|
}
|
|
}
|
|
if ($alteredIndexes) {
|
|
foreach ($alteredIndexes as $k => $v) {
|
|
$alterList[] .= "DROP INDEX \"$k\"";
|
|
$alterList[] .= "ADD " . $this->getIndexSqlDefinition($k, $v);
|
|
}
|
|
}
|
|
|
|
if ($alteredOptions && isset($alteredOptions[get_class($this)])) {
|
|
$indexList = $this->indexList($tableName);
|
|
$skip = false;
|
|
foreach ($indexList as $index) {
|
|
if ($index['type'] === 'fulltext') {
|
|
$skip = true;
|
|
break;
|
|
}
|
|
}
|
|
if ($skip) {
|
|
$this->alterationMessage(
|
|
sprintf(
|
|
"Table %s options not changed to %s due to fulltextsearch index",
|
|
$tableName,
|
|
$alteredOptions[get_class($this)]
|
|
),
|
|
"changed"
|
|
);
|
|
} else {
|
|
$this->query(sprintf("ALTER TABLE \"%s\" %s", $tableName, $alteredOptions[get_class($this)]));
|
|
$this->alterationMessage(
|
|
sprintf("Table %s options changed: %s", $tableName, $alteredOptions[get_class($this)]),
|
|
"changed"
|
|
);
|
|
}
|
|
}
|
|
|
|
$alterations = implode(",\n", $alterList);
|
|
$this->query("ALTER TABLE \"$tableName\" $alterations");
|
|
}
|
|
|
|
public function isView($tableName) {
|
|
$info = $this->query("SHOW /*!50002 FULL*/ TABLES LIKE '$tableName'")->record();
|
|
return $info && strtoupper($info['Table_type']) == 'VIEW';
|
|
}
|
|
|
|
public function renameTable($oldTableName, $newTableName) {
|
|
$this->query("ALTER TABLE \"$oldTableName\" RENAME \"$newTableName\"");
|
|
}
|
|
|
|
public function checkAndRepairTable($tableName) {
|
|
// If running PDO and not in emulated mode, check table will fail
|
|
if($this->database->getConnector() instanceof PDOConnector && !PDOConnector::is_emulate_prepare()) {
|
|
$this->alterationMessage('CHECK TABLE command disabled for PDO in native mode', 'notice');
|
|
return true;
|
|
}
|
|
|
|
// Perform check
|
|
if (!$this->runTableCheckCommand("CHECK TABLE \"$tableName\"")) {
|
|
if ($this->runTableCheckCommand("CHECK TABLE \"" . strtolower($tableName) . "\"")) {
|
|
$this->alterationMessage(
|
|
"Table $tableName: renamed from lowercase",
|
|
"repaired"
|
|
);
|
|
return $this->renameTable(strtolower($tableName), $tableName);
|
|
}
|
|
|
|
$this->alterationMessage(
|
|
"Table $tableName: repaired",
|
|
"repaired"
|
|
);
|
|
return $this->runTableCheckCommand("REPAIR TABLE \"$tableName\" USE_FRM");
|
|
} else {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Helper function used by checkAndRepairTable.
|
|
* @param string $sql Query to run.
|
|
* @return boolean Returns if the query returns a successful result.
|
|
*/
|
|
protected function runTableCheckCommand($sql) {
|
|
$testResults = $this->query($sql);
|
|
foreach ($testResults as $testRecord) {
|
|
if (strtolower($testRecord['Msg_text']) != 'ok') {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
public function hasTable($table) {
|
|
// MySQLi doesn't like parameterised queries for some queries
|
|
$sqlTable = $this->database->quoteString($table);
|
|
return (bool) ($this->query("SHOW TABLES LIKE $sqlTable")->value());
|
|
}
|
|
|
|
public function createField($tableName, $fieldName, $fieldSpec) {
|
|
$this->query("ALTER TABLE \"$tableName\" ADD \"$fieldName\" $fieldSpec");
|
|
}
|
|
|
|
public function databaseList() {
|
|
return $this->query("SHOW DATABASES")->column();
|
|
}
|
|
|
|
public function databaseExists($name) {
|
|
// MySQLi doesn't like parameterised queries for some queries
|
|
$sqlName = $this->database->quoteString($name);
|
|
return !!($this->query("SHOW DATABASES LIKE $sqlName")->value());
|
|
}
|
|
|
|
public function createDatabase($name) {
|
|
$this->query("CREATE DATABASE \"$name\" DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_general_ci");
|
|
}
|
|
|
|
public function dropDatabase($name) {
|
|
$this->query("DROP DATABASE \"$name\"");
|
|
}
|
|
|
|
/**
|
|
* Change the database type of the given field.
|
|
* @param string $tableName The name of the tbale the field is in.
|
|
* @param string $fieldName The name of the field to change.
|
|
* @param string $fieldSpec The new field specification
|
|
*/
|
|
public function alterField($tableName, $fieldName, $fieldSpec) {
|
|
$this->query("ALTER TABLE \"$tableName\" CHANGE \"$fieldName\" \"$fieldName\" $fieldSpec");
|
|
}
|
|
|
|
/**
|
|
* Change the database column name of the given field.
|
|
*
|
|
* @param string $tableName The name of the tbale the field is in.
|
|
* @param string $oldName The name of the field to change.
|
|
* @param string $newName The new name of the field
|
|
*/
|
|
public function renameField($tableName, $oldName, $newName) {
|
|
$fieldList = $this->fieldList($tableName);
|
|
if (array_key_exists($oldName, $fieldList)) {
|
|
$this->query("ALTER TABLE \"$tableName\" CHANGE \"$oldName\" \"$newName\" " . $fieldList[$oldName]);
|
|
}
|
|
}
|
|
|
|
protected static $_cache_collation_info = array();
|
|
|
|
public function fieldList($table) {
|
|
$fields = $this->query("SHOW FULL FIELDS IN \"$table\"");
|
|
foreach ($fields as $field) {
|
|
|
|
// ensure that '' is converted to \' in field specification (mostly for the benefit of ENUM values)
|
|
$fieldSpec = str_replace('\'\'', '\\\'', $field['Type']);
|
|
if (!$field['Null'] || $field['Null'] == 'NO') {
|
|
$fieldSpec .= ' not null';
|
|
}
|
|
|
|
if ($field['Collation'] && $field['Collation'] != 'NULL') {
|
|
// Cache collation info to cut down on database traffic
|
|
if (!isset(self::$_cache_collation_info[$field['Collation']])) {
|
|
self::$_cache_collation_info[$field['Collation']]
|
|
= $this->query("SHOW COLLATION LIKE '{$field['Collation']}'")->record();
|
|
}
|
|
$collInfo = self::$_cache_collation_info[$field['Collation']];
|
|
$fieldSpec .= " character set $collInfo[Charset] collate $field[Collation]";
|
|
}
|
|
|
|
if ($field['Default'] || $field['Default'] === "0") {
|
|
$fieldSpec .= " default " . $this->database->quoteString($field['Default']);
|
|
}
|
|
if ($field['Extra']) $fieldSpec .= " " . $field['Extra'];
|
|
|
|
$fieldList[$field['Field']] = $fieldSpec;
|
|
}
|
|
return $fieldList;
|
|
}
|
|
|
|
/**
|
|
* Create an index on a table.
|
|
*
|
|
* @param string $tableName The name of the table.
|
|
* @param string $indexName The name of the index.
|
|
* @param string $indexSpec The specification of the index, see {@link SS_Database::requireIndex()} for more
|
|
* details.
|
|
*/
|
|
public function createIndex($tableName, $indexName, $indexSpec) {
|
|
$this->query("ALTER TABLE \"$tableName\" ADD " . $this->getIndexSqlDefinition($indexName, $indexSpec));
|
|
}
|
|
|
|
/**
|
|
* Generate SQL suitable for creating this index
|
|
*
|
|
* @param string $indexName
|
|
* @param string|array $indexSpec See {@link requireTable()} for details
|
|
* @return string MySQL compatible ALTER TABLE syntax
|
|
*/
|
|
protected function getIndexSqlDefinition($indexName, $indexSpec) {
|
|
$indexSpec = $this->parseIndexSpec($indexName, $indexSpec);
|
|
if ($indexSpec['type'] == 'using') {
|
|
return "index \"$indexName\" using ({$indexSpec['value']})";
|
|
} else {
|
|
return "{$indexSpec['type']} \"$indexName\" ({$indexSpec['value']})";
|
|
}
|
|
}
|
|
|
|
public function alterIndex($tableName, $indexName, $indexSpec) {
|
|
$indexSpec = $this->parseIndexSpec($indexName, $indexSpec);
|
|
$this->query("ALTER TABLE \"$tableName\" DROP INDEX \"$indexName\"");
|
|
$this->query("ALTER TABLE \"$tableName\" ADD {$indexSpec['type']} \"$indexName\" {$indexSpec['value']}");
|
|
}
|
|
|
|
protected function indexKey($table, $index, $spec) {
|
|
// MySQL simply uses the same index name as SilverStripe does internally
|
|
return $index;
|
|
}
|
|
|
|
public function indexList($table) {
|
|
$indexes = $this->query("SHOW INDEXES IN \"$table\"");
|
|
$groupedIndexes = array();
|
|
$indexList = array();
|
|
|
|
foreach ($indexes as $index) {
|
|
$groupedIndexes[$index['Key_name']]['fields'][$index['Seq_in_index']] = $index['Column_name'];
|
|
|
|
if ($index['Index_type'] == 'FULLTEXT') {
|
|
$groupedIndexes[$index['Key_name']]['type'] = 'fulltext';
|
|
} else if (!$index['Non_unique']) {
|
|
$groupedIndexes[$index['Key_name']]['type'] = 'unique';
|
|
} else if ($index['Index_type'] == 'HASH') {
|
|
$groupedIndexes[$index['Key_name']]['type'] = 'hash';
|
|
} else if ($index['Index_type'] == 'RTREE') {
|
|
$groupedIndexes[$index['Key_name']]['type'] = 'rtree';
|
|
} else {
|
|
$groupedIndexes[$index['Key_name']]['type'] = 'index';
|
|
}
|
|
}
|
|
|
|
if ($groupedIndexes) {
|
|
foreach ($groupedIndexes as $index => $details) {
|
|
ksort($details['fields']);
|
|
$indexList[$index] = $this->parseIndexSpec($index, array(
|
|
'name' => $index,
|
|
'value' => $this->implodeColumnList($details['fields']),
|
|
'type' => $details['type']
|
|
));
|
|
}
|
|
}
|
|
|
|
return $indexList;
|
|
}
|
|
|
|
public function tableList() {
|
|
$tables = array();
|
|
foreach ($this->query("SHOW TABLES") as $record) {
|
|
$table = reset($record);
|
|
$tables[strtolower($table)] = $table;
|
|
}
|
|
return $tables;
|
|
}
|
|
|
|
public function enumValuesForField($tableName, $fieldName) {
|
|
// Get the enum of all page types from the SiteTree table
|
|
$classnameinfo = DB::query("DESCRIBE \"$tableName\" \"$fieldName\"")->first();
|
|
preg_match_all("/'[^,]+'/", $classnameinfo["Type"], $matches);
|
|
|
|
$classes = array();
|
|
foreach ($matches[0] as $value) {
|
|
$classes[] = stripslashes(trim($value, "'"));
|
|
}
|
|
return $classes;
|
|
}
|
|
|
|
public function dbDataType($type) {
|
|
$values = Array(
|
|
'unsigned integer' => 'UNSIGNED'
|
|
);
|
|
|
|
if (isset($values[$type])) return $values[$type];
|
|
else return '';
|
|
}
|
|
|
|
/**
|
|
* Return a boolean type-formatted string
|
|
*
|
|
* @param array $values Contains a tokenised list of info about this data type
|
|
* @return string
|
|
*/
|
|
public function boolean($values) {
|
|
//For reference, this is what typically gets passed to this function:
|
|
//$parts=Array('datatype'=>'tinyint', 'precision'=>1, 'sign'=>'unsigned', 'null'=>'not null',
|
|
//'default'=>$this->default);
|
|
//DB::requireField($this->tableName, $this->name, "tinyint(1) unsigned not null default
|
|
//'{$this->defaultVal}'");
|
|
return 'tinyint(1) unsigned not null' . $this->defaultClause($values);
|
|
}
|
|
|
|
/**
|
|
* Return a date type-formatted string
|
|
* For MySQL, we simply return the word 'date', no other parameters are necessary
|
|
*
|
|
* @param array $values Contains a tokenised list of info about this data type
|
|
* @return string
|
|
*/
|
|
public function date($values) {
|
|
//For reference, this is what typically gets passed to this function:
|
|
//$parts=Array('datatype'=>'date');
|
|
//DB::requireField($this->tableName, $this->name, "date");
|
|
return 'date';
|
|
}
|
|
|
|
/**
|
|
* Return a decimal type-formatted string
|
|
*
|
|
* @param array $values Contains a tokenised list of info about this data type
|
|
* @return string
|
|
*/
|
|
public function decimal($values) {
|
|
//For reference, this is what typically gets passed to this function:
|
|
//$parts=Array('datatype'=>'decimal', 'precision'=>"$this->wholeSize,$this->decimalSize");
|
|
//DB::requireField($this->tableName, $this->name, "decimal($this->wholeSize,$this->decimalSize)");
|
|
// Avoid empty strings being put in the db
|
|
if ($values['precision'] == '') {
|
|
$precision = 1;
|
|
} else {
|
|
$precision = $values['precision'];
|
|
}
|
|
|
|
$defaultValue = '';
|
|
if (isset($values['default']) && is_numeric($values['default'])) {
|
|
$decs = strpos($precision, ',') !== false
|
|
? (int) substr($precision, strpos($precision, ',') + 1)
|
|
: 0;
|
|
$defaultValue = ' default ' . number_format($values['default'], $decs, '.', '');
|
|
}
|
|
|
|
return "decimal($precision) not null $defaultValue";
|
|
}
|
|
|
|
/**
|
|
* Return a enum type-formatted string
|
|
*
|
|
* @param array $values Contains a tokenised list of info about this data type
|
|
* @return string
|
|
*/
|
|
public function enum($values) {
|
|
//For reference, this is what typically gets passed to this function:
|
|
//$parts=Array('datatype'=>'enum', 'enums'=>$this->enum, 'character set'=>'utf8', 'collate'=>
|
|
// 'utf8_general_ci', 'default'=>$this->default);
|
|
//DB::requireField($this->tableName, $this->name, "enum('" . implode("','", $this->enum) . "') character set
|
|
// utf8 collate utf8_general_ci default '{$this->default}'");
|
|
$valuesString = implode(",", Convert::raw2sql($values['enums'], true));
|
|
return "enum($valuesString) character set utf8 collate utf8_general_ci" . $this->defaultClause($values);
|
|
}
|
|
|
|
/**
|
|
* Return a set type-formatted string
|
|
*
|
|
* @param array $values Contains a tokenised list of info about this data type
|
|
* @return string
|
|
*/
|
|
public function set($values) {
|
|
//For reference, this is what typically gets passed to this function:
|
|
//$parts=Array('datatype'=>'enum', 'enums'=>$this->enum, 'character set'=>'utf8', 'collate'=>
|
|
// 'utf8_general_ci', 'default'=>$this->default);
|
|
//DB::requireField($this->tableName, $this->name, "enum('" . implode("','", $this->enum) . "') character set
|
|
//utf8 collate utf8_general_ci default '{$this->default}'");
|
|
$valuesString = implode(",", Convert::raw2sql($values['enums'], true));
|
|
return "set($valuesString) character set utf8 collate utf8_general_ci" . $this->defaultClause($values);
|
|
}
|
|
|
|
/**
|
|
* Return a float type-formatted string
|
|
* For MySQL, we simply return the word 'date', no other parameters are necessary
|
|
*
|
|
* @param array $values Contains a tokenised list of info about this data type
|
|
* @return string
|
|
*/
|
|
public function float($values) {
|
|
//For reference, this is what typically gets passed to this function:
|
|
//$parts=Array('datatype'=>'float');
|
|
//DB::requireField($this->tableName, $this->name, "float");
|
|
return "float not null" . $this->defaultClause($values);
|
|
}
|
|
|
|
/**
|
|
* Return a int type-formatted string
|
|
*
|
|
* @param array $values Contains a tokenised list of info about this data type
|
|
* @return string
|
|
*/
|
|
public function int($values) {
|
|
//For reference, this is what typically gets passed to this function:
|
|
//$parts=Array('datatype'=>'int', 'precision'=>11, 'null'=>'not null', 'default'=>(int)$this->default);
|
|
//DB::requireField($this->tableName, $this->name, "int(11) not null default '{$this->defaultVal}'");
|
|
return "int(11) not null" . $this->defaultClause($values);
|
|
}
|
|
|
|
/**
|
|
* Return a datetime type-formatted string
|
|
* For MySQL, we simply return the word 'datetime', no other parameters are necessary
|
|
*
|
|
* @param array $values Contains a tokenised list of info about this data type
|
|
* @return string
|
|
*/
|
|
public function ss_datetime($values) {
|
|
//For reference, this is what typically gets passed to this function:
|
|
//$parts=Array('datatype'=>'datetime');
|
|
//DB::requireField($this->tableName, $this->name, $values);
|
|
return 'datetime';
|
|
}
|
|
|
|
/**
|
|
* Return a text type-formatted string
|
|
*
|
|
* @param array $values Contains a tokenised list of info about this data type
|
|
* @return string
|
|
*/
|
|
public function text($values) {
|
|
//For reference, this is what typically gets passed to this function:
|
|
//$parts=Array('datatype'=>'mediumtext', 'character set'=>'utf8', 'collate'=>'utf8_general_ci');
|
|
//DB::requireField($this->tableName, $this->name, "mediumtext character set utf8 collate utf8_general_ci");
|
|
return 'mediumtext character set utf8 collate utf8_general_ci' . $this->defaultClause($values);
|
|
}
|
|
|
|
/**
|
|
* Return a time type-formatted string
|
|
* For MySQL, we simply return the word 'time', no other parameters are necessary
|
|
*
|
|
* @param array $values Contains a tokenised list of info about this data type
|
|
* @return string
|
|
*/
|
|
public function time($values) {
|
|
//For reference, this is what typically gets passed to this function:
|
|
//$parts=Array('datatype'=>'time');
|
|
//DB::requireField($this->tableName, $this->name, "time");
|
|
return 'time';
|
|
}
|
|
|
|
/**
|
|
* Return a varchar type-formatted string
|
|
*
|
|
* @param array $values Contains a tokenised list of info about this data type
|
|
* @return string
|
|
*/
|
|
public function varchar($values) {
|
|
//For reference, this is what typically gets passed to this function:
|
|
//$parts=Array('datatype'=>'varchar', 'precision'=>$this->size, 'character set'=>'utf8', 'collate'=>
|
|
//'utf8_general_ci');
|
|
//DB::requireField($this->tableName, $this->name, "varchar($this->size) character set utf8 collate
|
|
// utf8_general_ci");
|
|
$default = $this->defaultClause($values);
|
|
return "varchar({$values['precision']}) character set utf8 collate utf8_general_ci$default";
|
|
}
|
|
|
|
/*
|
|
* Return the MySQL-proprietary 'Year' datatype
|
|
*
|
|
* @param array $values Contains a tokenised list of info about this data type
|
|
* @return string
|
|
*/
|
|
public function year($values) {
|
|
return 'year(4)';
|
|
}
|
|
|
|
public function IdColumn($asDbValue = false, $hasAutoIncPK = true) {
|
|
return 'int(11) not null auto_increment';
|
|
}
|
|
|
|
/**
|
|
* Parses and escapes the default values for a specification
|
|
*
|
|
* @param array $values Contains a tokenised list of info about this data type
|
|
* @return string Default clause
|
|
*/
|
|
protected function defaultClause($values) {
|
|
if(isset($values['default'])) {
|
|
return ' default ' . $this->database->quoteString($values['default']);
|
|
}
|
|
return '';
|
|
}
|
|
|
|
}
|