diff --git a/core/lib/Drupal/Core/Command/DbDumpCommand.php b/core/lib/Drupal/Core/Command/DbDumpCommand.php index a9b4308d79dd8e236bbfd1510af2f87443db0c89..8f95455078d3c64f38e6f1a4be8c4be6117bed83 100644 --- a/core/lib/Drupal/Core/Command/DbDumpCommand.php +++ b/core/lib/Drupal/Core/Command/DbDumpCommand.php @@ -264,7 +264,10 @@ protected function getTableIndexes(Connection $connection, $table, &$definition) * The schema definition to modify. */ protected function getTableCollation(Connection $connection, $table, &$definition) { - $query = $connection->query("SHOW TABLE STATUS LIKE '{" . $table . "}'"); + // Remove identifier quotes from the table name. See + // \Drupal\Core\Database\Driver\mysql\Connection::identifierQuote(). + $table = trim($connection->prefixTables('{' . $table . '}'), '"'); + $query = $connection->query("SHOW TABLE STATUS WHERE NAME = :table_name", [':table_name' => $table]); $data = $query->fetchAssoc(); // Map the collation to a character set. For example, 'utf8mb4_general_ci' diff --git a/core/lib/Drupal/Core/Database/Connection.php b/core/lib/Drupal/Core/Database/Connection.php index 691a6ae84c4f20950328e47318ab5d978f6ee249..5b68cd9a8d5e9b6a89c8bbd39e0c8d3e968d9bad 100644 --- a/core/lib/Drupal/Core/Database/Connection.php +++ b/core/lib/Drupal/Core/Database/Connection.php @@ -144,9 +144,32 @@ abstract class Connection { * List of escaped database, table, and field names, keyed by unescaped names. * * @var array + * + * @deprecated in drupal:9.0.0 and is removed from drupal:10.0.0. This is no + * longer used. Use \Drupal\Core\Database\Connection::$escapedTables or + * \Drupal\Core\Database\Connection::$escapedFields instead. + * + * @see https://www.drupal.org/node/2986894 */ protected $escapedNames = []; + /** + * List of escaped table names, keyed by unescaped names. + * + * @var array + */ + protected $escapedTables = []; + + /** + * List of escaped field names, keyed by unescaped names. + * + * There are cases in which escapeField() is called on an empty string. In + * this case it should always return an empty string. + * + * @var array + */ + protected $escapedFields = ["" => ""]; + /** * List of escaped aliases names, keyed by unescaped aliases. * @@ -255,6 +278,11 @@ public function destroy() { * additional queries (such as inserting new user accounts). In rare cases, * such as creating an SQL function, a ; is needed and can be allowed by * changing this option to TRUE. + * - allow_square_brackets: By default, queries which contain square brackets + * will have them replaced with the identifier quote character for the + * database type. In rare cases, such as creating an SQL function, [] + * characters might be needed and can be allowed by changing this option to + * TRUE. * * @return array * An array of default query options. @@ -265,6 +293,7 @@ protected function defaultOptions() { 'return' => Database::RETURN_STATEMENT, 'throw_exception' => TRUE, 'allow_delimiter_in_query' => FALSE, + 'allow_square_brackets' => FALSE, ]; } @@ -299,6 +328,7 @@ protected function setPrefix($prefix) { $this->prefixes = ['default' => $prefix]; } + $identifier_quote = $this->identifierQuote(); // Set up variables for use in prefixTables(). Replace table-specific // prefixes first. $this->prefixSearch = []; @@ -306,14 +336,20 @@ protected function setPrefix($prefix) { foreach ($this->prefixes as $key => $val) { if ($key != 'default') { $this->prefixSearch[] = '{' . $key . '}'; - $this->prefixReplace[] = $val . $key; + // $val can point to another database like 'database.users'. In this + // instance we need to quote the identifiers correctly. + $val = str_replace('.', $identifier_quote . '.' . $identifier_quote, $val); + $this->prefixReplace[] = $identifier_quote . $val . $key . $identifier_quote; } } // Then replace remaining tables with the default prefix. $this->prefixSearch[] = '{'; - $this->prefixReplace[] = $this->prefixes['default']; + // $this->prefixes['default'] can point to another database like + // 'other_db.'. In this instance we need to quote the identifiers correctly. + // For example, "other_db"."PREFIX_table_name". + $this->prefixReplace[] = $identifier_quote . str_replace('.', $identifier_quote . '.' . $identifier_quote, $this->prefixes['default']); $this->prefixSearch[] = '}'; - $this->prefixReplace[] = ''; + $this->prefixReplace[] = $identifier_quote; // Set up a map of prefixed => un-prefixed tables. foreach ($this->prefixes as $table_name => $prefix) { @@ -323,6 +359,20 @@ protected function setPrefix($prefix) { } } + /** + * Returns the identifier quote character for the database type. + * + * The ANSI SQL standard identifier quote character is a double quotation + * mark. + * + * @return string + * The identifier quote character for the database type. + */ + protected function identifierQuote() { + @trigger_error('In drupal:10.0.0 this method will be abstract and contrib and custom drivers will have to implement it. See https://www.drupal.org/node/2986894', E_USER_DEPRECATED); + return ''; + } + /** * Appends a database prefix to all tables in a query. * @@ -341,6 +391,30 @@ public function prefixTables($sql) { return str_replace($this->prefixSearch, $this->prefixReplace, $sql); } + /** + * Quotes all identifiers in a query. + * + * Queries sent to Drupal should wrap all unquoted identifiers in square + * brackets. This function searches for this syntax and replaces them with the + * database specific identifier. In ANSI SQL this a double quote. + * + * Note that :variable[] is used to denote array arguments but + * Connection::expandArguments() is always called first. + * + * @param string $sql + * A string containing a partial or entire SQL query. + * + * @return string + * The string containing a partial or entire SQL query with all identifiers + * quoted. + * + * @internal + * This method should only be called by database API code. + */ + public function quoteIdentifiers($sql) { + return str_replace(['[', ']'], $this->identifierQuote(), $sql); + } + /** * Find the prefix for a table. * @@ -387,18 +461,25 @@ public function getFullQualifiedTableName($table) { /** * Prepares a query string and returns the prepared statement. * - * This method caches prepared statements, reusing them when - * possible. It also prefixes tables names enclosed in curly-braces. + * This method caches prepared statements, reusing them when possible. It also + * prefixes tables names enclosed in curly-braces and, optionally, quotes + * identifiers enclosed in square brackets. * * @param $query * The query string as SQL, with curly-braces surrounding the * table names. + * @param bool $quote_identifiers + * (optional) Quote any identifiers enclosed in square brackets. Defaults to + * TRUE. * * @return \Drupal\Core\Database\StatementInterface * A PDO prepared statement ready for its execute() method. */ - public function prepareQuery($query) { + public function prepareQuery($query, $quote_identifiers = TRUE) { $query = $this->prefixTables($query); + if ($quote_identifiers) { + $query = $this->quoteIdentifiers($query); + } return $this->connection->prepare($query); } @@ -494,7 +575,10 @@ public function getLogger() { * A table prefix-parsed string for the sequence name. */ public function makeSequenceName($table, $field) { - return $this->prefixTables('{' . $table . '}_' . $field . '_seq'); + $sequence_name = $this->prefixTables('{' . $table . '}_' . $field . '_seq'); + // Remove identifier quotes as we are constructing a new name from a + // prefixed and quoted table name. + return str_replace($this->identifierQuote(), '', $sequence_name); } /** @@ -628,7 +712,7 @@ public function query($query, array $args = [], $options = []) { if (strpos($query, ';') !== FALSE && empty($options['allow_delimiter_in_query'])) { throw new \InvalidArgumentException('; is not supported in SQL strings. Use only one statement at a time.'); } - $stmt = $this->prepareQuery($query); + $stmt = $this->prepareQuery($query, !$options['allow_square_brackets']); $stmt->execute($args, $options); } @@ -956,30 +1040,32 @@ public function schema() { * The sanitized database name. */ public function escapeDatabase($database) { - if (!isset($this->escapedNames[$database])) { - $this->escapedNames[$database] = preg_replace('/[^A-Za-z0-9_.]+/', '', $database); - } - return $this->escapedNames[$database]; + $database = preg_replace('/[^A-Za-z0-9_]+/', '', $database); + return $this->identifierQuote() . $database . $this->identifierQuote(); } /** * Escapes a table name string. * * Force all table names to be strictly alphanumeric-plus-underscore. - * For some database drivers, it may also wrap the table name in - * database-specific escape characters. + * Database drivers should never wrap the table name in database-specific + * escape characters. This is done in Connection::prefixTables(). The + * database-specific escape characters are added in Connection::setPrefix(). * * @param string $table * An unsanitized table name. * * @return string * The sanitized table name. + * + * @see \Drupal\Core\Database\Connection::prefixTables() + * @see \Drupal\Core\Database\Connection::setPrefix() */ public function escapeTable($table) { - if (!isset($this->escapedNames[$table])) { - $this->escapedNames[$table] = preg_replace('/[^A-Za-z0-9_.]+/', '', $table); + if (!isset($this->escapedTables[$table])) { + $this->escapedTables[$table] = preg_replace('/[^A-Za-z0-9_.]+/', '', $table); } - return $this->escapedNames[$table]; + return $this->escapedTables[$table]; } /** @@ -996,10 +1082,14 @@ public function escapeTable($table) { * The sanitized field name. */ public function escapeField($field) { - if (!isset($this->escapedNames[$field])) { - $this->escapedNames[$field] = preg_replace('/[^A-Za-z0-9_.]+/', '', $field); + if (!isset($this->escapedFields[$field])) { + $escaped = preg_replace('/[^A-Za-z0-9_.]+/', '', $field); + $identifier_quote = $this->identifierQuote(); + // Sometimes fields have the format table_alias.field. In such cases + // both identifiers should be quoted, for example, "table_alias"."field". + $this->escapedFields[$field] = $identifier_quote . str_replace('.', $identifier_quote . '.' . $identifier_quote, $escaped) . $identifier_quote; } - return $this->escapedNames[$field]; + return $this->escapedFields[$field]; } /** @@ -1018,7 +1108,7 @@ public function escapeField($field) { */ public function escapeAlias($field) { if (!isset($this->escapedAliases[$field])) { - $this->escapedAliases[$field] = preg_replace('/[^A-Za-z0-9_]+/', '', $field); + $this->escapedAliases[$field] = $this->identifierQuote() . preg_replace('/[^A-Za-z0-9_]+/', '', $field) . $this->identifierQuote(); } return $this->escapedAliases[$field]; } diff --git a/core/lib/Drupal/Core/Database/Driver/mysql/Connection.php b/core/lib/Drupal/Core/Database/Driver/mysql/Connection.php index 86e2f5725553165bacb960513fc863c5946937a4..c6d252ee09239fbb5b004df501e011f703487ca6 100644 --- a/core/lib/Drupal/Core/Database/Driver/mysql/Connection.php +++ b/core/lib/Drupal/Core/Database/Driver/mysql/Connection.php @@ -63,277 +63,6 @@ class Connection extends DatabaseConnection { */ const MIN_MAX_ALLOWED_PACKET = 1024; - /** - * The list of MySQL reserved key words. - * - * @link https://dev.mysql.com/doc/refman/8.0/en/keywords.html - */ - private $reservedKeyWords = [ - 'accessible', - 'add', - 'admin', - 'all', - 'alter', - 'analyze', - 'and', - 'as', - 'asc', - 'asensitive', - 'before', - 'between', - 'bigint', - 'binary', - 'blob', - 'both', - 'by', - 'call', - 'cascade', - 'case', - 'change', - 'char', - 'character', - 'check', - 'collate', - 'column', - 'condition', - 'constraint', - 'continue', - 'convert', - 'create', - 'cross', - 'cube', - 'cume_dist', - 'current_date', - 'current_time', - 'current_timestamp', - 'current_user', - 'cursor', - 'database', - 'databases', - 'day_hour', - 'day_microsecond', - 'day_minute', - 'day_second', - 'dec', - 'decimal', - 'declare', - 'default', - 'delayed', - 'delete', - 'dense_rank', - 'desc', - 'describe', - 'deterministic', - 'distinct', - 'distinctrow', - 'div', - 'double', - 'drop', - 'dual', - 'each', - 'else', - 'elseif', - 'empty', - 'enclosed', - 'escaped', - 'except', - 'exists', - 'exit', - 'explain', - 'false', - 'fetch', - 'first_value', - 'float', - 'float4', - 'float8', - 'for', - 'force', - 'foreign', - 'from', - 'fulltext', - 'function', - 'generated', - 'get', - 'grant', - 'group', - 'grouping', - 'groups', - 'having', - 'high_priority', - 'hour_microsecond', - 'hour_minute', - 'hour_second', - 'if', - 'ignore', - 'in', - 'index', - 'infile', - 'inner', - 'inout', - 'insensitive', - 'insert', - 'int', - 'int1', - 'int2', - 'int3', - 'int4', - 'int8', - 'integer', - 'interval', - 'into', - 'io_after_gtids', - 'io_before_gtids', - 'is', - 'iterate', - 'join', - 'json_table', - 'key', - 'keys', - 'kill', - 'lag', - 'last_value', - 'lead', - 'leading', - 'leave', - 'left', - 'like', - 'limit', - 'linear', - 'lines', - 'load', - 'localtime', - 'localtimestamp', - 'lock', - 'long', - 'longblob', - 'longtext', - 'loop', - 'low_priority', - 'master_bind', - 'master_ssl_verify_server_cert', - 'match', - 'maxvalue', - 'mediumblob', - 'mediumint', - 'mediumtext', - 'middleint', - 'minute_microsecond', - 'minute_second', - 'mod', - 'modifies', - 'natural', - 'not', - 'no_write_to_binlog', - 'nth_value', - 'ntile', - 'null', - 'numeric', - 'of', - 'on', - 'optimize', - 'optimizer_costs', - 'option', - 'optionally', - 'or', - 'order', - 'out', - 'outer', - 'outfile', - 'over', - 'partition', - 'percent_rank', - 'persist', - 'persist_only', - 'precision', - 'primary', - 'procedure', - 'purge', - 'range', - 'rank', - 'read', - 'reads', - 'read_write', - 'real', - 'recursive', - 'references', - 'regexp', - 'release', - 'rename', - 'repeat', - 'replace', - 'require', - 'resignal', - 'restrict', - 'return', - 'revoke', - 'right', - 'rlike', - 'row', - 'rows', - 'row_number', - 'schema', - 'schemas', - 'second_microsecond', - 'select', - 'sensitive', - 'separator', - 'set', - 'show', - 'signal', - 'smallint', - 'spatial', - 'specific', - 'sql', - 'sqlexception', - 'sqlstate', - 'sqlwarning', - 'sql_big_result', - 'sql_calc_found_rows', - 'sql_small_result', - 'ssl', - 'starting', - 'stored', - 'straight_join', - 'system', - 'table', - 'terminated', - 'then', - 'tinyblob', - 'tinyint', - 'tinytext', - 'to', - 'trailing', - 'trigger', - 'true', - 'undo', - 'union', - 'unique', - 'unlock', - 'unsigned', - 'update', - 'usage', - 'use', - 'using', - 'utc_date', - 'utc_time', - 'utc_timestamp', - 'values', - 'varbinary', - 'varchar', - 'varcharacter', - 'varying', - 'virtual', - 'when', - 'where', - 'while', - 'window', - 'with', - 'write', - 'xor', - 'year_month', - 'zerofill', - ]; - /** * Constructs a Connection object. */ @@ -467,49 +196,6 @@ public static function open(array &$connection_options = []) { return $pdo; } - /** - * {@inheritdoc} - */ - public function escapeField($field) { - $field = parent::escapeField($field); - return $this->quoteIdentifier($field); - } - - /** - * {@inheritdoc} - */ - public function escapeAlias($field) { - // Quote fields so that MySQL reserved words like 'function' can be used - // as aliases. - $field = parent::escapeAlias($field); - return $this->quoteIdentifier($field); - } - - /** - * Quotes an identifier if it matches a MySQL reserved keyword. - * - * @param string $identifier - * The field to check. - * - * @return string - * The identifier, quoted if it matches a MySQL reserved keyword. - */ - private function quoteIdentifier($identifier) { - // Quote identifiers so that MySQL reserved words like 'function' can be - // used as column names. Sometimes the 'table.column_name' format is passed - // in. For example, - // \Drupal\Core\Entity\Sql\SqlContentEntityStorage::buildQuery() adds a - // condition on "base.uid" while loading user entities. - if (strpos($identifier, '.') !== FALSE) { - list($table, $identifier) = explode('.', $identifier, 2); - } - if (in_array(strtolower($identifier), $this->reservedKeyWords, TRUE)) { - // Quote the string for MySQL reserved keywords. - $identifier = '"' . $identifier . '"'; - } - return isset($table) ? $table . '.' . $identifier : $identifier; - } - /** * {@inheritdoc} */ @@ -542,6 +228,15 @@ public function queryTemporary($query, array $args = [], array $options = []) { return $tablename; } + /** + * {@inheritdoc} + */ + protected function identifierQuote() { + // The database is using the ANSI option on set up so use ANSI quotes and + // not MySQL's custom backtick quote. + return '"'; + } + public function driver() { return 'mysql'; } diff --git a/core/lib/Drupal/Core/Database/Driver/mysql/Insert.php b/core/lib/Drupal/Core/Database/Driver/mysql/Insert.php index 3d397c527545c88b5eed4c4f7dd9537a2f13f764..c65642aed0df0f38afcb04d54357aa12de6bbaaf 100644 --- a/core/lib/Drupal/Core/Database/Driver/mysql/Insert.php +++ b/core/lib/Drupal/Core/Database/Driver/mysql/Insert.php @@ -43,7 +43,6 @@ public function __toString() { // 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); diff --git a/core/lib/Drupal/Core/Database/Driver/pgsql/Connection.php b/core/lib/Drupal/Core/Database/Driver/pgsql/Connection.php index 025b25039d9721da789748490cdca19d64d4fb87..ba781c3aa87fd5c3e9e7a95c27b5163265a5d92a 100644 --- a/core/lib/Drupal/Core/Database/Driver/pgsql/Connection.php +++ b/core/lib/Drupal/Core/Database/Driver/pgsql/Connection.php @@ -49,28 +49,6 @@ class Connection extends DatabaseConnection { 'NOT REGEXP' => ['operator' => '!~*'], ]; - /** - * The list of PostgreSQL reserved key words. - * - * @see http://www.postgresql.org/docs/9.4/static/sql-keywords-appendix.html - */ - protected $postgresqlReservedKeyWords = ['all', 'analyse', 'analyze', 'and', - 'any', 'array', 'as', 'asc', 'asymmetric', 'authorization', 'binary', 'both', - 'case', 'cast', 'check', 'collate', 'collation', 'column', 'concurrently', - 'constraint', 'create', 'cross', 'current_catalog', 'current_date', - 'current_role', 'current_schema', 'current_time', 'current_timestamp', - 'current_user', 'default', 'deferrable', 'desc', 'distinct', 'do', 'else', - 'end', 'except', 'false', 'fetch', 'for', 'foreign', 'freeze', 'from', 'full', - 'grant', 'group', 'having', 'ilike', 'in', 'initially', 'inner', 'intersect', - 'into', 'is', 'isnull', 'join', 'lateral', 'leading', 'left', 'like', 'limit', - 'localtime', 'localtimestamp', 'natural', 'not', 'notnull', 'null', 'offset', - 'on', 'only', 'or', 'order', 'outer', 'over', 'overlaps', 'placing', - 'primary', 'references', 'returning', 'right', 'select', 'session_user', - 'similar', 'some', 'symmetric', 'table', 'tablesample', 'then', 'to', - 'trailing', 'true', 'union', 'unique', 'user', 'using', 'variadic', 'verbose', - 'when', 'where', 'window', 'with', - ]; - /** * Constructs a connection object. */ @@ -205,12 +183,12 @@ public function query($query, array $args = [], $options = []) { return $return; } - public function prepareQuery($query) { + public function prepareQuery($query, $quote_identifiers = TRUE) { // mapConditionOperator converts some operations (LIKE, REGEXP, etc.) to // PostgreSQL equivalents (ILIKE, ~*, etc.). However PostgreSQL doesn't // automatically cast the fields to the right type for these operators, // so we need to alter the query and add the type-cast. - return parent::prepareQuery(preg_replace('/ ([^ ]+) +(I*LIKE|NOT +I*LIKE|~\*|!~\*) /i', ' ${1}::text ${2} ', $query)); + return parent::prepareQuery(preg_replace('/ ([^ ]+) +(I*LIKE|NOT +I*LIKE|~\*|!~\*) /i', ' ${1}::text ${2} ', $query), $quote_identifiers); } public function queryRange($query, $from, $count, array $args = [], array $options = []) { @@ -226,74 +204,8 @@ public function queryTemporary($query, array $args = [], array $options = []) { /** * {@inheritdoc} */ - public function escapeField($field) { - $escaped = parent::escapeField($field); - - // Remove any invalid start character. - $escaped = preg_replace('/^[^A-Za-z0-9_]/', '', $escaped); - - // The pgsql database driver does not support field names that contain - // periods (supported by PostgreSQL server) because this method may be - // called by a field with a table alias as part of SQL conditions or - // order by statements. This will consider a period as a table alias - // identifier, and split the string at the first period. - if (preg_match('/^([A-Za-z0-9_]+)"?[.]"?([A-Za-z0-9_.]+)/', $escaped, $parts)) { - $table = $parts[1]; - $column = $parts[2]; - - // Use escape alias because escapeField may contain multiple periods that - // need to be escaped. - $escaped = $this->escapeTable($table) . '.' . $this->escapeAlias($column); - } - else { - $escaped = $this->doEscape($escaped); - } - - return $escaped; - } - - /** - * {@inheritdoc} - */ - public function escapeAlias($field) { - $escaped = preg_replace('/[^A-Za-z0-9_]+/', '', $field); - $escaped = $this->doEscape($escaped); - return $escaped; - } - - /** - * {@inheritdoc} - */ - public function escapeTable($table) { - $escaped = parent::escapeTable($table); - - // Ensure that each part (database, schema and table) of the table name is - // properly and independently escaped. - $parts = explode('.', $escaped); - $parts = array_map([$this, 'doEscape'], $parts); - $escaped = implode('.', $parts); - - return $escaped; - } - - /** - * Escape a string if needed. - * - * @param $string - * The string to escape. - * @return string - * The escaped string. - */ - protected function doEscape($string) { - // Quote identifier to make it case-sensitive. - if (preg_match('/[A-Z]/', $string)) { - $string = '"' . $string . '"'; - } - elseif (in_array(strtolower($string), $this->postgresqlReservedKeyWords)) { - // Quote the string for PostgreSQL reserved key words. - $string = '"' . $string . '"'; - } - return $string; + protected function identifierQuote() { + return '"'; } public function driver() { diff --git a/core/lib/Drupal/Core/Database/Driver/pgsql/Install/Tasks.php b/core/lib/Drupal/Core/Database/Driver/pgsql/Install/Tasks.php index 056105e25df593a8fff448beb37aa20687baf54f..6e184aea2fabf53c1648587c6604227939c1c808 100644 --- a/core/lib/Drupal/Core/Database/Driver/pgsql/Install/Tasks.php +++ b/core/lib/Drupal/Core/Database/Driver/pgsql/Install/Tasks.php @@ -268,7 +268,7 @@ public function initializeDatabase() { \'SELECT array_to_string((string_to_array($1, $2)) [1:$3], $2);\' LANGUAGE \'sql\'', [], - ['allow_delimiter_in_query' => TRUE] + ['allow_delimiter_in_query' => TRUE, 'allow_square_brackets' => TRUE] ); } $connection->query('SELECT pg_advisory_unlock(1)'); diff --git a/core/lib/Drupal/Core/Database/Driver/pgsql/Schema.php b/core/lib/Drupal/Core/Database/Driver/pgsql/Schema.php index fd4579b62fafb9d01d3d68ec768b92d65cdf92ac..50398566ee7644ce612fe8b1818869c03b8209ae 100644 --- a/core/lib/Drupal/Core/Database/Driver/pgsql/Schema.php +++ b/core/lib/Drupal/Core/Database/Driver/pgsql/Schema.php @@ -550,7 +550,7 @@ public function renameTable($table, $new_name) { } // Get the schema and tablename for the old table. - $old_full_name = $this->connection->prefixTables('{' . $table . '}'); + $old_full_name = str_replace('"', '', $this->connection->prefixTables('{' . $table . '}')); list($old_schema, $old_table_name) = strpos($old_full_name, '.') ? explode('.', $old_full_name) : ['public', $old_full_name]; // Index names and constraint names are global in PostgreSQL, so we need to @@ -866,8 +866,10 @@ protected function introspectIndexSchema($table) { 'indexes' => [], ]; + // Get the schema and tablename for the table without identifier quotes. + $full_name = str_replace('"', '', $this->connection->prefixTables('{' . $table . '}')); $result = $this->connection->query("SELECT i.relname AS index_name, a.attname AS column_name FROM pg_class t, pg_class i, pg_index ix, pg_attribute a WHERE t.oid = ix.indrelid AND i.oid = ix.indexrelid AND a.attrelid = t.oid AND a.attnum = ANY(ix.indkey) AND t.relkind = 'r' AND t.relname = :table_name ORDER BY index_name ASC, column_name ASC", [ - ':table_name' => $this->connection->prefixTables('{' . $table . '}'), + ':table_name' => $full_name, ])->fetchAll(); foreach ($result as $row) { if (preg_match('/_pkey$/', $row->index_name)) { diff --git a/core/lib/Drupal/Core/Database/Driver/sqlite/Connection.php b/core/lib/Drupal/Core/Database/Driver/sqlite/Connection.php index fa382d3f572b0afeac087b165b5cea3d89252606..1fc90fe537ee3ad6f27543ed7bac24b31529a144 100644 --- a/core/lib/Drupal/Core/Database/Driver/sqlite/Connection.php +++ b/core/lib/Drupal/Core/Database/Driver/sqlite/Connection.php @@ -375,6 +375,13 @@ public function databaseType() { return 'sqlite'; } + /** + * {@inheritdoc} + */ + protected function identifierQuote() { + return '"'; + } + /** * Overrides \Drupal\Core\Database\Connection::createDatabase(). * @@ -398,8 +405,12 @@ public function mapConditionOperator($operator) { /** * {@inheritdoc} */ - public function prepareQuery($query) { - return $this->prepare($this->prefixTables($query)); + public function prepareQuery($query, $quote_identifiers = TRUE) { + $query = $this->prefixTables($query); + if ($quote_identifiers) { + $query = $this->quoteIdentifiers($query); + } + return $this->prepare($query); } public function nextId($existing_id = 0) { diff --git a/core/lib/Drupal/Core/Database/Driver/sqlite/Insert.php b/core/lib/Drupal/Core/Database/Driver/sqlite/Insert.php index 0c4fdb668b91488d45bd57ff64391b35747538be..4273dd6536fe3faa972d96f95d6a8a6e8ee391a3 100644 --- a/core/lib/Drupal/Core/Database/Driver/sqlite/Insert.php +++ b/core/lib/Drupal/Core/Database/Driver/sqlite/Insert.php @@ -35,14 +35,18 @@ public function __toString() { $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 = $this->insertFields ? ' (' . implode(', ', $this->insertFields) . ') ' : ' '; + $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(', ', $this->insertFields) . ') VALUES (' . implode(', ', $placeholders) . ')'; + return $comments . 'INSERT INTO {' . $this->table . '} (' . implode(', ', $insert_fields) . ') VALUES (' . implode(', ', $placeholders) . ')'; } } diff --git a/core/lib/Drupal/Core/Database/Driver/sqlite/Schema.php b/core/lib/Drupal/Core/Database/Driver/sqlite/Schema.php index 088bff2cf2956380d2f982f1e2abd5ada0c85c47..2286aedbba92a6d5dd8d0b11b9465e4f246b6dc6 100644 --- a/core/lib/Drupal/Core/Database/Driver/sqlite/Schema.php +++ b/core/lib/Drupal/Core/Database/Driver/sqlite/Schema.php @@ -165,6 +165,7 @@ protected function processField($field) { * 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'])) { diff --git a/core/lib/Drupal/Core/Database/Driver/sqlite/Upsert.php b/core/lib/Drupal/Core/Database/Driver/sqlite/Upsert.php index fc59afde252d44658670f0453aff65178ad4ddb2..ed9a0215335e35733aabe3411b218ee67f6941ce 100644 --- a/core/lib/Drupal/Core/Database/Driver/sqlite/Upsert.php +++ b/core/lib/Drupal/Core/Database/Driver/sqlite/Upsert.php @@ -18,6 +18,9 @@ public function __toString() { // 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 OR REPLACE INTO {' . $this->table . '} (' . implode(', ', $insert_fields) . ') VALUES '; diff --git a/core/lib/Drupal/Core/Database/Query/Select.php b/core/lib/Drupal/Core/Database/Query/Select.php index 27892ce5cb29da385c02b5eb3fe085278723c429..fb74d9f1c13eedaed115286508092d0a49476ec4 100644 --- a/core/lib/Drupal/Core/Database/Query/Select.php +++ b/core/lib/Drupal/Core/Database/Query/Select.php @@ -813,7 +813,8 @@ public function __toString() { foreach ($this->fields as $field) { // Always use the AS keyword for field aliases, as some // databases require it (e.g., PostgreSQL). - $fields[] = (isset($field['table']) ? $this->connection->escapeTable($field['table']) . '.' : '') . $this->connection->escapeField($field['field']) . ' AS ' . $this->connection->escapeAlias($field['alias']); + $table = isset($field['table']) ? $field['table'] . '.' : ''; + $fields[] = $this->connection->escapeField($table . $field['field']) . ' AS ' . $this->connection->escapeAlias($field['alias']); } foreach ($this->expressions as $expression) { $fields[] = $expression['expression'] . ' AS ' . $this->connection->escapeAlias($expression['alias']); @@ -845,7 +846,7 @@ public function __toString() { // Don't use the AS keyword for table aliases, as some // databases don't support it (e.g., Oracle). - $query .= $table_string . ' ' . $this->connection->escapeTable($table['alias']); + $query .= $table_string . ' ' . $this->connection->escapeAlias($table['alias']); if (!empty($table['condition'])) { $query .= ' ON ' . (string) $table['condition']; diff --git a/core/lib/Drupal/Core/Entity/Query/Sql/Condition.php b/core/lib/Drupal/Core/Entity/Query/Sql/Condition.php index 7f9351214d1e1474d146d4fe61007a326a12c105..02bcb34003cbdf4b31219201920c246d9f9c6d92 100644 --- a/core/lib/Drupal/Core/Entity/Query/Sql/Condition.php +++ b/core/lib/Drupal/Core/Entity/Query/Sql/Condition.php @@ -50,6 +50,16 @@ public function compile($conditionContainer) { else { $type = $this->nestedInsideOrCondition || strtoupper($this->conjunction) === 'OR' || $condition['operator'] === 'IS NULL' ? 'LEFT' : 'INNER'; $field = $tables->addField($condition['field'], $type, $condition['langcode']); + // If the field is trying to query on %delta for a single value field + // then the only supported delta is 0. No other value than 0 makes + // sense. \Drupal\Core\Entity\Query\Sql\Tables::addField() returns 0 as + // the field name for single value fields when querying on their %delta. + if ($field === 0) { + if ($condition['value'] != 0) { + $conditionContainer->alwaysFalse(); + } + continue; + } $condition['real_field'] = $field; static::translateCondition($condition, $sql_query, $tables->isFieldCaseSensitive($condition['field'])); diff --git a/core/lib/Drupal/Core/Entity/Query/Sql/Query.php b/core/lib/Drupal/Core/Entity/Query/Sql/Query.php index daf6bd62c006823c24567c5d73cf2dae8bc0a5e2..de25f69358de6c569047a762b8c9acbec39a5098 100644 --- a/core/lib/Drupal/Core/Entity/Query/Sql/Query.php +++ b/core/lib/Drupal/Core/Entity/Query/Sql/Query.php @@ -132,7 +132,7 @@ protected function prepare() { // Add a self-join to the base revision table if we're querying only the // latest revisions. if ($this->latestRevision && $revision_field) { - $this->sqlQuery->leftJoin($base_table, 'base_table_2', "base_table.$id_field = base_table_2.$id_field AND base_table.$revision_field < base_table_2.$revision_field"); + $this->sqlQuery->leftJoin($base_table, 'base_table_2', "[base_table].[$id_field] = [base_table_2].[$id_field] AND [base_table].[$revision_field] < [base_table_2].[$revision_field]"); $this->sqlQuery->isNull("base_table_2.$id_field"); } @@ -346,6 +346,7 @@ public function __toString() { // Replace table name brackets. $sql = $clone->connection->prefixTables((string) $clone->sqlQuery); + $sql = $clone->connection->quoteIdentifiers($sql); return strtr($sql, $quoted); } diff --git a/core/lib/Drupal/Core/Entity/Query/Sql/Tables.php b/core/lib/Drupal/Core/Entity/Query/Sql/Tables.php index 99cab9f1d0c68273556fab4f49ecb0122e967d55..106a7c0b455d2957753e2be313227e8752b31121 100644 --- a/core/lib/Drupal/Core/Entity/Query/Sql/Tables.php +++ b/core/lib/Drupal/Core/Entity/Query/Sql/Tables.php @@ -351,7 +351,7 @@ protected function ensureEntityTable($index_prefix, $property, $type, $langcode, // each join gets a separate alias. $key = $index_prefix . ($base_table === 'base_table' ? $table : $base_table); if (!isset($this->entityTables[$key])) { - $this->entityTables[$key] = $this->addJoin($type, $table, "%alias.$id_field = $base_table.$id_field", $langcode); + $this->entityTables[$key] = $this->addJoin($type, $table, "[%alias].[$id_field] = [$base_table].[$id_field]", $langcode); } return $this->entityTables[$key]; } @@ -377,7 +377,7 @@ protected function ensureFieldTable($index_prefix, &$field, $type, $langcode, $b if ($field->getCardinality() != 1) { $this->sqlQuery->addMetaData('simple_query', FALSE); } - $this->fieldTables[$index_prefix . $field_name] = $this->addJoin($type, $table, "%alias.$field_id_field = $base_table.$entity_id_field", $langcode, $delta); + $this->fieldTables[$index_prefix . $field_name] = $this->addJoin($type, $table, "[%alias].[$field_id_field] = [$base_table].[$entity_id_field]", $langcode, $delta); } return $this->fieldTables[$index_prefix . $field_name]; } @@ -408,12 +408,12 @@ protected function addJoin($type, $table, $join_condition, $langcode, $delta = N // tables have an hard-coded 'langcode' column. $langcode_key = $entity_type->getDataTable() == $table ? $entity_type->getKey('langcode') : 'langcode'; $placeholder = ':langcode' . $this->sqlQuery->nextPlaceholder(); - $join_condition .= ' AND %alias.' . $langcode_key . ' = ' . $placeholder; + $join_condition .= ' AND [%alias].[' . $langcode_key . '] = ' . $placeholder; $arguments[$placeholder] = $langcode; } if (isset($delta)) { $placeholder = ':delta' . $this->sqlQuery->nextPlaceholder(); - $join_condition .= ' AND %alias.delta = ' . $placeholder; + $join_condition .= ' AND [%alias].[delta] = ' . $placeholder; $arguments[$placeholder] = $delta; } return $this->sqlQuery->addJoin($type, $table, NULL, $join_condition, $arguments); diff --git a/core/modules/system/tests/modules/database_test/database_test.install b/core/modules/system/tests/modules/database_test/database_test.install index e8094b2b2194a939683a0779ee60efdff92a20d2..a2b4da4a17db34795637eedee3b5680f9ad7261c 100644 --- a/core/modules/system/tests/modules/database_test/database_test.install +++ b/core/modules/system/tests/modules/database_test/database_test.install @@ -313,20 +313,18 @@ function database_test_schema() { 'primary key' => ['name', 'age'], ]; - $schema['test_special_columns'] = [ - 'description' => 'A simple test table with special column names.', + // Hopefully no-one will ever name a table 'select' but this example is a + // reserved keyword in all supported SQL databases so it is a good test. + $schema['select'] = [ + 'description' => 'A test table with an ANSI reserved keyword as its name and one of its column names.', 'fields' => [ 'id' => [ 'description' => 'Simple unique ID.', 'type' => 'int', 'not null' => TRUE, ], - 'offset' => [ - 'description' => 'A column with preserved name.', - 'type' => 'text', - ], - 'function' => [ - 'description' => 'A column with reserved name in MySQL 8.', + 'update' => [ + 'description' => 'A column with reserved name.', 'type' => 'text', ], ], diff --git a/core/modules/views_ui/tests/src/Functional/PreviewTest.php b/core/modules/views_ui/tests/src/Functional/PreviewTest.php index f254097f1f3f7748c9f818184b18495e5f96c993..b913d7b758081cc777b88c4e32d878cddb5f9662 100644 --- a/core/modules/views_ui/tests/src/Functional/PreviewTest.php +++ b/core/modules/views_ui/tests/src/Functional/PreviewTest.php @@ -108,9 +108,9 @@ public function testPreviewUI() { $this->assertText(t('View render time')); $this->assertRaw('<strong>Query</strong>'); $query_string = <<<SQL -SELECT views_test_data.name AS views_test_data_name +SELECT "views_test_data"."name" AS "views_test_data_name" FROM -{views_test_data} views_test_data +{views_test_data} "views_test_data" WHERE (views_test_data.id = '100') SQL; $this->assertEscaped($query_string); diff --git a/core/modules/views_ui/tests/src/Functional/SettingsTest.php b/core/modules/views_ui/tests/src/Functional/SettingsTest.php index d5ce503523f17e273e3f910f2b6e16df83557877..949e0811e9902bc4da862813c6232a6fe5c890e5 100644 --- a/core/modules/views_ui/tests/src/Functional/SettingsTest.php +++ b/core/modules/views_ui/tests/src/Functional/SettingsTest.php @@ -2,6 +2,8 @@ namespace Drupal\Tests\views_ui\Functional; +use Drupal\Core\Database\Database; + /** * Tests all ui related settings under admin/structure/views/settings. * @@ -122,7 +124,7 @@ public function testEditUI() { $xpath = $this->xpath('//div[@class="views-query-info"]//pre'); $this->assertEqual(count($xpath), 1, 'The views sql is shown.'); $this->assertFalse(strpos($xpath[0]->getText(), 'db_condition_placeholder') !== FALSE, 'No placeholders are shown in the views sql.'); - $this->assertTrue(strpos($xpath[0]->getText(), "node_field_data.status = '1'") !== FALSE, 'The placeholders in the views sql is replace by the actual value.'); + $this->assertTrue(strpos($xpath[0]->getText(), Database::getConnection()->escapeField("node_field_data.status") . " = '1'") !== FALSE, 'The placeholders in the views sql is replace by the actual value.'); // Test the advanced settings form. diff --git a/core/modules/workspaces/src/EntityQuery/Tables.php b/core/modules/workspaces/src/EntityQuery/Tables.php index b791b92e2e8c929767e300c8fdcf2d9b6d2ae681..5c0c61b07d7ea5d715f0bf505146b622e24ed627 100644 --- a/core/modules/workspaces/src/EntityQuery/Tables.php +++ b/core/modules/workspaces/src/EntityQuery/Tables.php @@ -93,7 +93,8 @@ protected function addJoin($type, $table, $join_condition, $langcode, $delta = N // If those two conditions are met, we have to update the join condition // to also look for a possible workspace-specific revision using COALESCE. $condition_parts = explode(' = ', $join_condition); - list($base_table, $id_field) = explode('.', $condition_parts[1]); + $condition_parts_1 = str_replace(['[', ']'], '', $condition_parts[1]); + list($base_table, $id_field) = explode('.', $condition_parts_1); if (isset($this->baseTablesEntityType[$base_table])) { $entity_type_id = $this->baseTablesEntityType[$base_table]; diff --git a/core/tests/Drupal/KernelTests/Core/Database/BasicSyntaxTest.php b/core/tests/Drupal/KernelTests/Core/Database/BasicSyntaxTest.php index dac837433bf0ca95d94d7a870a516a2ac04c447d..e005283c9017e3a9a0ff10aa78b28104df51ff55 100644 --- a/core/tests/Drupal/KernelTests/Core/Database/BasicSyntaxTest.php +++ b/core/tests/Drupal/KernelTests/Core/Database/BasicSyntaxTest.php @@ -141,4 +141,33 @@ public function testGetFullQualifiedTableName() { $this->assertIdentical($num_matches, '4', 'Found 4 records.'); } + /** + * Tests allowing square brackets in queries. + * + * @see \Drupal\Core\Database\Connection::prepareQuery() + */ + public function testAllowSquareBrackets() { + $this->connection->insert('test') + ->fields(['name']) + ->values([ + 'name' => '[square]', + ]) + ->execute(); + + // Note that this is a very bad example query because arguments should be + // passed in via the $args parameter. + $result = $this->connection->query("select name from {test} where name = '[square]'", [], ['allow_square_brackets' => TRUE]); + $this->assertIdentical('[square]', $result->fetchField()); + + // Test that allow_square_brackets has no effect on arguments. + $result = $this->connection->query("select name from {test} where name = :value", [':value' => '[square]']); + $this->assertIdentical('[square]', $result->fetchField()); + $result = $this->connection->query("select name from {test} where name = :value", [':value' => '[square]'], ['allow_square_brackets' => TRUE]); + $this->assertIdentical('[square]', $result->fetchField()); + + // Test square brackets using the query builder. + $result = $this->connection->select('test')->fields('test', ['name'])->condition('name', '[square]')->execute(); + $this->assertIdentical('[square]', $result->fetchField()); + } + } diff --git a/core/tests/Drupal/KernelTests/Core/Database/ConnectionTest.php b/core/tests/Drupal/KernelTests/Core/Database/ConnectionTest.php index c9ab16a551addb3fdbd185472a91ef46d94bb435..7e0a5f89ac35674337d5fd18713d219b92d3ee5d 100644 --- a/core/tests/Drupal/KernelTests/Core/Database/ConnectionTest.php +++ b/core/tests/Drupal/KernelTests/Core/Database/ConnectionTest.php @@ -2,7 +2,6 @@ namespace Drupal\KernelTests\Core\Database; -use Drupal\Component\Render\FormattableMarkup; use Drupal\Core\Database\Database; use Drupal\Core\Database\DatabaseExceptionWrapper; @@ -156,23 +155,4 @@ public function testMultipleStatements() { } } - /** - * Test the escapeTable(), escapeField() and escapeAlias() methods with all possible reserved words in PostgreSQL. - */ - public function testPostgresqlReservedWords() { - if (Database::getConnection()->databaseType() !== 'pgsql') { - $this->markTestSkipped("This test only runs for PostgreSQL"); - } - - $db = Database::getConnection('default', 'default'); - $stmt = $db->query("SELECT word FROM pg_get_keywords() WHERE catcode IN ('R', 'T')"); - $stmt->execute(); - foreach ($stmt->fetchAllAssoc('word') as $word => $row) { - $expected = '"' . $word . '"'; - $this->assertIdentical($db->escapeTable($word), $expected, new FormattableMarkup('The reserved word %word was correctly escaped when used as a table name.', ['%word' => $word])); - $this->assertIdentical($db->escapeField($word), $expected, new FormattableMarkup('The reserved word %word was correctly escaped when used as a column name.', ['%word' => $word])); - $this->assertIdentical($db->escapeAlias($word), $expected, new FormattableMarkup('The reserved word %word was correctly escaped when used as an alias.', ['%word' => $word])); - } - } - } diff --git a/core/tests/Drupal/KernelTests/Core/Database/DatabaseTestBase.php b/core/tests/Drupal/KernelTests/Core/Database/DatabaseTestBase.php index ae468c686a9bb3b02c4400d28e6556c059a182ee..dc1151297072c6e6c64a3a1ad35a621e0184334a 100644 --- a/core/tests/Drupal/KernelTests/Core/Database/DatabaseTestBase.php +++ b/core/tests/Drupal/KernelTests/Core/Database/DatabaseTestBase.php @@ -35,8 +35,8 @@ protected function setUp() { 'test_task', 'test_null', 'test_serialized', - 'test_special_columns', 'TEST_UPPERCASE', + 'select', ]); self::addSampleData(); } @@ -157,11 +157,10 @@ public static function addSampleData() { ]) ->execute(); - $connection->insert('test_special_columns') + $connection->insert('select') ->fields([ 'id' => 1, - 'offset' => 'Offset value 1', - 'function' => 'Function value 1', + 'update' => 'Update value 1', ]) ->execute(); } diff --git a/core/tests/Drupal/KernelTests/Core/Database/DeleteTruncateTest.php b/core/tests/Drupal/KernelTests/Core/Database/DeleteTruncateTest.php index b4aa0a9b14dbd3e54d1b251de6fbceba82efdff8..4ca894b980de23258968bbc8497b03b046aa97cd 100644 --- a/core/tests/Drupal/KernelTests/Core/Database/DeleteTruncateTest.php +++ b/core/tests/Drupal/KernelTests/Core/Database/DeleteTruncateTest.php @@ -148,15 +148,15 @@ public function testTruncateTransactionRollback() { * Confirms that we can delete a single special column name record successfully. */ public function testSpecialColumnDelete() { - $num_records_before = $this->connection->query('SELECT COUNT(*) FROM {test_special_columns}')->fetchField(); + $num_records_before = $this->connection->query('SELECT COUNT(*) FROM {select}')->fetchField(); - $num_deleted = $this->connection->delete('test_special_columns') - ->condition('id', 1) + $num_deleted = $this->connection->delete('select') + ->condition('update', 'Update value 1') ->execute(); - $this->assertIdentical($num_deleted, 1, 'Deleted 1 special column record.'); + $this->assertEquals(1, $num_deleted, 'Deleted 1 special column record.'); - $num_records_after = $this->connection->query('SELECT COUNT(*) FROM {test_special_columns}')->fetchField(); - $this->assertEqual($num_records_before, $num_records_after + $num_deleted, 'Deletion adds up.'); + $num_records_after = $this->connection->query('SELECT COUNT(*) FROM {select}')->fetchField(); + $this->assertEquals($num_records_before, $num_records_after + $num_deleted, 'Deletion adds up.'); } } diff --git a/core/tests/Drupal/KernelTests/Core/Database/InsertTest.php b/core/tests/Drupal/KernelTests/Core/Database/InsertTest.php index 9f52a152664f1853c1ca5c3c65ec5f46472139a4..73fd33494f6a5f8619c80e64a2091e8940f1984c 100644 --- a/core/tests/Drupal/KernelTests/Core/Database/InsertTest.php +++ b/core/tests/Drupal/KernelTests/Core/Database/InsertTest.php @@ -198,20 +198,14 @@ public function testInsertSelectAll() { * Tests that we can INSERT INTO a special named column. */ public function testSpecialColumnInsert() { - $this->connection->insert('test_special_columns') + $this->connection->insert('select') ->fields([ 'id' => 2, - 'offset' => 'Offset value 2', - 'function' => 'foobar', + 'update' => 'Update value 2', ]) ->execute(); - $result = $this->connection->select('test_special_columns') - ->fields('test_special_columns', ['offset', 'function']) - ->condition('test_special_columns.function', 'foobar') - ->execute(); - $record = $result->fetch(); - $this->assertSame('Offset value 2', $record->offset); - $this->assertSame('foobar', $record->function); + $saved_value = $this->connection->query('SELECT [update] FROM {select} WHERE id = :id', [':id' => 2])->fetchField(); + $this->assertEquals('Update value 2', $saved_value); } } diff --git a/core/tests/Drupal/KernelTests/Core/Database/MergeTest.php b/core/tests/Drupal/KernelTests/Core/Database/MergeTest.php index b103b5fc6f6b49b0568f1ad3e9336605ef471003..e8c3b9cfa88b65d6b315a5e50033b142b88f43ac 100644 --- a/core/tests/Drupal/KernelTests/Core/Database/MergeTest.php +++ b/core/tests/Drupal/KernelTests/Core/Database/MergeTest.php @@ -230,4 +230,22 @@ public function testInvalidMerge() { $this->fail('No InvalidMergeQueryException thrown'); } + /** + * Tests that we can merge-insert with reserved keywords. + */ + public function testMergeWithReservedWords() { + $num_records_before = $this->connection->query('SELECT COUNT(*) FROM {select}')->fetchField(); + + $this->connection->merge('select') + ->key('id', 2) + ->execute(); + + $num_records_after = $this->connection->query('SELECT COUNT(*) FROM {select}')->fetchField(); + $this->assertEquals($num_records_before + 1, $num_records_after, 'Merge inserted properly.'); + + $person = $this->connection->query('SELECT * FROM {select} WHERE id = :id', [':id' => 2])->fetch(); + $this->assertEquals('', $person->update); + $this->assertEquals('2', $person->id); + } + } diff --git a/core/tests/Drupal/KernelTests/Core/Database/QueryTest.php b/core/tests/Drupal/KernelTests/Core/Database/QueryTest.php index 572c9c7ef951d3e617a0aa79beece46a692bf150..a8560b56c01aff0ffcfc416f6e575175d0b78565 100644 --- a/core/tests/Drupal/KernelTests/Core/Database/QueryTest.php +++ b/core/tests/Drupal/KernelTests/Core/Database/QueryTest.php @@ -150,4 +150,14 @@ public function testNumericExpressionSubstitution() { $this->assertEqual($count, $count_expected); } + /** + * Tests quoting identifiers in queries. + */ + public function testQuotingIdentifiers() { + // Use the table named an ANSI SQL reserved word with a column that is as + // well. + $result = $this->connection->query('SELECT [update] FROM {select}')->fetchObject(); + $this->assertEquals('Update value 1', $result->update); + } + } diff --git a/core/tests/Drupal/KernelTests/Core/Database/SchemaTest.php b/core/tests/Drupal/KernelTests/Core/Database/SchemaTest.php index 2a86e3470f3124f2f1b1e65cd91c2f47977dd61d..edf3a72a0e1c24a56ae994791c71d12e55ff4343 100644 --- a/core/tests/Drupal/KernelTests/Core/Database/SchemaTest.php +++ b/core/tests/Drupal/KernelTests/Core/Database/SchemaTest.php @@ -1252,4 +1252,27 @@ public function testFindTables() { Database::setActiveConnection('default'); } + /** + * Tests handling of uppercase table names. + */ + public function testUpperCaseTableName() { + $table_name = 'A_UPPER_CASE_TABLE_NAME'; + + // Create the tables. + $table_specification = [ + 'description' => 'Test table.', + 'fields' => [ + 'id' => [ + 'type' => 'int', + 'default' => NULL, + ], + ], + ]; + $this->schema->createTable($table_name, $table_specification); + + $this->assertTrue($this->schema->tableExists($table_name), 'Table with uppercase table name exists'); + $this->assertContains($table_name, $this->schema->findTables('%')); + $this->assertTrue($this->schema->dropTable($table_name), 'Table with uppercase table name dropped'); + } + } diff --git a/core/tests/Drupal/KernelTests/Core/Database/UpdateTest.php b/core/tests/Drupal/KernelTests/Core/Database/UpdateTest.php index b5b28092e8edcfeeeae2a28c735f5be6f9f64393..5578e760cbfb12a909b779fbe940da1ca7b4b58e 100644 --- a/core/tests/Drupal/KernelTests/Core/Database/UpdateTest.php +++ b/core/tests/Drupal/KernelTests/Core/Database/UpdateTest.php @@ -131,14 +131,16 @@ public function testUpdateAffectedRows() { * Confirm that we can update values in a column with special name. */ public function testSpecialColumnUpdate() { - $num_updated = $this->connection->update('test_special_columns') - ->fields(['offset' => 'New offset value']) + $num_updated = $this->connection->update('select') + ->fields([ + 'update' => 'New update value', + ]) ->condition('id', 1) ->execute(); $this->assertIdentical($num_updated, 1, 'Updated 1 special column record.'); - $saved_value = $this->connection->query('SELECT "offset" FROM {test_special_columns} WHERE id = :id', [':id' => 1])->fetchField(); - $this->assertIdentical($saved_value, 'New offset value', 'Updated special column name value successfully.'); + $saved_value = $this->connection->query('SELECT [update] FROM {select} WHERE id = :id', [':id' => 1])->fetchField(); + $this->assertEquals('New update value', $saved_value); } } diff --git a/core/tests/Drupal/KernelTests/Core/Database/UpsertTest.php b/core/tests/Drupal/KernelTests/Core/Database/UpsertTest.php index 461117797f7e1123101850eb4d6118e488c55277..dd91eb097a7e97d28f0d70b5382e1f8e2d390b42 100644 --- a/core/tests/Drupal/KernelTests/Core/Database/UpsertTest.php +++ b/core/tests/Drupal/KernelTests/Core/Database/UpsertTest.php @@ -54,39 +54,37 @@ public function testUpsert() { } /** - * Tests that we can upsert records with a special named column. + * Confirms that we can upsert records with keywords successfully. */ - public function testSpecialColumnUpsert() { - $num_records_before = $this->connection->query('SELECT COUNT(*) FROM {test_special_columns}')->fetchField(); - $upsert = $this->connection->upsert('test_special_columns') + public function testUpsertWithKeywords() { + $num_records_before = $this->connection->query('SELECT COUNT(*) FROM {select}')->fetchField(); + + $upsert = $this->connection->upsert('select') ->key('id') - ->fields(['id', 'offset', 'function']); + ->fields(['id', 'update']); // Add a new row. $upsert->values([ 'id' => 2, - 'offset' => 'Offset 2', - 'function' => 'Function 2', + 'update' => 'Update value 2', ]); // Update an existing row. $upsert->values([ 'id' => 1, - 'offset' => 'Offset 1 updated', - 'function' => 'Function 1 updated', + 'update' => 'Update value 1 updated', ]); $upsert->execute(); - $num_records_after = $this->connection->query('SELECT COUNT(*) FROM {test_special_columns}')->fetchField(); + + $num_records_after = $this->connection->query('SELECT COUNT(*) FROM {select}')->fetchField(); $this->assertEquals($num_records_before + 1, $num_records_after, 'Rows were inserted and updated properly.'); - $record = $this->connection->query('SELECT * FROM {test_special_columns} WHERE id = :id', [':id' => 1])->fetch(); - $this->assertEquals($record->offset, 'Offset 1 updated'); - $this->assertEquals($record->function, 'Function 1 updated'); + $record = $this->connection->query('SELECT * FROM {select} WHERE id = :id', [':id' => 1])->fetch(); + $this->assertEquals('Update value 1 updated', $record->update); - $record = $this->connection->query('SELECT * FROM {test_special_columns} WHERE id = :id', [':id' => 2])->fetch(); - $this->assertEquals($record->offset, 'Offset 2'); - $this->assertEquals($record->function, 'Function 2'); + $record = $this->connection->query('SELECT * FROM {select} WHERE id = :id', [':id' => 2])->fetch(); + $this->assertEquals('Update value 2', $record->update); } } diff --git a/core/tests/Drupal/KernelTests/Core/Entity/EntityQueryTest.php b/core/tests/Drupal/KernelTests/Core/Entity/EntityQueryTest.php index 43fb9c76a833aec5cb28d1c8029f3216598e15d5..dc64c0bc9248f00f41eed903b848ac12472f1482 100644 --- a/core/tests/Drupal/KernelTests/Core/Entity/EntityQueryTest.php +++ b/core/tests/Drupal/KernelTests/Core/Entity/EntityQueryTest.php @@ -1228,9 +1228,9 @@ public function testToString() { $expected = $connection->select("entity_test_mulrev", "base_table"); $expected->addField("base_table", "revision_id", "revision_id"); $expected->addField("base_table", "id", "id"); - $expected->join("entity_test_mulrev__$figures", "entity_test_mulrev__$figures", "entity_test_mulrev__$figures.entity_id = base_table.id"); - $expected->join("entity_test_mulrev__$figures", "entity_test_mulrev__{$figures}_2", "entity_test_mulrev__{$figures}_2.entity_id = base_table.id"); - $expected->addJoin("LEFT", "entity_test_mulrev__$figures", "entity_test_mulrev__{$figures}_3", "entity_test_mulrev__{$figures}_3.entity_id = base_table.id"); + $expected->join("entity_test_mulrev__$figures", "entity_test_mulrev__$figures", '"entity_test_mulrev__' . $figures . '"."entity_id" = "base_table"."id"'); + $expected->join("entity_test_mulrev__$figures", "entity_test_mulrev__{$figures}_2", '"entity_test_mulrev__' . $figures . '_2"."entity_id" = "base_table"."id"'); + $expected->addJoin("LEFT", "entity_test_mulrev__$figures", "entity_test_mulrev__{$figures}_3", '"entity_test_mulrev__' . $figures . '_3"."entity_id" = "base_table"."id"'); $expected->condition("entity_test_mulrev__$figures.{$figures}_color", ["blue"], "IN"); $expected->condition("entity_test_mulrev__{$figures}_2.{$figures}_color", ["red"], "IN"); $expected->isNull("entity_test_mulrev__{$figures}_3.{$figures}_color"); diff --git a/core/tests/Drupal/Tests/Core/Database/ConnectionTest.php b/core/tests/Drupal/Tests/Core/Database/ConnectionTest.php index 8325079986be84762ea6c7842861c273a7b34ca8..753fe87239c847b655ed416bb548ce4b9ae0af29 100644 --- a/core/tests/Drupal/Tests/Core/Database/ConnectionTest.php +++ b/core/tests/Drupal/Tests/Core/Database/ConnectionTest.php @@ -3,11 +3,13 @@ namespace Drupal\Tests\Core\Database; use Drupal\Tests\Core\Database\Stub\StubConnection; +use Drupal\Tests\Core\Database\Stub\StubPDO; use Drupal\Tests\UnitTestCase; /** * Tests the Connection class. * + * @coversDefaultClass \Drupal\Core\Database\Connection * @group Database */ class ConnectionTest extends UnitTestCase { @@ -76,12 +78,13 @@ public function providerTestPrefixTables() { 'SELECT * FROM test_table', 'test_', 'SELECT * FROM {table}', + '', ], [ - 'SELECT * FROM first_table JOIN second_thingie', + 'SELECT * FROM "first_table" JOIN "second"."thingie"', [ 'table' => 'first_', - 'thingie' => 'second_', + 'thingie' => 'second.', ], 'SELECT * FROM {table} JOIN {thingie}', ], @@ -93,49 +96,12 @@ public function providerTestPrefixTables() { * * @dataProvider providerTestPrefixTables */ - public function testPrefixTables($expected, $prefix_info, $query) { + public function testPrefixTables($expected, $prefix_info, $query, $quote_identifier = '"') { $mock_pdo = $this->createMock('Drupal\Tests\Core\Database\Stub\StubPDO'); - $connection = new StubConnection($mock_pdo, ['prefix' => $prefix_info]); + $connection = new StubConnection($mock_pdo, ['prefix' => $prefix_info], $quote_identifier); $this->assertEquals($expected, $connection->prefixTables($query)); } - /** - * Dataprovider for testEscapeMethods(). - * - * @return array - * Array of arrays with the following elements: - * - Expected escaped string. - * - String to escape. - */ - public function providerEscapeMethods() { - return [ - ['thing', 'thing'], - ['_item', '_item'], - ['item_', 'item_'], - ['_item_', '_item_'], - ['', '!@#$%^&*()-=+'], - ['123', '!1@2#3'], - ]; - } - - /** - * Test the various escaping methods. - * - * All tested together since they're basically the same method - * with different names. - * - * @dataProvider providerEscapeMethods - * @todo Separate test method for each escape method? - */ - public function testEscapeMethods($expected, $name) { - $mock_pdo = $this->createMock('Drupal\Tests\Core\Database\Stub\StubPDO'); - $connection = new StubConnection($mock_pdo, []); - $this->assertEquals($expected, $connection->escapeDatabase($name)); - $this->assertEquals($expected, $connection->escapeTable($name)); - $this->assertEquals($expected, $connection->escapeField($name)); - $this->assertEquals($expected, $connection->escapeAlias($name)); - } - /** * Dataprovider for testGetDriverClass(). * @@ -297,4 +263,129 @@ public function testFilterComments($expected, $comment) { ); } + /** + * Data provider for testEscapeTable. + * + * @return array + * An indexed array of where each value is an array of arguments to pass to + * testEscapeField. The first value is the expected value, and the second + * value is the value to test. + */ + public function providerEscapeTables() { + return [ + ['nocase', 'nocase'], + ['camelCase', 'camelCase'], + ['backtick', '`backtick`', '`'], + ['camelCase', '"camelCase"'], + ['camelCase', 'camel/Case'], + // Sometimes, table names are following the pattern database.schema.table. + ['camelCase.nocase.nocase', 'camelCase.nocase.nocase'], + ['nocase.camelCase.nocase', 'nocase.camelCase.nocase'], + ['nocase.nocase.camelCase', 'nocase.nocase.camelCase'], + ['camelCase.camelCase.camelCase', 'camelCase.camelCase.camelCase'], + ]; + } + + /** + * @covers ::escapeTable + * @dataProvider providerEscapeTables + */ + public function testEscapeTable($expected, $name, $identifier_quote = '"') { + $mock_pdo = $this->createMock(StubPDO::class); + $connection = new StubConnection($mock_pdo, [], $identifier_quote); + + $this->assertEquals($expected, $connection->escapeTable($name)); + } + + /** + * Data provider for testEscapeAlias. + * + * @return array + * Array of arrays with the following elements: + * - Expected escaped string. + * - String to escape. + */ + public function providerEscapeAlias() { + return [ + ['!nocase!', 'nocase', '!'], + ['`backtick`', 'backtick', '`'], + ['nocase', 'nocase', ''], + ['"camelCase"', '"camelCase"'], + ['"camelCase"', 'camelCase'], + ['"camelCase"', 'camel.Case'], + ]; + } + + /** + * @covers ::escapeAlias + * @dataProvider providerEscapeAlias + */ + public function testEscapeAlias($expected, $name, $identifier_quote = '"') { + $mock_pdo = $this->createMock(StubPDO::class); + $connection = new StubConnection($mock_pdo, [], $identifier_quote); + + $this->assertEquals($expected, $connection->escapeAlias($name)); + } + + /** + * Data provider for testEscapeField. + * + * @return array + * Array of arrays with the following elements: + * - Expected escaped string. + * - String to escape. + */ + public function providerEscapeFields() { + return [ + ['/title/', 'title', '/'], + ['`backtick`', 'backtick', '`'], + ['test.title', 'test.title', ''], + ['"isDefaultRevision"', 'isDefaultRevision'], + ['"isDefaultRevision"', '"isDefaultRevision"'], + ['"entity_test"."isDefaultRevision"', 'entity_test.isDefaultRevision'], + ['"entity_test"."isDefaultRevision"', '"entity_test"."isDefaultRevision"'], + ['"entityTest"."isDefaultRevision"', '"entityTest"."isDefaultRevision"'], + ['"entityTest"."isDefaultRevision"', 'entityTest.isDefaultRevision'], + ]; + } + + /** + * @covers ::escapeField + * @dataProvider providerEscapeFields + */ + public function testEscapeField($expected, $name, $identifier_quote = '"') { + $mock_pdo = $this->createMock(StubPDO::class); + $connection = new StubConnection($mock_pdo, [], $identifier_quote); + + $this->assertEquals($expected, $connection->escapeField($name)); + } + + /** + * Data provider for testEscapeDatabase. + * + * @return array + * An indexed array of where each value is an array of arguments to pass to + * testEscapeField. The first value is the expected value, and the second + * value is the value to test. + */ + public function providerEscapeDatabase() { + return [ + ['/name/', 'name', '/'], + ['`backtick`', 'backtick', '`'], + ['testname', 'test.name', ''], + ['"name"', 'name'], + ]; + } + + /** + * @covers ::escapeDatabase + * @dataProvider providerEscapeDatabase + */ + public function testEscapeDatabase($expected, $name, $identifier_quote = '"') { + $mock_pdo = $this->createMock(StubPDO::class); + $connection = new StubConnection($mock_pdo, [], $identifier_quote); + + $this->assertEquals($expected, $connection->escapeDatabase($name)); + } + } diff --git a/core/tests/Drupal/Tests/Core/Database/Driver/pgsql/PostgresqlConnectionTest.php b/core/tests/Drupal/Tests/Core/Database/Driver/pgsql/PostgresqlConnectionTest.php deleted file mode 100644 index d6ea4fbf4c605501455f22c60f3f07cc93c161d6..0000000000000000000000000000000000000000 --- a/core/tests/Drupal/Tests/Core/Database/Driver/pgsql/PostgresqlConnectionTest.php +++ /dev/null @@ -1,119 +0,0 @@ -<?php - -namespace Drupal\Tests\Core\Database\Driver\pgsql; - -use Drupal\Core\Database\Driver\pgsql\Connection; -use Drupal\Tests\UnitTestCase; - -/** - * @coversDefaultClass \Drupal\Core\Database\Driver\pgsql\Connection - * @group Database - */ -class PostgresqlConnectionTest extends UnitTestCase { - - /** - * Mock PDO object for use in tests. - * - * @var \PHPUnit\Framework\MockObject\MockObject|\Drupal\Tests\Core\Database\Stub\StubPDO - */ - protected $mockPdo; - - /** - * {@inheritdoc} - */ - protected function setUp() { - parent::setUp(); - $this->mockPdo = $this->createMock('Drupal\Tests\Core\Database\Stub\StubPDO'); - } - - /** - * Data provider for testEscapeTable. - * - * @return array - * An indexed array of where each value is an array of arguments to pass to - * testEscapeField. The first value is the expected value, and the second - * value is the value to test. - */ - public function providerEscapeTables() { - return [ - ['nocase', 'nocase'], - ['"camelCase"', 'camelCase'], - ['"camelCase"', '"camelCase"'], - ['"camelCase"', 'camel/Case'], - // Sometimes, table names are following the pattern database.schema.table. - ['"camelCase".nocase.nocase', 'camelCase.nocase.nocase'], - ['nocase."camelCase".nocase', 'nocase.camelCase.nocase'], - ['nocase.nocase."camelCase"', 'nocase.nocase.camelCase'], - ['"camelCase"."camelCase"."camelCase"', 'camelCase.camelCase.camelCase'], - ]; - } - - /** - * Data provider for testEscapeAlias. - * - * @return array - * Array of arrays with the following elements: - * - Expected escaped string. - * - String to escape. - */ - public function providerEscapeAlias() { - return [ - ['nocase', 'nocase'], - ['"camelCase"', '"camelCase"'], - ['"camelCase"', 'camelCase'], - ['"camelCase"', 'camel.Case'], - ]; - } - - /** - * Data provider for testEscapeField. - * - * @return array - * Array of arrays with the following elements: - * - Expected escaped string. - * - String to escape. - */ - public function providerEscapeFields() { - return [ - ['title', 'title'], - ['"isDefaultRevision"', 'isDefaultRevision'], - ['"isDefaultRevision"', '"isDefaultRevision"'], - ['entity_test."isDefaultRevision"', 'entity_test.isDefaultRevision'], - ['entity_test."isDefaultRevision"', '"entity_test"."isDefaultRevision"'], - ['"entityTest"."isDefaultRevision"', '"entityTest"."isDefaultRevision"'], - ['"entityTest"."isDefaultRevision"', 'entityTest.isDefaultRevision'], - ['entity_test."isDefaultRevision"', 'entity_test.is.Default.Revision'], - ]; - } - - /** - * @covers ::escapeTable - * @dataProvider providerEscapeTables - */ - public function testEscapeTable($expected, $name) { - $pgsql_connection = new Connection($this->mockPdo, []); - - $this->assertEquals($expected, $pgsql_connection->escapeTable($name)); - } - - /** - * @covers ::escapeAlias - * @dataProvider providerEscapeAlias - */ - public function testEscapeAlias($expected, $name) { - $pgsql_connection = new Connection($this->mockPdo, []); - - $this->assertEquals($expected, $pgsql_connection->escapeAlias($name)); - } - - /** - * @covers ::escapeField - * @dataProvider providerEscapeFields - */ - public function testEscapeField($expected, $name) { - $pgsql_connection = new Connection($this->mockPdo, []); - - $this->assertEquals($expected, $pgsql_connection->escapeField($name)); - } - -} diff --git a/core/tests/Drupal/Tests/Core/Database/OrderByTest.php b/core/tests/Drupal/Tests/Core/Database/OrderByTest.php index ef38627d9ff32cc75acc46af892bd3afa737d1ae..9c28331ca8db353a3cd923e7808c1140abfe6b0c 100644 --- a/core/tests/Drupal/Tests/Core/Database/OrderByTest.php +++ b/core/tests/Drupal/Tests/Core/Database/OrderByTest.php @@ -25,6 +25,9 @@ class OrderByTest extends UnitTestCase { protected function setUp() { $connection = $this->getMockBuilder('Drupal\Core\Database\Connection') ->disableOriginalConstructor() + // Prevent deprecation message being triggered by + // Connection::identifierQuote(). + ->setMethods(['identifierQuote']) ->getMockForAbstractClass(); $this->query = new Select($connection, 'test', NULL); } diff --git a/core/tests/Drupal/Tests/Core/Database/Stub/StubConnection.php b/core/tests/Drupal/Tests/Core/Database/Stub/StubConnection.php index 4692dffb8714b061ccd099a33e004abca3aaa0e6..7bdd86cc9ff50c88cbc3b0be67a658cd34237ba8 100644 --- a/core/tests/Drupal/Tests/Core/Database/Stub/StubConnection.php +++ b/core/tests/Drupal/Tests/Core/Database/Stub/StubConnection.php @@ -20,6 +20,35 @@ class StubConnection extends Connection { */ public $driver = 'stub'; + /** + * The identifier quote character. Can be set in the constructor for testing. + * + * @var string + */ + protected $identifierQuote = ''; + + /** + * Constructs a Connection object. + * + * @param \PDO $connection + * An object of the PDO class representing a database connection. + * @param array $connection_options + * An array of options for the connection. + * @param string $identifier_quote + * The identifier quote character. Defaults to an empty string. + */ + public function __construct(\PDO $connection, array $connection_options, $identifier_quote = '') { + $this->identifierQuote = $identifier_quote; + parent::__construct($connection, $connection_options); + } + + /** + * {@inheritdoc} + */ + protected function identifierQuote() { + return $this->identifierQuote; + } + /** * {@inheritdoc} */