mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 12:05:37 +00: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
363 lines
11 KiB
PHP
363 lines
11 KiB
PHP
<?php
|
|
|
|
/**
|
|
* MySQL connector class.
|
|
*
|
|
* Supported indexes for {@link requireTable()}:
|
|
*
|
|
* @package framework
|
|
* @subpackage model
|
|
*/
|
|
class MySQLDatabase extends SS_Database {
|
|
|
|
/**
|
|
* Default connection charset (may be overridden in $databaseConfig)
|
|
*
|
|
* @config
|
|
* @var String
|
|
*/
|
|
private static $connection_charset = null;
|
|
|
|
public function connect($parameters) {
|
|
// Ensure that driver is available (required by PDO)
|
|
if(empty($parameters['driver'])) {
|
|
$parameters['driver'] = $this->getDatabaseServer();
|
|
}
|
|
|
|
// Set charset
|
|
if( empty($parameters['charset'])
|
|
&& ($charset = Config::inst()->get('MySQLDatabase', 'connection_charset'))
|
|
) {
|
|
$parameters['charset'] = $charset;
|
|
}
|
|
|
|
// Notify connector of parameters
|
|
$this->connector->connect($parameters);
|
|
|
|
// This is important!
|
|
$this->setSQLMode('ANSI');
|
|
|
|
if (isset($parameters['timezone'])) {
|
|
$this->selectTimezone($parameters['timezone']);
|
|
}
|
|
|
|
// SS_Database subclass maintains responsibility for selecting database
|
|
// once connected in order to correctly handle schema queries about
|
|
// existence of database, error handling at the correct level, etc
|
|
if (!empty($parameters['database'])) {
|
|
$this->selectDatabase($parameters['database'], false, 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.
|
|
*
|
|
* @deprecated 3.2 Use "MySQLDatabase.connection_charset" config setting instead
|
|
*/
|
|
public static function set_connection_charset($charset = 'utf8') {
|
|
Deprecation::notice('3.1', 'Use "MySQLDatabase.connection_charset" config setting instead');
|
|
Config::inst()->update('MySQLDatabase', 'connection_charset', $charset);
|
|
}
|
|
|
|
/**
|
|
* Sets the SQL mode
|
|
*
|
|
* @param string $mode Connection mode
|
|
*/
|
|
public function setSQLMode($mode) {
|
|
if (empty($mode)) return;
|
|
$this->preparedQuery("SET sql_mode = ?", array($mode));
|
|
}
|
|
|
|
/**
|
|
* Sets the system timezone for the database connection
|
|
*
|
|
* @param string $timezone
|
|
*/
|
|
public function selectTimezone($timezone) {
|
|
if (empty($timezone)) return;
|
|
$this->preparedQuery("SET SESSION time_zone = ?", array($timezone));
|
|
}
|
|
|
|
public function supportsCollations() {
|
|
return true;
|
|
}
|
|
|
|
public function supportsTimezoneOverride() {
|
|
return true;
|
|
}
|
|
|
|
public function getDatabaseServer() {
|
|
return "mysql";
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
) {
|
|
if (!class_exists('SiteTree'))
|
|
throw new Exception('MySQLDatabase->searchEngine() requires "SiteTree" class');
|
|
if (!class_exists('File'))
|
|
throw new Exception('MySQLDatabase->searchEngine() requires "File" class');
|
|
|
|
$fileFilter = '';
|
|
$keywords = $this->escapeString($keywords);
|
|
$htmlEntityKeywords = htmlentities($keywords, ENT_NOQUOTES, 'UTF-8');
|
|
|
|
$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, MetaDescription) AGAINST ('$keywords' $boolean)
|
|
+ MATCH (Title, MenuTitle, Content, MetaDescription) 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, MetaDescription) "
|
|
. "AGAINST ('$relevanceKeywords') "
|
|
. "+ MATCH (Title, MenuTitle, Content, MetaDescription) AGAINST ('$htmlEntityRelevanceKeywords')";
|
|
$relevance['File'] = "MATCH (Filename, Title, Content) AGAINST ('$relevanceKeywords')";
|
|
} else {
|
|
$relevance['SiteTree'] = $relevance['File'] = 1;
|
|
$match['SiteTree'] = $match['File'] = "1 = 1";
|
|
}
|
|
|
|
// Generate initial DataLists and base table names
|
|
$lists = array();
|
|
$baseClasses = array('SiteTree' => '', 'File' => '');
|
|
foreach ($classesToSearch as $class) {
|
|
$lists[$class] = DataList::create($class)->where($notMatch . $match[$class] . $extraFilters[$class], "");
|
|
$baseClasses[$class] = '"' . $class . '"';
|
|
}
|
|
|
|
// Make column selection lists
|
|
$select = array(
|
|
'SiteTree' => array(
|
|
"ClassName", "$baseClasses[SiteTree].\"ID\"", "ParentID",
|
|
"Title", "MenuTitle", "URLSegment", "Content",
|
|
"LastEdited", "Created",
|
|
"Filename" => "_utf8''", "Name" => "_utf8''",
|
|
"Relevance" => $relevance['SiteTree'], "CanViewType"
|
|
),
|
|
'File' => array(
|
|
"ClassName", "$baseClasses[File].\"ID\"", "ParentID" => "_utf8''",
|
|
"Title", "MenuTitle" => "_utf8''", "URLSegment" => "_utf8''", "Content",
|
|
"LastEdited", "Created",
|
|
"Filename", "Name",
|
|
"Relevance" => $relevance['File'], "CanViewType" => "NULL"
|
|
),
|
|
);
|
|
|
|
// Process and combine queries
|
|
$querySQLs = array();
|
|
$queryParameters = array();
|
|
$totalCount = 0;
|
|
foreach ($lists as $class => $list) {
|
|
$query = $list->dataQuery()->query();
|
|
|
|
// There's no need to do all that joining
|
|
$query->setFrom(array(str_replace(array('"', '`'), '', $baseClasses[$class]) => $baseClasses[$class]));
|
|
$query->setSelect($select[$class]);
|
|
$query->setOrderBy(array());
|
|
|
|
$querySQLs[] = $query->sql($parameters);
|
|
$queryParameters = array_merge($queryParameters, $parameters);
|
|
|
|
$totalCount += $query->unlimitedRowCount();
|
|
}
|
|
$fullQuery = implode(" UNION ", $querySQLs) . " ORDER BY $sortBy LIMIT $limit";
|
|
|
|
// Get records
|
|
$records = $this->preparedQuery($fullQuery, $queryParameters);
|
|
|
|
$objects = array();
|
|
|
|
foreach ($records as $record) {
|
|
$objects[] = new $record['ClassName']($record);
|
|
}
|
|
|
|
$list = new PaginatedList(new ArrayList($objects));
|
|
$list->setPageStart($start);
|
|
$list->setPageLength($pageLength);
|
|
$list->setTotalItems($totalCount);
|
|
|
|
// The list has already been limited by the query above
|
|
$list->setLimitItems(false);
|
|
|
|
return $list;
|
|
}
|
|
|
|
public function supportsTransactions() {
|
|
return true;
|
|
}
|
|
|
|
public function transactionStart($transactionMode = false, $sessionCharacteristics = false) {
|
|
// This sets the isolation level for the NEXT transaction, not the current one.
|
|
if ($transactionMode) {
|
|
$this->query('SET TRANSACTION ' . $transactionMode . ';');
|
|
}
|
|
|
|
$this->query('START TRANSACTION;');
|
|
|
|
if ($sessionCharacteristics) {
|
|
$this->query('SET SESSION TRANSACTION ' . $sessionCharacteristics . ';');
|
|
}
|
|
}
|
|
|
|
public function transactionSavepoint($savepoint) {
|
|
$this->query("SAVEPOINT $savepoint;");
|
|
}
|
|
|
|
public function transactionRollback($savepoint = false) {
|
|
if ($savepoint) {
|
|
$this->query('ROLLBACK TO ' . $savepoint . ';');
|
|
} else {
|
|
$this->query('ROLLBACK');
|
|
}
|
|
}
|
|
|
|
public function transactionEnd($chain = false) {
|
|
$this->query('COMMIT AND ' . ($chain ? '' : 'NO ') . 'CHAIN;');
|
|
}
|
|
|
|
public function comparisonClause($field, $value, $exact = false, $negate = false, $caseSensitive = null,
|
|
$parameterised = false
|
|
) {
|
|
if ($exact && $caseSensitive === null) {
|
|
$comp = ($negate) ? '!=' : '=';
|
|
} else {
|
|
$comp = ($caseSensitive) ? 'LIKE BINARY' : 'LIKE';
|
|
if ($negate) $comp = 'NOT ' . $comp;
|
|
}
|
|
|
|
if($parameterised) {
|
|
return sprintf("%s %s ?", $field, $comp);
|
|
} else {
|
|
return sprintf("%s %s '%s'", $field, $comp, $value);
|
|
}
|
|
}
|
|
|
|
public 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')";
|
|
}
|
|
|
|
public 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";
|
|
}
|
|
|
|
public function datetimeDifferenceClause($date1, $date2) {
|
|
// First date format
|
|
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'";
|
|
}
|
|
// Second date format
|
|
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)";
|
|
}
|
|
|
|
public function supportsLocks() {
|
|
return true;
|
|
}
|
|
|
|
public function canLock($name) {
|
|
$id = $this->getLockIdentifier($name);
|
|
return (bool) $this->query(sprintf("SELECT IS_FREE_LOCK('%s')", $id))->value();
|
|
}
|
|
|
|
public function getLock($name, $timeout = 5) {
|
|
$id = $this->getLockIdentifier($name);
|
|
|
|
// MySQL auto-releases existing locks on subsequent GET_LOCK() calls,
|
|
// in contrast to PostgreSQL and SQL Server who stack the locks.
|
|
return (bool) $this->query(sprintf("SELECT GET_LOCK('%s', %d)", $id, $timeout))->value();
|
|
}
|
|
|
|
public function releaseLock($name) {
|
|
$id = $this->getLockIdentifier($name);
|
|
return (bool) $this->query(sprintf("SELECT RELEASE_LOCK('%s')", $id))->value();
|
|
}
|
|
|
|
protected function getLockIdentifier($name) {
|
|
// Prefix with database name
|
|
$dbName = $this->connector->getSelectedDatabase() ;
|
|
return $this->escapeString("{$dbName}_{$name}");
|
|
}
|
|
|
|
public function now() {
|
|
// MySQL uses NOW() to return the current date/time.
|
|
return 'NOW()';
|
|
}
|
|
|
|
public function random() {
|
|
return 'RAND()';
|
|
}
|
|
}
|