Update to Drupal 8.0.0-beta15. For more information, see: https://www.drupal.org/node/2563023
This commit is contained in:
parent
2720a9ec4b
commit
f3791f1da3
1898 changed files with 54300 additions and 11481 deletions
|
@ -138,6 +138,13 @@ abstract class Connection {
|
|||
*/
|
||||
protected $prefixReplace = array();
|
||||
|
||||
/**
|
||||
* List of un-prefixed table names, keyed by prefixed table names.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $unprefixedTablesMap = [];
|
||||
|
||||
/**
|
||||
* Constructs a Connection object.
|
||||
*
|
||||
|
@ -185,7 +192,9 @@ abstract class Connection {
|
|||
// Destroy all references to this connection by setting them to NULL.
|
||||
// The Statement class attribute only accepts a new value that presents a
|
||||
// proper callable, so we reset it to PDOStatement.
|
||||
$this->connection->setAttribute(\PDO::ATTR_STATEMENT_CLASS, array('PDOStatement', array()));
|
||||
if (!empty($this->statementClass)) {
|
||||
$this->connection->setAttribute(\PDO::ATTR_STATEMENT_CLASS, array('PDOStatement', array()));
|
||||
}
|
||||
$this->schema = NULL;
|
||||
}
|
||||
|
||||
|
@ -289,6 +298,13 @@ abstract class Connection {
|
|||
$this->prefixReplace[] = $this->prefixes['default'];
|
||||
$this->prefixSearch[] = '}';
|
||||
$this->prefixReplace[] = '';
|
||||
|
||||
// Set up a map of prefixed => un-prefixed tables.
|
||||
foreach ($this->prefixes as $table_name => $prefix) {
|
||||
if ($table_name !== 'default') {
|
||||
$this->unprefixedTablesMap[$prefix . $table_name] = $table_name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -327,6 +343,17 @@ abstract class Connection {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a list of individually prefixed table names.
|
||||
*
|
||||
* @return array
|
||||
* An array of un-prefixed table names, keyed by their fully qualified table
|
||||
* names (i.e. prefix + table_name).
|
||||
*/
|
||||
public function getUnprefixedTablesMap() {
|
||||
return $this->unprefixedTablesMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a fully qualified table name.
|
||||
*
|
||||
|
@ -502,7 +529,7 @@ abstract class Connection {
|
|||
* A sanitized version of the query comment string.
|
||||
*/
|
||||
protected function filterComment($comment = '') {
|
||||
return preg_replace('/(\/\*\s*)|(\s*\*\/)/', '', $comment);
|
||||
return strtr($comment, ['*' => ' * ']);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -786,6 +813,23 @@ abstract class Connection {
|
|||
return new $class($this, $table, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares and returns an UPSERT query object.
|
||||
*
|
||||
* @param string $table
|
||||
* The table to use for the upsert query.
|
||||
* @param array $options
|
||||
* (optional) An array of options on the query.
|
||||
*
|
||||
* @return \Drupal\Core\Database\Query\Upsert
|
||||
* A new Upsert query object.
|
||||
*
|
||||
* @see \Drupal\Core\Database\Query\Upsert
|
||||
*/
|
||||
public function upsert($table, array $options = array()) {
|
||||
$class = $this->getDriverClass('Upsert');
|
||||
return new $class($this, $table, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares and returns an UPDATE query object.
|
||||
|
@ -1216,6 +1260,13 @@ abstract class Connection {
|
|||
return $this->connection->getAttribute(\PDO::ATTR_SERVER_VERSION);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the version of the database client.
|
||||
*/
|
||||
public function clientVersion() {
|
||||
return $this->connection->getAttribute(\PDO::ATTR_CLIENT_VERSION);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if this driver supports transactions.
|
||||
*
|
||||
|
|
|
@ -28,6 +28,11 @@ class Connection extends DatabaseConnection {
|
|||
*/
|
||||
const DATABASE_NOT_FOUND = 1049;
|
||||
|
||||
/**
|
||||
* Error code for "Can't initialize character set" error.
|
||||
*/
|
||||
const UNSUPPORTED_CHARSET = 2019;
|
||||
|
||||
/**
|
||||
* Flag to indicate if the cleanup function in __destruct() should run.
|
||||
*
|
||||
|
@ -82,6 +87,13 @@ class Connection extends DatabaseConnection {
|
|||
* {@inheritdoc}
|
||||
*/
|
||||
public static function open(array &$connection_options = array()) {
|
||||
if (isset($connection_options['_dsn_utf8_fallback']) && $connection_options['_dsn_utf8_fallback'] === TRUE) {
|
||||
// Only used during the installer version check, as a fallback from utf8mb4.
|
||||
$charset = 'utf8';
|
||||
}
|
||||
else {
|
||||
$charset = 'utf8mb4';
|
||||
}
|
||||
// The DSN should use either a socket or a host/port.
|
||||
if (isset($connection_options['unix_socket'])) {
|
||||
$dsn = 'mysql:unix_socket=' . $connection_options['unix_socket'];
|
||||
|
@ -93,7 +105,7 @@ class Connection extends DatabaseConnection {
|
|||
// Character set is added to dsn to ensure PDO uses the proper character
|
||||
// set when escaping. This has security implications. See
|
||||
// https://www.drupal.org/node/1201452 for further discussion.
|
||||
$dsn .= ';charset=utf8mb4';
|
||||
$dsn .= ';charset=' . $charset;
|
||||
if (!empty($connection_options['database'])) {
|
||||
$dsn .= ';dbname=' . $connection_options['database'];
|
||||
}
|
||||
|
@ -124,10 +136,10 @@ class Connection extends DatabaseConnection {
|
|||
// certain one has been set; otherwise, MySQL defaults to
|
||||
// 'utf8mb4_general_ci' for utf8mb4.
|
||||
if (!empty($connection_options['collation'])) {
|
||||
$pdo->exec('SET NAMES utf8mb4 COLLATE ' . $connection_options['collation']);
|
||||
$pdo->exec('SET NAMES ' . $charset . ' COLLATE ' . $connection_options['collation']);
|
||||
}
|
||||
else {
|
||||
$pdo->exec('SET NAMES utf8mb4');
|
||||
$pdo->exec('SET NAMES ' . $charset);
|
||||
}
|
||||
|
||||
// Set MySQL init_commands if not already defined. Default Drupal's MySQL
|
||||
|
|
|
@ -55,30 +55,7 @@ class Insert extends QueryInsert {
|
|||
|
||||
$query = $comments . 'INSERT INTO {' . $this->table . '} (' . implode(', ', $insert_fields) . ') VALUES ';
|
||||
|
||||
$max_placeholder = 0;
|
||||
$values = array();
|
||||
if (count($this->insertValues)) {
|
||||
foreach ($this->insertValues as $insert_values) {
|
||||
$placeholders = array();
|
||||
|
||||
// Default fields aren't really placeholders, but this is the most convenient
|
||||
// way to handle them.
|
||||
$placeholders = array_pad($placeholders, count($this->defaultFields), 'default');
|
||||
|
||||
$new_placeholder = $max_placeholder + count($insert_values);
|
||||
for ($i = $max_placeholder; $i < $new_placeholder; ++$i) {
|
||||
$placeholders[] = ':db_insert_placeholder_' . $i;
|
||||
}
|
||||
$max_placeholder = $new_placeholder;
|
||||
$values[] = '(' . implode(', ', $placeholders) . ')';
|
||||
}
|
||||
}
|
||||
else {
|
||||
// If there are no values, then this is a default-only query. We still need to handle that.
|
||||
$placeholders = array_fill(0, count($this->defaultFields), 'default');
|
||||
$values[] = '(' . implode(', ', $placeholders) . ')';
|
||||
}
|
||||
|
||||
$values = $this->getInsertPlaceholderFragment($this->insertValues, $this->defaultFields);
|
||||
$query .= implode(', ', $values);
|
||||
|
||||
return $query;
|
||||
|
|
|
@ -16,6 +16,17 @@ use Drupal\Core\Database\DatabaseNotFoundException;
|
|||
* Specifies installation tasks for MySQL and equivalent databases.
|
||||
*/
|
||||
class Tasks extends InstallTasks {
|
||||
|
||||
/**
|
||||
* Minimum required MySQLnd version.
|
||||
*/
|
||||
const MYSQLND_MINIMUM_VERSION = '5.0.9';
|
||||
|
||||
/**
|
||||
* Minimum required libmysqlclient version.
|
||||
*/
|
||||
const LIBMYSQLCLIENT_MINIMUM_VERSION = '5.5.3';
|
||||
|
||||
/**
|
||||
* The PDO driver name for MySQL and equivalent databases.
|
||||
*
|
||||
|
@ -27,13 +38,6 @@ class Tasks extends InstallTasks {
|
|||
* Constructs a \Drupal\Core\Database\Driver\mysql\Install\Tasks object.
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->tasks[] = array(
|
||||
'arguments' => array(
|
||||
'SET NAMES utf8mb4',
|
||||
'The %name database server supports utf8mb4 character encoding.',
|
||||
'The %name database server must support utf8mb4 character encoding to work with Drupal. Make sure to use a database server that supports utf8mb4 character encoding, such as MySQL/MariaDB/Percona versions 5.5.3 and up.',
|
||||
),
|
||||
);
|
||||
$this->tasks[] = array(
|
||||
'arguments' => array(),
|
||||
'function' => 'ensureInnoDbAvailable',
|
||||
|
@ -62,7 +66,34 @@ class Tasks extends InstallTasks {
|
|||
// This doesn't actually test the connection.
|
||||
db_set_active();
|
||||
// Now actually do a check.
|
||||
Database::getConnection();
|
||||
try {
|
||||
Database::getConnection();
|
||||
}
|
||||
catch (\Exception $e) {
|
||||
// Detect utf8mb4 incompability.
|
||||
if ($e->getCode() == Connection::UNSUPPORTED_CHARSET) {
|
||||
$this->fail(t('Your MySQL server and PHP MySQL driver must support utf8mb4 character encoding. Make sure to use a database system that supports this (such as MySQL/MariaDB/Percona 5.5.3 and up), and that the utf8mb4 character set is compiled in. See the <a href="@documentation" target="_blank">MySQL documentation</a> for more information.', array('@documentation' => 'https://dev.mysql.com/doc/refman/5.0/en/cannot-initialize-character-set.html')));
|
||||
$info = Database::getConnectionInfo();
|
||||
$info_copy = $info;
|
||||
// Set a flag to fall back to utf8. Note: this flag should only be
|
||||
// used here and is for internal use only.
|
||||
$info_copy['default']['_dsn_utf8_fallback'] = TRUE;
|
||||
// In order to change the Database::$databaseInfo array, we need to
|
||||
// remove the active connection, then re-add it with the new info.
|
||||
Database::removeConnection('default');
|
||||
Database::addConnectionInfo('default', 'default', $info_copy['default']);
|
||||
// Connect with the new database info, using the utf8 character set so
|
||||
// that we can run the checkEngineVersion test.
|
||||
Database::getConnection();
|
||||
// Revert to the old settings.
|
||||
Database::removeConnection('default');
|
||||
Database::addConnectionInfo('default', 'default', $info['default']);
|
||||
}
|
||||
else {
|
||||
// Rethrow the exception.
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
$this->pass('Drupal can CONNECT to the database ok.');
|
||||
}
|
||||
catch (\Exception $e) {
|
||||
|
@ -121,4 +152,27 @@ class Tasks extends InstallTasks {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function checkEngineVersion() {
|
||||
parent::checkEngineVersion();
|
||||
|
||||
// Ensure that the MySQL driver supports utf8mb4 encoding.
|
||||
$version = Database::getConnection()->clientVersion();
|
||||
if (FALSE !== strpos($version, 'mysqlnd')) {
|
||||
// The mysqlnd driver supports utf8mb4 starting at version 5.0.9.
|
||||
$version = preg_replace('/^\D+([\d.]+).*/', '$1', $version);
|
||||
if (version_compare($version, self::MYSQLND_MINIMUM_VERSION, '<')) {
|
||||
$this->fail(t("The MySQLnd driver version %version is less than the minimum required version. Upgrade to MySQLnd version %mysqlnd_minimum_version or up, or alternatively switch mysql drivers to libmysqlclient version %libmysqlclient_minimum_version or up.", array('%version' => Database::getConnection()->version(), '%mysqlnd_minimum_version' => self::MYSQLND_MINIMUM_VERSION, '%libmysqlclient_minimum_version' => self::LIBMYSQLCLIENT_MINIMUM_VERSION)));
|
||||
}
|
||||
}
|
||||
else {
|
||||
// The libmysqlclient driver supports utf8mb4 starting at version 5.5.3.
|
||||
if (version_compare($version, self::LIBMYSQLCLIENT_MINIMUM_VERSION, '<')) {
|
||||
$this->fail(t("The libmysqlclient driver version %version is less than the minimum required version. Upgrade to libmysqlclient version %libmysqlclient_minimum_version or up, or alternatively switch mysql drivers to MySQLnd version %mysqlnd_minimum_version or up.", array('%version' => Database::getConnection()->version(), '%libmysqlclient_minimum_version' => self::LIBMYSQLCLIENT_MINIMUM_VERSION, '%mysqlnd_minimum_version' => self::MYSQLND_MINIMUM_VERSION)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ namespace Drupal\Core\Database\Driver\mysql;
|
|||
|
||||
use Drupal\Core\Database\Database;
|
||||
use Drupal\Core\Database\Query\Condition;
|
||||
use Drupal\Core\Database\SchemaException;
|
||||
use Drupal\Core\Database\SchemaObjectExistsException;
|
||||
use Drupal\Core\Database\SchemaObjectDoesNotExistException;
|
||||
use Drupal\Core\Database\Schema as DatabaseSchema;
|
||||
|
@ -60,8 +61,7 @@ class Schema extends DatabaseSchema {
|
|||
$info['table'] = substr($table, ++$pos);
|
||||
}
|
||||
else {
|
||||
$db_info = Database::getConnectionInfo();
|
||||
$info['database'] = $db_info[$this->connection->getTarget()]['database'];
|
||||
$info['database'] = $this->connection->getConnectionOptions()['database'];
|
||||
$info['table'] = $table;
|
||||
}
|
||||
return $info;
|
||||
|
@ -299,14 +299,17 @@ class Schema extends DatabaseSchema {
|
|||
* Shortens indexes to 191 characters if they apply to utf8mb4-encoded
|
||||
* fields, in order to comply with the InnoDB index limitation of 756 bytes.
|
||||
*
|
||||
* @param $spec
|
||||
* @param array $spec
|
||||
* The table specification.
|
||||
*
|
||||
* @return array
|
||||
* List of shortened indexes.
|
||||
*
|
||||
* @throws \Drupal\Core\Database\SchemaException
|
||||
* Thrown if field specification is missing.
|
||||
*/
|
||||
protected function getNormalizedIndexes($spec) {
|
||||
$indexes = $spec['indexes'];
|
||||
protected function getNormalizedIndexes(array $spec) {
|
||||
$indexes = isset($spec['indexes']) ? $spec['indexes'] : [];
|
||||
foreach ($indexes as $index_name => $index_fields) {
|
||||
foreach ($index_fields as $index_key => $index_field) {
|
||||
// Get the name of the field from the index specification.
|
||||
|
@ -323,6 +326,9 @@ class Schema extends DatabaseSchema {
|
|||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
throw new SchemaException("MySQL needs the '$field_name' field specification in order to normalize the '$index_name' index");
|
||||
}
|
||||
}
|
||||
}
|
||||
return $indexes;
|
||||
|
@ -486,7 +492,10 @@ class Schema extends DatabaseSchema {
|
|||
return TRUE;
|
||||
}
|
||||
|
||||
public function addIndex($table, $name, $fields) {
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function addIndex($table, $name, $fields, array $spec) {
|
||||
if (!$this->tableExists($table)) {
|
||||
throw new SchemaObjectDoesNotExistException(t("Cannot add index @name to table @table: table doesn't exist.", array('@table' => $table, '@name' => $name)));
|
||||
}
|
||||
|
@ -494,7 +503,10 @@ class Schema extends DatabaseSchema {
|
|||
throw new SchemaObjectExistsException(t("Cannot add index @name to table @table: index already exists.", array('@table' => $table, '@name' => $name)));
|
||||
}
|
||||
|
||||
$this->connection->query('ALTER TABLE {' . $table . '} ADD INDEX `' . $name . '` (' . $this->createKeySql($fields) . ')');
|
||||
$spec['indexes'][$name] = $fields;
|
||||
$indexes = $this->getNormalizedIndexes($spec);
|
||||
|
||||
$this->connection->query('ALTER TABLE {' . $table . '} ADD INDEX `' . $name . '` (' . $this->createKeySql($indexes[$name]) . ')');
|
||||
}
|
||||
|
||||
public function dropIndex($table, $name) {
|
||||
|
|
45
core/lib/Drupal/Core/Database/Driver/mysql/Upsert.php
Normal file
45
core/lib/Drupal/Core/Database/Driver/mysql/Upsert.php
Normal file
|
@ -0,0 +1,45 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\Core\Database\Driver\mysql\Upsert.
|
||||
*/
|
||||
|
||||
namespace Drupal\Core\Database\Driver\mysql;
|
||||
|
||||
use Drupal\Core\Database\Query\Upsert as QueryUpsert;
|
||||
|
||||
/**
|
||||
* Implements the Upsert query for the MySQL database driver.
|
||||
*/
|
||||
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);
|
||||
|
||||
$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) {
|
||||
$update[] = "$field = VALUES($field)";
|
||||
}
|
||||
|
||||
$query .= ' ON DUPLICATE KEY UPDATE ' . implode(', ', $update);
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
}
|
|
@ -383,6 +383,22 @@ class Connection extends DatabaseConnection {
|
|||
$this->rollback($savepoint_name);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function upsert($table, array $options = array()) {
|
||||
// Use the (faster) native Upsert implementation for PostgreSQL >= 9.5.
|
||||
if (version_compare($this->version(), '9.5', '>=')) {
|
||||
$class = $this->getDriverClass('NativeUpsert');
|
||||
}
|
||||
else {
|
||||
$class = $this->getDriverClass('Upsert');
|
||||
}
|
||||
|
||||
return new $class($this, $table, $options);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -128,30 +128,7 @@ class Insert extends QueryInsert {
|
|||
|
||||
$query = $comments . 'INSERT INTO {' . $this->table . '} (' . implode(', ', $insert_fields) . ') VALUES ';
|
||||
|
||||
$max_placeholder = 0;
|
||||
$values = array();
|
||||
if (count($this->insertValues)) {
|
||||
foreach ($this->insertValues as $insert_values) {
|
||||
$placeholders = array();
|
||||
|
||||
// Default fields aren't really placeholders, but this is the most convenient
|
||||
// way to handle them.
|
||||
$placeholders = array_pad($placeholders, count($this->defaultFields), 'default');
|
||||
|
||||
$new_placeholder = $max_placeholder + count($insert_values);
|
||||
for ($i = $max_placeholder; $i < $new_placeholder; ++$i) {
|
||||
$placeholders[] = ':db_insert_placeholder_' . $i;
|
||||
}
|
||||
$max_placeholder = $new_placeholder;
|
||||
$values[] = '(' . implode(', ', $placeholders) . ')';
|
||||
}
|
||||
}
|
||||
else {
|
||||
// If there are no values, then this is a default-only query. We still need to handle that.
|
||||
$placeholders = array_fill(0, count($this->defaultFields), 'default');
|
||||
$values[] = '(' . implode(', ', $placeholders) . ')';
|
||||
}
|
||||
|
||||
$values = $this->getInsertPlaceholderFragment($this->insertValues, $this->defaultFields);
|
||||
$query .= implode(', ', $values);
|
||||
|
||||
return $query;
|
||||
|
|
|
@ -180,8 +180,8 @@ class Tasks extends InstallTasks {
|
|||
* Verify that a binary data roundtrip returns the original string.
|
||||
*/
|
||||
protected function checkBinaryOutputSuccess() {
|
||||
$bytea_output = db_query("SELECT 'encoding'::bytea AS output")->fetchField();
|
||||
return ($bytea_output == 'encoding');
|
||||
$bytea_output = db_query("SHOW bytea_output")->fetchField();
|
||||
return ($bytea_output == 'escape');
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -192,17 +192,9 @@ class Tasks extends InstallTasks {
|
|||
// like we do with table names. This is so that we don't double up if more
|
||||
// than one instance of Drupal is running on a single database. We therefore
|
||||
// avoid trying to create them again in that case.
|
||||
|
||||
// At the same time checking for the existence of the function fixes
|
||||
// concurrency issues, when both try to update at the same time.
|
||||
try {
|
||||
// Create functions.
|
||||
db_query('CREATE OR REPLACE FUNCTION "greatest"(numeric, numeric) RETURNS numeric AS
|
||||
\'SELECT CASE WHEN (($1 > $2) OR ($2 IS NULL)) THEN $1 ELSE $2 END;\'
|
||||
LANGUAGE \'sql\''
|
||||
);
|
||||
db_query('CREATE OR REPLACE FUNCTION "greatest"(numeric, numeric, numeric) RETURNS numeric AS
|
||||
\'SELECT greatest($1, greatest($2, $3));\'
|
||||
LANGUAGE \'sql\''
|
||||
);
|
||||
// Don't use {} around pg_proc table.
|
||||
if (!db_query("SELECT COUNT(*) FROM pg_proc WHERE proname = 'rand'")->fetchField()) {
|
||||
db_query('CREATE OR REPLACE FUNCTION "rand"() RETURNS float AS
|
||||
|
@ -211,37 +203,17 @@ class Tasks extends InstallTasks {
|
|||
);
|
||||
}
|
||||
|
||||
db_query('CREATE OR REPLACE FUNCTION "substring_index"(text, text, integer) RETURNS text AS
|
||||
\'SELECT array_to_string((string_to_array($1, $2)) [1:$3], $2);\'
|
||||
LANGUAGE \'sql\''
|
||||
);
|
||||
|
||||
// Using || to concatenate in Drupal is not recommended because there are
|
||||
// database drivers for Drupal that do not support the syntax, however
|
||||
// they do support CONCAT(item1, item2) which we can replicate in
|
||||
// PostgreSQL. PostgreSQL requires the function to be defined for each
|
||||
// different argument variation the function can handle.
|
||||
db_query('CREATE OR REPLACE FUNCTION "concat"(anynonarray, anynonarray) RETURNS text AS
|
||||
\'SELECT CAST($1 AS text) || CAST($2 AS text);\'
|
||||
LANGUAGE \'sql\'
|
||||
');
|
||||
db_query('CREATE OR REPLACE FUNCTION "concat"(text, anynonarray) RETURNS text AS
|
||||
\'SELECT $1 || CAST($2 AS text);\'
|
||||
LANGUAGE \'sql\'
|
||||
');
|
||||
db_query('CREATE OR REPLACE FUNCTION "concat"(anynonarray, text) RETURNS text AS
|
||||
\'SELECT CAST($1 AS text) || $2;\'
|
||||
LANGUAGE \'sql\'
|
||||
');
|
||||
db_query('CREATE OR REPLACE FUNCTION "concat"(text, text) RETURNS text AS
|
||||
\'SELECT $1 || $2;\'
|
||||
LANGUAGE \'sql\'
|
||||
');
|
||||
if (!db_query("SELECT COUNT(*) FROM pg_proc WHERE proname = 'substring_index'")->fetchField()) {
|
||||
db_query('CREATE OR REPLACE FUNCTION "substring_index"(text, text, integer) RETURNS text AS
|
||||
\'SELECT array_to_string((string_to_array($1, $2)) [1:$3], $2);\'
|
||||
LANGUAGE \'sql\''
|
||||
);
|
||||
}
|
||||
|
||||
$this->pass(t('PostgreSQL has initialized itself.'));
|
||||
}
|
||||
catch (\Exception $e) {
|
||||
$this->fail(t('Drupal could not be correctly setup with the existing database. Revise any errors.'));
|
||||
$this->fail(t('Drupal could not be correctly setup with the existing database due to the following error: @error.', ['@error' => $e->getMessage()]));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
116
core/lib/Drupal/Core/Database/Driver/pgsql/NativeUpsert.php
Normal file
116
core/lib/Drupal/Core/Database/Driver/pgsql/NativeUpsert.php
Normal file
|
@ -0,0 +1,116 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\Core\Database\Driver\pgsql\NativeUpsert.
|
||||
*/
|
||||
|
||||
namespace Drupal\Core\Database\Driver\pgsql;
|
||||
|
||||
use Drupal\Core\Database\Query\Upsert as QueryUpsert;
|
||||
|
||||
/**
|
||||
* Implements the native Upsert query for the PostgreSQL database driver.
|
||||
*
|
||||
* @see http://www.postgresql.org/docs/9.5/static/sql-insert.html#SQL-ON-CONFLICT
|
||||
*/
|
||||
class NativeUpsert extends QueryUpsert {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function execute() {
|
||||
if (!$this->preExecute()) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
$stmt = $this->connection->prepareQuery((string) $this);
|
||||
|
||||
// Fetch the list of blobs and sequences used on that table.
|
||||
$table_information = $this->connection->schema()->queryTableInformation($this->table);
|
||||
|
||||
$max_placeholder = 0;
|
||||
$blobs = [];
|
||||
$blob_count = 0;
|
||||
foreach ($this->insertValues as $insert_values) {
|
||||
foreach ($this->insertFields as $idx => $field) {
|
||||
if (isset($table_information->blob_fields[$field])) {
|
||||
$blobs[$blob_count] = fopen('php://memory', 'a');
|
||||
fwrite($blobs[$blob_count], $insert_values[$idx]);
|
||||
rewind($blobs[$blob_count]);
|
||||
|
||||
$stmt->bindParam(':db_insert_placeholder_' . $max_placeholder++, $blobs[$blob_count], \PDO::PARAM_LOB);
|
||||
|
||||
// Pre-increment is faster in PHP than increment.
|
||||
++$blob_count;
|
||||
}
|
||||
else {
|
||||
$stmt->bindParam(':db_insert_placeholder_' . $max_placeholder++, $insert_values[$idx]);
|
||||
}
|
||||
}
|
||||
// Check if values for a serial field has been passed.
|
||||
if (!empty($table_information->serial_fields)) {
|
||||
foreach ($table_information->serial_fields as $index => $serial_field) {
|
||||
$serial_key = array_search($serial_field, $this->insertFields);
|
||||
if ($serial_key !== FALSE) {
|
||||
$serial_value = $insert_values[$serial_key];
|
||||
|
||||
// Sequences must be greater than or equal to 1.
|
||||
if ($serial_value === NULL || !$serial_value) {
|
||||
$serial_value = 1;
|
||||
}
|
||||
// Set the sequence to the bigger value of either the passed
|
||||
// value or the max value of the column. It can happen that another
|
||||
// thread calls nextval() which could lead to a serial number being
|
||||
// used twice. However, trying to insert a value into a serial
|
||||
// column should only be done in very rare cases and is not thread
|
||||
// safe by definition.
|
||||
$this->connection->query("SELECT setval('" . $table_information->sequences[$index] . "', GREATEST(MAX(" . $serial_field . "), :serial_value)) FROM {" . $this->table . "}", array(':serial_value' => (int)$serial_value));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$options = $this->queryOptions;
|
||||
if (!empty($table_information->sequences)) {
|
||||
$options['sequence_name'] = $table_information->sequences[0];
|
||||
}
|
||||
|
||||
$this->connection->query($stmt, [], $options);
|
||||
|
||||
// Re-initialize the values array so that we can re-use this query.
|
||||
$this->insertValues = [];
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@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($f) { return $this->connection->escapeField($f); }, $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) {
|
||||
$update[] = "$field = EXCLUDED.$field";
|
||||
}
|
||||
|
||||
$query .= ' ON CONFLICT (' . $this->connection->escapeField($this->key) . ') DO UPDATE SET ' . implode(', ', $update);
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
}
|
|
@ -93,9 +93,17 @@ class Schema extends DatabaseSchema {
|
|||
public function queryTableInformation($table) {
|
||||
// Generate a key to reference this table's information on.
|
||||
$key = $this->connection->prefixTables('{' . $table . '}');
|
||||
if (strpos($key, '.') === FALSE) {
|
||||
|
||||
// Take into account that temporary tables are stored in a different schema.
|
||||
// \Drupal\Core\Database\Connection::generateTemporaryTableName() sets the
|
||||
// 'db_temporary_' prefix to all temporary tables.
|
||||
if (strpos($key, '.') === FALSE && strpos($table, 'db_temporary_') === FALSE) {
|
||||
$key = 'public.' . $key;
|
||||
}
|
||||
else {
|
||||
$schema = $this->connection->query('SELECT nspname FROM pg_namespace WHERE oid = pg_my_temp_schema()')->fetchField();
|
||||
$key = $schema . '.' . $key;
|
||||
}
|
||||
|
||||
if (!isset($this->tableInformation[$key])) {
|
||||
// Split the key into schema and table for querying.
|
||||
|
@ -580,13 +588,28 @@ class Schema extends DatabaseSchema {
|
|||
/**
|
||||
* Helper function: check if a constraint (PK, FK, UK) exists.
|
||||
*
|
||||
* @param $table
|
||||
* @param string $table
|
||||
* The name of the table.
|
||||
* @param $name
|
||||
* The name of the constraint (typically 'pkey' or '[constraint]_key').
|
||||
* @param string $name
|
||||
* The name of the constraint (typically 'pkey' or '[constraint]__key').
|
||||
*
|
||||
* @return bool
|
||||
* TRUE if the constraint exists, FALSE otherwise.
|
||||
*/
|
||||
public function constraintExists($table, $name) {
|
||||
$constraint_name = $this->ensureIdentifiersLength($table, $name);
|
||||
// ::ensureIdentifiersLength() expects three parameters, although not
|
||||
// explicitly stated in its signature, thus we split our constraint name in
|
||||
// a proper name and a suffix.
|
||||
if ($name == 'pkey') {
|
||||
$suffix = $name;
|
||||
$name = '';
|
||||
}
|
||||
else {
|
||||
$pos = strrpos($name, '__');
|
||||
$suffix = substr($name, $pos + 2);
|
||||
$name = substr($name, 0, $pos);
|
||||
}
|
||||
$constraint_name = $this->ensureIdentifiersLength($table, $name, $suffix);
|
||||
// Remove leading and trailing quotes because the index name is in a WHERE
|
||||
// clause and not used as an identifier.
|
||||
$constraint_name = str_replace('"', '', $constraint_name);
|
||||
|
@ -637,7 +660,10 @@ class Schema extends DatabaseSchema {
|
|||
return TRUE;
|
||||
}
|
||||
|
||||
public function addIndex($table, $name, $fields) {
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function addIndex($table, $name, $fields, array $spec) {
|
||||
if (!$this->tableExists($table)) {
|
||||
throw new SchemaObjectDoesNotExistException(t("Cannot add index @name to table @table: table doesn't exist.", array('@table' => $table, '@name' => $name)));
|
||||
}
|
||||
|
@ -779,7 +805,10 @@ class Schema extends DatabaseSchema {
|
|||
}
|
||||
if (isset($new_keys['indexes'])) {
|
||||
foreach ($new_keys['indexes'] as $name => $fields) {
|
||||
$this->addIndex($table, $name, $fields);
|
||||
// Even though $new_keys is not a full schema it still has 'indexes' and
|
||||
// so is a partial schema. Technically addIndex() doesn't do anything
|
||||
// with it so passing an empty array would work as well.
|
||||
$this->addIndex($table, $name, $fields, $new_keys);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
88
core/lib/Drupal/Core/Database/Driver/pgsql/Upsert.php
Normal file
88
core/lib/Drupal/Core/Database/Driver/pgsql/Upsert.php
Normal file
|
@ -0,0 +1,88 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\Core\Database\Driver\pgsql\Upsert.
|
||||
*/
|
||||
|
||||
namespace Drupal\Core\Database\Driver\pgsql;
|
||||
|
||||
use Drupal\Core\Database\Query\Upsert as QueryUpsert;
|
||||
|
||||
/**
|
||||
* Implements the Upsert query for the PostgreSQL database driver.
|
||||
*/
|
||||
class Upsert extends QueryUpsert {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function execute() {
|
||||
if (!$this->preExecute()) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Default options for upsert queries.
|
||||
$this->queryOptions += array(
|
||||
'throw_exception' => TRUE,
|
||||
);
|
||||
|
||||
// Default fields are always placed first for consistency.
|
||||
$insert_fields = array_merge($this->defaultFields, $this->insertFields);
|
||||
|
||||
$table = $this->connection->escapeTable($this->table);
|
||||
|
||||
// We have to execute multiple queries, therefore we wrap everything in a
|
||||
// transaction so that it is atomic where possible.
|
||||
$transaction = $this->connection->startTransaction();
|
||||
|
||||
try {
|
||||
// First, lock the table we're upserting into.
|
||||
$this->connection->query('LOCK TABLE {' . $table . '} IN SHARE ROW EXCLUSIVE MODE', [], $this->queryOptions);
|
||||
|
||||
// Second, delete all items first so we can do one insert.
|
||||
$unique_key_position = array_search($this->key, $insert_fields);
|
||||
$delete_ids = [];
|
||||
foreach ($this->insertValues as $insert_values) {
|
||||
$delete_ids[] = $insert_values[$unique_key_position];
|
||||
}
|
||||
|
||||
// Delete in chunks when a large array is passed.
|
||||
foreach (array_chunk($delete_ids, 1000) as $delete_ids_chunk) {
|
||||
$this->connection->delete($this->table, $this->queryOptions)
|
||||
->condition($this->key, $delete_ids_chunk, 'IN')
|
||||
->execute();
|
||||
}
|
||||
|
||||
// Third, insert all the values.
|
||||
$insert = $this->connection->insert($this->table, $this->queryOptions)
|
||||
->fields($insert_fields);
|
||||
foreach ($this->insertValues as $insert_values) {
|
||||
$insert->values($insert_values);
|
||||
}
|
||||
$insert->execute();
|
||||
}
|
||||
catch (\Exception $e) {
|
||||
// One of the queries failed, rollback the whole batch.
|
||||
$transaction->rollback();
|
||||
|
||||
// Rethrow the exception for the calling code.
|
||||
throw $e;
|
||||
}
|
||||
|
||||
// Re-initialize the values array so that we can re-use this query.
|
||||
$this->insertValues = array();
|
||||
|
||||
// Transaction commits here where $transaction looses scope.
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function __toString() {
|
||||
// Nothing to do.
|
||||
}
|
||||
|
||||
}
|
|
@ -154,9 +154,9 @@ class Connection extends DatabaseConnection {
|
|||
|
||||
// We can prune the database file if it doesn't have any tables.
|
||||
if ($count == 0) {
|
||||
// Detach the database.
|
||||
$this->query('DETACH DATABASE :schema', array(':schema' => $prefix));
|
||||
// Destroy the database file.
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
@ -168,6 +168,18 @@ class Connection extends DatabaseConnection {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
|
|
@ -582,7 +582,10 @@ class Schema extends DatabaseSchema {
|
|||
return $key_definition;
|
||||
}
|
||||
|
||||
public function addIndex($table, $name, $fields) {
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function addIndex($table, $name, $fields, array $spec) {
|
||||
if (!$this->tableExists($table)) {
|
||||
throw new SchemaObjectDoesNotExistException(t("Cannot add index @name to table @table: table doesn't exist.", array('@table' => $table, '@name' => $name)));
|
||||
}
|
||||
|
@ -693,16 +696,31 @@ class Schema extends DatabaseSchema {
|
|||
$this->alterTable($table, $old_schema, $new_schema);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function findTables($table_expression) {
|
||||
// Don't add the prefix, $table_expression already includes the prefix.
|
||||
$info = $this->getPrefixInfo($table_expression, FALSE);
|
||||
$tables = [];
|
||||
|
||||
// Can't use query placeholders for the schema because the query would have
|
||||
// to be :prefixsqlite_master, which does not work.
|
||||
$result = db_query("SELECT name FROM " . $info['schema'] . ".sqlite_master WHERE type = :type AND name LIKE :table_name", array(
|
||||
':type' => 'table',
|
||||
':table_name' => $info['table'],
|
||||
));
|
||||
return $result->fetchAllKeyed(0, 0);
|
||||
// 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 = db_query("SELECT name FROM " . $schema . ".sqlite_master WHERE type = :type AND name LIKE :table_name AND name NOT LIKE :pattern", array(
|
||||
':type' => 'table',
|
||||
':table_name' => $table_expression,
|
||||
':pattern' => 'sqlite_%',
|
||||
));
|
||||
$tables += $result->fetchAllKeyed(0, 0);
|
||||
}
|
||||
|
||||
return $tables;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
35
core/lib/Drupal/Core/Database/Driver/sqlite/Upsert.php
Normal file
35
core/lib/Drupal/Core/Database/Driver/sqlite/Upsert.php
Normal file
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\Core\Database\Driver\sqlite\Upsert.
|
||||
*/
|
||||
|
||||
namespace Drupal\Core\Database\Driver\sqlite;
|
||||
|
||||
use Drupal\Core\Database\Query\Upsert as QueryUpsert;
|
||||
|
||||
/**
|
||||
* Implements the Upsert query for the SQLite database driver.
|
||||
*/
|
||||
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);
|
||||
|
||||
$query = $comments . 'INSERT OR REPLACE INTO {' . $this->table . '} (' . implode(', ', $insert_fields) . ') VALUES ';
|
||||
|
||||
$values = $this->getInsertPlaceholderFragment($this->insertValues, $this->defaultFields);
|
||||
$query .= implode(', ', $values);
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
}
|
|
@ -7,7 +7,6 @@
|
|||
|
||||
namespace Drupal\Core\Database\Install;
|
||||
|
||||
use Drupal\Component\Utility\SafeMarkup;
|
||||
use Drupal\Core\Database\Database;
|
||||
|
||||
/**
|
||||
|
@ -80,7 +79,10 @@ abstract class Tasks {
|
|||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $results = array();
|
||||
protected $results = array(
|
||||
'fail' => array(),
|
||||
'pass' => array(),
|
||||
);
|
||||
|
||||
/**
|
||||
* Ensure the PDO driver is supported by the version of PHP in use.
|
||||
|
@ -93,14 +95,14 @@ abstract class Tasks {
|
|||
* Assert test as failed.
|
||||
*/
|
||||
protected function fail($message) {
|
||||
$this->results[$message] = FALSE;
|
||||
$this->results['fail'][] = $message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert test as a pass.
|
||||
*/
|
||||
protected function pass($message) {
|
||||
$this->results[$message] = TRUE;
|
||||
$this->results['pass'][] = $message;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -128,6 +130,9 @@ abstract class Tasks {
|
|||
|
||||
/**
|
||||
* Run database tasks and tests to see if Drupal can run on the database.
|
||||
*
|
||||
* @return array
|
||||
* A list of error messages.
|
||||
*/
|
||||
public function runTasks() {
|
||||
// We need to establish a connection before we can run tests.
|
||||
|
@ -143,21 +148,11 @@ abstract class Tasks {
|
|||
}
|
||||
}
|
||||
else {
|
||||
throw new TaskException(t("Failed to run all tasks against the database server. The task %task wasn't found.", array('%task' => $task['function'])));
|
||||
$this->fail(t("Failed to run all tasks against the database server. The task %task wasn't found.", array('%task' => $task['function'])));
|
||||
}
|
||||
}
|
||||
}
|
||||
// Check for failed results and compile message
|
||||
$message = '';
|
||||
foreach ($this->results as $result => $success) {
|
||||
if (!$success) {
|
||||
$message = SafeMarkup::isSafe($result) ? $result : SafeMarkup::checkPlain($result);
|
||||
}
|
||||
}
|
||||
if (!empty($message)) {
|
||||
$message = SafeMarkup::set('Resolve all issues below to continue the installation. For help configuring your database server, see the <a href="https://www.drupal.org/getting-started/install">installation handbook</a>, or contact your hosting provider.' . $message);
|
||||
throw new TaskException($message);
|
||||
}
|
||||
return $this->results['fail'];
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -196,8 +191,9 @@ abstract class Tasks {
|
|||
* Check the engine version.
|
||||
*/
|
||||
protected function checkEngineVersion() {
|
||||
// Ensure that the database server has the right version.
|
||||
if ($this->minimumVersion() && version_compare(Database::getConnection()->version(), $this->minimumVersion(), '<')) {
|
||||
$this->fail(t("The database version %version is less than the minimum required version %minimum_version.", array('%version' => Database::getConnection()->version(), '%minimum_version' => $this->minimumVersion())));
|
||||
$this->fail(t("The database server version %version is less than the minimum required version %minimum_version.", array('%version' => Database::getConnection()->version(), '%minimum_version' => $this->minimumVersion())));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -16,43 +16,7 @@ use Drupal\Core\Database\Database;
|
|||
*/
|
||||
class Insert extends Query {
|
||||
|
||||
/**
|
||||
* The table on which to insert.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $table;
|
||||
|
||||
/**
|
||||
* An array of fields on which to insert.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $insertFields = array();
|
||||
|
||||
/**
|
||||
* An array of fields that should be set to their database-defined defaults.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $defaultFields = array();
|
||||
|
||||
/**
|
||||
* A nested array of values to insert.
|
||||
*
|
||||
* $insertValues is an array of arrays. Each sub-array is either an
|
||||
* associative array whose keys are field names and whose values are field
|
||||
* values to insert, or a non-associative array of values in the same order
|
||||
* as $insertFields.
|
||||
*
|
||||
* Whether multiple insert sets will be run in a single query or multiple
|
||||
* queries is left to individual drivers to implement in whatever manner is
|
||||
* most appropriate. The order of values in each sub-array must match the
|
||||
* order of fields in $insertFields.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $insertValues = array();
|
||||
use InsertTrait;
|
||||
|
||||
/**
|
||||
* A SelectQuery object to fetch the rows that should be inserted.
|
||||
|
@ -79,96 +43,6 @@ class Insert extends Query {
|
|||
$this->table = $table;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a set of field->value pairs to be inserted.
|
||||
*
|
||||
* This method may only be called once. Calling it a second time will be
|
||||
* ignored. To queue up multiple sets of values to be inserted at once,
|
||||
* use the values() method.
|
||||
*
|
||||
* @param $fields
|
||||
* An array of fields on which to insert. This array may be indexed or
|
||||
* associative. If indexed, the array is taken to be the list of fields.
|
||||
* If associative, the keys of the array are taken to be the fields and
|
||||
* the values are taken to be corresponding values to insert. If a
|
||||
* $values argument is provided, $fields must be indexed.
|
||||
* @param $values
|
||||
* An array of fields to insert into the database. The values must be
|
||||
* specified in the same order as the $fields array.
|
||||
*
|
||||
* @return \Drupal\Core\Database\Query\Insert
|
||||
* The called object.
|
||||
*/
|
||||
public function fields(array $fields, array $values = array()) {
|
||||
if (empty($this->insertFields)) {
|
||||
if (empty($values)) {
|
||||
if (!is_numeric(key($fields))) {
|
||||
$values = array_values($fields);
|
||||
$fields = array_keys($fields);
|
||||
}
|
||||
}
|
||||
$this->insertFields = $fields;
|
||||
if (!empty($values)) {
|
||||
$this->insertValues[] = $values;
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds another set of values to the query to be inserted.
|
||||
*
|
||||
* If $values is a numeric-keyed array, it will be assumed to be in the same
|
||||
* order as the original fields() call. If it is associative, it may be
|
||||
* in any order as long as the keys of the array match the names of the
|
||||
* fields.
|
||||
*
|
||||
* @param $values
|
||||
* An array of values to add to the query.
|
||||
*
|
||||
* @return \Drupal\Core\Database\Query\Insert
|
||||
* The called object.
|
||||
*/
|
||||
public function values(array $values) {
|
||||
if (is_numeric(key($values))) {
|
||||
$this->insertValues[] = $values;
|
||||
}
|
||||
else {
|
||||
// Reorder the submitted values to match the fields array.
|
||||
foreach ($this->insertFields as $key) {
|
||||
$insert_values[$key] = $values[$key];
|
||||
}
|
||||
// For consistency, the values array is always numerically indexed.
|
||||
$this->insertValues[] = array_values($insert_values);
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specifies fields for which the database defaults should be used.
|
||||
*
|
||||
* If you want to force a given field to use the database-defined default,
|
||||
* not NULL or undefined, use this method to instruct the database to use
|
||||
* default values explicitly. In most cases this will not be necessary
|
||||
* unless you are inserting a row that is all default values, as you cannot
|
||||
* specify no values in an INSERT query.
|
||||
*
|
||||
* Specifying a field both in fields() and in useDefaults() is an error
|
||||
* and will not execute.
|
||||
*
|
||||
* @param $fields
|
||||
* An array of values for which to use the default values
|
||||
* specified in the table definition.
|
||||
*
|
||||
* @return \Drupal\Core\Database\Query\Insert
|
||||
* The called object.
|
||||
*/
|
||||
public function useDefaults(array $fields) {
|
||||
$this->defaultFields = $fields;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the fromQuery on this InsertQuery object.
|
||||
*
|
||||
|
@ -265,13 +139,13 @@ class Insert extends Query {
|
|||
/**
|
||||
* Preprocesses and validates the query.
|
||||
*
|
||||
* @return
|
||||
* @return bool
|
||||
* TRUE if the validation was successful, FALSE if not.
|
||||
*
|
||||
* @throws \Drupal\Core\Database\Query\FieldsOverlapException
|
||||
* @throws \Drupal\Core\Database\Query\NoFieldsException
|
||||
*/
|
||||
public function preExecute() {
|
||||
protected function preExecute() {
|
||||
// Confirm that the user did not try to specify an identical
|
||||
// field and default field.
|
||||
if (array_intersect($this->insertFields, $this->defaultFields)) {
|
||||
|
|
184
core/lib/Drupal/Core/Database/Query/InsertTrait.php
Normal file
184
core/lib/Drupal/Core/Database/Query/InsertTrait.php
Normal file
|
@ -0,0 +1,184 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\Core\Database\Query\InsertTrait.
|
||||
*/
|
||||
|
||||
namespace Drupal\Core\Database\Query;
|
||||
|
||||
/**
|
||||
* Provides common functionality for INSERT and UPSERT queries.
|
||||
*
|
||||
* @ingroup database
|
||||
*/
|
||||
trait InsertTrait {
|
||||
|
||||
/**
|
||||
* The table on which to insert.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $table;
|
||||
|
||||
/**
|
||||
* An array of fields on which to insert.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $insertFields = array();
|
||||
|
||||
/**
|
||||
* An array of fields that should be set to their database-defined defaults.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $defaultFields = array();
|
||||
|
||||
/**
|
||||
* A nested array of values to insert.
|
||||
*
|
||||
* $insertValues is an array of arrays. Each sub-array is either an
|
||||
* associative array whose keys are field names and whose values are field
|
||||
* values to insert, or a non-associative array of values in the same order
|
||||
* as $insertFields.
|
||||
*
|
||||
* Whether multiple insert sets will be run in a single query or multiple
|
||||
* queries is left to individual drivers to implement in whatever manner is
|
||||
* most appropriate. The order of values in each sub-array must match the
|
||||
* order of fields in $insertFields.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $insertValues = array();
|
||||
|
||||
/**
|
||||
* Adds a set of field->value pairs to be inserted.
|
||||
*
|
||||
* This method may only be called once. Calling it a second time will be
|
||||
* ignored. To queue up multiple sets of values to be inserted at once,
|
||||
* use the values() method.
|
||||
*
|
||||
* @param array $fields
|
||||
* An array of fields on which to insert. This array may be indexed or
|
||||
* associative. If indexed, the array is taken to be the list of fields.
|
||||
* If associative, the keys of the array are taken to be the fields and
|
||||
* the values are taken to be corresponding values to insert. If a
|
||||
* $values argument is provided, $fields must be indexed.
|
||||
* @param array $values
|
||||
* (optional) An array of fields to insert into the database. The values
|
||||
* must be specified in the same order as the $fields array.
|
||||
*
|
||||
* @return $this
|
||||
* The called object.
|
||||
*/
|
||||
public function fields(array $fields, array $values = array()) {
|
||||
if (empty($this->insertFields)) {
|
||||
if (empty($values)) {
|
||||
if (!is_numeric(key($fields))) {
|
||||
$values = array_values($fields);
|
||||
$fields = array_keys($fields);
|
||||
}
|
||||
}
|
||||
$this->insertFields = $fields;
|
||||
if (!empty($values)) {
|
||||
$this->insertValues[] = $values;
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds another set of values to the query to be inserted.
|
||||
*
|
||||
* If $values is a numeric-keyed array, it will be assumed to be in the same
|
||||
* order as the original fields() call. If it is associative, it may be
|
||||
* in any order as long as the keys of the array match the names of the
|
||||
* fields.
|
||||
*
|
||||
* @param array $values
|
||||
* An array of values to add to the query.
|
||||
*
|
||||
* @return $this
|
||||
* The called object.
|
||||
*/
|
||||
public function values(array $values) {
|
||||
if (is_numeric(key($values))) {
|
||||
$this->insertValues[] = $values;
|
||||
}
|
||||
elseif ($this->insertFields) {
|
||||
// Reorder the submitted values to match the fields array.
|
||||
foreach ($this->insertFields as $key) {
|
||||
$insert_values[$key] = $values[$key];
|
||||
}
|
||||
// For consistency, the values array is always numerically indexed.
|
||||
$this->insertValues[] = array_values($insert_values);
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specifies fields for which the database defaults should be used.
|
||||
*
|
||||
* If you want to force a given field to use the database-defined default,
|
||||
* not NULL or undefined, use this method to instruct the database to use
|
||||
* default values explicitly. In most cases this will not be necessary
|
||||
* unless you are inserting a row that is all default values, as you cannot
|
||||
* specify no values in an INSERT query.
|
||||
*
|
||||
* Specifying a field both in fields() and in useDefaults() is an error
|
||||
* and will not execute.
|
||||
*
|
||||
* @param array $fields
|
||||
* An array of values for which to use the default values
|
||||
* specified in the table definition.
|
||||
*
|
||||
* @return $this
|
||||
* The called object.
|
||||
*/
|
||||
public function useDefaults(array $fields) {
|
||||
$this->defaultFields = $fields;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the query placeholders for values that will be inserted.
|
||||
*
|
||||
* @param array $nested_insert_values
|
||||
* A nested array of values to insert.
|
||||
* @param array $default_fields
|
||||
* An array of fields that should be set to their database-defined defaults.
|
||||
*
|
||||
* @return array
|
||||
* An array of insert placeholders.
|
||||
*/
|
||||
protected function getInsertPlaceholderFragment(array $nested_insert_values, array $default_fields) {
|
||||
$max_placeholder = 0;
|
||||
$values = array();
|
||||
if ($nested_insert_values) {
|
||||
foreach ($nested_insert_values as $insert_values) {
|
||||
$placeholders = array();
|
||||
|
||||
// Default fields aren't really placeholders, but this is the most convenient
|
||||
// way to handle them.
|
||||
$placeholders = array_pad($placeholders, count($default_fields), 'default');
|
||||
|
||||
$new_placeholder = $max_placeholder + count($insert_values);
|
||||
for ($i = $max_placeholder; $i < $new_placeholder; ++$i) {
|
||||
$placeholders[] = ':db_insert_placeholder_' . $i;
|
||||
}
|
||||
$max_placeholder = $new_placeholder;
|
||||
$values[] = '(' . implode(', ', $placeholders) . ')';
|
||||
}
|
||||
}
|
||||
else {
|
||||
// If there are no values, then this is a default-only query. We still need to handle that.
|
||||
$placeholders = array_fill(0, count($default_fields), 'default');
|
||||
$values[] = '(' . implode(', ', $placeholders) . ')';
|
||||
}
|
||||
|
||||
return $values;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\Core\Database\Query\NoUniqueFieldException.
|
||||
*/
|
||||
|
||||
namespace Drupal\Core\Database\Query;
|
||||
|
||||
use Drupal\Core\Database\DatabaseException;
|
||||
|
||||
/**
|
||||
* Exception thrown if an upsert query doesn't specify a unique field.
|
||||
*/
|
||||
class NoUniqueFieldException extends \InvalidArgumentException implements DatabaseException {}
|
|
@ -147,6 +147,9 @@ interface SelectInterface extends ConditionInterface, AlterableInterface, Extend
|
|||
* For some database drivers, it may also wrap the field name in
|
||||
* database-specific escape characters.
|
||||
*
|
||||
* @param string $string
|
||||
* An unsanitized field name.
|
||||
*
|
||||
* @return
|
||||
* The sanitized field name string.
|
||||
*/
|
||||
|
|
119
core/lib/Drupal/Core/Database/Query/Upsert.php
Normal file
119
core/lib/Drupal/Core/Database/Query/Upsert.php
Normal file
|
@ -0,0 +1,119 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\Core\Database\Query\Upsert.
|
||||
*/
|
||||
|
||||
namespace Drupal\Core\Database\Query;
|
||||
|
||||
use Drupal\Core\Database\Connection;
|
||||
use Drupal\Core\Database\Database;
|
||||
|
||||
/**
|
||||
* General class for an abstracted "Upsert" (UPDATE or INSERT) query operation.
|
||||
*
|
||||
* This class can only be used with a table with a single unique index.
|
||||
* Often, this will be the primary key. On such a table this class works like
|
||||
* Insert except the rows will be set to the desired values even if the key
|
||||
* existed before.
|
||||
*/
|
||||
abstract class Upsert extends Query {
|
||||
|
||||
use InsertTrait;
|
||||
|
||||
/**
|
||||
* The unique or primary key of the table.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $key;
|
||||
|
||||
/**
|
||||
* Constructs an Upsert object.
|
||||
*
|
||||
* @param \Drupal\Core\Database\Connection $connection
|
||||
* A Connection object.
|
||||
* @param string $table
|
||||
* Name of the table to associate with this query.
|
||||
* @param array $options
|
||||
* (optional) An array of database options.
|
||||
*/
|
||||
public function __construct(Connection $connection, $table, array $options = []) {
|
||||
$options['return'] = Database::RETURN_AFFECTED;
|
||||
parent::__construct($connection, $options);
|
||||
$this->table = $table;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the unique / primary key field to be used as condition for this query.
|
||||
*
|
||||
* @param string $field
|
||||
* The name of the field to set.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function key($field) {
|
||||
$this->key = $field;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Preprocesses and validates the query.
|
||||
*
|
||||
* @return bool
|
||||
* TRUE if the validation was successful, FALSE otherwise.
|
||||
*
|
||||
* @throws \Drupal\Core\Database\Query\NoUniqueFieldException
|
||||
* @throws \Drupal\Core\Database\Query\FieldsOverlapException
|
||||
* @throws \Drupal\Core\Database\Query\NoFieldsException
|
||||
*/
|
||||
protected function preExecute() {
|
||||
// Confirm that the user set the unique/primary key of the table.
|
||||
if (!$this->key) {
|
||||
throw new NoUniqueFieldException('There is no unique field specified.');
|
||||
}
|
||||
|
||||
// Confirm that the user did not try to specify an identical
|
||||
// field and default field.
|
||||
if (array_intersect($this->insertFields, $this->defaultFields)) {
|
||||
throw new FieldsOverlapException('You may not specify the same field to have a value and a schema-default value.');
|
||||
}
|
||||
|
||||
// Don't execute query without fields.
|
||||
if (count($this->insertFields) + count($this->defaultFields) == 0) {
|
||||
throw new NoFieldsException('There are no fields available to insert with.');
|
||||
}
|
||||
|
||||
// If no values have been added, silently ignore this query. This can happen
|
||||
// if values are added conditionally, so we don't want to throw an
|
||||
// exception.
|
||||
return isset($this->insertValues[0]) || $this->insertFields;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function execute() {
|
||||
if (!$this->preExecute()) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
$max_placeholder = 0;
|
||||
$values = array();
|
||||
foreach ($this->insertValues as $insert_values) {
|
||||
foreach ($insert_values as $value) {
|
||||
$values[':db_insert_placeholder_' . $max_placeholder++] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
$last_insert_id = $this->connection->query((string) $this, $values, $this->queryOptions);
|
||||
|
||||
// Re-initialize the values array so that we can re-use this query.
|
||||
$this->insertValues = array();
|
||||
|
||||
return $last_insert_id;
|
||||
}
|
||||
|
||||
}
|
|
@ -16,6 +16,11 @@ use Drupal\Core\Database\Query\PlaceholderInterface;
|
|||
*/
|
||||
abstract class Schema implements PlaceholderInterface {
|
||||
|
||||
/**
|
||||
* The database connection.
|
||||
*
|
||||
* @var \Drupal\Core\Database\Connection
|
||||
*/
|
||||
protected $connection;
|
||||
|
||||
/**
|
||||
|
@ -173,25 +178,62 @@ abstract class Schema implements PlaceholderInterface {
|
|||
}
|
||||
|
||||
/**
|
||||
* Find all tables that are like the specified base table name.
|
||||
* Finds all tables that are like the specified base table name.
|
||||
*
|
||||
* @param $table_expression
|
||||
* An SQL expression, for example "simpletest%" (without the quotes).
|
||||
* BEWARE: this is not prefixed, the caller should take care of that.
|
||||
* @param string $table_expression
|
||||
* An SQL expression, for example "cache_%" (without the quotes).
|
||||
*
|
||||
* @return
|
||||
* Array, both the keys and the values are the matching tables.
|
||||
* @return array
|
||||
* Both the keys and the values are the matching tables.
|
||||
*/
|
||||
public function findTables($table_expression) {
|
||||
$condition = $this->buildTableNameCondition($table_expression, 'LIKE', FALSE);
|
||||
|
||||
// Load all the tables up front in order to take into account per-table
|
||||
// prefixes. The actual matching is done at the bottom of the method.
|
||||
$condition = $this->buildTableNameCondition('%', 'LIKE');
|
||||
$condition->compile($this->connection, $this);
|
||||
|
||||
$individually_prefixed_tables = $this->connection->getUnprefixedTablesMap();
|
||||
$default_prefix = $this->connection->tablePrefix();
|
||||
$default_prefix_length = strlen($default_prefix);
|
||||
$tables = [];
|
||||
// Normally, we would heartily discourage the use of string
|
||||
// concatenation for conditionals like this however, we
|
||||
// couldn't use db_select() here because it would prefix
|
||||
// information_schema.tables and the query would fail.
|
||||
// Don't use {} around information_schema.tables table.
|
||||
return $this->connection->query("SELECT table_name FROM information_schema.tables WHERE " . (string) $condition, $condition->arguments())->fetchAllKeyed(0, 0);
|
||||
$results = $this->connection->query("SELECT table_name FROM information_schema.tables WHERE " . (string) $condition, $condition->arguments());
|
||||
foreach ($results as $table) {
|
||||
// Take into account tables that have an individual prefix.
|
||||
if (isset($individually_prefixed_tables[$table->table_name])) {
|
||||
$prefix_length = strlen($this->connection->tablePrefix($individually_prefixed_tables[$table->table_name]));
|
||||
}
|
||||
elseif ($default_prefix && substr($table->table_name, 0, $default_prefix_length) !== $default_prefix) {
|
||||
// This table name does not start the default prefix, which means that
|
||||
// it is not managed by Drupal so it should be excluded from the result.
|
||||
continue;
|
||||
}
|
||||
else {
|
||||
$prefix_length = $default_prefix_length;
|
||||
}
|
||||
|
||||
// Remove the prefix from the returned tables.
|
||||
$unprefixed_table_name = substr($table->table_name, $prefix_length);
|
||||
|
||||
// The pattern can match a table which is the same as the prefix. That
|
||||
// will become an empty string when we remove the prefix, which will
|
||||
// probably surprise the caller, besides not being a prefixed table. So
|
||||
// remove it.
|
||||
if (!empty($unprefixed_table_name)) {
|
||||
$tables[$unprefixed_table_name] = $unprefixed_table_name;
|
||||
}
|
||||
}
|
||||
|
||||
// Convert the table expression from its SQL LIKE syntax to a regular
|
||||
// expression and escape the delimiter that will be used for matching.
|
||||
$table_expression = str_replace(array('%', '_'), array('.*?', '.'), preg_quote($table_expression, '/'));
|
||||
$tables = preg_grep('/^' . $table_expression . '$/i', $tables);
|
||||
|
||||
return $tables;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -413,13 +455,51 @@ abstract class Schema implements PlaceholderInterface {
|
|||
* @code
|
||||
* $fields = ['foo', ['bar', 4]];
|
||||
* @endcode
|
||||
* @param array $spec
|
||||
* The table specification for the table to be altered. This is used in
|
||||
* order to be able to ensure that the index length is not too long.
|
||||
* This schema definition can usually be obtained through hook_schema(), or
|
||||
* in case the table was created by the Entity API, through the schema
|
||||
* handler listed in the entity class definition. For reference, see
|
||||
* SqlContentEntityStorageSchema::getDedicatedTableSchema() and
|
||||
* SqlContentEntityStorageSchema::getSharedTableFieldSchema().
|
||||
*
|
||||
* In order to prevent human error, it is recommended to pass in the
|
||||
* complete table specification. However, in the edge case of the complete
|
||||
* table specification not being available, we can pass in a partial table
|
||||
* definition containing only the fields that apply to the index:
|
||||
* @code
|
||||
* $spec = [
|
||||
* // Example partial specification for a table:
|
||||
* 'fields' => [
|
||||
* 'example_field' => [
|
||||
* 'description' => 'An example field',
|
||||
* 'type' => 'varchar',
|
||||
* 'length' => 32,
|
||||
* 'not null' => TRUE,
|
||||
* 'default' => '',
|
||||
* ],
|
||||
* ],
|
||||
* 'indexes' => [
|
||||
* 'table_example_field' => ['example_field'],
|
||||
* ],
|
||||
* ];
|
||||
* @endcode
|
||||
* Note that the above is a partial table definition and that we would
|
||||
* usually pass a complete table definition as obtained through
|
||||
* hook_schema() instead.
|
||||
*
|
||||
* @see schemaapi
|
||||
* @see hook_schema()
|
||||
*
|
||||
* @throws \Drupal\Core\Database\SchemaObjectDoesNotExistException
|
||||
* If the specified table doesn't exist.
|
||||
* @throws \Drupal\Core\Database\SchemaObjectExistsException
|
||||
* If the specified table already has an index by that name.
|
||||
*
|
||||
* @todo remove the $spec argument whenever schema introspection is added.
|
||||
*/
|
||||
abstract public function addIndex($table, $name, $fields);
|
||||
abstract public function addIndex($table, $name, $fields, array $spec);
|
||||
|
||||
/**
|
||||
* Drop an index.
|
||||
|
|
Reference in a new issue