Merge pull request #15 from helpfulrobot/convert-to-psr-2

Converted to PSR-2
This commit is contained in:
Damian Mooyman 2015-12-18 10:05:28 +13:00
commit 608458ccb8
6 changed files with 1672 additions and 1497 deletions

View File

@ -5,167 +5,182 @@
* *
* @package SQLite3 * @package SQLite3
*/ */
class SQLite3Connector extends DBConnector { class SQLite3Connector extends DBConnector
{
/** /**
* The name of the database. * The name of the database.
* *
* @var string * @var string
*/ */
protected $databaseName; protected $databaseName;
/** /**
* Connection to the DBMS. * Connection to the DBMS.
* *
* @var SQLite3 * @var SQLite3
*/ */
protected $dbConn; protected $dbConn;
public function connect($parameters, $selectDB = false) { public function connect($parameters, $selectDB = false)
$file = $parameters['filepath']; {
$this->dbConn = empty($parameters['key']) $file = $parameters['filepath'];
? new SQLite3($file, SQLITE3_OPEN_READWRITE | SQLITE3_OPEN_CREATE) $this->dbConn = empty($parameters['key'])
: new SQLite3($file, SQLITE3_OPEN_READWRITE | SQLITE3_OPEN_CREATE, $parameters['key']); ? new SQLite3($file, SQLITE3_OPEN_READWRITE | SQLITE3_OPEN_CREATE)
$this->dbConn->busyTimeout(60000); : new SQLite3($file, SQLITE3_OPEN_READWRITE | SQLITE3_OPEN_CREATE, $parameters['key']);
$this->databaseName = $parameters['database']; $this->dbConn->busyTimeout(60000);
} $this->databaseName = $parameters['database'];
}
public function affectedRows() { public function affectedRows()
return $this->dbConn->changes(); {
} return $this->dbConn->changes();
}
public function getGeneratedID($table) { public function getGeneratedID($table)
return $this->dbConn->lastInsertRowID(); {
} return $this->dbConn->lastInsertRowID();
}
public function getLastError() { public function getLastError()
$message = $this->dbConn->lastErrorMsg(); {
return $message === 'not an error' ? null : $message; $message = $this->dbConn->lastErrorMsg();
} return $message === 'not an error' ? null : $message;
}
public function getSelectedDatabase() { public function getSelectedDatabase()
return $this->databaseName; {
} return $this->databaseName;
}
public function getVersion() { public function getVersion()
$version = SQLite3::version(); {
return trim($version['versionString']); $version = SQLite3::version();
} return trim($version['versionString']);
}
public function isActive() { public function isActive()
return $this->databaseName && $this->dbConn; {
} return $this->databaseName && $this->dbConn;
}
/** /**
* Prepares the list of parameters in preparation for passing to mysqli_stmt_bind_param * Prepares the list of parameters in preparation for passing to mysqli_stmt_bind_param
* *
* @param array $parameters List of parameters * @param array $parameters List of parameters
* @return array List of parameters types and values * @return array List of parameters types and values
*/ */
public function parsePreparedParameters($parameters) { public function parsePreparedParameters($parameters)
$values = array(); {
foreach($parameters as $value) { $values = array();
$phpType = gettype($value); foreach ($parameters as $value) {
$sqlType = null; $phpType = gettype($value);
$sqlType = null;
// Allow overriding of parameter type using an associative array // Allow overriding of parameter type using an associative array
if($phpType === 'array') { if ($phpType === 'array') {
$phpType = $value['type']; $phpType = $value['type'];
$value = $value['value']; $value = $value['value'];
} }
// Convert php variable type to one that makes mysqli_stmt_bind_param happy // Convert php variable type to one that makes mysqli_stmt_bind_param happy
// @see http://www.php.net/manual/en/mysqli-stmt.bind-param.php // @see http://www.php.net/manual/en/mysqli-stmt.bind-param.php
switch($phpType) { switch ($phpType) {
case 'boolean': case 'boolean':
case 'integer': case 'integer':
$sqlType = SQLITE3_INTEGER; $sqlType = SQLITE3_INTEGER;
break; break;
case 'float': // Not actually returnable from gettype case 'float': // Not actually returnable from gettype
case 'double': case 'double':
$sqlType = SQLITE3_FLOAT; $sqlType = SQLITE3_FLOAT;
break; break;
case 'object': // Allowed if the object or resource has a __toString method case 'object': // Allowed if the object or resource has a __toString method
case 'resource': case 'resource':
case 'string': case 'string':
$sqlType = SQLITE3_TEXT; $sqlType = SQLITE3_TEXT;
break; break;
case 'NULL': case 'NULL':
$sqlType = SQLITE3_NULL; $sqlType = SQLITE3_NULL;
break; break;
case 'blob': case 'blob':
$sqlType = SQLITE3_BLOB; $sqlType = SQLITE3_BLOB;
break; break;
case 'array': case 'array':
case 'unknown type': case 'unknown type':
default: default:
user_error("Cannot bind parameter \"$value\" as it is an unsupported type ($phpType)", E_USER_ERROR); user_error("Cannot bind parameter \"$value\" as it is an unsupported type ($phpType)", E_USER_ERROR);
break; break;
} }
$values[] = array( $values[] = array(
'type' => $sqlType, 'type' => $sqlType,
'value' => $value 'value' => $value
); );
} }
return $values; return $values;
} }
public function preparedQuery($sql, $parameters, $errorLevel = E_USER_ERROR) { public function preparedQuery($sql, $parameters, $errorLevel = E_USER_ERROR)
// Type check, identify, and prepare parameters for passing to the statement bind function {
$parsedParameters = $this->parsePreparedParameters($parameters); // Type check, identify, and prepare parameters for passing to the statement bind function
$parsedParameters = $this->parsePreparedParameters($parameters);
// Prepare statement // Prepare statement
$statement = @$this->dbConn->prepare($sql); $statement = @$this->dbConn->prepare($sql);
if($statement) { if ($statement) {
// Bind and run to statement // Bind and run to statement
for($i = 0; $i < count($parsedParameters); $i++) { for ($i = 0; $i < count($parsedParameters); $i++) {
$value = $parsedParameters[$i]['value']; $value = $parsedParameters[$i]['value'];
$type = $parsedParameters[$i]['type']; $type = $parsedParameters[$i]['type'];
$statement->bindValue($i+1, $value, $type); $statement->bindValue($i+1, $value, $type);
} }
// Return successful result // Return successful result
$handle = $statement->execute(); $handle = $statement->execute();
if ($handle) { if ($handle) {
return new SQLite3Query($this, $handle); return new SQLite3Query($this, $handle);
} }
} }
// Handle error // Handle error
$values = $this->parameterValues($parameters); $values = $this->parameterValues($parameters);
$this->databaseError($this->getLastError(), $errorLevel, $sql, $values); $this->databaseError($this->getLastError(), $errorLevel, $sql, $values);
return null; return null;
} }
public function query($sql, $errorLevel = E_USER_ERROR) { public function query($sql, $errorLevel = E_USER_ERROR)
// Return successful result {
$handle = @$this->dbConn->query($sql); // Return successful result
if ($handle) { $handle = @$this->dbConn->query($sql);
return new SQLite3Query($this, $handle); if ($handle) {
} return new SQLite3Query($this, $handle);
}
// Handle error // Handle error
$this->databaseError($this->getLastError(), $errorLevel, $sql); $this->databaseError($this->getLastError(), $errorLevel, $sql);
return null; return null;
} }
public function quoteString($value) { public function quoteString($value)
return "'".$this->escapeString($value)."'"; {
} return "'".$this->escapeString($value)."'";
}
public function escapeString($value) { public function escapeString($value)
return $this->dbConn->escapeString($value); {
} return $this->dbConn->escapeString($value);
}
public function selectDatabase($name) { public function selectDatabase($name)
if($name !== $this->databaseName) { {
user_error("SQLite3Connector can't change databases. Please create a new database connection", E_USER_ERROR); if ($name !== $this->databaseName) {
} user_error("SQLite3Connector can't change databases. Please create a new database connection", E_USER_ERROR);
return true; }
} return true;
}
public function unloadDatabase() { public function unloadDatabase()
$this->dbConn->close(); {
$this->databaseName = null; $this->dbConn->close();
} $this->databaseName = null;
}
} }

View File

@ -5,484 +5,530 @@
* *
* @package SQLite3 * @package SQLite3
*/ */
class SQLite3Database extends SS_Database { class SQLite3Database extends SS_Database
{
/** /**
* Database schema manager object * Database schema manager object
* *
* @var SQLite3SchemaManager * @var SQLite3SchemaManager
*/ */
protected $schemaManager = null; protected $schemaManager = null;
/* /*
* This holds the parameters that the original connection was created with, * This holds the parameters that the original connection was created with,
* so we can switch back to it if necessary (used for unit tests) * so we can switch back to it if necessary (used for unit tests)
* *
* @var array * @var array
*/ */
protected $parameters; protected $parameters;
/* /*
* if we're on a In-Memory db * if we're on a In-Memory db
* *
* @var boolean * @var boolean
*/ */
protected $livesInMemory = false; protected $livesInMemory = false;
/** /**
* List of default pragma values * List of default pragma values
* *
* @todo Migrate to SS config * @todo Migrate to SS config
* *
* @var array * @var array
*/ */
public static $default_pragma = array( public static $default_pragma = array(
'encoding' => '"UTF-8"', 'encoding' => '"UTF-8"',
'locking_mode' => 'NORMAL' 'locking_mode' => 'NORMAL'
); );
/** /**
* Extension used to distinguish between sqllite database files and other files. * Extension used to distinguish between sqllite database files and other files.
* Required to handle multiple databases. * Required to handle multiple databases.
* *
* @return string * @return string
*/ */
public static function database_extension() { public static function database_extension()
return Config::inst()->get('SQLite3Database', 'database_extension'); {
} return Config::inst()->get('SQLite3Database', 'database_extension');
}
/** /**
* Check if a database name has a valid extension * Check if a database name has a valid extension
* *
* @param string $name * @param string $name
* @return boolean * @return boolean
*/ */
public static function is_valid_database_name($name) { public static function is_valid_database_name($name)
$extension = self::database_extension(); {
if(empty($extension)) return true; $extension = self::database_extension();
if (empty($extension)) {
return true;
}
return substr_compare($name, $extension, -strlen($extension), strlen($extension)) === 0; return substr_compare($name, $extension, -strlen($extension), strlen($extension)) === 0;
} }
/** /**
* Connect to a SQLite3 database. * Connect to a SQLite3 database.
* @param array $parameters An map of parameters, which should include: * @param array $parameters An map of parameters, which should include:
* - database: The database to connect to, with the correct file extension (.sqlite) * - database: The database to connect to, with the correct file extension (.sqlite)
* - path: the path to the SQLite3 database file * - path: the path to the SQLite3 database file
* - key: the encryption key (needs testing) * - key: the encryption key (needs testing)
* - memory: use the faster In-Memory database for unit tests * - memory: use the faster In-Memory database for unit tests
*/ */
public function connect($parameters) { public function connect($parameters)
{
if (!empty($parameters['memory'])) {
Deprecation::notice(
'1.4.0',
"\$databaseConfig['memory'] is deprecated. Use \$databaseConfig['path'] = ':memory:' instead.",
Deprecation::SCOPE_GLOBAL
);
unset($parameters['memory']);
$parameters['path'] = ':memory:';
}
if(!empty($parameters['memory'])) { //We will store these connection parameters for use elsewhere (ie, unit tests)
Deprecation::notice( $this->parameters = $parameters;
'1.4.0', $this->schemaManager->flushCache();
"\$databaseConfig['memory'] is deprecated. Use \$databaseConfig['path'] = ':memory:' instead.",
Deprecation::SCOPE_GLOBAL
);
unset($parameters['memory']);
$parameters['path'] = ':memory:';
}
//We will store these connection parameters for use elsewhere (ie, unit tests) // Ensure database name is set
$this->parameters = $parameters; if (empty($parameters['database'])) {
$this->schemaManager->flushCache(); $parameters['database'] = 'database' . self::database_extension();
}
$dbName = $parameters['database'];
if (!self::is_valid_database_name($dbName)) {
// If not using the correct file extension for database files then the
// results of SQLite3SchemaManager::databaseList will be unpredictable
$extension = self::database_extension();
Deprecation::notice('3.2', "SQLite3Database now expects a database file with extension \"$extension\". Behaviour may be unpredictable otherwise.");
}
// Ensure database name is set // use the very lightspeed SQLite In-Memory feature for testing
if(empty($parameters['database'])) { if ($this->getLivesInMemory()) {
$parameters['database'] = 'database' . self::database_extension(); $file = ':memory:';
} } else {
$dbName = $parameters['database']; // Ensure path is given
if(!self::is_valid_database_name($dbName)) { if (empty($parameters['path'])) {
// If not using the correct file extension for database files then the $parameters['path'] = ASSETS_PATH . '/.sqlitedb';
// results of SQLite3SchemaManager::databaseList will be unpredictable }
$extension = self::database_extension();
Deprecation::notice('3.2', "SQLite3Database now expects a database file with extension \"$extension\". Behaviour may be unpredictable otherwise.");
}
// use the very lightspeed SQLite In-Memory feature for testing //assumes that the path to dbname will always be provided:
if($this->getLivesInMemory()) { $file = $parameters['path'] . '/' . $dbName;
$file = ':memory:'; if (!file_exists($parameters['path'])) {
} else { SQLiteDatabaseConfigurationHelper::create_db_dir($parameters['path']);
// Ensure path is given SQLiteDatabaseConfigurationHelper::secure_db_dir($parameters['path']);
if(empty($parameters['path'])) { }
$parameters['path'] = ASSETS_PATH . '/.sqlitedb'; }
}
//assumes that the path to dbname will always be provided: // 'path' and 'database' are merged into the full file path, which
$file = $parameters['path'] . '/' . $dbName; // is the format that connectors such as PDOConnector expect
if(!file_exists($parameters['path'])) { $parameters['filepath'] = $file;
SQLiteDatabaseConfigurationHelper::create_db_dir($parameters['path']);
SQLiteDatabaseConfigurationHelper::secure_db_dir($parameters['path']);
}
}
// 'path' and 'database' are merged into the full file path, which // Ensure that driver is available (required by PDO)
// is the format that connectors such as PDOConnector expect if (empty($parameters['driver'])) {
$parameters['filepath'] = $file; $parameters['driver'] = $this->getDatabaseServer();
}
// Ensure that driver is available (required by PDO) $this->connector->connect($parameters, true);
if(empty($parameters['driver'])) {
$parameters['driver'] = $this->getDatabaseServer();
}
$this->connector->connect($parameters, true); foreach (self::$default_pragma as $pragma => $value) {
$this->setPragma($pragma, $value);
}
foreach(self::$default_pragma as $pragma => $value) { if (empty(self::$default_pragma['locking_mode'])) {
$this->setPragma($pragma, $value); self::$default_pragma['locking_mode'] = $this->getPragma('locking_mode');
} }
}
if(empty(self::$default_pragma['locking_mode'])) { /**
self::$default_pragma['locking_mode'] = $this->getPragma('locking_mode'); * Retrieve parameters used to connect to this SQLLite database
} *
} * @return array
*/
public function getParameters()
{
return $this->parameters;
}
/** public function getLivesInMemory()
* Retrieve parameters used to connect to this SQLLite database {
* return isset($this->parameters['path']) && $this->parameters['path'] === ':memory:';
* @return array }
*/
public function getParameters() {
return $this->parameters;
}
public function getLivesInMemory() { public function supportsCollations()
return isset($this->parameters['path']) && $this->parameters['path'] === ':memory:'; {
} return true;
}
public function supportsCollations() { public function supportsTimezoneOverride()
return true; {
} return false;
}
public function supportsTimezoneOverride() { /**
return false; * Execute PRAGMA commands.
} *
* @param string pragma name
* @param string value to set
*/
public function setPragma($pragma, $value)
{
$this->query("PRAGMA $pragma = $value");
}
/** /**
* Execute PRAGMA commands. * Gets pragma value.
* *
* @param string pragma name * @param string pragma name
* @param string value to set * @return string the pragma value
*/ */
public function setPragma($pragma, $value) { public function getPragma($pragma)
$this->query("PRAGMA $pragma = $value"); {
} return $this->query("PRAGMA $pragma")->value();
}
/** public function getDatabaseServer()
* Gets pragma value. {
* return "sqlite";
* @param string pragma name }
* @return string the pragma value
*/
public function getPragma($pragma) {
return $this->query("PRAGMA $pragma")->value();
}
public function getDatabaseServer() { public function selectDatabase($name, $create = false, $errorLevel = E_USER_ERROR)
return "sqlite"; {
} if (!$this->schemaManager->databaseExists($name)) {
// Check DB creation permisson
if (!$create) {
if ($errorLevel !== false) {
user_error("Attempted to connect to non-existing database \"$name\"", $errorLevel);
}
// Unselect database
$this->connector->unloadDatabase();
return false;
}
$this->schemaManager->createDatabase($name);
}
public function selectDatabase($name, $create = false, $errorLevel = E_USER_ERROR) { // Reconnect using the existing parameters
if (!$this->schemaManager->databaseExists($name)) { $parameters = $this->parameters;
// Check DB creation permisson $parameters['database'] = $name;
if (!$create) { $this->connect($parameters);
if ($errorLevel !== false) { return true;
user_error("Attempted to connect to non-existing database \"$name\"", $errorLevel); }
}
// Unselect database
$this->connector->unloadDatabase();
return false;
}
$this->schemaManager->createDatabase($name);
}
// Reconnect using the existing parameters public function now()
$parameters = $this->parameters; {
$parameters['database'] = $name; return "datetime('now', 'localtime')";
$this->connect($parameters); }
return true;
}
function now(){ public function random()
return "datetime('now', 'localtime')"; {
} return 'random()';
}
function random(){ /**
return 'random()'; * The core search engine configuration.
} * @todo There is a fulltext search for SQLite making use of virtual tables, the fts3 extension and the
* MATCH operator
* there are a few issues with fts:
* - shared cached lock doesn't allow to create virtual tables on versions prior to 3.6.17
* - there must not be more than one MATCH operator per statement
* - the fts3 extension needs to be available
* for now we use the MySQL implementation with the MATCH()AGAINST() uglily replaced with LIKE
*
* @param string $keywords Keywords as a space separated string
* @return object DataObjectSet of result pages
*/
public function searchEngine($classesToSearch, $keywords, $start, $pageLength, $sortBy = "Relevance DESC",
$extraFilter = "", $booleanSearch = false, $alternativeFileFilter = "", $invertedMatch = false
) {
$keywords = $this->escapeString(str_replace(array('*', '+', '-', '"', '\''), '', $keywords));
$htmlEntityKeywords = htmlentities(utf8_decode($keywords));
/** $extraFilters = array('SiteTree' => '', 'File' => '');
* The core search engine configuration.
* @todo There is a fulltext search for SQLite making use of virtual tables, the fts3 extension and the
* MATCH operator
* there are a few issues with fts:
* - shared cached lock doesn't allow to create virtual tables on versions prior to 3.6.17
* - there must not be more than one MATCH operator per statement
* - the fts3 extension needs to be available
* for now we use the MySQL implementation with the MATCH()AGAINST() uglily replaced with LIKE
*
* @param string $keywords Keywords as a space separated string
* @return object DataObjectSet of result pages
*/
public function searchEngine($classesToSearch, $keywords, $start, $pageLength, $sortBy = "Relevance DESC",
$extraFilter = "", $booleanSearch = false, $alternativeFileFilter = "", $invertedMatch = false
) {
$keywords = $this->escapeString(str_replace(array('*','+','-','"','\''), '', $keywords));
$htmlEntityKeywords = htmlentities(utf8_decode($keywords));
$extraFilters = array('SiteTree' => '', 'File' => ''); if ($extraFilter) {
$extraFilters['SiteTree'] = " AND $extraFilter";
if($extraFilter) { if ($alternativeFileFilter) {
$extraFilters['SiteTree'] = " AND $extraFilter"; $extraFilters['File'] = " AND $alternativeFileFilter";
} else {
$extraFilters['File'] = $extraFilters['SiteTree'];
}
}
if($alternativeFileFilter) $extraFilters['File'] = " AND $alternativeFileFilter"; // Always ensure that only pages with ShowInSearch = 1 can be searched
else $extraFilters['File'] = $extraFilters['SiteTree']; $extraFilters['SiteTree'] .= ' AND ShowInSearch <> 0';
} // File.ShowInSearch was added later, keep the database driver backwards compatible
// by checking for its existence first
$fields = $this->getSchemaManager()->fieldList('File');
if (array_key_exists('ShowInSearch', $fields)) {
$extraFilters['File'] .= " AND ShowInSearch <> 0";
}
// Always ensure that only pages with ShowInSearch = 1 can be searched $limit = $start . ", " . (int) $pageLength;
$extraFilters['SiteTree'] .= ' AND ShowInSearch <> 0';
// File.ShowInSearch was added later, keep the database driver backwards compatible
// by checking for its existence first
$fields = $this->getSchemaManager()->fieldList('File');
if(array_key_exists('ShowInSearch', $fields)) {
$extraFilters['File'] .= " AND ShowInSearch <> 0";
}
$limit = $start . ", " . (int) $pageLength; $notMatch = $invertedMatch ? "NOT " : "";
if ($keywords) {
$notMatch = $invertedMatch ? "NOT " : ""; $match['SiteTree'] = "
if($keywords) {
$match['SiteTree'] = "
(Title LIKE '%$keywords%' OR MenuTitle LIKE '%$keywords%' OR Content LIKE '%$keywords%' OR MetaDescription LIKE '%$keywords%' OR (Title LIKE '%$keywords%' OR MenuTitle LIKE '%$keywords%' OR Content LIKE '%$keywords%' OR MetaDescription LIKE '%$keywords%' OR
Title LIKE '%$htmlEntityKeywords%' OR MenuTitle LIKE '%$htmlEntityKeywords%' OR Content LIKE '%$htmlEntityKeywords%' OR MetaDescription LIKE '%$htmlEntityKeywords%') Title LIKE '%$htmlEntityKeywords%' OR MenuTitle LIKE '%$htmlEntityKeywords%' OR Content LIKE '%$htmlEntityKeywords%' OR MetaDescription LIKE '%$htmlEntityKeywords%')
"; ";
$match['File'] = "(Name LIKE '%$keywords%' OR Title LIKE '%$keywords%') AND ClassName = 'File'"; $match['File'] = "(Name LIKE '%$keywords%' OR Title LIKE '%$keywords%') AND ClassName = 'File'";
// We make the relevance search by converting a boolean mode search into a normal one // We make the relevance search by converting a boolean mode search into a normal one
$relevanceKeywords = $keywords; $relevanceKeywords = $keywords;
$htmlEntityRelevanceKeywords = $htmlEntityKeywords; $htmlEntityRelevanceKeywords = $htmlEntityKeywords;
$relevance['SiteTree'] = "(Title LIKE '%$relevanceKeywords%' OR MenuTitle LIKE '%$relevanceKeywords%' OR Content LIKE '%$relevanceKeywords%' OR MetaDescription LIKE '%$relevanceKeywords%') + (Title LIKE '%$htmlEntityRelevanceKeywords%' OR MenuTitle LIKE '%$htmlEntityRelevanceKeywords%' OR Content LIKE '%$htmlEntityRelevanceKeywords%' OR MetaDescription LIKE '%$htmlEntityRelevanceKeywords%')"; $relevance['SiteTree'] = "(Title LIKE '%$relevanceKeywords%' OR MenuTitle LIKE '%$relevanceKeywords%' OR Content LIKE '%$relevanceKeywords%' OR MetaDescription LIKE '%$relevanceKeywords%') + (Title LIKE '%$htmlEntityRelevanceKeywords%' OR MenuTitle LIKE '%$htmlEntityRelevanceKeywords%' OR Content LIKE '%$htmlEntityRelevanceKeywords%' OR MetaDescription LIKE '%$htmlEntityRelevanceKeywords%')";
$relevance['File'] = "(Name LIKE '%$relevanceKeywords%' OR Title LIKE '%$relevanceKeywords%')"; $relevance['File'] = "(Name LIKE '%$relevanceKeywords%' OR Title LIKE '%$relevanceKeywords%')";
} else { } else {
$relevance['SiteTree'] = $relevance['File'] = 1; $relevance['SiteTree'] = $relevance['File'] = 1;
$match['SiteTree'] = $match['File'] = "1 = 1"; $match['SiteTree'] = $match['File'] = "1 = 1";
} }
// Generate initial queries and base table names // Generate initial queries and base table names
$baseClasses = array('SiteTree' => '', 'File' => ''); $baseClasses = array('SiteTree' => '', 'File' => '');
$queries = array(); $queries = array();
foreach($classesToSearch as $class) { foreach ($classesToSearch as $class) {
$queries[$class] = DataList::create($class)->where($notMatch . $match[$class] . $extraFilters[$class], "")->dataQuery()->query(); $queries[$class] = DataList::create($class)->where($notMatch . $match[$class] . $extraFilters[$class], "")->dataQuery()->query();
$fromArr = $queries[$class]->getFrom(); $fromArr = $queries[$class]->getFrom();
$baseClasses[$class] = reset($fromArr); $baseClasses[$class] = reset($fromArr);
} }
// Make column selection lists // Make column selection lists
$select = array( $select = array(
'SiteTree' => array( 'SiteTree' => array(
"\"ClassName\"", "\"ClassName\"",
"\"ID\"", "\"ID\"",
"\"ParentID\"", "\"ParentID\"",
"\"Title\"", "\"Title\"",
"\"URLSegment\"", "\"URLSegment\"",
"\"Content\"", "\"Content\"",
"\"LastEdited\"", "\"LastEdited\"",
"\"Created\"", "\"Created\"",
"NULL AS \"Name\"", "NULL AS \"Name\"",
"\"CanViewType\"", "\"CanViewType\"",
"$relevance[SiteTree] AS Relevance" "$relevance[SiteTree] AS Relevance"
), ),
'File' => array( 'File' => array(
"\"ClassName\"", "\"ClassName\"",
"\"ID\"", "\"ID\"",
"NULL AS \"ParentID\"", "NULL AS \"ParentID\"",
"\"Title\"", "\"Title\"",
"NULL AS \"URLSegment\"", "NULL AS \"URLSegment\"",
"NULL AS \"Content\"", "NULL AS \"Content\"",
"\"LastEdited\"", "\"LastEdited\"",
"\"Created\"", "\"Created\"",
"\"Name\"", "\"Name\"",
"NULL AS \"CanViewType\"", "NULL AS \"CanViewType\"",
"$relevance[File] AS Relevance" "$relevance[File] AS Relevance"
) )
); );
// Process queries // Process queries
foreach($classesToSearch as $class) { foreach ($classesToSearch as $class) {
// There's no need to do all that joining // There's no need to do all that joining
$queries[$class]->setFrom($baseClasses[$class]); $queries[$class]->setFrom($baseClasses[$class]);
$queries[$class]->setSelect(array()); $queries[$class]->setSelect(array());
foreach($select[$class] as $clause) { foreach ($select[$class] as $clause) {
if(preg_match('/^(.*) +AS +"?([^"]*)"?/i', $clause, $matches)) { if (preg_match('/^(.*) +AS +"?([^"]*)"?/i', $clause, $matches)) {
$queries[$class]->selectField($matches[1], $matches[2]); $queries[$class]->selectField($matches[1], $matches[2]);
} else { } else {
$queries[$class]->selectField(str_replace('"', '', $clause)); $queries[$class]->selectField(str_replace('"', '', $clause));
} }
} }
$queries[$class]->setOrderBy(array()); $queries[$class]->setOrderBy(array());
} }
// Combine queries // Combine queries
$querySQLs = array(); $querySQLs = array();
$queryParameters = array(); $queryParameters = array();
$totalCount = 0; $totalCount = 0;
foreach($queries as $query) { foreach ($queries as $query) {
$querySQLs[] = $query->sql($parameters); $querySQLs[] = $query->sql($parameters);
$queryParameters = array_merge($queryParameters, $parameters); $queryParameters = array_merge($queryParameters, $parameters);
$totalCount += $query->unlimitedRowCount(); $totalCount += $query->unlimitedRowCount();
} }
$fullQuery = implode(" UNION ", $querySQLs) . " ORDER BY $sortBy LIMIT $limit"; $fullQuery = implode(" UNION ", $querySQLs) . " ORDER BY $sortBy LIMIT $limit";
// Get records // Get records
$records = $this->preparedQuery($fullQuery, $queryParameters); $records = $this->preparedQuery($fullQuery, $queryParameters);
foreach($records as $record) { foreach ($records as $record) {
$objects[] = new $record['ClassName']($record); $objects[] = new $record['ClassName']($record);
} }
if(isset($objects)) $doSet = new ArrayList($objects); if (isset($objects)) {
else $doSet = new ArrayList(); $doSet = new ArrayList($objects);
$list = new PaginatedList($doSet); } else {
$list->setPageStart($start); $doSet = new ArrayList();
$list->setPageLEngth($pageLength); }
$list->setTotalItems($totalCount); $list = new PaginatedList($doSet);
return $list; $list->setPageStart($start);
} $list->setPageLEngth($pageLength);
$list->setTotalItems($totalCount);
return $list;
}
/* /*
* Does this database support transactions? * Does this database support transactions?
*/ */
public function supportsTransactions(){ public function supportsTransactions()
return version_compare($this->getVersion(), '3.6', '>='); {
} return version_compare($this->getVersion(), '3.6', '>=');
}
public function supportsExtensions($extensions = array('partitions', 'tablespaces', 'clustering')){ public function supportsExtensions($extensions = array('partitions', 'tablespaces', 'clustering'))
{
if (isset($extensions['partitions'])) {
return true;
} elseif (isset($extensions['tablespaces'])) {
return true;
} elseif (isset($extensions['clustering'])) {
return true;
} else {
return false;
}
}
if(isset($extensions['partitions'])) public function transactionStart($transaction_mode = false, $session_characteristics = false)
return true; {
elseif(isset($extensions['tablespaces'])) $this->query('BEGIN');
return true; }
elseif(isset($extensions['clustering']))
return true;
else
return false;
}
public function transactionStart($transaction_mode = false, $session_characteristics = false) { public function transactionSavepoint($savepoint)
$this->query('BEGIN'); {
} $this->query("SAVEPOINT \"$savepoint\"");
}
public function transactionSavepoint($savepoint) { public function transactionRollback($savepoint = false)
$this->query("SAVEPOINT \"$savepoint\""); {
} if ($savepoint) {
$this->query("ROLLBACK TO $savepoint;");
} else {
$this->query('ROLLBACK;');
}
}
public function transactionRollback($savepoint = false){ public function transactionEnd($chain = false)
{
$this->query('COMMIT;');
}
if($savepoint) { public function clearTable($table)
$this->query("ROLLBACK TO $savepoint;"); {
} else { $this->query("DELETE FROM \"$table\"");
$this->query('ROLLBACK;'); }
}
}
public function transactionEnd($chain = false){ public function comparisonClause($field, $value, $exact = false, $negate = false, $caseSensitive = null,
$this->query('COMMIT;'); $parameterised = false
} ) {
if ($exact && !$caseSensitive) {
$comp = ($negate) ? '!=' : '=';
} else {
if ($caseSensitive) {
// GLOB uses asterisks as wildcards.
// Replace them in search string, without replacing escaped percetage signs.
$comp = 'GLOB';
$value = preg_replace('/^%([^\\\\])/', '*$1', $value);
$value = preg_replace('/([^\\\\])%$/', '$1*', $value);
$value = preg_replace('/([^\\\\])%/', '$1*', $value);
} else {
$comp = 'LIKE';
}
if ($negate) {
$comp = 'NOT ' . $comp;
}
}
public function clearTable($table) { if ($parameterised) {
$this->query("DELETE FROM \"$table\""); return sprintf("%s %s ?", $field, $comp);
} } else {
return sprintf("%s %s '%s'", $field, $comp, $value);
}
}
public function comparisonClause($field, $value, $exact = false, $negate = false, $caseSensitive = null, public function formattedDatetimeClause($date, $format)
$parameterised = false {
) { preg_match_all('/%(.)/', $format, $matches);
if($exact && !$caseSensitive) { foreach ($matches[1] as $match) {
$comp = ($negate) ? '!=' : '='; if (array_search($match, array('Y', 'm', 'd', 'H', 'i', 's', 'U')) === false) {
} else { user_error('formattedDatetimeClause(): unsupported format character %' . $match, E_USER_WARNING);
if($caseSensitive) { }
// GLOB uses asterisks as wildcards. }
// Replace them in search string, without replacing escaped percetage signs.
$comp = 'GLOB';
$value = preg_replace('/^%([^\\\\])/', '*$1', $value);
$value = preg_replace('/([^\\\\])%$/', '$1*', $value);
$value = preg_replace('/([^\\\\])%/', '$1*', $value);
} else {
$comp = 'LIKE';
}
if($negate) $comp = 'NOT ' . $comp;
}
if($parameterised) { $translate = array(
return sprintf("%s %s ?", $field, $comp); '/%i/' => '%M',
} else { '/%s/' => '%S',
return sprintf("%s %s '%s'", $field, $comp, $value); '/%U/' => '%s',
} );
} $format = preg_replace(array_keys($translate), array_values($translate), $format);
function formattedDatetimeClause($date, $format) { $modifiers = array();
preg_match_all('/%(.)/', $format, $matches); if ($format == '%s' && $date != 'now') {
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); $modifiers[] = 'utc';
}
if ($format != '%s' && $date == 'now') {
$modifiers[] = 'localtime';
}
$translate = array( if (preg_match('/^now$/i', $date)) {
'/%i/' => '%M', $date = "'now'";
'/%s/' => '%S', } elseif (preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/i', $date)) {
'/%U/' => '%s', $date = "'$date'";
); }
$format = preg_replace(array_keys($translate), array_values($translate), $format);
$modifiers = array(); $modifier = empty($modifiers) ? '' : ", '" . implode("', '", $modifiers) . "'";
if($format == '%s' && $date != 'now') $modifiers[] = 'utc'; return "strftime('$format', $date$modifier)";
if($format != '%s' && $date == 'now') $modifiers[] = 'localtime'; }
if(preg_match('/^now$/i', $date)) { public function datetimeIntervalClause($date, $interval)
$date = "'now'"; {
} else if(preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/i', $date)) { $modifiers = array();
$date = "'$date'"; if ($date == 'now') {
} $modifiers[] = 'localtime';
}
$modifier = empty($modifiers) ? '' : ", '" . implode("', '", $modifiers) . "'"; if (preg_match('/^now$/i', $date)) {
return "strftime('$format', $date$modifier)"; $date = "'now'";
} } elseif (preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/i', $date)) {
$date = "'$date'";
}
function datetimeIntervalClause($date, $interval) { $modifier = empty($modifiers) ? '' : ", '" . implode("', '", $modifiers) . "'";
$modifiers = array(); return "datetime($date$modifier, '$interval')";
if($date == 'now') $modifiers[] = 'localtime'; }
if(preg_match('/^now$/i', $date)) { public function datetimeDifferenceClause($date1, $date2)
$date = "'now'"; {
} else if(preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/i', $date)) { $modifiers1 = array();
$date = "'$date'"; $modifiers2 = array();
}
$modifier = empty($modifiers) ? '' : ", '" . implode("', '", $modifiers) . "'"; if ($date1 == 'now') {
return "datetime($date$modifier, '$interval')"; $modifiers1[] = 'localtime';
} }
if ($date2 == 'now') {
$modifiers2[] = 'localtime';
}
function datetimeDifferenceClause($date1, $date2) { if (preg_match('/^now$/i', $date1)) {
$modifiers1 = array(); $date1 = "'now'";
$modifiers2 = array(); } elseif (preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/i', $date1)) {
$date1 = "'$date1'";
}
if($date1 == 'now') $modifiers1[] = 'localtime'; if (preg_match('/^now$/i', $date2)) {
if($date2 == 'now') $modifiers2[] = 'localtime'; $date2 = "'now'";
} elseif (preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/i', $date2)) {
$date2 = "'$date2'";
}
if(preg_match('/^now$/i', $date1)) { $modifier1 = empty($modifiers1) ? '' : ", '" . implode("', '", $modifiers1) . "'";
$date1 = "'now'"; $modifier2 = empty($modifiers2) ? '' : ", '" . implode("', '", $modifiers2) . "'";
} 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)) { return "strftime('%s', $date1$modifier1) - strftime('%s', $date2$modifier2)";
$date2 = "'now'"; }
} else if(preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/i', $date2)) {
$date2 = "'$date2'";
}
$modifier1 = empty($modifiers1) ? '' : ", '" . implode("', '", $modifiers1) . "'";
$modifier2 = empty($modifiers2) ? '' : ", '" . implode("', '", $modifiers2) . "'";
return "strftime('%s', $date1$modifier1) - strftime('%s', $date2$modifier2)";
}
} }

View File

@ -5,58 +5,70 @@
* *
* @package SQLite3 * @package SQLite3
*/ */
class SQLite3Query extends SS_Query { class SQLite3Query extends SS_Query
{
/** /**
* The SQLite3Connector object that created this result set. * The SQLite3Connector object that created this result set.
* *
* @var SQLite3Connector * @var SQLite3Connector
*/ */
protected $database; protected $database;
/** /**
* The internal sqlite3 handle that points to the result set. * The internal sqlite3 handle that points to the result set.
* *
* @var SQLite3Result * @var SQLite3Result
*/ */
protected $handle; protected $handle;
/** /**
* Hook the result-set given into a Query class, suitable for use by framework. * Hook the result-set given into a Query class, suitable for use by framework.
* @param SQLite3Connector $database The database object that created this query. * @param SQLite3Connector $database The database object that created this query.
* @param SQLite3Result $handle the internal sqlite3 handle that is points to the resultset. * @param SQLite3Result $handle the internal sqlite3 handle that is points to the resultset.
*/ */
public function __construct(SQLite3Connector $database, SQLite3Result $handle) { public function __construct(SQLite3Connector $database, SQLite3Result $handle)
$this->database = $database; {
$this->handle = $handle; $this->database = $database;
} $this->handle = $handle;
}
public function __destruct() { public function __destruct()
if($this->handle) $this->handle->finalize(); {
} if ($this->handle) {
$this->handle->finalize();
}
}
public function seek($row) { public function seek($row)
$this->handle->reset(); {
$i=0; $this->handle->reset();
while($i < $row && $row = @$this->handle->fetchArray()) $i++; $i=0;
return true; while ($i < $row && $row = @$this->handle->fetchArray()) {
} $i++;
}
return true;
}
/** /**
* @todo This looks terrible but there is no SQLite3::get_num_rows() implementation * @todo This looks terrible but there is no SQLite3::get_num_rows() implementation
*/ */
public function numRecords() { public function numRecords()
$c=0; {
while($this->handle->fetchArray()) $c++; $c=0;
$this->handle->reset(); while ($this->handle->fetchArray()) {
return $c; $c++;
} }
$this->handle->reset();
return $c;
}
public function nextRecord() { public function nextRecord()
if($data = $this->handle->fetchArray(SQLITE3_ASSOC)) { {
return $data; if ($data = $this->handle->fetchArray(SQLITE3_ASSOC)) {
} else { return $data;
return false; } else {
} return false;
} }
}
} }

View File

@ -5,90 +5,94 @@
* *
* @package SQLite3 * @package SQLite3
*/ */
class SQLite3QueryBuilder extends DBQueryBuilder { class SQLite3QueryBuilder extends DBQueryBuilder
{
/** /**
* @param SQLInsert $query * @param SQLInsert $query
* @param array $parameters * @param array $parameters
* @return string * @return string
*/ */
protected function buildInsertQuery(SQLInsert $query, array &$parameters) { protected function buildInsertQuery(SQLInsert $query, array &$parameters)
// Multi-row insert requires SQLite specific syntax prior to 3.7.11 {
// For backwards compatibility reasons include the "union all select" syntax // Multi-row insert requires SQLite specific syntax prior to 3.7.11
// For backwards compatibility reasons include the "union all select" syntax
$nl = $this->getSeparator(); $nl = $this->getSeparator();
$into = $query->getInto(); $into = $query->getInto();
// Column identifiers // Column identifiers
$columns = $query->getColumns(); $columns = $query->getColumns();
// Build all rows // Build all rows
$rowParts = array(); $rowParts = array();
foreach($query->getRows() as $row) { foreach ($query->getRows() as $row) {
// Build all columns in this row // Build all columns in this row
$assignments = $row->getAssignments(); $assignments = $row->getAssignments();
// Join SET components together, considering parameters // Join SET components together, considering parameters
$parts = array(); $parts = array();
foreach($columns as $column) { foreach ($columns as $column) {
// Check if this column has a value for this row // Check if this column has a value for this row
if(isset($assignments[$column])) { if (isset($assignments[$column])) {
// Assigment is a single item array, expand with a loop here // Assigment is a single item array, expand with a loop here
foreach($assignments[$column] as $assignmentSQL => $assignmentParameters) { foreach ($assignments[$column] as $assignmentSQL => $assignmentParameters) {
$parts[] = $assignmentSQL; $parts[] = $assignmentSQL;
$parameters = array_merge($parameters, $assignmentParameters); $parameters = array_merge($parameters, $assignmentParameters);
break; break;
} }
} else { } else {
// This row is missing a value for a column used by another row // This row is missing a value for a column used by another row
$parts[] = '?'; $parts[] = '?';
$parameters[] = null; $parameters[] = null;
} }
} }
$rowParts[] = implode(', ', $parts); $rowParts[] = implode(', ', $parts);
} }
$columnSQL = implode(', ', $columns); $columnSQL = implode(', ', $columns);
$sql = "INSERT INTO {$into}{$nl}($columnSQL){$nl}SELECT " . implode("{$nl}UNION ALL SELECT ", $rowParts); $sql = "INSERT INTO {$into}{$nl}($columnSQL){$nl}SELECT " . implode("{$nl}UNION ALL SELECT ", $rowParts);
return $sql; return $sql;
} }
/** /**
* Return the LIMIT clause ready for inserting into a query. * Return the LIMIT clause ready for inserting into a query.
* *
* @param SQLSelect $query The expression object to build from * @param SQLSelect $query The expression object to build from
* @param array $parameters Out parameter for the resulting query parameters * @param array $parameters Out parameter for the resulting query parameters
* @return string The finalised limit SQL fragment * @return string The finalised limit SQL fragment
*/ */
public function buildLimitFragment(SQLSelect $query, array &$parameters) { public function buildLimitFragment(SQLSelect $query, array &$parameters)
$nl = $this->getSeparator(); {
$nl = $this->getSeparator();
// Ensure limit is given // Ensure limit is given
$limit = $query->getLimit(); $limit = $query->getLimit();
if(empty($limit)) return ''; if (empty($limit)) {
return '';
}
// For literal values return this as the limit SQL // For literal values return this as the limit SQL
if( ! is_array($limit)) { if (! is_array($limit)) {
return "{$nl}LIMIT $limit"; return "{$nl}LIMIT $limit";
} }
// Assert that the array version provides the 'limit' key // Assert that the array version provides the 'limit' key
if( ! array_key_exists('limit', $limit) || ($limit['limit'] !== null && ! is_numeric($limit['limit']))) { if (! array_key_exists('limit', $limit) || ($limit['limit'] !== null && ! is_numeric($limit['limit']))) {
throw new InvalidArgumentException( throw new InvalidArgumentException(
'SQLite3QueryBuilder::buildLimitSQL(): Wrong format for $limit: '. var_export($limit, true) 'SQLite3QueryBuilder::buildLimitSQL(): Wrong format for $limit: '. var_export($limit, true)
); );
} }
$clause = "{$nl}"; $clause = "{$nl}";
if($limit['limit'] !== null) { if ($limit['limit'] !== null) {
$clause .= "LIMIT {$limit['limit']} "; $clause .= "LIMIT {$limit['limit']} ";
} else { } else {
$clause .= "LIMIT -1 "; $clause .= "LIMIT -1 ";
} }
if(isset($limit['start']) && is_numeric($limit['start']) && $limit['start'] !== 0) {
$clause .= "OFFSET {$limit['start']}";
}
return $clause;
}
if (isset($limit['start']) && is_numeric($limit['start']) && $limit['start'] !== 0) {
$clause .= "OFFSET {$limit['start']}";
}
return $clause;
}
} }

View File

@ -5,611 +5,690 @@
* *
* @package SQLite3 * @package SQLite3
*/ */
class SQLite3SchemaManager extends DBSchemaManager { class SQLite3SchemaManager extends DBSchemaManager
{
/** /**
* Instance of the database controller this schema belongs to * Instance of the database controller this schema belongs to
* *
* @var SQLite3Database * @var SQLite3Database
*/ */
protected $database = null; protected $database = null;
/** /**
* Flag indicating whether or not the database has been checked and repaired * Flag indicating whether or not the database has been checked and repaired
* *
* @var boolean * @var boolean
*/ */
protected static $checked_and_repaired = false; protected static $checked_and_repaired = false;
/** /**
* Should schema be vacuumed during checkeAndRepairTable? * Should schema be vacuumed during checkeAndRepairTable?
* *
* @var boolean * @var boolean
*/ */
public static $vacuum = true; public static $vacuum = true;
public function createDatabase($name) { public function createDatabase($name)
// Ensure that any existing database is cleared before connection {
$this->dropDatabase($name); // Ensure that any existing database is cleared before connection
} $this->dropDatabase($name);
}
public function dropDatabase($name) { public function dropDatabase($name)
// No need to delete database files if operating purely within memory {
if($this->database->getLivesInMemory()) return; // No need to delete database files if operating purely within memory
if ($this->database->getLivesInMemory()) {
return;
}
// If using file based database ensure any existing file is removed // If using file based database ensure any existing file is removed
$parameters = $this->database->getParameters(); $parameters = $this->database->getParameters();
$fullpath = $parameters['path'] . '/' . $name; $fullpath = $parameters['path'] . '/' . $name;
if(is_writable($fullpath)) unlink($fullpath); if (is_writable($fullpath)) {
} unlink($fullpath);
}
}
function databaseList() { public function databaseList()
$parameters = $this->database->getParameters(); {
$parameters = $this->database->getParameters();
// If in-memory use the current database name only // If in-memory use the current database name only
if($this->database->getLivesInMemory()) { if ($this->database->getLivesInMemory()) {
return array($parameters['database']); return array($parameters['database']);
} }
// If using file based database enumerate files in the database directory // If using file based database enumerate files in the database directory
$directory = $parameters['path']; $directory = $parameters['path'];
$files = scandir($directory); $files = scandir($directory);
// Filter each file in this directory // Filter each file in this directory
$databases = array(); $databases = array();
if($files !== false) foreach($files as $file) { if ($files !== false) {
foreach ($files as $file) {
// Filter non-files // Filter non-files
if(!is_file("$directory/$file")) continue; if (!is_file("$directory/$file")) {
continue;
}
// Filter those with correct extension // Filter those with correct extension
if(!SQLite3Database::is_valid_database_name($file)) continue; if (!SQLite3Database::is_valid_database_name($file)) {
continue;
}
$databases[] = $file; $databases[] = $file;
} }
return $databases; }
} return $databases;
}
public function databaseExists($name) { public function databaseExists($name)
$databases = $this->databaseList(); {
return in_array($name, $databases); $databases = $this->databaseList();
} return in_array($name, $databases);
}
/** /**
* Empties any cached enum values * Empties any cached enum values
*/ */
public function flushCache() { public function flushCache()
$this->enum_map = array(); {
} $this->enum_map = array();
}
function schemaUpdate($callback) { public function schemaUpdate($callback)
// Set locking mode {
$this->database->setPragma('locking_mode', 'EXCLUSIVE'); // Set locking mode
$this->checkAndRepairTable(); $this->database->setPragma('locking_mode', 'EXCLUSIVE');
$this->flushCache(); $this->checkAndRepairTable();
$this->flushCache();
// Initiate schema update // Initiate schema update
$error = null; $error = null;
try { try {
parent::schemaUpdate($callback); parent::schemaUpdate($callback);
} catch(Exception $ex) { } catch (Exception $ex) {
$error = $ex; $error = $ex;
} }
// Revert locking mode // Revert locking mode
$this->database->setPragma('locking_mode', SQLite3Database::$default_pragma['locking_mode']); $this->database->setPragma('locking_mode', SQLite3Database::$default_pragma['locking_mode']);
if($error) throw $error; if ($error) {
} throw $error;
}
}
/** /**
* Empty a specific table * Empty a specific table
* *
* @param string $table * @param string $table
*/ */
public function clearTable($table) { public function clearTable($table)
if($table != 'SQLiteEnums') $this->dbConn->query("DELETE FROM \"$table\""); {
} if ($table != 'SQLiteEnums') {
$this->dbConn->query("DELETE FROM \"$table\"");
}
}
public function createTable($table, $fields = null, $indexes = null, $options = null, $advancedOptions = null) { public function createTable($table, $fields = null, $indexes = null, $options = null, $advancedOptions = null)
if(!isset($fields['ID'])) $fields['ID'] = $this->IdColumn(); {
if (!isset($fields['ID'])) {
$fields['ID'] = $this->IdColumn();
}
$fieldSchemata = array(); $fieldSchemata = array();
if($fields) foreach($fields as $k => $v) { if ($fields) {
$fieldSchemata[] = "\"$k\" $v"; foreach ($fields as $k => $v) {
} $fieldSchemata[] = "\"$k\" $v";
$fieldSchemas = implode(",\n", $fieldSchemata); }
}
$fieldSchemas = implode(",\n", $fieldSchemata);
// Switch to "CREATE TEMPORARY TABLE" for temporary tables // Switch to "CREATE TEMPORARY TABLE" for temporary tables
$temporary = empty($options['temporary']) ? "" : "TEMPORARY"; $temporary = empty($options['temporary']) ? "" : "TEMPORARY";
$this->query("CREATE $temporary TABLE \"$table\" ( $this->query("CREATE $temporary TABLE \"$table\" (
$fieldSchemas $fieldSchemas
)"); )");
if($indexes) { if ($indexes) {
foreach($indexes as $indexName => $indexDetails) { foreach ($indexes as $indexName => $indexDetails) {
$this->createIndex($table, $indexName, $indexDetails); $this->createIndex($table, $indexName, $indexDetails);
} }
} }
return $table; return $table;
} }
public function alterTable($tableName, $newFields = null, $newIndexes = null, $alteredFields = null, public function alterTable($tableName, $newFields = null, $newIndexes = null, $alteredFields = null,
$alteredIndexes = null, $alteredOptions = null, $advancedOptions = null $alteredIndexes = null, $alteredOptions = null, $advancedOptions = null
) { ) {
if($newFields) foreach($newFields as $fieldName => $fieldSpec) { if ($newFields) {
$this->createField($tableName, $fieldName, $fieldSpec); foreach ($newFields as $fieldName => $fieldSpec) {
} $this->createField($tableName, $fieldName, $fieldSpec);
}
if($alteredFields) foreach($alteredFields as $fieldName => $fieldSpec) { }
$this->alterField($tableName, $fieldName, $fieldSpec);
} if ($alteredFields) {
foreach ($alteredFields as $fieldName => $fieldSpec) {
if($newIndexes) foreach($newIndexes as $indexName => $indexSpec) { $this->alterField($tableName, $fieldName, $fieldSpec);
$this->createIndex($tableName, $indexName, $indexSpec); }
} }
if($alteredIndexes) foreach($alteredIndexes as $indexName => $indexSpec) { if ($newIndexes) {
$this->alterIndex($tableName, $indexName, $indexSpec); foreach ($newIndexes as $indexName => $indexSpec) {
} $this->createIndex($tableName, $indexName, $indexSpec);
} }
}
public function renameTable($oldTableName, $newTableName) {
$this->query("ALTER TABLE \"$oldTableName\" RENAME TO \"$newTableName\""); if ($alteredIndexes) {
} foreach ($alteredIndexes as $indexName => $indexSpec) {
$this->alterIndex($tableName, $indexName, $indexSpec);
public function checkAndRepairTable($tableName = null) { }
$ok = true; }
}
if(!SapphireTest::using_temp_db() && !self::$checked_and_repaired) {
$this->alterationMessage("Checking database integrity", "repaired"); public function renameTable($oldTableName, $newTableName)
{
// Check for any tables with failed integrity $this->query("ALTER TABLE \"$oldTableName\" RENAME TO \"$newTableName\"");
if($messages = $this->query('PRAGMA integrity_check')) { }
foreach($messages as $message) if($message['integrity_check'] != 'ok') {
Debug::show($message['integrity_check']); public function checkAndRepairTable($tableName = null)
$ok = false; {
} $ok = true;
}
if (!SapphireTest::using_temp_db() && !self::$checked_and_repaired) {
// If enabled vacuum (clean and rebuild) the database $this->alterationMessage("Checking database integrity", "repaired");
if(self::$vacuum) {
$this->query('VACUUM', E_USER_NOTICE); // Check for any tables with failed integrity
$message = $this->database->getConnector()->getLastError(); if ($messages = $this->query('PRAGMA integrity_check')) {
if(preg_match('/authoriz/', $message)) { foreach ($messages as $message) {
$this->alterationMessage("VACUUM | $message", "error"); if ($message['integrity_check'] != 'ok') {
} else { Debug::show($message['integrity_check']);
$this->alterationMessage("VACUUMing", "repaired"); $ok = false;
} }
} }
self::$checked_and_repaired = true; }
}
// If enabled vacuum (clean and rebuild) the database
return $ok; if (self::$vacuum) {
} $this->query('VACUUM', E_USER_NOTICE);
$message = $this->database->getConnector()->getLastError();
public function createField($table, $field, $spec) { if (preg_match('/authoriz/', $message)) {
$this->query("ALTER TABLE \"$table\" ADD \"$field\" $spec"); $this->alterationMessage("VACUUM | $message", "error");
} } else {
$this->alterationMessage("VACUUMing", "repaired");
/** }
* Change the database type of the given field. }
* @param string $tableName The name of the tbale the field is in. self::$checked_and_repaired = true;
* @param string $fieldName The name of the field to change. }
* @param string $fieldSpec The new field specification
*/ return $ok;
public function alterField($tableName, $fieldName, $fieldSpec) { }
$oldFieldList = $this->fieldList($tableName);
$fieldNameList = '"' . implode('","', array_keys($oldFieldList)) . '"'; public function createField($table, $field, $spec)
{
if(!empty($_REQUEST['avoidConflict']) && Director::isDev()) { $this->query("ALTER TABLE \"$table\" ADD \"$field\" $spec");
$fieldSpec = preg_replace('/\snot null\s/i', ' NOT NULL ON CONFLICT REPLACE ', $fieldSpec); }
}
/**
// Skip non-existing columns * Change the database type of the given field.
if(!array_key_exists($fieldName, $oldFieldList)) return; * @param string $tableName The name of the tbale the field is in.
* @param string $fieldName The name of the field to change.
// Update field spec * @param string $fieldSpec The new field specification
$newColsSpec = array(); */
foreach($oldFieldList as $name => $oldSpec) { public function alterField($tableName, $fieldName, $fieldSpec)
$newColsSpec[] = "\"$name\" " . ($name == $fieldName ? $fieldSpec : $oldSpec); {
} $oldFieldList = $this->fieldList($tableName);
$fieldNameList = '"' . implode('","', array_keys($oldFieldList)) . '"';
$queries = array(
"BEGIN TRANSACTION", if (!empty($_REQUEST['avoidConflict']) && Director::isDev()) {
"CREATE TABLE \"{$tableName}_alterfield_{$fieldName}\"(" . implode(',', $newColsSpec) . ")", $fieldSpec = preg_replace('/\snot null\s/i', ' NOT NULL ON CONFLICT REPLACE ', $fieldSpec);
"INSERT INTO \"{$tableName}_alterfield_{$fieldName}\" SELECT {$fieldNameList} FROM \"$tableName\"", }
"DROP TABLE \"$tableName\"",
"ALTER TABLE \"{$tableName}_alterfield_{$fieldName}\" RENAME TO \"$tableName\"", // Skip non-existing columns
"COMMIT" if (!array_key_exists($fieldName, $oldFieldList)) {
); return;
}
// Remember original indexes
$indexList = $this->indexList($tableName); // Update field spec
$newColsSpec = array();
// Then alter the table column foreach ($oldFieldList as $name => $oldSpec) {
foreach($queries as $query) $this->query($query.';'); $newColsSpec[] = "\"$name\" " . ($name == $fieldName ? $fieldSpec : $oldSpec);
}
// Recreate the indexes
foreach($indexList as $indexName => $indexSpec) { $queries = array(
$this->createIndex($tableName, $indexName, $indexSpec); "BEGIN TRANSACTION",
} "CREATE TABLE \"{$tableName}_alterfield_{$fieldName}\"(" . implode(',', $newColsSpec) . ")",
} "INSERT INTO \"{$tableName}_alterfield_{$fieldName}\" SELECT {$fieldNameList} FROM \"$tableName\"",
"DROP TABLE \"$tableName\"",
public function renameField($tableName, $oldName, $newName) { "ALTER TABLE \"{$tableName}_alterfield_{$fieldName}\" RENAME TO \"$tableName\"",
$oldFieldList = $this->fieldList($tableName); "COMMIT"
);
// Skip non-existing columns
if(!array_key_exists($oldName, $oldFieldList)) return; // Remember original indexes
$indexList = $this->indexList($tableName);
// Determine column mappings
$oldCols = array(); // Then alter the table column
$newColsSpec = array(); foreach ($queries as $query) {
foreach($oldFieldList as $name => $spec) { $this->query($query.';');
$oldCols[] = "\"$name\"" . (($name == $oldName) ? " AS $newName" : ''); }
$newColsSpec[] = "\"" . (($name == $oldName) ? $newName : $name) . "\" $spec";
} // Recreate the indexes
foreach ($indexList as $indexName => $indexSpec) {
// SQLite doesn't support direct renames through ALTER TABLE $this->createIndex($tableName, $indexName, $indexSpec);
$queries = array( }
"BEGIN TRANSACTION", }
"CREATE TABLE \"{$tableName}_renamefield_{$oldName}\" (" . implode(',', $newColsSpec) . ")",
"INSERT INTO \"{$tableName}_renamefield_{$oldName}\" SELECT " . implode(',', $oldCols) . " FROM \"$tableName\"", public function renameField($tableName, $oldName, $newName)
"DROP TABLE \"$tableName\"", {
"ALTER TABLE \"{$tableName}_renamefield_{$oldName}\" RENAME TO \"$tableName\"", $oldFieldList = $this->fieldList($tableName);
"COMMIT"
); // Skip non-existing columns
if (!array_key_exists($oldName, $oldFieldList)) {
// Remember original indexes return;
$oldIndexList = $this->indexList($tableName); }
// Then alter the table column // Determine column mappings
foreach($queries as $query) $this->query($query.';'); $oldCols = array();
$newColsSpec = array();
// Recreate the indexes foreach ($oldFieldList as $name => $spec) {
foreach($oldIndexList as $indexName => $indexSpec) { $oldCols[] = "\"$name\"" . (($name == $oldName) ? " AS $newName" : '');
// Rename columns to new columns $newColsSpec[] = "\"" . (($name == $oldName) ? $newName : $name) . "\" $spec";
$indexSpec['value'] = preg_replace("/\"$oldName\"/i", "\"$newName\"", $indexSpec['value']); }
$this->createIndex($tableName, $indexName, $indexSpec);
} // SQLite doesn't support direct renames through ALTER TABLE
} $queries = array(
"BEGIN TRANSACTION",
public function fieldList($table) { "CREATE TABLE \"{$tableName}_renamefield_{$oldName}\" (" . implode(',', $newColsSpec) . ")",
$sqlCreate = $this->preparedQuery( "INSERT INTO \"{$tableName}_renamefield_{$oldName}\" SELECT " . implode(',', $oldCols) . " FROM \"$tableName\"",
'SELECT sql FROM sqlite_master WHERE type = ? AND name = ?', "DROP TABLE \"$tableName\"",
array('table', $table) "ALTER TABLE \"{$tableName}_renamefield_{$oldName}\" RENAME TO \"$tableName\"",
)->record(); "COMMIT"
);
$fieldList = array();
if($sqlCreate && $sqlCreate['sql']) { // Remember original indexes
preg_match('/^[\s]*CREATE[\s]+TABLE[\s]+[\'"]?[a-zA-Z0-9_\\\]+[\'"]?[\s]*\((.+)\)[\s]*$/ims', $oldIndexList = $this->indexList($tableName);
$sqlCreate['sql'], $matches
); // Then alter the table column
$fields = isset($matches[1]) foreach ($queries as $query) {
? preg_split('/,(?=(?:[^\'"]*$)|(?:[^\'"]*[\'"][^\'"]*[\'"][^\'"]*)*$)/x', $matches[1]) $this->query($query.';');
: array(); }
foreach($fields as $field) {
$details = preg_split('/\s/', trim($field)); // Recreate the indexes
$name = array_shift($details); foreach ($oldIndexList as $indexName => $indexSpec) {
$name = str_replace('"', '', trim($name)); // Rename columns to new columns
$fieldList[$name] = implode(' ', $details); $indexSpec['value'] = preg_replace("/\"$oldName\"/i", "\"$newName\"", $indexSpec['value']);
} $this->createIndex($tableName, $indexName, $indexSpec);
} }
return $fieldList; }
}
public function fieldList($table)
/** {
* Create an index on a table. $sqlCreate = $this->preparedQuery(
* 'SELECT sql FROM sqlite_master WHERE type = ? AND name = ?',
* @param string $tableName The name of the table. array('table', $table)
* @param string $indexName The name of the index. )->record();
* @param array $indexSpec The specification of the index, see Database::requireIndex() for more details.
*/ $fieldList = array();
public function createIndex($tableName, $indexName, $indexSpec) { if ($sqlCreate && $sqlCreate['sql']) {
$parsedSpec = $this->parseIndexSpec($indexName, $indexSpec); preg_match('/^[\s]*CREATE[\s]+TABLE[\s]+[\'"]?[a-zA-Z0-9_\\\]+[\'"]?[\s]*\((.+)\)[\s]*$/ims',
$sqliteName = $this->buildSQLiteIndexName($tableName, $indexName); $sqlCreate['sql'], $matches
$columns = $parsedSpec['value']; );
$unique = ($parsedSpec['type'] == 'unique') ? 'UNIQUE' : ''; $fields = isset($matches[1])
$this->query("CREATE $unique INDEX IF NOT EXISTS \"$sqliteName\" ON \"$tableName\" ($columns)"); ? preg_split('/,(?=(?:[^\'"]*$)|(?:[^\'"]*[\'"][^\'"]*[\'"][^\'"]*)*$)/x', $matches[1])
} : array();
foreach ($fields as $field) {
public function alterIndex($tableName, $indexName, $indexSpec) { $details = preg_split('/\s/', trim($field));
// Drop existing index $name = array_shift($details);
$sqliteName = $this->buildSQLiteIndexName($tableName, $indexName); $name = str_replace('"', '', trim($name));
$this->query("DROP INDEX IF EXISTS \"$sqliteName\""); $fieldList[$name] = implode(' ', $details);
}
// Create the index }
$this->createIndex($tableName, $indexName, $indexSpec); return $fieldList;
} }
/** /**
* Builds the internal SQLLite index name given the silverstripe table and index name. * Create an index on a table.
* *
* The name is built using the table and index name in order to prevent name collisions * @param string $tableName The name of the table.
* between indexes of the same name across multiple tables * @param string $indexName The name of the index.
* * @param array $indexSpec The specification of the index, see Database::requireIndex() for more details.
* @param string $tableName */
* @param string $indexName public function createIndex($tableName, $indexName, $indexSpec)
* @return string The SQLite3 name of the index {
*/ $parsedSpec = $this->parseIndexSpec($indexName, $indexSpec);
protected function buildSQLiteIndexName($tableName, $indexName) { $sqliteName = $this->buildSQLiteIndexName($tableName, $indexName);
return "{$tableName}_{$indexName}"; $columns = $parsedSpec['value'];
} $unique = ($parsedSpec['type'] == 'unique') ? 'UNIQUE' : '';
$this->query("CREATE $unique INDEX IF NOT EXISTS \"$sqliteName\" ON \"$tableName\" ($columns)");
protected function parseIndexSpec($name, $spec) { }
$spec = parent::parseIndexSpec($name, $spec);
public function alterIndex($tableName, $indexName, $indexSpec)
// Only allow index / unique index types {
if(!in_array($spec['type'], array('index', 'unique'))) { // Drop existing index
$spec['type'] = 'index'; $sqliteName = $this->buildSQLiteIndexName($tableName, $indexName);
} $this->query("DROP INDEX IF EXISTS \"$sqliteName\"");
return $spec; // Create the index
} $this->createIndex($tableName, $indexName, $indexSpec);
}
public function indexKey($table, $index, $spec) {
return $this->buildSQLiteIndexName($table, $index); /**
} * Builds the internal SQLLite index name given the silverstripe table and index name.
*
public function indexList($table) { * The name is built using the table and index name in order to prevent name collisions
$indexList = array(); * between indexes of the same name across multiple tables
*
// Enumerate each index and related fields * @param string $tableName
foreach($this->query("PRAGMA index_list(\"$table\")") as $index) { * @param string $indexName
* @return string The SQLite3 name of the index
// The SQLite internal index name, not the actual Silverstripe name */
$indexName = $index["name"]; protected function buildSQLiteIndexName($tableName, $indexName)
$indexType = $index['unique'] ? 'unique' : 'index'; {
return "{$tableName}_{$indexName}";
// Determine a clean list of column names within this index }
$list = array();
foreach($this->query("PRAGMA index_info(\"$indexName\")") as $details) { protected function parseIndexSpec($name, $spec)
$list[] = preg_replace('/^"?(.*)"?$/', '$1', $details['name']); {
} $spec = parent::parseIndexSpec($name, $spec);
// Safely encode this spec // Only allow index / unique index types
$indexList[$indexName] = $this->parseIndexSpec($indexName, array( if (!in_array($spec['type'], array('index', 'unique'))) {
'name' => $indexName, $spec['type'] = 'index';
'value' => $this->implodeColumnList($list), }
'type' => $indexType
)); return $spec;
} }
return $indexList; public function indexKey($table, $index, $spec)
} {
return $this->buildSQLiteIndexName($table, $index);
public function tableList() { }
$tables = array();
$result = $this->preparedQuery('SELECT name FROM sqlite_master WHERE type = ?', array('table')); public function indexList($table)
foreach($result as $record) { {
$table = reset($record); $indexList = array();
$tables[strtolower($table)] = $table;
} // Enumerate each index and related fields
return $tables; foreach ($this->query("PRAGMA index_list(\"$table\")") as $index) {
}
// The SQLite internal index name, not the actual Silverstripe name
/** $indexName = $index["name"];
* Return a boolean type-formatted string $indexType = $index['unique'] ? 'unique' : 'index';
*
* @params array $values Contains a tokenised list of info about this data type // Determine a clean list of column names within this index
* @return string $list = array();
*/ foreach ($this->query("PRAGMA index_info(\"$indexName\")") as $details) {
public function boolean($values) { $list[] = preg_replace('/^"?(.*)"?$/', '$1', $details['name']);
$default = empty($values['default']) ? 0 : (int)$values['default']; }
return "BOOL NOT NULL DEFAULT $default";
} // Safely encode this spec
$indexList[$indexName] = $this->parseIndexSpec($indexName, array(
/** 'name' => $indexName,
* Return a date type-formatted string 'value' => $this->implodeColumnList($list),
* 'type' => $indexType
* @params array $values Contains a tokenised list of info about this data type ));
* @return string }
*/
public function date($values){ return $indexList;
return "TEXT"; }
}
public function tableList()
/** {
* Return a decimal type-formatted string $tables = array();
* $result = $this->preparedQuery('SELECT name FROM sqlite_master WHERE type = ?', array('table'));
* @params array $values Contains a tokenised list of info about this data type foreach ($result as $record) {
* @return string $table = reset($record);
*/ $tables[strtolower($table)] = $table;
public function decimal($values, $asDbValue = false) { }
$default = isset($values['default']) && is_numeric($values['default']) ? $values['default'] : 0; return $tables;
return "NUMERIC NOT NULL DEFAULT $default"; }
}
/**
/** * Return a boolean type-formatted string
* Cached list of enum values indexed by table.column *
* * @params array $values Contains a tokenised list of info about this data type
* @var array * @return string
*/ */
protected $enum_map = array(); public function boolean($values)
{
/** $default = empty($values['default']) ? 0 : (int)$values['default'];
* Return a enum type-formatted string return "BOOL NOT NULL DEFAULT $default";
* }
* enums are not supported. as a workaround to store allowed values we creates an additional table
* /**
* @params array $values Contains a tokenised list of info about this data type * Return a date type-formatted string
* @return string *
*/ * @params array $values Contains a tokenised list of info about this data type
public function enum($values){ * @return string
$tablefield = $values['table'] . '.' . $values['name']; */
$enumValues = implode(',', $values['enums']); public function date($values)
{
// Ensure the cache table exists return "TEXT";
if(empty($this->enum_map)) { }
$this->query("CREATE TABLE IF NOT EXISTS \"SQLiteEnums\" (\"TableColumn\" TEXT PRIMARY KEY, \"EnumList\" TEXT)");
} /**
* Return a decimal type-formatted string
// Ensure the table row exists *
if(empty($this->enum_map[$tablefield]) || $this->enum_map[$tablefield] != $enumValues) { * @params array $values Contains a tokenised list of info about this data type
$this->preparedQuery( * @return string
"REPLACE INTO SQLiteEnums (TableColumn, EnumList) VALUES (?, ?)", */
array($tablefield, $enumValues) public function decimal($values, $asDbValue = false)
); {
$this->enum_map[$tablefield] = $enumValues; $default = isset($values['default']) && is_numeric($values['default']) ? $values['default'] : 0;
} return "NUMERIC NOT NULL DEFAULT $default";
}
// Set default
if(!empty($values['default'])) { /**
$default = str_replace(array('"',"'","\\","\0"), "", $values['default']); * Cached list of enum values indexed by table.column
return "TEXT DEFAULT '$default'"; *
} else { * @var array
return 'TEXT'; */
} protected $enum_map = array();
}
/**
/** * Return a enum type-formatted string
* Return a set type-formatted string *
* This type doesn't exist in SQLite either * enums are not supported. as a workaround to store allowed values we creates an additional table
* *
* @see SQLite3SchemaManager::enum() * @params array $values Contains a tokenised list of info about this data type
* * @return string
* @params array $values Contains a tokenised list of info about this data type */
* @return string public function enum($values)
*/ {
public function set($values) { $tablefield = $values['table'] . '.' . $values['name'];
return $this->enum($values); $enumValues = implode(',', $values['enums']);
}
// Ensure the cache table exists
/** if (empty($this->enum_map)) {
* Return a float type-formatted string $this->query("CREATE TABLE IF NOT EXISTS \"SQLiteEnums\" (\"TableColumn\" TEXT PRIMARY KEY, \"EnumList\" TEXT)");
* }
* @params array $values Contains a tokenised list of info about this data type
* @return string // Ensure the table row exists
*/ if (empty($this->enum_map[$tablefield]) || $this->enum_map[$tablefield] != $enumValues) {
public function float($values, $asDbValue = false){ $this->preparedQuery(
return "REAL"; "REPLACE INTO SQLiteEnums (TableColumn, EnumList) VALUES (?, ?)",
} array($tablefield, $enumValues)
);
/** $this->enum_map[$tablefield] = $enumValues;
* Return a Double type-formatted string }
*
* @params array $values Contains a tokenised list of info about this data type // Set default
* @return string if (!empty($values['default'])) {
*/ $default = str_replace(array('"', "'", "\\", "\0"), "", $values['default']);
public function double($values, $asDbValue = false){ return "TEXT DEFAULT '$default'";
return "REAL"; } else {
} return 'TEXT';
}
/** }
* Return a int type-formatted string
* /**
* @params array $values Contains a tokenised list of info about this data type * Return a set type-formatted string
* @return string * This type doesn't exist in SQLite either
*/ *
public function int($values, $asDbValue = false){ * @see SQLite3SchemaManager::enum()
return "INTEGER({$values['precision']}) " . strtoupper($values['null']) . " DEFAULT " . (int)$values['default']; *
} * @params array $values Contains a tokenised list of info about this data type
* @return string
/** */
* Return a bigint type-formatted string public function set($values)
* {
* @params array $values Contains a tokenised list of info about this data type return $this->enum($values);
* @return string }
*/
public function bigint($values, $asDbValue = false){ /**
return $this->int($values, $asDbValue); * Return a float type-formatted string
} *
* @params array $values Contains a tokenised list of info about this data type
/** * @return string
* Return a datetime type-formatted string */
* For SQLite3, we simply return the word 'TEXT', no other parameters are necessary public function float($values, $asDbValue = false)
* {
* @params array $values Contains a tokenised list of info about this data type return "REAL";
* @return string }
*/
public function ss_datetime($values, $asDbValue = false) { /**
return "DATETIME"; * Return a Double type-formatted string
} *
* @params array $values Contains a tokenised list of info about this data type
/** * @return string
* Return a text type-formatted string */
* public function double($values, $asDbValue = false)
* @params array $values Contains a tokenised list of info about this data type {
* @return string return "REAL";
*/ }
public function text($values, $asDbValue = false) {
return 'TEXT'; /**
} * Return a int type-formatted string
*
/** * @params array $values Contains a tokenised list of info about this data type
* Return a time type-formatted string * @return string
* */
* @params array $values Contains a tokenised list of info about this data type public function int($values, $asDbValue = false)
* @return string {
*/ return "INTEGER({$values['precision']}) " . strtoupper($values['null']) . " DEFAULT " . (int)$values['default'];
public function time($values) { }
return "TEXT";
} /**
* Return a bigint type-formatted string
/** *
* Return a varchar type-formatted string * @params array $values Contains a tokenised list of info about this data type
* * @return string
* @params array $values Contains a tokenised list of info about this data type */
* @return string public function bigint($values, $asDbValue = false)
*/ {
public function varchar($values, $asDbValue = false) { return $this->int($values, $asDbValue);
return "VARCHAR({$values['precision']}) COLLATE NOCASE"; }
}
/**
/* * Return a datetime type-formatted string
* Return a 4 digit numeric type. MySQL has a proprietary 'Year' type. * For SQLite3, we simply return the word 'TEXT', no other parameters are necessary
* For SQLite3 we use TEXT *
*/ * @params array $values Contains a tokenised list of info about this data type
public function year($values, $asDbValue = false){ * @return string
return "TEXT"; */
} public function ss_datetime($values, $asDbValue = false)
{
public function IdColumn($asDbValue = false, $hasAutoIncPK = true){ return "DATETIME";
return 'INTEGER PRIMARY KEY AUTOINCREMENT'; }
}
/**
function hasTable($tableName) { * Return a text type-formatted string
return (bool)$this->preparedQuery( *
'SELECT name FROM sqlite_master WHERE type = ? AND name = ?', * @params array $values Contains a tokenised list of info about this data type
array('table', $tableName) * @return string
)->first(); */
} public function text($values, $asDbValue = false)
{
/** return 'TEXT';
* Return enum values for the given field }
*
* @return array /**
*/ * Return a time type-formatted string
public function enumValuesForField($tableName, $fieldName) { *
$tablefield = "$tableName.$fieldName"; * @params array $values Contains a tokenised list of info about this data type
* @return string
// Check already cached values for this field */
if(!empty($this->enum_map[$tablefield])) { public function time($values)
return explode(',', $this->enum_map[$tablefield]); {
} return "TEXT";
}
// Retrieve and cache these details from the database
$classnameinfo = $this->preparedQuery( /**
"SELECT EnumList FROM SQLiteEnums WHERE TableColumn = ?", * Return a varchar type-formatted string
array($tablefield) *
)->first(); * @params array $values Contains a tokenised list of info about this data type
if($classnameinfo) { * @return string
$valueList = $classnameinfo['EnumList']; */
$this->enum_map[$tablefield] = $valueList; public function varchar($values, $asDbValue = false)
return explode(',', $valueList); {
} return "VARCHAR({$values['precision']}) COLLATE NOCASE";
}
// Fallback to empty list
return array(); /*
} * Return a 4 digit numeric type. MySQL has a proprietary 'Year' type.
* For SQLite3 we use TEXT
function dbDataType($type){ */
$values = array( public function year($values, $asDbValue = false)
'unsigned integer' => 'INT' {
); return "TEXT";
}
if(isset($values[$type])) return $values[$type];
else return ''; public function IdColumn($asDbValue = false, $hasAutoIncPK = true)
} {
return 'INTEGER PRIMARY KEY AUTOINCREMENT';
}
public function hasTable($tableName)
{
return (bool)$this->preparedQuery(
'SELECT name FROM sqlite_master WHERE type = ? AND name = ?',
array('table', $tableName)
)->first();
}
/**
* Return enum values for the given field
*
* @return array
*/
public function enumValuesForField($tableName, $fieldName)
{
$tablefield = "$tableName.$fieldName";
// Check already cached values for this field
if (!empty($this->enum_map[$tablefield])) {
return explode(',', $this->enum_map[$tablefield]);
}
// Retrieve and cache these details from the database
$classnameinfo = $this->preparedQuery(
"SELECT EnumList FROM SQLiteEnums WHERE TableColumn = ?",
array($tablefield)
)->first();
if ($classnameinfo) {
$valueList = $classnameinfo['EnumList'];
$this->enum_map[$tablefield] = $valueList;
return explode(',', $valueList);
}
// Fallback to empty list
return array();
}
public function dbDataType($type)
{
$values = array(
'unsigned integer' => 'INT'
);
if (isset($values[$type])) {
return $values[$type];
} else {
return '';
}
}
} }

View File

@ -8,200 +8,219 @@
* *
* @package SQLite3 * @package SQLite3
*/ */
class SQLiteDatabaseConfigurationHelper implements DatabaseConfigurationHelper { class SQLiteDatabaseConfigurationHelper implements DatabaseConfigurationHelper
{
/** /**
* Create a connection of the appropriate type * Create a connection of the appropriate type
* *
* @param array $databaseConfig * @param array $databaseConfig
* @param string $error Error message passed by value * @param string $error Error message passed by value
* @return mixed|null Either the connection object, or null if error * @return mixed|null Either the connection object, or null if error
*/ */
protected function createConnection($databaseConfig, &$error) { protected function createConnection($databaseConfig, &$error)
$error = null; {
try { $error = null;
if(!file_exists($databaseConfig['path'])) { try {
self::create_db_dir($databaseConfig['path']); if (!file_exists($databaseConfig['path'])) {
self::secure_db_dir($databaseConfig['path']); self::create_db_dir($databaseConfig['path']);
} self::secure_db_dir($databaseConfig['path']);
$file = $databaseConfig['path'] . '/' . $databaseConfig['database']; }
$conn = null; $file = $databaseConfig['path'] . '/' . $databaseConfig['database'];
$conn = null;
switch($databaseConfig['type']) { switch ($databaseConfig['type']) {
case 'SQLite3Database': case 'SQLite3Database':
if(empty($databaseConfig['key'])) { if (empty($databaseConfig['key'])) {
$conn = @new SQLite3($file, SQLITE3_OPEN_READWRITE | SQLITE3_OPEN_CREATE); $conn = @new SQLite3($file, SQLITE3_OPEN_READWRITE | SQLITE3_OPEN_CREATE);
} else { } else {
$conn = @new SQLite3($file, SQLITE3_OPEN_READWRITE | SQLITE3_OPEN_CREATE, $databaseConfig['key']); $conn = @new SQLite3($file, SQLITE3_OPEN_READWRITE | SQLITE3_OPEN_CREATE, $databaseConfig['key']);
} }
break; break;
case 'SQLite3PDODatabase': case 'SQLite3PDODatabase':
// May throw a PDOException if fails // May throw a PDOException if fails
$conn = @new PDO("sqlite:$file"); $conn = @new PDO("sqlite:$file");
break; break;
default: default:
$error = 'Invalid connection type'; $error = 'Invalid connection type';
return null; return null;
} }
if($conn) { if ($conn) {
return $conn; return $conn;
} else { } else {
$error = 'Unknown connection error'; $error = 'Unknown connection error';
return null; return null;
} }
} catch(Exception $ex) { } catch (Exception $ex) {
$error = $ex->getMessage(); $error = $ex->getMessage();
return null; return null;
} }
} }
public function requireDatabaseFunctions($databaseConfig) { public function requireDatabaseFunctions($databaseConfig)
$data = DatabaseAdapterRegistry::get_adapter($databaseConfig['type']); {
return !empty($data['supported']); $data = DatabaseAdapterRegistry::get_adapter($databaseConfig['type']);
} return !empty($data['supported']);
}
public function requireDatabaseServer($databaseConfig) { public function requireDatabaseServer($databaseConfig)
$path = $databaseConfig['path']; {
$error = ''; $path = $databaseConfig['path'];
$success = false; $error = '';
$success = false;
if(!$path) { if (!$path) {
$error = 'No database path provided'; $error = 'No database path provided';
} elseif(is_writable($path) || (!file_exists($path) && is_writable(dirname($path)))) { } elseif (is_writable($path) || (!file_exists($path) && is_writable(dirname($path)))) {
// check if folder is writeable // check if folder is writeable
$success = true; $success = true;
} else { } else {
$error = "Permission denied"; $error = "Permission denied";
} }
return array( return array(
'success' => $success, 'success' => $success,
'error' => $error, 'error' => $error,
'path' => $path 'path' => $path
); );
} }
/** /**
* Ensure a database connection is possible using credentials provided. * Ensure a database connection is possible using credentials provided.
* *
* @todo Validate path * @todo Validate path
* *
* @param array $databaseConfig Associative array of db configuration, e.g. "type", "path" etc * @param array $databaseConfig Associative array of db configuration, e.g. "type", "path" etc
* @return array Result - e.g. array('success' => true, 'error' => 'details of error') * @return array Result - e.g. array('success' => true, 'error' => 'details of error')
*/ */
public function requireDatabaseConnection($databaseConfig) { public function requireDatabaseConnection($databaseConfig)
// Do additional validation around file paths {
if(empty($databaseConfig['path'])) return array( // Do additional validation around file paths
'success' => false, if (empty($databaseConfig['path'])) {
'error' => "Missing directory path" return array(
); 'success' => false,
if(empty($databaseConfig['database'])) return array( 'error' => "Missing directory path"
'success' => false, );
'error' => "Missing database filename" }
); if (empty($databaseConfig['database'])) {
return array(
'success' => false,
'error' => "Missing database filename"
);
}
// Create and secure db directory // Create and secure db directory
$path = $databaseConfig['path']; $path = $databaseConfig['path'];
$dirCreated = self::create_db_dir($path); $dirCreated = self::create_db_dir($path);
if(!$dirCreated) return array( if (!$dirCreated) {
'success' => false, return array(
'error' => sprintf('Cannot create path: "%s"', $path) 'success' => false,
); 'error' => sprintf('Cannot create path: "%s"', $path)
$dirSecured = self::secure_db_dir($path); );
if(!$dirSecured) return array( }
'success' => false, $dirSecured = self::secure_db_dir($path);
'error' => sprintf('Cannot secure path through .htaccess: "%s"', $path) if (!$dirSecured) {
); return array(
'success' => false,
'error' => sprintf('Cannot secure path through .htaccess: "%s"', $path)
);
}
$conn = $this->createConnection($databaseConfig, $error); $conn = $this->createConnection($databaseConfig, $error);
$success = !empty($conn); $success = !empty($conn);
return array( return array(
'success' => $success, 'success' => $success,
'connection' => $conn, 'connection' => $conn,
'error' => $error 'error' => $error
); );
} }
public function getDatabaseVersion($databaseConfig) { public function getDatabaseVersion($databaseConfig)
$version = 0; {
$version = 0;
switch($databaseConfig['type']) { switch ($databaseConfig['type']) {
case 'SQLite3Database': case 'SQLite3Database':
$info = SQLite3::version(); $info = SQLite3::version();
$version = trim($info['versionString']); $version = trim($info['versionString']);
break; break;
case 'SQLite3PDODatabase': case 'SQLite3PDODatabase':
// Fallback to using sqlite_version() query // Fallback to using sqlite_version() query
$conn = $this->createConnection($databaseConfig, $error); $conn = $this->createConnection($databaseConfig, $error);
if($conn) { if ($conn) {
$version = $conn->getAttribute(PDO::ATTR_SERVER_VERSION); $version = $conn->getAttribute(PDO::ATTR_SERVER_VERSION);
} }
break; break;
} }
return $version; return $version;
} }
public function requireDatabaseVersion($databaseConfig) { public function requireDatabaseVersion($databaseConfig)
$success = false; {
$error = ''; $success = false;
$version = $this->getDatabaseVersion($databaseConfig); $error = '';
$version = $this->getDatabaseVersion($databaseConfig);
if($version) { if ($version) {
$success = version_compare($version, '3.3', '>='); $success = version_compare($version, '3.3', '>=');
if(!$success) { if (!$success) {
$error = "Your SQLite3 library version is $version. It's recommended you use at least 3.3."; $error = "Your SQLite3 library version is $version. It's recommended you use at least 3.3.";
} }
} }
return array( return array(
'success' => $success, 'success' => $success,
'error' => $error 'error' => $error
); );
} }
public function requireDatabaseOrCreatePermissions($databaseConfig) { public function requireDatabaseOrCreatePermissions($databaseConfig)
$conn = $this->createConnection($databaseConfig, $error); {
$success = $alreadyExists = !empty($conn); $conn = $this->createConnection($databaseConfig, $error);
return array( $success = $alreadyExists = !empty($conn);
'success' => $success, return array(
'alreadyExists' => $alreadyExists, 'success' => $success,
); 'alreadyExists' => $alreadyExists,
} );
}
/** /**
* Creates the provided directory and prepares it for * Creates the provided directory and prepares it for
* storing SQLlite. Use {@link secure_db_dir()} to * storing SQLlite. Use {@link secure_db_dir()} to
* secure it against unauthorized access. * secure it against unauthorized access.
* *
* @param String $path Absolute path, usually with a hidden folder. * @param String $path Absolute path, usually with a hidden folder.
* @return boolean * @return boolean
*/ */
public static function create_db_dir($path) { public static function create_db_dir($path)
return file_exists($path) || mkdir($path); {
} return file_exists($path) || mkdir($path);
}
/** /**
* Secure the provided directory via web-access * Secure the provided directory via web-access
* by placing a .htaccess file in it. * by placing a .htaccess file in it.
* This is just required if the database directory * This is just required if the database directory
* is placed within a publically accessible webroot (the * is placed within a publically accessible webroot (the
* default path is in a hidden folder within assets/). * default path is in a hidden folder within assets/).
* *
* @param String $path Absolute path, containing a SQLite datatbase * @param String $path Absolute path, containing a SQLite datatbase
* @return boolean * @return boolean
*/ */
public static function secure_db_dir($path) { public static function secure_db_dir($path)
return (is_writeable($path)) ? file_put_contents($path . '/.htaccess', 'deny from all') : false; {
} return (is_writeable($path)) ? file_put_contents($path . '/.htaccess', 'deny from all') : false;
}
public function requireDatabaseAlterPermissions($databaseConfig) { public function requireDatabaseAlterPermissions($databaseConfig)
// no concept of table-specific permissions; If you can connect you can alter schema {
return array( // no concept of table-specific permissions; If you can connect you can alter schema
'success' => true, return array(
'applies' => false 'success' => true,
); 'applies' => false
} );
}
} }