Initial Drupal 11 with DDEV setup
This commit is contained in:
5
web/core/modules/sqlite/sqlite.info.yml
Normal file
5
web/core/modules/sqlite/sqlite.info.yml
Normal file
@ -0,0 +1,5 @@
|
||||
name: SQLite
|
||||
type: module
|
||||
description: 'Provides the SQLite database driver.'
|
||||
package: Core
|
||||
version: VERSION
|
||||
@ -0,0 +1,537 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\sqlite\Driver\Database\sqlite;
|
||||
|
||||
use Drupal\Component\Utility\FilterArray;
|
||||
use Drupal\Core\Database\Connection as DatabaseConnection;
|
||||
use Drupal\Core\Database\DatabaseNotFoundException;
|
||||
use Drupal\Core\Database\ExceptionHandler;
|
||||
use Drupal\Core\Database\StatementInterface;
|
||||
use Drupal\Core\Database\SupportsTemporaryTablesInterface;
|
||||
use Drupal\Core\Database\Transaction\TransactionManagerInterface;
|
||||
|
||||
/**
|
||||
* SQLite implementation of \Drupal\Core\Database\Connection.
|
||||
*/
|
||||
class Connection extends DatabaseConnection implements SupportsTemporaryTablesInterface {
|
||||
|
||||
/**
|
||||
* Error code for "Unable to open database file" error.
|
||||
*/
|
||||
const DATABASE_NOT_FOUND = 14;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $statementWrapperClass = NULL;
|
||||
|
||||
/**
|
||||
* A map of condition operators to SQLite operators.
|
||||
*
|
||||
* We don't want to override any of the defaults.
|
||||
*
|
||||
* @var string[][]
|
||||
*/
|
||||
protected static $sqliteConditionOperatorMap = [
|
||||
'LIKE' => ['postfix' => " ESCAPE '\\'"],
|
||||
'NOT LIKE' => ['postfix' => " ESCAPE '\\'"],
|
||||
'LIKE BINARY' => ['postfix' => " ESCAPE '\\'", 'operator' => 'GLOB'],
|
||||
'NOT LIKE BINARY' => ['postfix' => " ESCAPE '\\'", 'operator' => 'NOT GLOB'],
|
||||
];
|
||||
|
||||
/**
|
||||
* 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 = [];
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
public $tableDropped = FALSE;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $transactionalDDLSupport = TRUE;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $identifierQuotes = ['"', '"'];
|
||||
|
||||
/**
|
||||
* Constructs a \Drupal\sqlite\Driver\Database\sqlite\Connection object.
|
||||
*/
|
||||
public function __construct(\PDO $connection, array $connection_options) {
|
||||
parent::__construct($connection, $connection_options);
|
||||
|
||||
// Empty prefix means query the main database -- no need to attach anything.
|
||||
$prefix = $this->connectionOptions['prefix'] ?? '';
|
||||
if ($prefix !== '') {
|
||||
$this->attachDatabase($prefix);
|
||||
// Add a ., so queries become prefix.table, which is proper syntax for
|
||||
// querying an attached database.
|
||||
$prefix .= '.';
|
||||
}
|
||||
|
||||
// Regenerate the prefix.
|
||||
$this->setPrefix($prefix);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function open(array &$connection_options = []) {
|
||||
// Allow PDO options to be overridden.
|
||||
$connection_options += [
|
||||
'pdo' => [],
|
||||
];
|
||||
$connection_options['pdo'] += [
|
||||
\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
|
||||
// Convert numeric values to strings when fetching.
|
||||
\PDO::ATTR_STRINGIFY_FETCHES => TRUE,
|
||||
];
|
||||
|
||||
try {
|
||||
$pdo = new PDOConnection('sqlite:' . $connection_options['database'], '', '', $connection_options['pdo']);
|
||||
}
|
||||
catch (\PDOException $e) {
|
||||
if ($e->getCode() == static::DATABASE_NOT_FOUND) {
|
||||
throw new DatabaseNotFoundException($e->getMessage(), $e->getCode(), $e);
|
||||
}
|
||||
// SQLite doesn't have a distinct error code for access denied, so don't
|
||||
// deal with that case.
|
||||
throw $e;
|
||||
}
|
||||
|
||||
// Create functions needed by SQLite.
|
||||
$pdo->sqliteCreateFunction('if', [__CLASS__, 'sqlFunctionIf']);
|
||||
$pdo->sqliteCreateFunction('greatest', [__CLASS__, 'sqlFunctionGreatest']);
|
||||
$pdo->sqliteCreateFunction('least', [__CLASS__, 'sqlFunctionLeast']);
|
||||
$pdo->sqliteCreateFunction('pow', 'pow', 2);
|
||||
$pdo->sqliteCreateFunction('exp', 'exp', 1);
|
||||
$pdo->sqliteCreateFunction('length', 'strlen', 1);
|
||||
$pdo->sqliteCreateFunction('md5', 'md5', 1);
|
||||
$pdo->sqliteCreateFunction('concat', [__CLASS__, 'sqlFunctionConcat']);
|
||||
$pdo->sqliteCreateFunction('concat_ws', [__CLASS__, 'sqlFunctionConcatWs']);
|
||||
$pdo->sqliteCreateFunction('substring', [__CLASS__, 'sqlFunctionSubstring'], 3);
|
||||
$pdo->sqliteCreateFunction('substring_index', [__CLASS__, 'sqlFunctionSubstringIndex'], 3);
|
||||
$pdo->sqliteCreateFunction('rand', [__CLASS__, 'sqlFunctionRand']);
|
||||
$pdo->sqliteCreateFunction('regexp', [__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', [__CLASS__, 'sqlFunctionLikeBinary']);
|
||||
|
||||
// Create a user-space case-insensitive collation with UTF-8 support.
|
||||
$pdo->sqliteCreateCollation('NOCASE_UTF8', ['Drupal\Component\Utility\Unicode', 'strcasecmp']);
|
||||
|
||||
// Set SQLite init_commands if not already defined. Enable the Write-Ahead
|
||||
// Logging (WAL) for SQLite. See https://www.drupal.org/node/2348137 and
|
||||
// https://www.sqlite.org/wal.html.
|
||||
$connection_options += [
|
||||
'init_commands' => [],
|
||||
];
|
||||
$connection_options['init_commands'] += [
|
||||
'wal' => "PRAGMA journal_mode=WAL",
|
||||
];
|
||||
|
||||
// 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', [':type' => 'table', ':pattern' => 'sqlite_%'])->fetchField();
|
||||
|
||||
// We can prune the database file if it doesn't have any tables.
|
||||
if ($count == 0 && $this->connectionOptions['database'] != ':memory:' && file_exists($this->connectionOptions['database'] . '-' . $prefix)) {
|
||||
// Detach the database.
|
||||
$this->query('DETACH DATABASE :schema', [':schema' => $prefix]);
|
||||
// Destroy the database file.
|
||||
unlink($this->connectionOptions['database'] . '-' . $prefix);
|
||||
}
|
||||
}
|
||||
catch (\Exception) {
|
||||
// Ignore the exception and continue. There is nothing we can do here
|
||||
// to report the error or fail safe.
|
||||
}
|
||||
}
|
||||
}
|
||||
parent::__destruct();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function attachDatabase(string $database): void {
|
||||
// Only attach the database once.
|
||||
if (!isset($this->attachedDatabases[$database])) {
|
||||
// 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.
|
||||
$database_file = $this->connectionOptions['database'] !== ':memory:' ? $this->connectionOptions['database'] . '-' . $database : $this->connectionOptions['database'];
|
||||
$this->query('ATTACH DATABASE :database_file AS :database', [':database_file' => $database_file, ':database' => $database]);
|
||||
$this->attachedDatabases[$database] = $database;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all the attached databases.
|
||||
*
|
||||
* @return array
|
||||
* An array of attached database names.
|
||||
*
|
||||
* @see \Drupal\sqlite\Driver\Database\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 LEAST() SQL function.
|
||||
*/
|
||||
public static function sqlFunctionLeast() {
|
||||
// Remove all NULL, FALSE and empty strings values but leaves 0 (zero)
|
||||
// values.
|
||||
$values = FilterArray::removeEmptyStrings(func_get_args());
|
||||
|
||||
return count($values) < 1 ? NULL : min($values);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(['%', '_'], ['.*?', '.'], preg_quote($pattern, '/'));
|
||||
return preg_match('/^' . $pattern . '$/', $subject);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function queryRange($query, $from, $count, array $args = [], array $options = []) {
|
||||
return $this->query($query . ' LIMIT ' . (int) $from . ', ' . (int) $count, $args, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function queryTemporary($query, array $args = [], array $options = []) {
|
||||
$tablename = 'db_temporary_' . uniqid();
|
||||
|
||||
$this->query('CREATE TEMPORARY TABLE ' . $tablename . ' AS ' . $query, $args, $options);
|
||||
|
||||
// Temporary tables always live in the temp database, which means that
|
||||
// they cannot be fully qualified table names since they do not live
|
||||
// in the main SQLite database. We provide the fully-qualified name
|
||||
// ourselves to prevent Drupal from applying prefixes.
|
||||
// @see https://www.sqlite.org/lang_createtable.html
|
||||
return 'temp.' . $tablename;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function driver() {
|
||||
return 'sqlite';
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
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::service('file_system')->mkdir($db_directory->getPathName(), 0755, TRUE)) {
|
||||
throw new DatabaseNotFoundException('Unable to create database directory ' . $db_directory->getPathName());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function mapConditionOperator($operator) {
|
||||
return static::$sqliteConditionOperatorMap[$operator] ?? NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function prepareStatement(string $query, array $options, bool $allow_row_count = FALSE): StatementInterface {
|
||||
assert(!isset($options['return']), 'Passing "return" option to prepareStatement() has no effect. See https://www.drupal.org/node/3185520');
|
||||
if (isset($options['fetch']) && is_int($options['fetch'])) {
|
||||
@trigger_error("Passing the 'fetch' key as an integer to \$options in prepareStatement() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\Statement\FetchAs enum instead. See https://www.drupal.org/node/3488338", E_USER_DEPRECATED);
|
||||
}
|
||||
|
||||
try {
|
||||
$query = $this->preprocessStatement($query, $options);
|
||||
$statement = new Statement($this->connection, $this, $query, $options['pdo'] ?? [], $allow_row_count);
|
||||
}
|
||||
catch (\Exception $e) {
|
||||
$this->exceptionHandler()->handleStatementException($e, $query, $options);
|
||||
}
|
||||
return $statement;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getFullQualifiedTableName($table) {
|
||||
$prefix = $this->getPrefix();
|
||||
|
||||
// Don't include the SQLite database file name as part of the table name.
|
||||
return $prefix . $table;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function createConnectionOptionsFromUrl($url, $root) {
|
||||
if ($root !== NULL) {
|
||||
@trigger_error("Passing the \$root value to " . __METHOD__ . "() is deprecated in drupal:11.2.0 and will be removed in drupal:12.0.0. There is no replacement. See https://www.drupal.org/node/3511287", E_USER_DEPRECATED);
|
||||
}
|
||||
$database = parent::createConnectionOptionsFromUrl($url, NULL);
|
||||
|
||||
// A SQLite database path with two leading slashes indicates a system path.
|
||||
// Otherwise the path is relative to the Drupal root.
|
||||
$url_components = parse_url($url);
|
||||
if ($url_components['path'][0] === '/') {
|
||||
$url_components['path'] = substr($url_components['path'], 1);
|
||||
}
|
||||
$database['database'] = $url_components['path'];
|
||||
|
||||
// User credentials and system port are irrelevant for SQLite.
|
||||
unset(
|
||||
$database['username'],
|
||||
$database['password'],
|
||||
$database['port']
|
||||
);
|
||||
|
||||
return $database;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function createUrlFromConnectionOptions(array $connection_options) {
|
||||
if (!isset($connection_options['driver'], $connection_options['database'])) {
|
||||
throw new \InvalidArgumentException("As a minimum, the connection options array must contain at least the 'driver' and 'database' keys");
|
||||
}
|
||||
|
||||
$db_url = 'sqlite://localhost/' . $connection_options['database'] . '?module=sqlite';
|
||||
|
||||
if (isset($connection_options['prefix']) && $connection_options['prefix'] !== '') {
|
||||
$db_url .= '#' . $connection_options['prefix'];
|
||||
}
|
||||
|
||||
return $db_url;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function exceptionHandler() {
|
||||
return new ExceptionHandler();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function select($table, $alias = NULL, array $options = []) {
|
||||
return new Select($this, $table, $alias, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function insert($table, array $options = []) {
|
||||
return new Insert($this, $table, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function upsert($table, array $options = []) {
|
||||
return new Upsert($this, $table, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function truncate($table, array $options = []) {
|
||||
return new Truncate($this, $table, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function schema() {
|
||||
if (empty($this->schema)) {
|
||||
$this->schema = new Schema($this);
|
||||
}
|
||||
return $this->schema;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function driverTransactionManager(): TransactionManagerInterface {
|
||||
return new TransactionManager($this);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\sqlite\Driver\Database\sqlite;
|
||||
|
||||
use Drupal\Core\Database\Query\Delete as QueryDelete;
|
||||
|
||||
@trigger_error('Extending from \Drupal\sqlite\Driver\Database\sqlite\Delete is deprecated in drupal:11.0.0 and is removed from drupal:12.0.0. Extend from the base class instead. See https://www.drupal.org/node/3256524', E_USER_DEPRECATED);
|
||||
|
||||
/**
|
||||
* SQLite implementation of \Drupal\Core\Database\Query\Delete.
|
||||
*/
|
||||
class Delete extends QueryDelete {
|
||||
}
|
||||
114
web/core/modules/sqlite/src/Driver/Database/sqlite/Insert.php
Normal file
114
web/core/modules/sqlite/src/Driver/Database/sqlite/Insert.php
Normal file
@ -0,0 +1,114 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\sqlite\Driver\Database\sqlite;
|
||||
|
||||
use Drupal\Core\Database\DatabaseExceptionWrapper;
|
||||
use Drupal\Core\Database\Query\Insert as QueryInsert;
|
||||
|
||||
/**
|
||||
* SQLite implementation of \Drupal\Core\Database\Query\Insert.
|
||||
*
|
||||
* We ignore all the default fields and use the clever SQLite syntax:
|
||||
* INSERT INTO table DEFAULT VALUES
|
||||
* for degenerated "default only" queries.
|
||||
*/
|
||||
class Insert extends QueryInsert {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function execute() {
|
||||
if (!$this->preExecute()) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// If we're selecting from a SelectQuery, finish building the query and
|
||||
// pass it back, as any remaining options are irrelevant.
|
||||
if (!empty($this->fromQuery)) {
|
||||
// The SelectQuery may contain arguments, load and pass them through.
|
||||
return $this->connection->query((string) $this, $this->fromQuery->getArguments(), $this->queryOptions);
|
||||
}
|
||||
|
||||
// If there are any fields in the query, execute normal INSERT statements.
|
||||
if (count($this->insertFields)) {
|
||||
$stmt = $this->connection->prepareStatement((string) $this, $this->queryOptions);
|
||||
|
||||
if (count($this->insertValues) === 1) {
|
||||
// Inserting a single row does not require a transaction to be atomic,
|
||||
// and executes faster without a transaction wrapper.
|
||||
$insert_values = $this->insertValues[0];
|
||||
try {
|
||||
$stmt->execute($insert_values, $this->queryOptions);
|
||||
}
|
||||
catch (\Exception $e) {
|
||||
$this->connection->exceptionHandler()->handleExecutionException($e, $stmt, $insert_values, $this->queryOptions);
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Inserting multiple rows requires a transaction to be atomic, and
|
||||
// executes faster as a single transaction.
|
||||
try {
|
||||
$transaction = $this->connection->startTransaction();
|
||||
}
|
||||
catch (\PDOException $e) {
|
||||
// $this->connection->exceptionHandler()->handleExecutionException()
|
||||
// requires a $statement argument, so we cannot use that.
|
||||
throw new DatabaseExceptionWrapper($e->getMessage(), 0, $e);
|
||||
}
|
||||
foreach ($this->insertValues as $insert_values) {
|
||||
try {
|
||||
$stmt->execute($insert_values, $this->queryOptions);
|
||||
}
|
||||
catch (\Exception $e) {
|
||||
// One of the INSERTs failed, rollback the whole batch.
|
||||
$transaction->rollBack();
|
||||
$this->connection->exceptionHandler()->handleExecutionException($e, $stmt, $insert_values, $this->queryOptions);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Re-initialize the values array so that we can re-use this query.
|
||||
$this->insertValues = [];
|
||||
}
|
||||
// If there are no fields in the query, execute an INSERT statement that
|
||||
// only populates default values.
|
||||
else {
|
||||
$stmt = $this->connection->prepareStatement("INSERT INTO {{$this->table}} DEFAULT VALUES", $this->queryOptions);
|
||||
try {
|
||||
$stmt->execute(NULL, $this->queryOptions);
|
||||
}
|
||||
catch (\Exception $e) {
|
||||
$this->connection->exceptionHandler()->handleExecutionException($e, $stmt, [], $this->queryOptions);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->connection->lastInsertId();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function __toString() {
|
||||
// Create a sanitized comment string to prepend to the query.
|
||||
$comments = $this->connection->makeComment($this->comments);
|
||||
|
||||
// Produce as many generic placeholders as necessary.
|
||||
$placeholders = [];
|
||||
if (!empty($this->insertFields)) {
|
||||
$placeholders = array_fill(0, count($this->insertFields), '?');
|
||||
}
|
||||
|
||||
$insert_fields = array_map(function ($field) {
|
||||
return $this->connection->escapeField($field);
|
||||
}, $this->insertFields);
|
||||
|
||||
// If we're selecting from a SelectQuery, finish building the query and
|
||||
// pass it back, as any remaining options are irrelevant.
|
||||
if (!empty($this->fromQuery)) {
|
||||
$insert_fields_string = $insert_fields ? ' (' . implode(', ', $insert_fields) . ') ' : ' ';
|
||||
return $comments . 'INSERT INTO {' . $this->table . '}' . $insert_fields_string . $this->fromQuery;
|
||||
}
|
||||
|
||||
return $comments . 'INSERT INTO {' . $this->table . '} (' . implode(', ', $insert_fields) . ') VALUES (' . implode(', ', $placeholders) . ')';
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,119 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\sqlite\Driver\Database\sqlite\Install;
|
||||
|
||||
use Drupal\Core\Database\Database;
|
||||
use Drupal\Core\StringTranslation\StringTranslationTrait;
|
||||
use Drupal\sqlite\Driver\Database\sqlite\Connection;
|
||||
use Drupal\Core\Database\DatabaseNotFoundException;
|
||||
use Drupal\Core\Database\Install\Tasks as InstallTasks;
|
||||
|
||||
/**
|
||||
* Specifies installation tasks for SQLite databases.
|
||||
*/
|
||||
class Tasks extends InstallTasks {
|
||||
|
||||
use StringTranslationTrait;
|
||||
|
||||
/**
|
||||
* Minimum required SQLite version.
|
||||
*
|
||||
* Use to build sqlite library with json1 option for JSON datatype support.
|
||||
*
|
||||
* @see https://www.sqlite.org/json1.html
|
||||
*/
|
||||
const SQLITE_MINIMUM_VERSION = '3.45';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $pdoDriver = 'sqlite';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function name() {
|
||||
return $this->t('SQLite');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function minimumVersion() {
|
||||
return static::SQLITE_MINIMUM_VERSION;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getFormOptions(array $database) {
|
||||
$form = parent::getFormOptions($database);
|
||||
|
||||
// Remove the options that only apply to client/server style databases.
|
||||
unset($form['username'], $form['password'], $form['advanced_options']['host'], $form['advanced_options']['port']);
|
||||
|
||||
// Make the text more accurate for SQLite.
|
||||
$form['database']['#title'] = $this->t('Database file');
|
||||
$form['database']['#description'] = $this->t('The absolute path to the file where @drupal data will be stored. This must be writable by the web server and should exist outside of the web root.', ['@drupal' => drupal_install_profile_distribution_name()]);
|
||||
$default_database = \Drupal::getContainer()->getParameter('site.path') . '/files/.ht.sqlite';
|
||||
$form['database']['#default_value'] = empty($database['database']) ? $default_database : $database['database'];
|
||||
return $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function connect() {
|
||||
try {
|
||||
// This doesn't actually test the connection.
|
||||
Database::setActiveConnection();
|
||||
// Now actually do a check.
|
||||
Database::getConnection();
|
||||
$this->pass('Drupal can CONNECT to the database ok.');
|
||||
}
|
||||
catch (\Exception $e) {
|
||||
// Attempt to create the database if it is not found.
|
||||
if ($e->getCode() == Connection::DATABASE_NOT_FOUND) {
|
||||
// Remove the database string from connection info.
|
||||
$connection_info = Database::getConnectionInfo();
|
||||
$database = $connection_info['default']['database'];
|
||||
|
||||
// We cannot use \Drupal::service('file_system')->getTempDirectory()
|
||||
// here because we haven't yet successfully connected to the database.
|
||||
$connection_info['default']['database'] = \Drupal::service('file_system')->tempnam(sys_get_temp_dir(), 'sqlite');
|
||||
|
||||
// In order to change the Database::$databaseInfo array, need to remove
|
||||
// the active connection, then re-add it with the new info.
|
||||
Database::removeConnection('default');
|
||||
Database::addConnectionInfo('default', 'default', $connection_info['default']);
|
||||
|
||||
try {
|
||||
Database::getConnection()->createDatabase($database);
|
||||
Database::closeConnection();
|
||||
|
||||
// Now, restore the database config.
|
||||
Database::removeConnection('default');
|
||||
$connection_info['default']['database'] = $database;
|
||||
Database::addConnectionInfo('default', 'default', $connection_info['default']);
|
||||
|
||||
// Check the database connection.
|
||||
Database::getConnection();
|
||||
$this->pass('Drupal can CONNECT to the database ok.');
|
||||
}
|
||||
catch (DatabaseNotFoundException $e) {
|
||||
// Still no dice; probably a permission issue. Raise the error to the
|
||||
// installer.
|
||||
$this->fail($this->t('Failed to open or create database file %database. The database engine reports the following message when attempting to create the database: %error.', ['%database' => $database, '%error' => $e->getMessage()]));
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Database connection failed for some other reason than a non-existent
|
||||
// database.
|
||||
$this->fail($this->t('Failed to connect to database. The database engine reports the following message: %error.<ul><li>Does the database file exist?</li><li>Does web server have permission to write to the database file?</li>Does the web server have permission to write to the directory the database file should be created in?</li></ul>', ['%error' => $e->getMessage()]));
|
||||
return FALSE;
|
||||
}
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
}
|
||||
13
web/core/modules/sqlite/src/Driver/Database/sqlite/Merge.php
Normal file
13
web/core/modules/sqlite/src/Driver/Database/sqlite/Merge.php
Normal file
@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\sqlite\Driver\Database\sqlite;
|
||||
|
||||
use Drupal\Core\Database\Query\Merge as QueryMerge;
|
||||
|
||||
@trigger_error('Extending from \Drupal\sqlite\Driver\Database\sqlite\Merge is deprecated in drupal:11.0.0 and is removed from drupal:12.0.0. Extend from the base class instead. See https://www.drupal.org/node/3256524', E_USER_DEPRECATED);
|
||||
|
||||
/**
|
||||
* SQLite implementation of \Drupal\Core\Database\Query\Merge.
|
||||
*/
|
||||
class Merge extends QueryMerge {
|
||||
}
|
||||
@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\sqlite\Driver\Database\sqlite;
|
||||
|
||||
/**
|
||||
* SQLite-specific implementation of a PDO connection.
|
||||
*
|
||||
* SQLite does not implement row locks, so when it acquires a lock, it locks
|
||||
* the entire database. To improve performance, by default SQLite tries to
|
||||
* defer acquiring a write lock until the first write operation of a
|
||||
* transaction rather than when the transaction is started. Unfortunately, this
|
||||
* seems to be incompatible with how Drupal uses transactions, and frequently
|
||||
* leads to deadlocks.
|
||||
*
|
||||
* Therefore, this class overrides \PDO to begin transactions with a
|
||||
* BEGIN IMMEDIATE TRANSACTION statement, for which SQLite acquires the write
|
||||
* lock immediately. This can incur some performance cost in a high concurrency
|
||||
* environment: it adds approximately 5% to the time it takes to execute Drupal
|
||||
* core's entire test suite on DrupalCI, and it potentially could add more in a
|
||||
* higher concurrency environment. However, under high enough concurrency of a
|
||||
* Drupal application, SQLite isn't the best choice anyway, and a database
|
||||
* engine that implements row locking, such as MySQL or PostgreSQL, is more
|
||||
* suitable.
|
||||
*
|
||||
* Because of https://bugs.php.net/42766 we have to create such a transaction
|
||||
* manually which means we must also override commit() and rollback().
|
||||
*
|
||||
* @see https://www.drupal.org/project/drupal/issues/1120020
|
||||
*/
|
||||
class PDOConnection extends \PDO {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function beginTransaction(): bool {
|
||||
return $this->exec('BEGIN IMMEDIATE TRANSACTION') !== FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function commit(): bool {
|
||||
return $this->exec('COMMIT') !== FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function rollBack(): bool {
|
||||
return $this->exec('ROLLBACK') !== FALSE;
|
||||
}
|
||||
|
||||
}
|
||||
836
web/core/modules/sqlite/src/Driver/Database/sqlite/Schema.php
Normal file
836
web/core/modules/sqlite/src/Driver/Database/sqlite/Schema.php
Normal file
@ -0,0 +1,836 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\sqlite\Driver\Database\sqlite;
|
||||
|
||||
use Drupal\Core\Database\SchemaObjectExistsException;
|
||||
use Drupal\Core\Database\SchemaObjectDoesNotExistException;
|
||||
use Drupal\Core\Database\Schema as DatabaseSchema;
|
||||
|
||||
// cspell:ignore autoincrement autoindex
|
||||
|
||||
/**
|
||||
* @ingroup schemaapi
|
||||
* @{
|
||||
*/
|
||||
|
||||
/**
|
||||
* SQLite implementation of \Drupal\Core\Database\Schema.
|
||||
*/
|
||||
class Schema extends DatabaseSchema {
|
||||
|
||||
/**
|
||||
* Override DatabaseSchema::$defaultSchema.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $defaultSchema = 'main';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function tableExists($table, $add_prefix = TRUE) {
|
||||
$info = $this->getPrefixInfo($table, $add_prefix);
|
||||
|
||||
// Don't use {} around sqlite_master table.
|
||||
return (bool) $this->connection->query('SELECT 1 FROM [' . $info['schema'] . '].sqlite_master WHERE type = :type AND name = :name', [':type' => 'table', ':name' => $info['table']])->fetchField();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function fieldExists($table, $column) {
|
||||
$schema = $this->introspectSchema($table);
|
||||
return !empty($schema['fields'][$column]);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function createTableSql($name, $table) {
|
||||
if (!empty($table['primary key']) && is_array($table['primary key'])) {
|
||||
$this->ensureNotNullPrimaryKey($table['primary key'], $table['fields']);
|
||||
}
|
||||
|
||||
$sql = [];
|
||||
$sql[] = "CREATE TABLE {" . $name . "} (\n" . $this->createColumnsSql($name, $table) . "\n)\n";
|
||||
return array_merge($sql, $this->createIndexSql($name, $table));
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the SQL expression for indexes.
|
||||
*/
|
||||
protected function createIndexSql($tablename, $schema) {
|
||||
$sql = [];
|
||||
$info = $this->getPrefixInfo($tablename);
|
||||
if (!empty($schema['unique keys'])) {
|
||||
foreach ($schema['unique keys'] as $key => $fields) {
|
||||
$sql[] = 'CREATE UNIQUE INDEX [' . $info['schema'] . '].[' . $info['table'] . '_' . $key . '] ON [' . $info['table'] . '] (' . $this->createKeySql($fields) . ")\n";
|
||||
}
|
||||
}
|
||||
if (!empty($schema['indexes'])) {
|
||||
foreach ($schema['indexes'] as $key => $fields) {
|
||||
$sql[] = 'CREATE INDEX [' . $info['schema'] . '].[' . $info['table'] . '_' . $key . '] ON [' . $info['table'] . '] (' . $this->createKeySql($fields) . ")\n";
|
||||
}
|
||||
}
|
||||
return $sql;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the SQL expression for creating columns.
|
||||
*/
|
||||
protected function createColumnsSql($tablename, $schema) {
|
||||
$sql_array = [];
|
||||
|
||||
// Add the SQL statement for each field.
|
||||
foreach ($schema['fields'] as $name => $field) {
|
||||
if (isset($field['type']) && $field['type'] == 'serial') {
|
||||
if (isset($schema['primary key']) && ($key = array_search($name, $schema['primary key'])) !== FALSE) {
|
||||
unset($schema['primary key'][$key]);
|
||||
}
|
||||
}
|
||||
$sql_array[] = $this->createFieldSql($name, $this->processField($field));
|
||||
}
|
||||
|
||||
// Process keys.
|
||||
if (!empty($schema['primary key'])) {
|
||||
$sql_array[] = " PRIMARY KEY (" . $this->createKeySql($schema['primary key']) . ")";
|
||||
}
|
||||
|
||||
return implode(", \n", $sql_array);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the SQL expression for keys.
|
||||
*/
|
||||
protected function createKeySql($fields) {
|
||||
$return = [];
|
||||
foreach ($fields as $field) {
|
||||
if (is_array($field)) {
|
||||
$return[] = '[' . $field[0] . ']';
|
||||
}
|
||||
else {
|
||||
$return[] = '[' . $field . ']';
|
||||
}
|
||||
}
|
||||
return implode(', ', $return);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set database-engine specific properties for a field.
|
||||
*
|
||||
* @param array $field
|
||||
* A field description array, as specified in the schema documentation.
|
||||
*/
|
||||
protected function processField($field) {
|
||||
if (!isset($field['size'])) {
|
||||
$field['size'] = 'normal';
|
||||
}
|
||||
|
||||
// Set the correct database-engine specific datatype.
|
||||
// In case one is already provided, force it to uppercase.
|
||||
if (isset($field['sqlite_type'])) {
|
||||
$field['sqlite_type'] = mb_strtoupper($field['sqlite_type']);
|
||||
}
|
||||
else {
|
||||
$map = $this->getFieldTypeMap();
|
||||
$field['sqlite_type'] = $map[$field['type'] . ':' . $field['size']];
|
||||
|
||||
// Numeric fields with a specified scale have to be stored as floats.
|
||||
if ($field['sqlite_type'] === 'NUMERIC' && isset($field['scale'])) {
|
||||
$field['sqlite_type'] = 'FLOAT';
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($field['type']) && $field['type'] == 'serial') {
|
||||
$field['auto_increment'] = TRUE;
|
||||
}
|
||||
|
||||
return $field;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an SQL string for a field to be used in table create or alter.
|
||||
*
|
||||
* Before passing a field out of a schema definition into this function it has
|
||||
* to be processed by self::processField().
|
||||
*
|
||||
* @param string $name
|
||||
* Name of the field.
|
||||
* @param array $spec
|
||||
* The field specification, as per the schema data structure format.
|
||||
*/
|
||||
protected function createFieldSql($name, $spec) {
|
||||
$name = $this->connection->escapeField($name);
|
||||
if (!empty($spec['auto_increment'])) {
|
||||
$sql = $name . " INTEGER PRIMARY KEY AUTOINCREMENT";
|
||||
if (!empty($spec['unsigned'])) {
|
||||
$sql .= ' CHECK (' . $name . '>= 0)';
|
||||
}
|
||||
}
|
||||
else {
|
||||
$sql = $name . ' ' . $spec['sqlite_type'];
|
||||
|
||||
if (in_array($spec['sqlite_type'], ['VARCHAR', 'TEXT'])) {
|
||||
if (isset($spec['length'])) {
|
||||
$sql .= '(' . $spec['length'] . ')';
|
||||
}
|
||||
|
||||
if (isset($spec['binary']) && $spec['binary'] === FALSE) {
|
||||
$sql .= ' COLLATE NOCASE_UTF8';
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($spec['not null'])) {
|
||||
if ($spec['not null']) {
|
||||
$sql .= ' NOT NULL';
|
||||
}
|
||||
else {
|
||||
$sql .= ' NULL';
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($spec['unsigned'])) {
|
||||
$sql .= ' CHECK (' . $name . '>= 0)';
|
||||
}
|
||||
|
||||
if (isset($spec['default'])) {
|
||||
if (is_string($spec['default'])) {
|
||||
$spec['default'] = $this->connection->quote($spec['default']);
|
||||
}
|
||||
$sql .= ' DEFAULT ' . $spec['default'];
|
||||
}
|
||||
|
||||
if (empty($spec['not null']) && !isset($spec['default'])) {
|
||||
$sql .= ' DEFAULT NULL';
|
||||
}
|
||||
}
|
||||
return $sql;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getFieldTypeMap() {
|
||||
// Put :normal last so it gets preserved by array_flip. This makes
|
||||
// it much easier for modules (such as schema.module) to map
|
||||
// database types back into schema types.
|
||||
// $map does not use drupal_static as its value never changes.
|
||||
static $map = [
|
||||
'varchar_ascii:normal' => 'VARCHAR',
|
||||
|
||||
'varchar:normal' => 'VARCHAR',
|
||||
'char:normal' => 'CHAR',
|
||||
|
||||
'text:tiny' => 'TEXT',
|
||||
'text:small' => 'TEXT',
|
||||
'text:medium' => 'TEXT',
|
||||
'text:big' => 'TEXT',
|
||||
'text:normal' => 'TEXT',
|
||||
|
||||
'serial:tiny' => 'INTEGER',
|
||||
'serial:small' => 'INTEGER',
|
||||
'serial:medium' => 'INTEGER',
|
||||
'serial:big' => 'INTEGER',
|
||||
'serial:normal' => 'INTEGER',
|
||||
|
||||
'int:tiny' => 'INTEGER',
|
||||
'int:small' => 'INTEGER',
|
||||
'int:medium' => 'INTEGER',
|
||||
'int:big' => 'INTEGER',
|
||||
'int:normal' => 'INTEGER',
|
||||
|
||||
'float:tiny' => 'FLOAT',
|
||||
'float:small' => 'FLOAT',
|
||||
'float:medium' => 'FLOAT',
|
||||
'float:big' => 'FLOAT',
|
||||
'float:normal' => 'FLOAT',
|
||||
|
||||
'numeric:normal' => 'NUMERIC',
|
||||
|
||||
'blob:big' => 'BLOB',
|
||||
'blob:normal' => 'BLOB',
|
||||
|
||||
// Only the SQLite driver has this field map to due to a fatal error
|
||||
// error caused by this driver's schema on table introspection.
|
||||
// @todo Add support to all drivers in https://drupal.org/i/3343634
|
||||
'json:normal' => 'JSON',
|
||||
];
|
||||
return $map;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function renameTable($table, $new_name) {
|
||||
if (!$this->tableExists($table)) {
|
||||
throw new SchemaObjectDoesNotExistException("Cannot rename '$table' to '$new_name': table '$table' doesn't exist.");
|
||||
}
|
||||
if ($this->tableExists($new_name)) {
|
||||
throw new SchemaObjectExistsException("Cannot rename '$table' to '$new_name': table '$new_name' already exists.");
|
||||
}
|
||||
|
||||
$schema = $this->introspectSchema($table);
|
||||
|
||||
// SQLite doesn't allow you to rename tables outside of the current
|
||||
// database. So the syntax '... RENAME TO database.table' would fail.
|
||||
// So we must determine the full table name here rather than surrounding
|
||||
// the table with curly braces in case the db_prefix contains a reference
|
||||
// to a database outside of our existing database.
|
||||
$info = $this->getPrefixInfo($new_name);
|
||||
$this->executeDdlStatement('ALTER TABLE {' . $table . '} RENAME TO [' . $info['table'] . ']');
|
||||
|
||||
// Drop the indexes, there is no RENAME INDEX command in SQLite.
|
||||
if (!empty($schema['unique keys'])) {
|
||||
foreach ($schema['unique keys'] as $key => $fields) {
|
||||
$this->dropIndex($table, $key);
|
||||
}
|
||||
}
|
||||
if (!empty($schema['indexes'])) {
|
||||
foreach ($schema['indexes'] as $index => $fields) {
|
||||
$this->dropIndex($table, $index);
|
||||
}
|
||||
}
|
||||
|
||||
// Recreate the indexes.
|
||||
$statements = $this->createIndexSql($new_name, $schema);
|
||||
foreach ($statements as $statement) {
|
||||
$this->executeDdlStatement($statement);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function dropTable($table) {
|
||||
if (!$this->tableExists($table)) {
|
||||
return FALSE;
|
||||
}
|
||||
$this->connection->tableDropped = TRUE;
|
||||
$this->executeDdlStatement('DROP TABLE {' . $table . '}');
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function addField($table, $field, $specification, $keys_new = []) {
|
||||
if (!$this->tableExists($table)) {
|
||||
throw new SchemaObjectDoesNotExistException("Cannot add field '$table.$field': table doesn't exist.");
|
||||
}
|
||||
if ($this->fieldExists($table, $field)) {
|
||||
throw new SchemaObjectExistsException("Cannot add field '$table.$field': field already exists.");
|
||||
}
|
||||
if (isset($keys_new['primary key']) && in_array($field, $keys_new['primary key'], TRUE)) {
|
||||
$this->ensureNotNullPrimaryKey($keys_new['primary key'], [$field => $specification]);
|
||||
}
|
||||
|
||||
// SQLite doesn't have a full-featured ALTER TABLE statement. It only
|
||||
// supports adding new fields to a table, in some simple cases. In most
|
||||
// cases, we have to create a new table and copy the data over.
|
||||
if (empty($keys_new) && (empty($specification['not null']) || isset($specification['default']))) {
|
||||
// When we don't have to create new keys and we are not creating a NOT
|
||||
// NULL column without a default value, we can use the quicker version.
|
||||
$query = 'ALTER TABLE {' . $table . '} ADD ' . $this->createFieldSql($field, $this->processField($specification));
|
||||
$this->executeDdlStatement($query);
|
||||
|
||||
// Apply the initial value if set.
|
||||
if (isset($specification['initial_from_field'])) {
|
||||
if (isset($specification['initial'])) {
|
||||
$expression = 'COALESCE(' . $specification['initial_from_field'] . ', :default_initial_value)';
|
||||
$arguments = [':default_initial_value' => $specification['initial']];
|
||||
}
|
||||
else {
|
||||
$expression = $specification['initial_from_field'];
|
||||
$arguments = [];
|
||||
}
|
||||
$this->connection->update($table)
|
||||
->expression($field, $expression, $arguments)
|
||||
->execute();
|
||||
}
|
||||
elseif (isset($specification['initial'])) {
|
||||
$this->connection->update($table)
|
||||
->fields([$field => $specification['initial']])
|
||||
->execute();
|
||||
}
|
||||
}
|
||||
else {
|
||||
// We cannot add the field directly. Use the slower table alteration
|
||||
// method, starting from the old schema.
|
||||
$old_schema = $this->introspectSchema($table);
|
||||
$new_schema = $old_schema;
|
||||
|
||||
// Add the new field.
|
||||
$new_schema['fields'][$field] = $specification;
|
||||
|
||||
// Build the mapping between the old fields and the new fields.
|
||||
$mapping = [];
|
||||
if (isset($specification['initial_from_field'])) {
|
||||
// If we have an initial value, copy it over.
|
||||
if (isset($specification['initial'])) {
|
||||
$expression = 'COALESCE(' . $specification['initial_from_field'] . ', :default_initial_value)';
|
||||
$arguments = [':default_initial_value' => $specification['initial']];
|
||||
}
|
||||
else {
|
||||
$expression = $specification['initial_from_field'];
|
||||
$arguments = [];
|
||||
}
|
||||
$mapping[$field] = [
|
||||
'expression' => $expression,
|
||||
'arguments' => $arguments,
|
||||
];
|
||||
}
|
||||
elseif (isset($specification['initial'])) {
|
||||
// If we have an initial value, copy it over.
|
||||
$mapping[$field] = [
|
||||
'expression' => ':new_field_initial',
|
||||
'arguments' => [':new_field_initial' => $specification['initial']],
|
||||
];
|
||||
}
|
||||
else {
|
||||
// Else use the default of the field.
|
||||
$mapping[$field] = NULL;
|
||||
}
|
||||
|
||||
// Add the new indexes.
|
||||
$new_schema = array_merge($new_schema, $keys_new);
|
||||
|
||||
$this->alterTable($table, $old_schema, $new_schema, $mapping);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a table with a new schema containing the old content.
|
||||
*
|
||||
* As SQLite does not support ALTER TABLE (with a few exceptions) it is
|
||||
* necessary to create a new table and copy over the old content.
|
||||
*
|
||||
* @param string $table
|
||||
* Name of the table to be altered.
|
||||
* @param array $old_schema
|
||||
* The old schema array for the table.
|
||||
* @param array $new_schema
|
||||
* The new schema array for the table.
|
||||
* @param array $mapping
|
||||
* An optional mapping between the fields of the old specification and the
|
||||
* fields of the new specification. An associative array, whose keys are
|
||||
* the fields of the new table, and values can take two possible forms:
|
||||
* - a simple string, which is interpreted as the name of a field of the
|
||||
* old table,
|
||||
* - an associative array with two keys 'expression' and 'arguments',
|
||||
* that will be used as an expression field.
|
||||
*/
|
||||
protected function alterTable($table, $old_schema, $new_schema, array $mapping = []) {
|
||||
$i = 0;
|
||||
do {
|
||||
$new_table = $table . '_' . $i++;
|
||||
} while ($this->tableExists($new_table));
|
||||
|
||||
$this->createTable($new_table, $new_schema);
|
||||
|
||||
// Build a SQL query to migrate the data from the old table to the new.
|
||||
$select = $this->connection->select($table);
|
||||
|
||||
// Complete the mapping.
|
||||
$possible_keys = array_keys($new_schema['fields']);
|
||||
$mapping += array_combine($possible_keys, $possible_keys);
|
||||
|
||||
// Now add the fields.
|
||||
foreach ($mapping as $field_alias => $field_source) {
|
||||
// Just ignore this field (ie. use its default value).
|
||||
if (!isset($field_source)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (is_array($field_source)) {
|
||||
$select->addExpression($field_source['expression'], $field_alias, $field_source['arguments']);
|
||||
}
|
||||
else {
|
||||
$select->addField($table, $field_source, $field_alias);
|
||||
}
|
||||
}
|
||||
|
||||
// Execute the data migration query.
|
||||
$this->connection->insert($new_table)
|
||||
->from($select)
|
||||
->execute();
|
||||
|
||||
$old_count = $this->connection->query('SELECT COUNT(*) FROM {' . $table . '}')->fetchField();
|
||||
$new_count = $this->connection->query('SELECT COUNT(*) FROM {' . $new_table . '}')->fetchField();
|
||||
if ($old_count == $new_count) {
|
||||
$this->dropTable($table);
|
||||
$this->renameTable($new_table, $table);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find out the schema of a table.
|
||||
*
|
||||
* This function uses introspection methods provided by the database to
|
||||
* create a schema array. This is useful, for example, during update when
|
||||
* the old schema is not available.
|
||||
*
|
||||
* @param string $table
|
||||
* Name of the table.
|
||||
*
|
||||
* @return array
|
||||
* An array representing the schema.
|
||||
*
|
||||
* @throws \Exception
|
||||
* If a column of the table could not be parsed.
|
||||
*/
|
||||
protected function introspectSchema($table) {
|
||||
$mapped_fields = array_flip($this->getFieldTypeMap());
|
||||
$schema = [
|
||||
'fields' => [],
|
||||
'primary key' => [],
|
||||
'unique keys' => [],
|
||||
'indexes' => [],
|
||||
];
|
||||
|
||||
$info = $this->getPrefixInfo($table);
|
||||
$result = $this->connection->query('PRAGMA [' . $info['schema'] . '].table_info([' . $info['table'] . '])');
|
||||
foreach ($result as $row) {
|
||||
if (preg_match('/^([^(]+)\((.*)\)$/', $row->type, $matches)) {
|
||||
$type = $matches[1];
|
||||
$length = $matches[2];
|
||||
}
|
||||
else {
|
||||
$type = $row->type;
|
||||
$length = NULL;
|
||||
}
|
||||
if (isset($mapped_fields[$type])) {
|
||||
[$type, $size] = explode(':', $mapped_fields[$type]);
|
||||
$schema['fields'][$row->name] = [
|
||||
'type' => $type,
|
||||
'size' => $size,
|
||||
'not null' => !empty($row->notnull) || $row->pk !== "0",
|
||||
];
|
||||
if ($length) {
|
||||
$schema['fields'][$row->name]['length'] = $length;
|
||||
}
|
||||
|
||||
// Convert the default into a properly typed value.
|
||||
if ($row->dflt_value === 'NULL') {
|
||||
$schema['fields'][$row->name]['default'] = NULL;
|
||||
}
|
||||
elseif (is_string($row->dflt_value) && $row->dflt_value[0] === '\'') {
|
||||
// Remove the wrapping single quotes. And replace duplicate single
|
||||
// quotes with a single quote.
|
||||
$schema['fields'][$row->name]['default'] = str_replace("''", "'", substr($row->dflt_value, 1, -1));
|
||||
}
|
||||
elseif (is_numeric($row->dflt_value)) {
|
||||
// Adding 0 to a string will cause PHP to convert it to a float or
|
||||
// an integer depending on what the string is. For example:
|
||||
// - '1' + 0 = 1
|
||||
// - '1.0' + 0 = 1.0
|
||||
$schema['fields'][$row->name]['default'] = $row->dflt_value + 0;
|
||||
}
|
||||
else {
|
||||
$schema['fields'][$row->name]['default'] = $row->dflt_value;
|
||||
}
|
||||
// $row->pk contains a number that reflects the primary key order. We
|
||||
// use that as the key and sort (by key) below to return the primary key
|
||||
// in the same order that it is stored in.
|
||||
if ($row->pk) {
|
||||
$schema['primary key'][$row->pk] = $row->name;
|
||||
}
|
||||
}
|
||||
else {
|
||||
throw new \Exception("Unable to parse the column type " . $row->type);
|
||||
}
|
||||
}
|
||||
ksort($schema['primary key']);
|
||||
// Re-key the array because $row->pk starts counting at 1.
|
||||
$schema['primary key'] = array_values($schema['primary key']);
|
||||
|
||||
$indexes = [];
|
||||
$result = $this->connection->query('PRAGMA [' . $info['schema'] . '].index_list([' . $info['table'] . '])');
|
||||
foreach ($result as $row) {
|
||||
if (!str_starts_with($row->name, 'sqlite_autoindex_')) {
|
||||
$indexes[] = [
|
||||
'schema_key' => $row->unique ? 'unique keys' : 'indexes',
|
||||
'name' => $row->name,
|
||||
];
|
||||
}
|
||||
}
|
||||
foreach ($indexes as $index) {
|
||||
$name = $index['name'];
|
||||
// Get index name without prefix.
|
||||
$index_name = substr($name, strlen($info['table']) + 1);
|
||||
$result = $this->connection->query('PRAGMA [' . $info['schema'] . '].index_info([' . $name . '])');
|
||||
foreach ($result as $row) {
|
||||
$schema[$index['schema_key']][$index_name][] = $row->name;
|
||||
}
|
||||
}
|
||||
return $schema;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function dropField($table, $field) {
|
||||
if (!$this->fieldExists($table, $field)) {
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
$old_schema = $this->introspectSchema($table);
|
||||
$new_schema = $old_schema;
|
||||
|
||||
unset($new_schema['fields'][$field]);
|
||||
|
||||
// Drop the primary key if the field to drop is part of it. This is
|
||||
// consistent with the behavior on PostgreSQL.
|
||||
// @see \Drupal\mysql\Driver\Database\mysql\Schema::dropField()
|
||||
if (isset($new_schema['primary key']) && in_array($field, $new_schema['primary key'], TRUE)) {
|
||||
unset($new_schema['primary key']);
|
||||
}
|
||||
|
||||
// Handle possible index changes.
|
||||
foreach ($new_schema['indexes'] as $index => $fields) {
|
||||
foreach ($fields as $key => $field_name) {
|
||||
if ($field_name == $field) {
|
||||
unset($new_schema['indexes'][$index][$key]);
|
||||
}
|
||||
}
|
||||
// If this index has no more fields then remove it.
|
||||
if (empty($new_schema['indexes'][$index])) {
|
||||
unset($new_schema['indexes'][$index]);
|
||||
}
|
||||
}
|
||||
$this->alterTable($table, $old_schema, $new_schema);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function changeField($table, $field, $field_new, $spec, $keys_new = []) {
|
||||
if (!$this->fieldExists($table, $field)) {
|
||||
throw new SchemaObjectDoesNotExistException("Cannot change the definition of field '$table.$field': field doesn't exist.");
|
||||
}
|
||||
if (($field != $field_new) && $this->fieldExists($table, $field_new)) {
|
||||
throw new SchemaObjectExistsException("Cannot rename field '$table.$field' to '$field_new': target field already exists.");
|
||||
}
|
||||
if (isset($keys_new['primary key']) && in_array($field_new, $keys_new['primary key'], TRUE)) {
|
||||
$this->ensureNotNullPrimaryKey($keys_new['primary key'], [$field_new => $spec]);
|
||||
}
|
||||
|
||||
$old_schema = $this->introspectSchema($table);
|
||||
$new_schema = $old_schema;
|
||||
|
||||
// Map the old field to the new field.
|
||||
if ($field != $field_new) {
|
||||
$mapping[$field_new] = $field;
|
||||
}
|
||||
else {
|
||||
$mapping = [];
|
||||
}
|
||||
|
||||
// Remove the previous definition and swap in the new one.
|
||||
unset($new_schema['fields'][$field]);
|
||||
$new_schema['fields'][$field_new] = $spec;
|
||||
|
||||
// Map the former indexes to the new column name.
|
||||
$new_schema['primary key'] = $this->mapKeyDefinition($new_schema['primary key'], $mapping);
|
||||
foreach (['unique keys', 'indexes'] as $k) {
|
||||
foreach ($new_schema[$k] as &$key_definition) {
|
||||
$key_definition = $this->mapKeyDefinition($key_definition, $mapping);
|
||||
}
|
||||
}
|
||||
|
||||
// Add in the keys from $keys_new.
|
||||
if (isset($keys_new['primary key'])) {
|
||||
$new_schema['primary key'] = $keys_new['primary key'];
|
||||
}
|
||||
foreach (['unique keys', 'indexes'] as $k) {
|
||||
if (!empty($keys_new[$k])) {
|
||||
$new_schema[$k] = $keys_new[$k] + $new_schema[$k];
|
||||
}
|
||||
}
|
||||
|
||||
$this->alterTable($table, $old_schema, $new_schema, $mapping);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renames columns in an index definition according to a new mapping.
|
||||
*
|
||||
* @param array $key_definition
|
||||
* The key definition.
|
||||
* @param array $mapping
|
||||
* The new mapping.
|
||||
*/
|
||||
protected function mapKeyDefinition(array $key_definition, array $mapping) {
|
||||
foreach ($key_definition as &$field) {
|
||||
// The key definition can be an array such as [$field, $length].
|
||||
if (is_array($field)) {
|
||||
$field = &$field[0];
|
||||
}
|
||||
|
||||
$mapped_field = array_search($field, $mapping, TRUE);
|
||||
if ($mapped_field !== FALSE) {
|
||||
$field = $mapped_field;
|
||||
}
|
||||
}
|
||||
return $key_definition;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function addIndex($table, $name, $fields, array $spec) {
|
||||
if (!$this->tableExists($table)) {
|
||||
throw new SchemaObjectDoesNotExistException("Cannot add index '$name' to table '$table': table doesn't exist.");
|
||||
}
|
||||
if ($this->indexExists($table, $name)) {
|
||||
throw new SchemaObjectExistsException("Cannot add index '$name' to table '$table': index already exists.");
|
||||
}
|
||||
|
||||
$schema['indexes'][$name] = $fields;
|
||||
$statements = $this->createIndexSql($table, $schema);
|
||||
foreach ($statements as $statement) {
|
||||
$this->executeDdlStatement($statement);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function indexExists($table, $name) {
|
||||
$info = $this->getPrefixInfo($table);
|
||||
|
||||
return $this->connection->query('PRAGMA [' . $info['schema'] . '].index_info([' . $info['table'] . '_' . $name . '])')->fetchField() != '';
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function dropIndex($table, $name) {
|
||||
if (!$this->indexExists($table, $name)) {
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
$info = $this->getPrefixInfo($table);
|
||||
|
||||
$this->executeDdlStatement('DROP INDEX [' . $info['schema'] . '].[' . $info['table'] . '_' . $name . ']');
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function addUniqueKey($table, $name, $fields) {
|
||||
if (!$this->tableExists($table)) {
|
||||
throw new SchemaObjectDoesNotExistException("Cannot add unique key '$name' to table '$table': table doesn't exist.");
|
||||
}
|
||||
if ($this->indexExists($table, $name)) {
|
||||
throw new SchemaObjectExistsException("Cannot add unique key '$name' to table '$table': unique key already exists.");
|
||||
}
|
||||
|
||||
$schema['unique keys'][$name] = $fields;
|
||||
$statements = $this->createIndexSql($table, $schema);
|
||||
foreach ($statements as $statement) {
|
||||
$this->executeDdlStatement($statement);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function dropUniqueKey($table, $name) {
|
||||
if (!$this->indexExists($table, $name)) {
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
$info = $this->getPrefixInfo($table);
|
||||
|
||||
$this->executeDdlStatement('DROP INDEX [' . $info['schema'] . '].[' . $info['table'] . '_' . $name . ']');
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function addPrimaryKey($table, $fields) {
|
||||
if (!$this->tableExists($table)) {
|
||||
throw new SchemaObjectDoesNotExistException("Cannot add primary key to table '$table': table doesn't exist.");
|
||||
}
|
||||
|
||||
$old_schema = $this->introspectSchema($table);
|
||||
$new_schema = $old_schema;
|
||||
|
||||
if (!empty($new_schema['primary key'])) {
|
||||
throw new SchemaObjectExistsException("Cannot add primary key to table '$table': primary key already exists.");
|
||||
}
|
||||
|
||||
$new_schema['primary key'] = $fields;
|
||||
$this->ensureNotNullPrimaryKey($new_schema['primary key'], $new_schema['fields']);
|
||||
$this->alterTable($table, $old_schema, $new_schema);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function dropPrimaryKey($table) {
|
||||
$old_schema = $this->introspectSchema($table);
|
||||
$new_schema = $old_schema;
|
||||
|
||||
if (empty($new_schema['primary key'])) {
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
unset($new_schema['primary key']);
|
||||
$this->alterTable($table, $old_schema, $new_schema);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function findPrimaryKeyColumns($table) {
|
||||
if (!$this->tableExists($table)) {
|
||||
return FALSE;
|
||||
}
|
||||
$schema = $this->introspectSchema($table);
|
||||
return $schema['primary key'];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function introspectIndexSchema($table) {
|
||||
if (!$this->tableExists($table)) {
|
||||
throw new SchemaObjectDoesNotExistException("The table $table doesn't exist.");
|
||||
}
|
||||
$schema = $this->introspectSchema($table);
|
||||
unset($schema['fields']);
|
||||
return $schema;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function findTables($table_expression) {
|
||||
$tables = [];
|
||||
|
||||
// The SQLite implementation doesn't need to use the same filtering strategy
|
||||
// as the parent one because individually prefixed tables live in their own
|
||||
// schema (database), which means that neither the main database nor any
|
||||
// attached one will contain a prefixed table name, so we just need to loop
|
||||
// over all known schemas and filter by the user-supplied table expression.
|
||||
$attached_dbs = $this->connection->getAttachedDatabases();
|
||||
foreach ($attached_dbs as $schema) {
|
||||
// Can't use query placeholders for the schema because the query would
|
||||
// have to be :prefixsqlite_master, which does not work. We also need to
|
||||
// ignore the internal SQLite tables.
|
||||
$result = $this->connection->query("SELECT name FROM [" . $schema . "].sqlite_master WHERE type = :type AND name LIKE :table_name AND name NOT LIKE :pattern", [
|
||||
':type' => 'table',
|
||||
':table_name' => $table_expression,
|
||||
':pattern' => 'sqlite_%',
|
||||
]);
|
||||
$tables += $result->fetchAllKeyed(0, 0);
|
||||
}
|
||||
|
||||
return $tables;
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\sqlite\Driver\Database\sqlite;
|
||||
|
||||
use Drupal\Core\Database\Query\Select as QuerySelect;
|
||||
|
||||
/**
|
||||
* SQLite implementation of \Drupal\Core\Database\Query\Select.
|
||||
*/
|
||||
class Select extends QuerySelect {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function forUpdate($set = TRUE) {
|
||||
// SQLite does not support FOR UPDATE so nothing to do.
|
||||
return $this;
|
||||
}
|
||||
|
||||
}
|
||||
117
web/core/modules/sqlite/src/Driver/Database/sqlite/Statement.php
Normal file
117
web/core/modules/sqlite/src/Driver/Database/sqlite/Statement.php
Normal file
@ -0,0 +1,117 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\sqlite\Driver\Database\sqlite;
|
||||
|
||||
use Drupal\Core\Database\StatementInterface;
|
||||
use Drupal\Core\Database\StatementPrefetchIterator;
|
||||
|
||||
/**
|
||||
* SQLite implementation of \Drupal\Core\Database\Statement.
|
||||
*
|
||||
* The PDO SQLite driver only closes SELECT statements when the PDOStatement
|
||||
* destructor is called and SQLite does not allow data change (INSERT,
|
||||
* UPDATE etc) on a table which has open SELECT statements. This is a
|
||||
* user-space mock of PDOStatement that buffers all the data and doesn't
|
||||
* have those limitations.
|
||||
*/
|
||||
class Statement extends StatementPrefetchIterator implements StatementInterface {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* The PDO SQLite layer doesn't replace numeric placeholders in queries
|
||||
* correctly, and this makes numeric expressions (such as COUNT(*) >= :count)
|
||||
* fail. We replace numeric placeholders in the query ourselves to work
|
||||
* around this bug.
|
||||
*
|
||||
* See http://bugs.php.net/bug.php?id=45259 for more details.
|
||||
*/
|
||||
protected function getStatement(string $query, ?array &$args = []): object {
|
||||
if (is_array($args) && !empty($args)) {
|
||||
// Check if $args is a simple numeric array.
|
||||
if (range(0, count($args) - 1) === array_keys($args)) {
|
||||
// In that case, we have unnamed placeholders.
|
||||
$count = 0;
|
||||
$new_args = [];
|
||||
foreach ($args as $value) {
|
||||
if (is_float($value) || is_int($value)) {
|
||||
if (is_float($value)) {
|
||||
// Force the conversion to float so as not to loose precision
|
||||
// in the automatic cast.
|
||||
$value = sprintf('%F', $value);
|
||||
}
|
||||
$query = substr_replace($query, $value, strpos($query, '?'), 1);
|
||||
}
|
||||
else {
|
||||
$placeholder = ':db_statement_placeholder_' . $count++;
|
||||
$query = substr_replace($query, $placeholder, strpos($query, '?'), 1);
|
||||
$new_args[$placeholder] = $value;
|
||||
}
|
||||
}
|
||||
$args = $new_args;
|
||||
}
|
||||
else {
|
||||
// Else, this is using named placeholders.
|
||||
foreach ($args as $placeholder => $value) {
|
||||
if (is_float($value) || is_int($value)) {
|
||||
if (is_float($value)) {
|
||||
// Force the conversion to float so as not to loose precision
|
||||
// in the automatic cast.
|
||||
$value = sprintf('%F', $value);
|
||||
}
|
||||
|
||||
// We will remove this placeholder from the query as PDO throws an
|
||||
// exception if the number of placeholders in the query and the
|
||||
// arguments does not match.
|
||||
unset($args[$placeholder]);
|
||||
// PDO allows placeholders to not be prefixed by a colon. See
|
||||
// http://marc.info/?l=php-internals&m=111234321827149&w=2 for
|
||||
// more.
|
||||
if ($placeholder[0] != ':') {
|
||||
$placeholder = ":$placeholder";
|
||||
}
|
||||
// When replacing the placeholders, make sure we search for the
|
||||
// exact placeholder. For example, if searching for
|
||||
// ':db_placeholder_1', do not replace ':db_placeholder_11'.
|
||||
$query = preg_replace('/' . preg_quote($placeholder, NULL) . '\b/', $value, $query);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this->clientConnection->prepare($query);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function execute($args = [], $options = []) {
|
||||
if (isset($options['fetch']) && is_int($options['fetch'])) {
|
||||
@trigger_error("Passing the 'fetch' key as an integer to \$options in execute() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\Statement\FetchAs enum instead. See https://www.drupal.org/node/3488338", E_USER_DEPRECATED);
|
||||
}
|
||||
|
||||
try {
|
||||
$return = parent::execute($args, $options);
|
||||
}
|
||||
catch (\PDOException $e) {
|
||||
// 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) {
|
||||
// The schema has changed. SQLite specifies that we must resend the
|
||||
// query.
|
||||
$return = parent::execute($args, $options);
|
||||
}
|
||||
else {
|
||||
// Rethrow the exception.
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\sqlite\Driver\Database\sqlite;
|
||||
|
||||
use Drupal\Core\Database\Transaction\ClientConnectionTransactionState;
|
||||
use Drupal\Core\Database\Transaction\TransactionManagerBase;
|
||||
|
||||
/**
|
||||
* SQLite implementation of TransactionManagerInterface.
|
||||
*/
|
||||
class TransactionManager extends TransactionManagerBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function beginClientTransaction(): bool {
|
||||
return $this->connection->getClientConnection()->beginTransaction();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function rollbackClientTransaction(): bool {
|
||||
$clientRollback = $this->connection->getClientConnection()->rollBack();
|
||||
$this->setConnectionTransactionState($clientRollback ?
|
||||
ClientConnectionTransactionState::RolledBack :
|
||||
ClientConnectionTransactionState::RollbackFailed
|
||||
);
|
||||
return $clientRollback;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function commitClientTransaction(): bool {
|
||||
$clientCommit = $this->connection->getClientConnection()->commit();
|
||||
$this->setConnectionTransactionState($clientCommit ?
|
||||
ClientConnectionTransactionState::Committed :
|
||||
ClientConnectionTransactionState::CommitFailed
|
||||
);
|
||||
return $clientCommit;
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\sqlite\Driver\Database\sqlite;
|
||||
|
||||
use Drupal\Core\Database\Query\Truncate as QueryTruncate;
|
||||
|
||||
/**
|
||||
* SQLite implementation of \Drupal\Core\Database\Query\Truncate.
|
||||
*
|
||||
* SQLite doesn't support TRUNCATE, but a DELETE query with no condition has
|
||||
* exactly the effect (it is implemented by DROPing the table).
|
||||
*/
|
||||
class Truncate extends QueryTruncate {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function __toString() {
|
||||
// Create a sanitized comment string to prepend to the query.
|
||||
$comments = $this->connection->makeComment($this->comments);
|
||||
|
||||
return $comments . 'DELETE FROM {' . $this->connection->escapeTable($this->table) . '} ';
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\sqlite\Driver\Database\sqlite;
|
||||
|
||||
use Drupal\Core\Database\Query\Update as QueryUpdate;
|
||||
|
||||
@trigger_error('Extending from \Drupal\sqlite\Driver\Database\sqlite\Update is deprecated in drupal:11.0.0 and is removed from drupal:12.0.0. Extend from the base class instead. See https://www.drupal.org/node/3256524', E_USER_DEPRECATED);
|
||||
|
||||
/**
|
||||
* SQLite implementation of \Drupal\Core\Database\Query\Update.
|
||||
*/
|
||||
class Update extends QueryUpdate {
|
||||
}
|
||||
@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\sqlite\Driver\Database\sqlite;
|
||||
|
||||
use Drupal\Core\Database\Query\Upsert as QueryUpsert;
|
||||
|
||||
/**
|
||||
* SQLite implementation of \Drupal\Core\Database\Query\Upsert.
|
||||
*
|
||||
* @see https://www.sqlite.org/lang_UPSERT.html
|
||||
*/
|
||||
class Upsert extends QueryUpsert {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function __toString() {
|
||||
// Create a sanitized comment string to prepend to the query.
|
||||
$comments = $this->connection->makeComment($this->comments);
|
||||
|
||||
// Default fields are always placed first for consistency.
|
||||
$insert_fields = array_merge($this->defaultFields, $this->insertFields);
|
||||
$insert_fields = array_map(function ($field) {
|
||||
return $this->connection->escapeField($field);
|
||||
}, $insert_fields);
|
||||
|
||||
$query = $comments . 'INSERT INTO {' . $this->table . '} (' . implode(', ', $insert_fields) . ') VALUES ';
|
||||
|
||||
$values = $this->getInsertPlaceholderFragment($this->insertValues, $this->defaultFields);
|
||||
$query .= implode(', ', $values);
|
||||
|
||||
// Updating the unique / primary key is not necessary.
|
||||
unset($insert_fields[$this->key]);
|
||||
|
||||
$update = [];
|
||||
foreach ($insert_fields as $field) {
|
||||
// The "excluded." prefix causes the field to refer to the value for field
|
||||
// that would have been inserted had there been no conflict.
|
||||
$update[] = "$field = EXCLUDED.$field";
|
||||
}
|
||||
|
||||
$query .= ' ON CONFLICT (' . $this->connection->escapeField($this->key) . ') DO UPDATE SET ' . implode(', ', $update);
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
}
|
||||
33
web/core/modules/sqlite/src/Hook/SqliteHooks.php
Normal file
33
web/core/modules/sqlite/src/Hook/SqliteHooks.php
Normal file
@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\sqlite\Hook;
|
||||
|
||||
use Drupal\Core\Routing\RouteMatchInterface;
|
||||
use Drupal\Core\Hook\Attribute\Hook;
|
||||
use Drupal\Core\StringTranslation\StringTranslationTrait;
|
||||
|
||||
/**
|
||||
* Hook implementations for sqlite.
|
||||
*/
|
||||
class SqliteHooks {
|
||||
|
||||
use StringTranslationTrait;
|
||||
|
||||
/**
|
||||
* Implements hook_help().
|
||||
*/
|
||||
#[Hook('help')]
|
||||
public function help($route_name, RouteMatchInterface $route_match): ?string {
|
||||
switch ($route_name) {
|
||||
case 'help.page.sqlite':
|
||||
$output = '';
|
||||
$output .= '<h2>' . $this->t('About') . '</h2>';
|
||||
$output .= '<p>' . $this->t('The SQLite module provides the connection between Drupal and a SQLite database. For more information, see the <a href=":sqlite">online documentation for the SQLite module</a>.', [
|
||||
':sqlite' => 'https://www.drupal.org/docs/core-modules-and-themes/core-modules/sqlite-module',
|
||||
]) . '</p>';
|
||||
return $output;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
}
|
||||
14
web/core/modules/sqlite/tests/src/Functional/GenericTest.php
Normal file
14
web/core/modules/sqlite/tests/src/Functional/GenericTest.php
Normal file
@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\sqlite\Functional;
|
||||
|
||||
use Drupal\Tests\system\Functional\Module\GenericModuleTestBase;
|
||||
|
||||
/**
|
||||
* Generic module test for sqlite.
|
||||
*
|
||||
* @group sqlite
|
||||
*/
|
||||
class GenericTest extends GenericModuleTestBase {}
|
||||
@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\sqlite\Kernel\sqlite;
|
||||
|
||||
use Drupal\KernelTests\Core\Database\DriverSpecificKernelTestBase;
|
||||
|
||||
/**
|
||||
* Tests exceptions thrown by queries.
|
||||
*
|
||||
* @group Database
|
||||
*/
|
||||
class DatabaseExceptionWrapperTest extends DriverSpecificKernelTestBase {
|
||||
|
||||
/**
|
||||
* Tests Connection::prepareStatement exception on execution.
|
||||
*/
|
||||
public function testPrepareStatementFailOnExecution(): void {
|
||||
$this->expectException(\PDOException::class);
|
||||
$stmt = $this->connection->prepareStatement('bananas', []);
|
||||
$stmt->execute();
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\sqlite\Kernel\sqlite\Plugin\views;
|
||||
|
||||
use Drupal\Tests\views\Kernel\Plugin\CastedIntFieldJoinTestBase;
|
||||
|
||||
/**
|
||||
* Tests SQLite specific cast handling.
|
||||
*
|
||||
* @group Database
|
||||
*/
|
||||
class SqliteCastedIntFieldJoinTest extends CastedIntFieldJoinTestBase {
|
||||
|
||||
/**
|
||||
* The db type that should be used for casting fields as integers.
|
||||
*/
|
||||
protected string $castingType = 'INTEGER';
|
||||
|
||||
}
|
||||
104
web/core/modules/sqlite/tests/src/Kernel/sqlite/SchemaTest.php
Normal file
104
web/core/modules/sqlite/tests/src/Kernel/sqlite/SchemaTest.php
Normal file
@ -0,0 +1,104 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\sqlite\Kernel\sqlite;
|
||||
|
||||
use Drupal\KernelTests\Core\Database\DriverSpecificSchemaTestBase;
|
||||
|
||||
/**
|
||||
* Tests schema API for the SQLite driver.
|
||||
*
|
||||
* @group Database
|
||||
*/
|
||||
class SchemaTest extends DriverSpecificSchemaTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function checkSchemaComment(string $description, string $table, ?string $column = NULL): void {
|
||||
// The sqlite driver schema does not support fetching table/column
|
||||
// comments.
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function tryInsertExpectsIntegrityConstraintViolationException(string $tableName): void {
|
||||
// Sqlite does not throw an IntegrityConstraintViolationException here.
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function testTableWithSpecificDataType(): void {
|
||||
$table_specification = [
|
||||
'description' => 'Schema table description.',
|
||||
'fields' => [
|
||||
'timestamp' => [
|
||||
'sqlite_type' => 'datetime',
|
||||
'not null' => FALSE,
|
||||
'default' => NULL,
|
||||
],
|
||||
],
|
||||
];
|
||||
$this->schema->createTable('test_timestamp', $table_specification);
|
||||
$this->assertTrue($this->schema->tableExists('test_timestamp'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers \Drupal\sqlite\Driver\Database\sqlite\Schema::introspectIndexSchema
|
||||
*/
|
||||
public function testIntrospectIndexSchema(): void {
|
||||
$table_specification = [
|
||||
'fields' => [
|
||||
'id' => [
|
||||
'type' => 'int',
|
||||
'not null' => TRUE,
|
||||
'default' => 0,
|
||||
],
|
||||
'test_field_1' => [
|
||||
'type' => 'int',
|
||||
'not null' => TRUE,
|
||||
'default' => 0,
|
||||
],
|
||||
'test_field_2' => [
|
||||
'type' => 'int',
|
||||
'default' => 0,
|
||||
],
|
||||
'test_field_3' => [
|
||||
'type' => 'int',
|
||||
'default' => 0,
|
||||
],
|
||||
'test_field_4' => [
|
||||
'type' => 'int',
|
||||
'default' => 0,
|
||||
],
|
||||
'test_field_5' => [
|
||||
'type' => 'int',
|
||||
'default' => 0,
|
||||
],
|
||||
],
|
||||
'primary key' => ['id', 'test_field_1'],
|
||||
'unique keys' => [
|
||||
'test_field_2' => ['test_field_2'],
|
||||
'test_field_3_test_field_4' => ['test_field_3', 'test_field_4'],
|
||||
],
|
||||
'indexes' => [
|
||||
'test_field_4' => ['test_field_4'],
|
||||
'test_field_4_test_field_5' => ['test_field_4', 'test_field_5'],
|
||||
],
|
||||
];
|
||||
|
||||
$table_name = strtolower($this->getRandomGenerator()->name());
|
||||
$this->schema->createTable($table_name, $table_specification);
|
||||
|
||||
unset($table_specification['fields']);
|
||||
|
||||
$introspect_index_schema = new \ReflectionMethod(get_class($this->schema), 'introspectIndexSchema');
|
||||
$index_schema = $introspect_index_schema->invoke($this->schema, $table_name);
|
||||
|
||||
$this->assertEquals($table_specification, $index_schema);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\sqlite\Kernel\sqlite;
|
||||
|
||||
use Drupal\KernelTests\Core\Database\SchemaUniquePrefixedKeysIndexTestBase;
|
||||
|
||||
/**
|
||||
* Tests adding UNIQUE keys to tables.
|
||||
*
|
||||
* @group Database
|
||||
*/
|
||||
class SchemaUniquePrefixedKeysIndexTest extends SchemaUniquePrefixedKeysIndexTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected string $columnValue = '1234567890 foo';
|
||||
|
||||
}
|
||||
@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\sqlite\Kernel\sqlite;
|
||||
|
||||
use Drupal\KernelTests\Core\Database\DriverSpecificSyntaxTestBase;
|
||||
|
||||
/**
|
||||
* Tests SQLite syntax interpretation.
|
||||
*
|
||||
* @group Database
|
||||
*/
|
||||
class SyntaxTest extends DriverSpecificSyntaxTestBase {
|
||||
}
|
||||
@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\sqlite\Kernel\sqlite;
|
||||
|
||||
use Drupal\KernelTests\Core\Database\TemporaryQueryTestBase;
|
||||
|
||||
/**
|
||||
* Tests the temporary query functionality.
|
||||
*
|
||||
* @group Database
|
||||
*/
|
||||
class TemporaryQueryTest extends TemporaryQueryTestBase {
|
||||
|
||||
/**
|
||||
* Confirms that temporary tables work.
|
||||
*/
|
||||
public function testTemporaryQuery(): void {
|
||||
parent::testTemporaryQuery();
|
||||
|
||||
$connection = $this->getConnection();
|
||||
|
||||
$table_name_test = $connection->queryTemporary('SELECT [name] FROM {test}', []);
|
||||
|
||||
// Assert that the table is indeed a temporary one.
|
||||
$this->assertStringContainsString("temp.", $table_name_test);
|
||||
|
||||
// Assert that both have the same field names.
|
||||
$normal_table_fields = $connection->query("SELECT * FROM {test}")->fetch();
|
||||
$temp_table_name = $connection->queryTemporary('SELECT * FROM {test}');
|
||||
$temp_table_fields = $connection->query("SELECT * FROM $temp_table_name")->fetch();
|
||||
|
||||
$normal_table_fields = array_keys(get_object_vars($normal_table_fields));
|
||||
$temp_table_fields = array_keys(get_object_vars($temp_table_fields));
|
||||
|
||||
$this->assertEmpty(array_diff($normal_table_fields, $temp_table_fields));
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\sqlite\Kernel\sqlite;
|
||||
|
||||
use Drupal\KernelTests\Core\Database\DriverSpecificTransactionTestBase;
|
||||
|
||||
/**
|
||||
* Tests transaction for the SQLite driver.
|
||||
*
|
||||
* @group Database
|
||||
*/
|
||||
class TransactionTest extends DriverSpecificTransactionTestBase {
|
||||
}
|
||||
63
web/core/modules/sqlite/tests/src/Unit/ConnectionTest.php
Normal file
63
web/core/modules/sqlite/tests/src/Unit/ConnectionTest.php
Normal file
@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\sqlite\Unit;
|
||||
|
||||
use Drupal\sqlite\Driver\Database\sqlite\Connection;
|
||||
use Drupal\Tests\Core\Database\Stub\StubPDO;
|
||||
use Drupal\Tests\UnitTestCase;
|
||||
use PHPUnit\Framework\Attributes\IgnoreDeprecations;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \Drupal\sqlite\Driver\Database\sqlite\Connection
|
||||
* @group Database
|
||||
*/
|
||||
class ConnectionTest extends UnitTestCase {
|
||||
|
||||
/**
|
||||
* @covers ::createConnectionOptionsFromUrl
|
||||
* @dataProvider providerCreateConnectionOptionsFromUrl
|
||||
*
|
||||
* @param string $url
|
||||
* SQLite URL.
|
||||
* @param string $expected
|
||||
* Expected connection option.
|
||||
*/
|
||||
public function testCreateConnectionOptionsFromUrl(string $url, string $expected): void {
|
||||
$sqlite_connection = new Connection($this->createMock(StubPDO::class), []);
|
||||
$database = $sqlite_connection->createConnectionOptionsFromUrl($url, NULL);
|
||||
$this->assertEquals('sqlite', $database['driver']);
|
||||
$this->assertEquals($expected, $database['database']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for testCreateConnectionOptionsFromUrl.
|
||||
*
|
||||
* @return string[][]
|
||||
* Associative array of arrays with the following elements:
|
||||
* - SQLite database URL
|
||||
* - Expected database connection option
|
||||
*/
|
||||
public static function providerCreateConnectionOptionsFromUrl(): array {
|
||||
return [
|
||||
'sqlite relative path' => ['sqlite://localhost/tmp/test', 'tmp/test'],
|
||||
'sqlite absolute path' => ['sqlite://localhost//tmp/test', '/tmp/test'],
|
||||
'in memory sqlite path' => ['sqlite://localhost/:memory:', ':memory:'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirms deprecation of the $root argument.
|
||||
*/
|
||||
#[IgnoreDeprecations]
|
||||
public function testDeprecationOfRootInConnectionOptionsFromUrl(): void {
|
||||
$this->expectDeprecation('Passing the $root value to Drupal\sqlite\Driver\Database\sqlite\Connection::createConnectionOptionsFromUrl() is deprecated in drupal:11.2.0 and will be removed in drupal:12.0.0. There is no replacement. See https://www.drupal.org/node/3511287');
|
||||
$root = dirname(__DIR__, 8);
|
||||
$sqlite_connection = new Connection($this->createMock(StubPDO::class), []);
|
||||
$database = $sqlite_connection->createConnectionOptionsFromUrl('sqlite://localhost/tmp/test', $root);
|
||||
$this->assertEquals('sqlite', $database['driver']);
|
||||
$this->assertEquals('tmp/test', $database['database']);
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user