mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 14:05:37 +02:00
a8a10f8a1a
The transactionStart() naming is 3.x, and used by some modules which are otherwise still 2.x compatible. Specifically, this was added to avoid branching the payment module into separate 2.x and 3.x compatible branches.
1114 lines
36 KiB
PHP
Executable File
1114 lines
36 KiB
PHP
Executable File
<?php
|
|
/**
|
|
* MySQL connector class.
|
|
*
|
|
* Supported indexes for {@link requireTable()}:
|
|
*
|
|
* @package sapphire
|
|
* @subpackage model
|
|
*/
|
|
class MySQLDatabase extends SS_Database {
|
|
/**
|
|
* Connection to the DBMS.
|
|
* @var resource
|
|
*/
|
|
private $dbConn;
|
|
|
|
/**
|
|
* True if we are connected to a database.
|
|
* @var boolean
|
|
*/
|
|
private $active;
|
|
|
|
/**
|
|
* The name of the database.
|
|
* @var string
|
|
*/
|
|
private $database;
|
|
|
|
private static $connection_charset = null;
|
|
|
|
private $supportsTransactions=false;
|
|
|
|
/**
|
|
* Sets the character set for the MySQL database connection.
|
|
*
|
|
* The character set connection should be set to 'utf8' for SilverStripe version 2.4.0 and
|
|
* later.
|
|
*
|
|
* However, sites created before version 2.4.0 should leave this unset or data that isn't 7-bit
|
|
* safe will be corrupted. As such, the installer comes with this set in mysite/_config.php by
|
|
* default in versions 2.4.0 and later.
|
|
*/
|
|
public static function set_connection_charset($charset = 'utf8') {
|
|
self::$connection_charset = $charset;
|
|
}
|
|
|
|
/**
|
|
* Connect to a MySQL database.
|
|
* @param array $parameters An map of parameters, which should include:
|
|
* - server: The server, eg, localhost
|
|
* - username: The username to log on with
|
|
* - password: The password to log on with
|
|
* - database: The database to connect to
|
|
* - timezone: (optional) The timezone offset. For example: +12:00, "Pacific/Auckland", or "SYSTEM"
|
|
*/
|
|
public function __construct($parameters) {
|
|
$this->dbConn = mysql_connect($parameters['server'], $parameters['username'], $parameters['password'], true);
|
|
|
|
if(self::$connection_charset) {
|
|
$this->query("SET CHARACTER SET '" . self::$connection_charset . "'");
|
|
$this->query("SET NAMES '" . self::$connection_charset . "'");
|
|
}
|
|
|
|
$this->active = mysql_select_db($parameters['database'], $this->dbConn);
|
|
$this->database = $parameters['database'];
|
|
|
|
if(!$this->dbConn) {
|
|
$this->databaseError("Couldn't connect to MySQL database");
|
|
}
|
|
|
|
if(isset($parameters['timezone'])) $this->query(sprintf("SET SESSION time_zone = '%s'", $parameters['timezone']));
|
|
$this->query("SET sql_mode = 'ANSI'");
|
|
}
|
|
|
|
/**
|
|
* Not implemented, needed for PDO
|
|
*/
|
|
public function getConnect($parameters) {
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Returns true if this database supports collations
|
|
* @return boolean
|
|
*/
|
|
public function supportsCollations() {
|
|
return $this->getVersion() >= 4.1;
|
|
}
|
|
|
|
/**
|
|
* Get the version of MySQL.
|
|
* @return string
|
|
*/
|
|
public function getVersion() {
|
|
return mysql_get_server_info($this->dbConn);
|
|
}
|
|
|
|
/**
|
|
* Get the database server, namely mysql.
|
|
* @return string
|
|
*/
|
|
public function getDatabaseServer() {
|
|
return "mysql";
|
|
}
|
|
|
|
public function query($sql, $errorLevel = E_USER_ERROR) {
|
|
if(isset($_REQUEST['previewwrite']) && in_array(strtolower(substr($sql,0,strpos($sql,' '))), array('insert','update','delete','replace'))) {
|
|
Debug::message("Will execute: $sql");
|
|
return;
|
|
}
|
|
|
|
if(isset($_REQUEST['showqueries'])) {
|
|
$starttime = microtime(true);
|
|
}
|
|
|
|
$handle = mysql_query($sql, $this->dbConn);
|
|
|
|
if(isset($_REQUEST['showqueries'])) {
|
|
$endtime = round(microtime(true) - $starttime,4);
|
|
Debug::message("\n$sql\n{$endtime}ms\n", false);
|
|
}
|
|
|
|
if(!$handle && $errorLevel) $this->databaseError("Couldn't run query: $sql | " . mysql_error($this->dbConn), $errorLevel);
|
|
return new MySQLQuery($this, $handle);
|
|
}
|
|
|
|
public function getGeneratedID($table) {
|
|
return mysql_insert_id($this->dbConn);
|
|
}
|
|
|
|
public function isActive() {
|
|
return $this->active ? true : false;
|
|
}
|
|
|
|
public function createDatabase() {
|
|
$this->query("CREATE DATABASE `$this->database`");
|
|
$this->query("USE `$this->database`");
|
|
|
|
$this->tableList = $this->fieldList = $this->indexList = null;
|
|
|
|
if(mysql_select_db($this->database, $this->dbConn)) {
|
|
$this->active = true;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Drop the database that this object is currently connected to.
|
|
* Use with caution.
|
|
*/
|
|
public function dropDatabase() {
|
|
$this->dropDatabaseByName($this->database);
|
|
}
|
|
|
|
/**
|
|
* Drop the database that this object is currently connected to.
|
|
* Use with caution.
|
|
*/
|
|
public function dropDatabaseByName($dbName) {
|
|
$this->query("DROP DATABASE \"$dbName\"");
|
|
}
|
|
|
|
/**
|
|
* Returns the name of the currently selected database
|
|
*/
|
|
public function currentDatabase() {
|
|
return $this->database;
|
|
}
|
|
|
|
/**
|
|
* Switches to the given database.
|
|
* If the database doesn't exist, you should call createDatabase() after calling selectDatabase()
|
|
*/
|
|
public function selectDatabase($dbname) {
|
|
$this->database = $dbname;
|
|
if($this->databaseExists($this->database)) {
|
|
if(mysql_select_db($this->database, $this->dbConn)) $this->active = true;
|
|
}
|
|
$this->tableList = $this->fieldList = $this->indexList = null;
|
|
}
|
|
|
|
/**
|
|
* Returns true if the named database exists.
|
|
*/
|
|
public function databaseExists($name) {
|
|
$SQL_name = Convert::raw2sql($name);
|
|
return $this->query("SHOW DATABASES LIKE '$SQL_name'")->value() ? true : false;
|
|
}
|
|
|
|
/**
|
|
* Returns a column
|
|
*/
|
|
public function allDatabaseNames() {
|
|
return $this->query("SHOW DATABASES")->column();
|
|
}
|
|
|
|
/**
|
|
* Create a new table.
|
|
* @param $tableName The name of the table
|
|
* @param $fields A map of field names to field types
|
|
* @param $indexes A map of indexes
|
|
* @param $options An map of additional options. The available keys are as follows:
|
|
* - 'MSSQLDatabase'/'MySQLDatabase'/'PostgreSQLDatabase' - database-specific options such as "engine" for MySQL.
|
|
* - 'temporary' - If true, then a temporary table will be created
|
|
* @return The table name generated. This may be different from the table name, for example with temporary tables.
|
|
*/
|
|
public function createTable($table, $fields = null, $indexes = null, $options = null, $advancedOptions = null) {
|
|
$fieldSchemas = $indexSchemas = "";
|
|
|
|
$addOptions = empty($options[get_class($this)]) ? "ENGINE=MyISAM" : $options[get_class($this)];
|
|
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* Alter a table's schema.
|
|
* @param $table The name of the table to alter
|
|
* @param $newFields New fields, a map of field name => field schema
|
|
* @param $newIndexes New indexes, a map of index name => index type
|
|
* @param $alteredFields Updated fields, a map of field name => field schema
|
|
* @param $alteredIndexes Updated indexes, a map of index name => index type
|
|
* @param $alteredOptions
|
|
*/
|
|
public function alterTable($tableName, $newFields = null, $newIndexes = null, $alteredFields = null, $alteredIndexes = null, $alteredOptions = null, $advancedOptions = null) {
|
|
$fieldSchemas = $indexSchemas = "";
|
|
$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);
|
|
}
|
|
|
|
$alterations = implode(",\n", $alterList);
|
|
$this->query("ALTER TABLE \"$tableName\" $alterations");
|
|
|
|
if($alteredOptions && isset($alteredOptions[get_class($this)])) {
|
|
$this->query(sprintf("ALTER TABLE \"%s\" %s", $tableName, $alteredOptions[get_class($this)]));
|
|
DB::alteration_message(
|
|
sprintf("Table %s options changed: %s", $tableName, $alteredOptions[get_class($this)]),
|
|
"changed"
|
|
);
|
|
}
|
|
}
|
|
|
|
public function renameTable($oldTableName, $newTableName) {
|
|
$this->query("ALTER TABLE \"$oldTableName\" RENAME \"$newTableName\"");
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Checks a table's integrity and repairs it if necessary.
|
|
* @var string $tableName The name of the table.
|
|
* @return boolean Return true if the table has integrity after the method is complete.
|
|
*/
|
|
public function checkAndRepairTable($tableName) {
|
|
if(!$this->runTableCheckCommand("CHECK TABLE \"$tableName\"")) {
|
|
if($this->runTableCheckCommand("CHECK TABLE \"".strtolower($tableName)."\"")){
|
|
DB::alteration_message("Table $tableName: renamed from lowercase","repaired");
|
|
return $this->renameTable(strtolower($tableName),$tableName);
|
|
}
|
|
|
|
DB::alteration_message("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 createField($tableName, $fieldName, $fieldSpec) {
|
|
$this->query("ALTER TABLE \"$tableName\" ADD \"$fieldName\" $fieldSpec");
|
|
}
|
|
|
|
/**
|
|
* 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]);
|
|
}
|
|
}
|
|
|
|
private static $_cache_collation_info = array();
|
|
|
|
public function fieldList($table) {
|
|
$fields = DB::query("SHOW FULL FIELDS IN \"$table\"");
|
|
foreach($fields as $field) {
|
|
$fieldSpec = $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']] = DB::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") {
|
|
if(is_numeric($field['Default']))
|
|
$fieldSpec .= " default " . Convert::raw2sql($field['Default']);
|
|
else
|
|
$fieldSpec .= " default '" . Convert::raw2sql($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));
|
|
}
|
|
|
|
/**
|
|
* This takes the index spec which has been provided by a class (ie static $indexes = blah blah)
|
|
* and turns it into a proper string.
|
|
* Some indexes may be arrays, such as fulltext and unique indexes, and this allows database-specific
|
|
* arrays to be created. See {@link requireTable()} for details on the index format.
|
|
*
|
|
* @see http://dev.mysql.com/doc/refman/5.0/en/create-index.html
|
|
*
|
|
* @param string|array $indexSpec
|
|
* @return string MySQL compatible ALTER TABLE syntax
|
|
*/
|
|
public function convertIndexSpec($indexSpec){
|
|
if(is_array($indexSpec)){
|
|
//Here we create a db-specific version of whatever index we need to create.
|
|
switch($indexSpec['type']){
|
|
case 'fulltext':
|
|
$indexSpec='fulltext (' . str_replace(' ', '', $indexSpec['value']) . ')';
|
|
break;
|
|
case 'unique':
|
|
$indexSpec='unique (' . $indexSpec['value'] . ')';
|
|
break;
|
|
case 'btree':
|
|
$indexSpec='using btree (' . $indexSpec['value'] . ')';
|
|
break;
|
|
case 'hash':
|
|
$indexSpec='using hash (' . $indexSpec['value'] . ')';
|
|
break;
|
|
}
|
|
}
|
|
|
|
return $indexSpec;
|
|
}
|
|
|
|
/**
|
|
* @param string $indexName
|
|
* @param string|array $indexSpec See {@link requireTable()} for details
|
|
* @return string MySQL compatible ALTER TABLE syntax
|
|
*/
|
|
protected function getIndexSqlDefinition($indexName, $indexSpec=null) {
|
|
|
|
$indexSpec=$this->convertIndexSpec($indexSpec);
|
|
|
|
$indexSpec = trim($indexSpec);
|
|
if($indexSpec[0] != '(') list($indexType, $indexFields) = explode(' ',$indexSpec,2);
|
|
else $indexFields = $indexSpec;
|
|
|
|
if(!isset($indexType))
|
|
$indexType = "index";
|
|
|
|
if($indexType=='using')
|
|
return "index \"$indexName\" using $indexFields";
|
|
else {
|
|
return "$indexType \"$indexName\" $indexFields";
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* MySQL does not need any transformations done on the index that's created, so we can just return it as-is
|
|
*/
|
|
function getDbSqlDefinition($tableName, $indexName, $indexSpec){
|
|
return $indexName;
|
|
}
|
|
|
|
/**
|
|
* Alter 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 alterIndex($tableName, $indexName, $indexSpec) {
|
|
|
|
$indexSpec=$this->convertIndexSpec($indexSpec);
|
|
|
|
$indexSpec = trim($indexSpec);
|
|
if($indexSpec[0] != '(') {
|
|
list($indexType, $indexFields) = explode(' ',$indexSpec,2);
|
|
} else {
|
|
$indexFields = $indexSpec;
|
|
}
|
|
|
|
if(!$indexType) {
|
|
$indexType = "index";
|
|
}
|
|
|
|
$this->query("ALTER TABLE \"$tableName\" DROP INDEX \"$indexName\"");
|
|
$this->query("ALTER TABLE \"$tableName\" ADD $indexType \"$indexName\" $indexFields");
|
|
}
|
|
|
|
/**
|
|
* Return the list of indexes in a table.
|
|
* @param string $table The table name.
|
|
* @return array
|
|
*/
|
|
public function indexList($table) {
|
|
$indexes = DB::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'] = '';
|
|
}
|
|
}
|
|
|
|
if($groupedIndexes) {
|
|
foreach($groupedIndexes as $index => $details) {
|
|
ksort($details['fields']);
|
|
$indexList[$index] = $details['type'] . '(' . implode(',',$details['fields']) . ')';
|
|
}
|
|
}
|
|
|
|
return $indexList;
|
|
}
|
|
|
|
/**
|
|
* Returns a list of all the tables in the database.
|
|
* @return array
|
|
*/
|
|
public function tableList() {
|
|
$tables = array();
|
|
foreach($this->query("SHOW TABLES") as $record) {
|
|
$table = reset($record);
|
|
$tables[strtolower($table)] = $table;
|
|
}
|
|
return $tables;
|
|
}
|
|
|
|
/**
|
|
* Return the number of rows affected by the previous operation.
|
|
* @return int
|
|
*/
|
|
public function affectedRows() {
|
|
return mysql_affected_rows($this->dbConn);
|
|
}
|
|
|
|
function databaseError($msg, $errorLevel = E_USER_ERROR) {
|
|
// try to extract and format query
|
|
if(preg_match('/Couldn\'t run query: ([^\|]*)\|\s*(.*)/', $msg, $matches)) {
|
|
$formatter = new SQLFormatter();
|
|
$msg = "Couldn't run query: \n" . $formatter->formatPlain($matches[1]) . "\n\n" . $matches[2];
|
|
}
|
|
|
|
user_error($msg, $errorLevel);
|
|
}
|
|
|
|
/**
|
|
* 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 default ' . (int)$values['default'];
|
|
}
|
|
|
|
/**
|
|
* 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'])) {
|
|
$defaultValue = ' default ' . $values['default'];
|
|
}
|
|
|
|
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}'");
|
|
|
|
return 'enum(\'' . implode('\',\'', $values['enums']) . '\') character set utf8 collate utf8_general_ci default \'' . $values['default'] . '\'';
|
|
}
|
|
|
|
/**
|
|
* 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}'");
|
|
$default = empty($values['default']) ? '' : " default '$values[default]'";
|
|
return 'set(\'' . implode('\',\'', $values['enums']) . '\') character set utf8 collate utf8_general_ci' . $default;
|
|
}
|
|
|
|
/**
|
|
* 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 default ' . $values['default'];
|
|
}
|
|
|
|
/**
|
|
* 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 default ' . (int)$values['default'];
|
|
}
|
|
|
|
/**
|
|
* 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';
|
|
}
|
|
|
|
/**
|
|
* 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");
|
|
|
|
return 'varchar(' . $values['precision'] . ') character set utf8 collate utf8_general_ci';
|
|
}
|
|
|
|
/*
|
|
* Return the MySQL-proprietary 'Year' datatype
|
|
*/
|
|
public function year($values){
|
|
return 'year(4)';
|
|
}
|
|
/**
|
|
* This returns the column which is the primary key for each table
|
|
* In Postgres, it is a SERIAL8, which is the equivalent of an auto_increment
|
|
*
|
|
* @return string
|
|
*/
|
|
function IdColumn(){
|
|
return 'int(11) not null auto_increment';
|
|
}
|
|
|
|
/**
|
|
* Returns the SQL command to get all the tables in this database
|
|
*/
|
|
function allTablesSQL(){
|
|
return "SHOW TABLES;";
|
|
}
|
|
|
|
/**
|
|
* Returns true if the given table is exists in the current database
|
|
* NOTE: Experimental; introduced for db-abstraction and may changed before 2.4 is released.
|
|
*/
|
|
public function hasTable($table) {
|
|
$SQL_table = Convert::raw2sql($table);
|
|
return (bool)($this->query("SHOW TABLES LIKE '$SQL_table'")->value());
|
|
}
|
|
|
|
/**
|
|
* Returns the values of the given enum field
|
|
* NOTE: Experimental; introduced for db-abstraction and may changed before 2.4 is released.
|
|
*/
|
|
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[] = trim($value, "'");
|
|
}
|
|
return $classes;
|
|
}
|
|
|
|
/**
|
|
* The core search engine, used by this class and its subclasses to do fun stuff.
|
|
* Searches both SiteTree and File.
|
|
*
|
|
* @param string $keywords Keywords as a string.
|
|
*/
|
|
public function searchEngine($classesToSearch, $keywords, $start, $pageLength, $sortBy = "Relevance DESC", $extraFilter = "", $booleanSearch = false, $alternativeFileFilter = "", $invertedMatch = false) {
|
|
$fileFilter = '';
|
|
$keywords = Convert::raw2sql($keywords);
|
|
$htmlEntityKeywords = htmlentities($keywords,ENT_NOQUOTES);
|
|
|
|
$extraFilters = array('SiteTree' => '', 'File' => '');
|
|
|
|
if($booleanSearch) $boolean = "IN BOOLEAN MODE";
|
|
|
|
if($extraFilter) {
|
|
$extraFilters['SiteTree'] = " AND $extraFilter";
|
|
|
|
if($alternativeFileFilter) $extraFilters['File'] = " AND $alternativeFileFilter";
|
|
else $extraFilters['File'] = $extraFilters['SiteTree'];
|
|
}
|
|
|
|
// Always ensure that only pages with ShowInSearch = 1 can be searched
|
|
$extraFilters['SiteTree'] .= " AND ShowInSearch <> 0";
|
|
|
|
// File.ShowInSearch was added later, keep the database driver backwards compatible
|
|
// by checking for its existence first
|
|
$fields = $this->fieldList('File');
|
|
if(array_key_exists('ShowInSearch', $fields)) $extraFilters['File'] .= " AND ShowInSearch <> 0";
|
|
|
|
$limit = $start . ", " . (int) $pageLength;
|
|
|
|
$notMatch = $invertedMatch ? "NOT " : "";
|
|
if($keywords) {
|
|
$match['SiteTree'] = "
|
|
MATCH (Title, MenuTitle, Content, MetaTitle, MetaDescription, MetaKeywords) AGAINST ('$keywords' $boolean)
|
|
+ MATCH (Title, MenuTitle, Content, MetaTitle, MetaDescription, MetaKeywords) AGAINST ('$htmlEntityKeywords' $boolean)
|
|
";
|
|
$match['File'] = "MATCH (Filename, Title, Content) AGAINST ('$keywords' $boolean) AND ClassName = 'File'";
|
|
|
|
// We make the relevance search by converting a boolean mode search into a normal one
|
|
$relevanceKeywords = str_replace(array('*','+','-'),'',$keywords);
|
|
$htmlEntityRelevanceKeywords = str_replace(array('*','+','-'),'',$htmlEntityKeywords);
|
|
$relevance['SiteTree'] = "MATCH (Title, MenuTitle, Content, MetaTitle, MetaDescription, MetaKeywords) AGAINST ('$relevanceKeywords') + MATCH (Title, MenuTitle, Content, MetaTitle, MetaDescription, MetaKeywords) AGAINST ('$htmlEntityRelevanceKeywords')";
|
|
$relevance['File'] = "MATCH (Filename, Title, Content) AGAINST ('$relevanceKeywords')";
|
|
} else {
|
|
$relevance['SiteTree'] = $relevance['File'] = 1;
|
|
$match['SiteTree'] = $match['File'] = "1 = 1";
|
|
}
|
|
|
|
// Generate initial queries and base table names
|
|
$baseClasses = array('SiteTree' => '', 'File' => '');
|
|
foreach($classesToSearch as $class) {
|
|
$queries[$class] = singleton($class)->extendedSQL($notMatch . $match[$class] . $extraFilters[$class], "");
|
|
$baseClasses[$class] = reset($queries[$class]->from);
|
|
}
|
|
|
|
// Make column selection lists
|
|
$select = array(
|
|
'SiteTree' => array("ClassName","$baseClasses[SiteTree].ID","ParentID","Title","MenuTitle","URLSegment","Content","LastEdited","Created","_utf8'' AS Filename", "_utf8'' AS Name", "$relevance[SiteTree] AS Relevance", "CanViewType"),
|
|
'File' => array("ClassName","$baseClasses[File].ID","_utf8'' AS ParentID","Title","_utf8'' AS MenuTitle","_utf8'' AS URLSegment","Content","LastEdited","Created","Filename","Name","$relevance[File] AS Relevance","NULL AS CanViewType"),
|
|
);
|
|
|
|
// Process queries
|
|
foreach($classesToSearch as $class) {
|
|
// There's no need to do all that joining
|
|
$queries[$class]->from = array(str_replace('`','',$baseClasses[$class]) => $baseClasses[$class]);
|
|
$queries[$class]->select = $select[$class];
|
|
$queries[$class]->orderby = null;
|
|
}
|
|
|
|
// Combine queries
|
|
$querySQLs = array();
|
|
$totalCount = 0;
|
|
foreach($queries as $query) {
|
|
$querySQLs[] = $query->sql();
|
|
$totalCount += $query->unlimitedRowCount();
|
|
}
|
|
$fullQuery = implode(" UNION ", $querySQLs) . " ORDER BY $sortBy LIMIT $limit";
|
|
|
|
// Get records
|
|
$records = DB::query($fullQuery);
|
|
|
|
foreach($records as $record)
|
|
$objects[] = new $record['ClassName']($record);
|
|
|
|
|
|
if(isset($objects)) $doSet = new DataObjectSet($objects);
|
|
else $doSet = new DataObjectSet();
|
|
|
|
$doSet->setPageLimits($start, $pageLength, $totalCount);
|
|
return $doSet;
|
|
}
|
|
|
|
/**
|
|
* MySQL uses NOW() to return the current date/time.
|
|
*/
|
|
function now(){
|
|
return 'NOW()';
|
|
}
|
|
|
|
/*
|
|
* Returns the database-specific version of the random() function
|
|
*/
|
|
function random(){
|
|
return 'RAND()';
|
|
}
|
|
|
|
/*
|
|
* This is a lookup table for data types.
|
|
* For instance, Postgres uses 'INT', while MySQL uses 'UNSIGNED'
|
|
* So this is a DB-specific list of equivilents.
|
|
*/
|
|
function dbDataType($type){
|
|
$values=Array(
|
|
'unsigned integer'=>'UNSIGNED'
|
|
);
|
|
|
|
if(isset($values[$type]))
|
|
return $values[$type];
|
|
else return '';
|
|
}
|
|
|
|
/*
|
|
* This will return text which has been escaped in a database-friendly manner.
|
|
*/
|
|
function addslashes($value){
|
|
return mysql_real_escape_string($value, $this->dbConn);
|
|
}
|
|
|
|
/*
|
|
* This changes the index name depending on database requirements.
|
|
* MySQL doesn't need any changes.
|
|
*/
|
|
function modifyIndex($index){
|
|
return $index;
|
|
}
|
|
|
|
/**
|
|
* Returns a SQL fragment for querying a fulltext search index
|
|
* @param $fields array The list of field names to search on
|
|
* @param $keywords string The search query
|
|
* @param $booleanSearch A MySQL-specific flag to switch to boolean search
|
|
*/
|
|
function fullTextSearchSQL($fields, $keywords, $booleanSearch = false) {
|
|
$boolean = $booleanSearch ? "IN BOOLEAN MODE" : "";
|
|
$fieldNames = '"' . implode('", "', $fields) . '"';
|
|
|
|
$SQL_keywords = Convert::raw2sql($keywords);
|
|
$SQL_htmlEntityKeywords = Convert::raw2sql(htmlentities($keywords));
|
|
|
|
return "(MATCH ($fieldNames) AGAINST ('$SQL_keywords' $boolean) + MATCH ($fieldNames) AGAINST ('$SQL_htmlEntityKeywords' $boolean))";
|
|
}
|
|
|
|
/*
|
|
* Does this database support transactions?
|
|
*/
|
|
public function supportsTransactions(){
|
|
return $this->supportsTransactions;
|
|
}
|
|
|
|
/*
|
|
* This is a quick lookup to discover if the database supports particular extensions
|
|
* Currently, MySQL supports no extensions
|
|
*/
|
|
public function supportsExtensions($extensions=Array('partitions', 'tablespaces', 'clustering')){
|
|
if(isset($extensions['partitions']))
|
|
return false;
|
|
elseif(isset($extensions['tablespaces']))
|
|
return false;
|
|
elseif(isset($extensions['clustering']))
|
|
return false;
|
|
else
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Forwards compatible NO-OP method for better cross release
|
|
* compatibility of modules (2.x and 3.x).
|
|
*/
|
|
public function transactionStart($transaction_mode=false, $session_characteristics=false) {
|
|
// NO-OP
|
|
}
|
|
|
|
/*
|
|
* Start a prepared transaction
|
|
* See http://developer.postgresql.org/pgdocs/postgres/sql-set-transaction.html for details on transaction isolation options
|
|
*/
|
|
public function startTransaction($transaction_mode=false, $session_characteristics=false){
|
|
//Transactions not set up for MySQL yet
|
|
}
|
|
|
|
/*
|
|
* Create a savepoint that you can jump back to if you encounter problems
|
|
*/
|
|
public function transactionSavepoint($savepoint){
|
|
//Transactions not set up for MySQL yet
|
|
}
|
|
|
|
/*
|
|
* Rollback or revert to a savepoint if your queries encounter problems
|
|
* If you encounter a problem at any point during a transaction, you may
|
|
* need to rollback that particular query, or return to a savepoint
|
|
*/
|
|
public function transactionRollback($savepoint=false){
|
|
//Transactions not set up for MySQL yet
|
|
}
|
|
|
|
/**
|
|
* Forwards compatible NO-OP method for better cross release
|
|
* compatibility of modules (2.x and 3.x).
|
|
*/
|
|
public function transactionEnd($transaction_mode=false, $session_characteristics=false) {
|
|
// NO-OP
|
|
}
|
|
|
|
/*
|
|
* Commit everything inside this transaction so far
|
|
*/
|
|
public function endTransaction(){
|
|
//Transactions not set up for MySQL yet
|
|
}
|
|
|
|
/**
|
|
* Function to return an SQL datetime expression that can be used with MySQL
|
|
* used for querying a datetime in a certain format
|
|
* @param string $date to be formated, can be either 'now', literal datetime like '1973-10-14 10:30:00' or field name, e.g. '"SiteTree"."Created"'
|
|
* @param string $format to be used, supported specifiers:
|
|
* %Y = Year (four digits)
|
|
* %m = Month (01..12)
|
|
* %d = Day (01..31)
|
|
* %H = Hour (00..23)
|
|
* %i = Minutes (00..59)
|
|
* %s = Seconds (00..59)
|
|
* %U = unix timestamp, can only be used on it's own
|
|
* @return string SQL datetime expression to query for a formatted datetime
|
|
*/
|
|
function formattedDatetimeClause($date, $format) {
|
|
|
|
preg_match_all('/%(.)/', $format, $matches);
|
|
foreach($matches[1] as $match) if(array_search($match, array('Y','m','d','H','i','s','U')) === false) user_error('formattedDatetimeClause(): unsupported format character %' . $match, E_USER_WARNING);
|
|
|
|
if(preg_match('/^now$/i', $date)) {
|
|
$date = "NOW()";
|
|
} else if(preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/i', $date)) {
|
|
$date = "'$date'";
|
|
}
|
|
|
|
if($format == '%U') return "UNIX_TIMESTAMP($date)";
|
|
|
|
return "DATE_FORMAT($date, '$format')";
|
|
|
|
}
|
|
|
|
/**
|
|
* Function to return an SQL datetime expression that can be used with MySQL
|
|
* used for querying a datetime addition
|
|
* @param string $date, can be either 'now', literal datetime like '1973-10-14 10:30:00' or field name, e.g. '"SiteTree"."Created"'
|
|
* @param string $interval to be added, use the format [sign][integer] [qualifier], e.g. -1 Day, +15 minutes, +1 YEAR
|
|
* supported qualifiers:
|
|
* - years
|
|
* - months
|
|
* - days
|
|
* - hours
|
|
* - minutes
|
|
* - seconds
|
|
* This includes the singular forms as well
|
|
* @return string SQL datetime expression to query for a datetime (YYYY-MM-DD hh:mm:ss) which is the result of the addition
|
|
*/
|
|
function datetimeIntervalClause($date, $interval) {
|
|
|
|
$interval = preg_replace('/(year|month|day|hour|minute|second)s/i', '$1', $interval);
|
|
|
|
if(preg_match('/^now$/i', $date)) {
|
|
$date = "NOW()";
|
|
} else if(preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/i', $date)) {
|
|
$date = "'$date'";
|
|
}
|
|
|
|
return "$date + INTERVAL $interval";
|
|
}
|
|
|
|
/**
|
|
* Function to return an SQL datetime expression that can be used with MySQL
|
|
* used for querying a datetime substraction
|
|
* @param string $date1, can be either 'now', literal datetime like '1973-10-14 10:30:00' or field name, e.g. '"SiteTree"."Created"'
|
|
* @param string $date2 to be substracted of $date1, can be either 'now', literal datetime like '1973-10-14 10:30:00' or field name, e.g. '"SiteTree"."Created"'
|
|
* @return string SQL datetime expression to query for the interval between $date1 and $date2 in seconds which is the result of the substraction
|
|
*/
|
|
function datetimeDifferenceClause($date1, $date2) {
|
|
|
|
if(preg_match('/^now$/i', $date1)) {
|
|
$date1 = "NOW()";
|
|
} else if(preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/i', $date1)) {
|
|
$date1 = "'$date1'";
|
|
}
|
|
|
|
if(preg_match('/^now$/i', $date2)) {
|
|
$date2 = "NOW()";
|
|
} else if(preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/i', $date2)) {
|
|
$date2 = "'$date2'";
|
|
}
|
|
|
|
return "UNIX_TIMESTAMP($date1) - UNIX_TIMESTAMP($date2)";
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A result-set from a MySQL database.
|
|
* @package sapphire
|
|
* @subpackage model
|
|
*/
|
|
class MySQLQuery extends SS_Query {
|
|
/**
|
|
* The MySQLDatabase object that created this result set.
|
|
* @var MySQLDatabase
|
|
*/
|
|
private $database;
|
|
|
|
/**
|
|
* The internal MySQL handle that points to the result set.
|
|
* @var resource
|
|
*/
|
|
private $handle;
|
|
|
|
/**
|
|
* Hook the result-set given into a Query class, suitable for use by sapphire.
|
|
* @param database The database object that created this query.
|
|
* @param handle the internal mysql handle that is points to the resultset.
|
|
*/
|
|
public function __construct(MySQLDatabase $database, $handle) {
|
|
$this->database = $database;
|
|
$this->handle = $handle;
|
|
}
|
|
|
|
public function __destruct() {
|
|
if(is_resource($this->handle)) mysql_free_result($this->handle);
|
|
}
|
|
|
|
public function seek($row) {
|
|
return is_resource($this->handle) ? mysql_data_seek($this->handle, $row) : false;
|
|
}
|
|
|
|
public function numRecords() {
|
|
return is_resource($this->handle) ? mysql_num_rows($this->handle) : false;
|
|
}
|
|
|
|
public function nextRecord() {
|
|
// Coalesce rather than replace common fields.
|
|
if(is_resource($this->handle) && $data = mysql_fetch_row($this->handle)) {
|
|
foreach($data as $columnIdx => $value) {
|
|
$columnName = mysql_field_name($this->handle, $columnIdx);
|
|
// $value || !$ouput[$columnName] means that the *last* occurring value is shown
|
|
// !$ouput[$columnName] means that the *first* occurring value is shown
|
|
if(isset($value) || !isset($output[$columnName])) {
|
|
$output[$columnName] = $value;
|
|
}
|
|
}
|
|
return $output;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
|
|
}
|
|
|
|
?>
|