Code Coverage |
||||||||||
Classes and Traits |
Functions and Methods |
Lines |
||||||||
Total | |
0.00% |
0 / 1 |
|
20.00% |
5 / 25 |
CRAP | |
41.51% |
44 / 106 |
Connection | |
0.00% |
0 / 1 |
|
20.00% |
5 / 25 |
593.08 | |
41.51% |
44 / 106 |
__construct | |
0.00% |
0 / 1 |
6.01 | |
93.33% |
14 / 15 |
|||
open | |
0.00% |
0 / 1 |
2 | |
95.00% |
19 / 20 |
|||
__destruct | |
0.00% |
0 / 1 |
19.12 | |
28.57% |
2 / 7 |
|||
getAttachedDatabases | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 1 |
|||
sqlFunctionIf | |
0.00% |
0 / 1 |
6 | |
0.00% |
0 / 1 |
|||
sqlFunctionGreatest | |
0.00% |
0 / 1 |
20 | |
0.00% |
0 / 7 |
|||
sqlFunctionConcat | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 2 |
|||
sqlFunctionConcatWs | |
0.00% |
0 / 1 |
12 | |
0.00% |
0 / 4 |
|||
anonymous function | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 1 |
|||
sqlFunctionSubstring | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 1 |
|||
sqlFunctionSubstringIndex | |
0.00% |
0 / 1 |
20 | |
0.00% |
0 / 8 |
|||
sqlFunctionRand | |
0.00% |
0 / 1 |
6 | |
0.00% |
0 / 3 |
|||
sqlFunctionRegexp | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 2 |
|||
sqlFunctionLikeBinary | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 2 |
|||
prepare | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
|||
handleQueryException | |
0.00% |
0 / 1 |
4.12 | |
50.00% |
2 / 4 |
|||
queryRange | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 1 |
|||
queryTemporary | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 6 |
|||
driver | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
|||
databaseType | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 1 |
|||
createDatabase | |
0.00% |
0 / 1 |
12 | |
0.00% |
0 / 4 |
|||
mapConditionOperator | |
100.00% |
1 / 1 |
2 | |
100.00% |
2 / 2 |
|||
prepareQuery | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
|||
nextId | |
0.00% |
0 / 1 |
6 | |
0.00% |
0 / 8 |
|||
getFullQualifiedTableName | |
100.00% |
1 / 1 |
1 | |
100.00% |
2 / 2 |
<?php | |
/** | |
* @file | |
* Contains \Drupal\Core\Database\Driver\sqlite\Connection. | |
*/ | |
namespace Drupal\Core\Database\Driver\sqlite; | |
use Drupal\Core\Database\Database; | |
use Drupal\Core\Database\DatabaseNotFoundException; | |
use Drupal\Core\Database\Connection as DatabaseConnection; | |
/** | |
* SQLite implementation of \Drupal\Core\Database\Connection. | |
*/ | |
class Connection extends DatabaseConnection { | |
/** | |
* Error code for "Unable to open database file" error. | |
*/ | |
const DATABASE_NOT_FOUND = 14; | |
/** | |
* Whether or not the active transaction (if any) will be rolled back. | |
* | |
* @var bool | |
*/ | |
protected $willRollback; | |
/** | |
* All databases attached to the current database. This is used to allow | |
* prefixes to be safely handled without locking the table | |
* | |
* @var array | |
*/ | |
protected $attachedDatabases = array(); | |
/** | |
* Whether or not a table has been dropped this request: the destructor will | |
* only try to get rid of unnecessary databases if there is potential of them | |
* being empty. | |
* | |
* This variable is set to public because Schema needs to | |
* access it. However, it should not be manually set. | |
* | |
* @var bool | |
*/ | |
var $tableDropped = FALSE; | |
/** | |
* Constructs a \Drupal\Core\Database\Driver\sqlite\Connection object. | |
*/ | |
public function __construct(\PDO $connection, array $connection_options) { | |
// We don't need a specific PDOStatement class here, we simulate it in | |
// static::prepare(). | |
$this->statementClass = NULL; | |
parent::__construct($connection, $connection_options); | |
// This driver defaults to transaction support, except if explicitly passed FALSE. | |
$this->transactionSupport = $this->transactionalDDLSupport = !isset($connection_options['transactions']) || $connection_options['transactions'] !== FALSE; | |
$this->connectionOptions = $connection_options; | |
// Attach one database for each registered prefix. | |
$prefixes = $this->prefixes; | |
foreach ($prefixes as &$prefix) { | |
// Empty prefix means query the main database -- no need to attach anything. | |
if (!empty($prefix)) { | |
// Only attach the database once. | |
if (!isset($this->attachedDatabases[$prefix])) { | |
$this->attachedDatabases[$prefix] = $prefix; | |
if ($connection_options['database'] === ':memory:') { | |
// In memory database use ':memory:' as database name. According to | |
// http://www.sqlite.org/inmemorydb.html it will open a unique | |
// database so attaching it twice is not a problem. | |
$this->query('ATTACH DATABASE :database AS :prefix', array(':database' => $connection_options['database'], ':prefix' => $prefix)); | |
} | |
else { | |
$this->query('ATTACH DATABASE :database AS :prefix', array(':database' => $connection_options['database'] . '-' . $prefix, ':prefix' => $prefix)); | |
} | |
} | |
// Add a ., so queries become prefix.table, which is proper syntax for | |
// querying an attached database. | |
$prefix .= '.'; | |
} | |
} | |
// Regenerate the prefixes replacement table. | |
$this->setPrefix($prefixes); | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public static function open(array &$connection_options = array()) { | |
// Allow PDO options to be overridden. | |
$connection_options += array( | |
'pdo' => array(), | |
); | |
$connection_options['pdo'] += array( | |
\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION, | |
// Convert numeric values to strings when fetching. | |
\PDO::ATTR_STRINGIFY_FETCHES => TRUE, | |
); | |
$pdo = new \PDO('sqlite:' . $connection_options['database'], '', '', $connection_options['pdo']); | |
// Create functions needed by SQLite. | |
$pdo->sqliteCreateFunction('if', array(__CLASS__, 'sqlFunctionIf')); | |
$pdo->sqliteCreateFunction('greatest', array(__CLASS__, 'sqlFunctionGreatest')); | |
$pdo->sqliteCreateFunction('pow', 'pow', 2); | |
$pdo->sqliteCreateFunction('exp', 'exp', 1); | |
$pdo->sqliteCreateFunction('length', 'strlen', 1); | |
$pdo->sqliteCreateFunction('md5', 'md5', 1); | |
$pdo->sqliteCreateFunction('concat', array(__CLASS__, 'sqlFunctionConcat')); | |
$pdo->sqliteCreateFunction('concat_ws', array(__CLASS__, 'sqlFunctionConcatWs')); | |
$pdo->sqliteCreateFunction('substring', array(__CLASS__, 'sqlFunctionSubstring'), 3); | |
$pdo->sqliteCreateFunction('substring_index', array(__CLASS__, 'sqlFunctionSubstringIndex'), 3); | |
$pdo->sqliteCreateFunction('rand', array(__CLASS__, 'sqlFunctionRand')); | |
$pdo->sqliteCreateFunction('regexp', array(__CLASS__, 'sqlFunctionRegexp')); | |
// SQLite does not support the LIKE BINARY operator, so we overload the | |
// non-standard GLOB operator for case-sensitive matching. Another option | |
// would have been to override another non-standard operator, MATCH, but | |
// that does not support the NOT keyword prefix. | |
$pdo->sqliteCreateFunction('glob', array(__CLASS__, 'sqlFunctionLikeBinary')); | |
// Create a user-space case-insensitive collation with UTF-8 support. | |
$pdo->sqliteCreateCollation('NOCASE_UTF8', array('Drupal\Component\Utility\Unicode', 'strcasecmp')); | |
// Execute sqlite init_commands. | |
if (isset($connection_options['init_commands'])) { | |
$pdo->exec(implode('; ', $connection_options['init_commands'])); | |
} | |
return $pdo; | |
} | |
/** | |
* Destructor for the SQLite connection. | |
* | |
* We prune empty databases on destruct, but only if tables have been | |
* dropped. This is especially needed when running the test suite, which | |
* creates and destroy databases several times in a row. | |
*/ | |
public function __destruct() { | |
if ($this->tableDropped && !empty($this->attachedDatabases)) { | |
foreach ($this->attachedDatabases as $prefix) { | |
// Check if the database is now empty, ignore the internal SQLite tables. | |
try { | |
$count = $this->query('SELECT COUNT(*) FROM ' . $prefix . '.sqlite_master WHERE type = :type AND name NOT LIKE :pattern', array(':type' => 'table', ':pattern' => 'sqlite_%'))->fetchField(); | |
// We can prune the database file if it doesn't have any tables. | |
if ($count == 0) { | |
// Detaching the database fails at this point, but no other queries | |
// are executed after the connection is destructed so we can simply | |
// remove the database file. | |
unlink($this->connectionOptions['database'] . '-' . $prefix); | |
} | |
} | |
catch (\Exception $e) { | |
// Ignore the exception and continue. There is nothing we can do here | |
// to report the error or fail safe. | |
} | |
} | |
} | |
} | |
/** | |
* Gets all the attached databases. | |
* | |
* @return array | |
* An array of attached database names. | |
* | |
* @see \Drupal\Core\Database\Driver\sqlite\Connection::__construct() | |
*/ | |
public function getAttachedDatabases() { | |
return $this->attachedDatabases; | |
} | |
/** | |
* SQLite compatibility implementation for the IF() SQL function. | |
*/ | |
public static function sqlFunctionIf($condition, $expr1, $expr2 = NULL) { | |
return $condition ? $expr1 : $expr2; | |
} | |
/** | |
* SQLite compatibility implementation for the GREATEST() SQL function. | |
*/ | |
public static function sqlFunctionGreatest() { | |
$args = func_get_args(); | |
foreach ($args as $v) { | |
if (!isset($v)) { | |
unset($args); | |
} | |
} | |
if (count($args)) { | |
return max($args); | |
} | |
else { | |
return NULL; | |
} | |
} | |
/** | |
* SQLite compatibility implementation for the CONCAT() SQL function. | |
*/ | |
public static function sqlFunctionConcat() { | |
$args = func_get_args(); | |
return implode('', $args); | |
} | |
/** | |
* SQLite compatibility implementation for the CONCAT_WS() SQL function. | |
* | |
* @see http://dev.mysql.com/doc/refman/5.6/en/string-functions.html#function_concat-ws | |
*/ | |
public static function sqlFunctionConcatWs() { | |
$args = func_get_args(); | |
$separator = array_shift($args); | |
// If the separator is NULL, the result is NULL. | |
if ($separator === FALSE || is_null($separator)) { | |
return NULL; | |
} | |
// Skip any NULL values after the separator argument. | |
$args = array_filter($args, function ($value) { | |
return !is_null($value); | |
}); | |
return implode($separator, $args); | |
} | |
/** | |
* SQLite compatibility implementation for the SUBSTRING() SQL function. | |
*/ | |
public static function sqlFunctionSubstring($string, $from, $length) { | |
return substr($string, $from - 1, $length); | |
} | |
/** | |
* SQLite compatibility implementation for the SUBSTRING_INDEX() SQL function. | |
*/ | |
public static function sqlFunctionSubstringIndex($string, $delimiter, $count) { | |
// If string is empty, simply return an empty string. | |
if (empty($string)) { | |
return ''; | |
} | |
$end = 0; | |
for ($i = 0; $i < $count; $i++) { | |
$end = strpos($string, $delimiter, $end + 1); | |
if ($end === FALSE) { | |
$end = strlen($string); | |
} | |
} | |
return substr($string, 0, $end); | |
} | |
/** | |
* SQLite compatibility implementation for the RAND() SQL function. | |
*/ | |
public static function sqlFunctionRand($seed = NULL) { | |
if (isset($seed)) { | |
mt_srand($seed); | |
} | |
return mt_rand() / mt_getrandmax(); | |
} | |
/** | |
* SQLite compatibility implementation for the REGEXP SQL operator. | |
* | |
* The REGEXP operator is natively known, but not implemented by default. | |
* | |
* @see http://www.sqlite.org/lang_expr.html#regexp | |
*/ | |
public static function sqlFunctionRegexp($pattern, $subject) { | |
// preg_quote() cannot be used here, since $pattern may contain reserved | |
// regular expression characters already (such as ^, $, etc). Therefore, | |
// use a rare character as PCRE delimiter. | |
$pattern = '#' . addcslashes($pattern, '#') . '#i'; | |
return preg_match($pattern, $subject); | |
} | |
/** | |
* SQLite compatibility implementation for the LIKE BINARY SQL operator. | |
* | |
* SQLite supports case-sensitive LIKE operations through the | |
* 'case_sensitive_like' PRAGMA statement, but only for ASCII characters, so | |
* we have to provide our own implementation with UTF-8 support. | |
* | |
* @see https://sqlite.org/pragma.html#pragma_case_sensitive_like | |
* @see https://sqlite.org/lang_expr.html#like | |
*/ | |
public static function sqlFunctionLikeBinary($pattern, $subject) { | |
// Replace the SQL LIKE wildcard meta-characters with the equivalent regular | |
// expression meta-characters and escape the delimiter that will be used for | |
// matching. | |
$pattern = str_replace(array('%', '_'), array('.*?', '.'), preg_quote($pattern, '/')); | |
return preg_match('/^' . $pattern . '$/', $subject); | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function prepare($statement, array $driver_options = array()) { | |
return new Statement($this->connection, $this, $statement, $driver_options); | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
protected function handleQueryException(\PDOException $e, $query, array $args = array(), $options = array()) { | |
// The database schema might be changed by another process in between the | |
// time that the statement was prepared and the time the statement was run | |
// (e.g. usually happens when running tests). In this case, we need to | |
// re-run the query. | |
// @see http://www.sqlite.org/faq.html#q15 | |
// @see http://www.sqlite.org/rescode.html#schema | |
if (!empty($e->errorInfo[1]) && $e->errorInfo[1] === 17) { | |
return $this->query($query, $args, $options); | |
} | |
parent::handleQueryException($e, $query, $args, $options); | |
} | |
public function queryRange($query, $from, $count, array $args = array(), array $options = array()) { | |
return $this->query($query . ' LIMIT ' . (int) $from . ', ' . (int) $count, $args, $options); | |
} | |
public function queryTemporary($query, array $args = array(), array $options = array()) { | |
// Generate a new temporary table name and protect it from prefixing. | |
// SQLite requires that temporary tables to be non-qualified. | |
$tablename = $this->generateTemporaryTableName(); | |
$prefixes = $this->prefixes; | |
$prefixes[$tablename] = ''; | |
$this->setPrefix($prefixes); | |
$this->query('CREATE TEMPORARY TABLE ' . $tablename . ' AS ' . $query, $args, $options); | |
return $tablename; | |
} | |
public function driver() { | |
return 'sqlite'; | |
} | |
public function databaseType() { | |
return 'sqlite'; | |
} | |
/** | |
* Overrides \Drupal\Core\Database\Connection::createDatabase(). | |
* | |
* @param string $database | |
* The name of the database to create. | |
* | |
* @throws \Drupal\Core\Database\DatabaseNotFoundException | |
*/ | |
public function createDatabase($database) { | |
// Verify the database is writable. | |
$db_directory = new \SplFileInfo(dirname($database)); | |
if (!$db_directory->isDir() && !drupal_mkdir($db_directory->getPathName(), 0755, TRUE)) { | |
throw new DatabaseNotFoundException('Unable to create database directory ' . $db_directory->getPathName()); | |
} | |
} | |
public function mapConditionOperator($operator) { | |
// We don't want to override any of the defaults. | |
static $specials = array( | |
'LIKE' => array('postfix' => " ESCAPE '\\'"), | |
'NOT LIKE' => array('postfix' => " ESCAPE '\\'"), | |
'LIKE BINARY' => array('postfix' => " ESCAPE '\\'", 'operator' => 'GLOB'), | |
'NOT LIKE BINARY' => array('postfix' => " ESCAPE '\\'", 'operator' => 'NOT GLOB'), | |
); | |
return isset($specials[$operator]) ? $specials[$operator] : NULL; | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function prepareQuery($query) { | |
return $this->prepare($this->prefixTables($query)); | |
} | |
public function nextId($existing_id = 0) { | |
$this->startTransaction(); | |
// We can safely use literal queries here instead of the slower query | |
// builder because if a given database breaks here then it can simply | |
// override nextId. However, this is unlikely as we deal with short strings | |
// and integers and no known databases require special handling for those | |
// simple cases. If another transaction wants to write the same row, it will | |
// wait until this transaction commits. Also, the return value needs to be | |
// set to RETURN_AFFECTED as if it were a real update() query otherwise it | |
// is not possible to get the row count properly. | |
$affected = $this->query('UPDATE {sequences} SET value = GREATEST(value, :existing_id) + 1', array( | |
':existing_id' => $existing_id, | |
), array('return' => Database::RETURN_AFFECTED)); | |
if (!$affected) { | |
$this->query('INSERT INTO {sequences} (value) VALUES (:existing_id + 1)', array( | |
':existing_id' => $existing_id, | |
)); | |
} | |
// The transaction gets committed when the transaction object gets destroyed | |
// because it gets out of scope. | |
return $this->query('SELECT value FROM {sequences}')->fetchField(); | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function getFullQualifiedTableName($table) { | |
$prefix = $this->tablePrefix($table); | |
// Don't include the SQLite database file name as part of the table name. | |
return $prefix . $table; | |
} | |
} |