first commit

This commit is contained in:
2024-07-15 12:33:27 +02:00
commit ce50ae282b
22084 changed files with 2623791 additions and 0 deletions

View File

@@ -0,0 +1,10 @@
name: PostgreSQL
type: module
description: 'Provides the PostgreSQL database driver.'
package: Core
# version: VERSION
# Information added by Drupal.org packaging script on 2024-07-04
version: '10.3.1'
project: 'drupal'
datestamp: 1720094222

View File

@@ -0,0 +1,50 @@
<?php
/**
* @file
* Install, update and uninstall functions for the pgsql module.
*/
use Drupal\Core\Database\Database;
use Drupal\pgsql\Update10101;
/**
* Implements hook_requirements().
*/
function pgsql_requirements() {
$requirements = [];
// Test with PostgreSQL databases for the status of the pg_trgm extension.
if (Database::isActiveConnection()) {
$connection = Database::getConnection();
// Set the requirement just for postgres.
if ($connection->driver() == 'pgsql') {
$requirements['pgsql_extension_pg_trgm'] = [
'severity' => REQUIREMENT_OK,
'title' => t('PostgreSQL pg_trgm extension'),
'value' => t('Available'),
'description' => 'The pg_trgm PostgreSQL extension is present.',
];
// If the extension is not available, set the requirement error.
if (!$connection->schema()->extensionExists('pg_trgm')) {
$requirements['pgsql_extension_pg_trgm']['severity'] = REQUIREMENT_ERROR;
$requirements['pgsql_extension_pg_trgm']['value'] = t('Not created');
$requirements['pgsql_extension_pg_trgm']['description'] = t('The <a href=":pg_trgm">pg_trgm</a> PostgreSQL extension is not present. The extension is required by Drupal 10 to improve performance when using PostgreSQL. See <a href=":requirements">Drupal database server requirements</a> for more information.', [
':pg_trgm' => 'https://www.postgresql.org/docs/current/pgtrgm.html',
':requirements' => 'https://www.drupal.org/docs/system-requirements/database-server-requirements',
]);
}
}
}
return $requirements;
}
/**
* Update sequences' owner created from serial columns in PostgreSQL.
*/
function pgsql_update_10101(&$sandbox) {
\Drupal::classResolver(Update10101::class)->update($sandbox);
}

22
core/modules/pgsql/pgsql.module Executable file
View File

@@ -0,0 +1,22 @@
<?php
/**
* @file
* The PostgreSQL module provides the connection between Drupal and a PostgreSQL database.
*/
use Drupal\Core\Routing\RouteMatchInterface;
/**
* Implements hook_help().
*/
function pgsql_help($route_name, RouteMatchInterface $route_match) {
switch ($route_name) {
case 'help.page.pgsql':
$output = '';
$output .= '<h2>' . t('About') . '</h2>';
$output .= '<p>' . t('The PostgreSQL module provides the connection between Drupal and a PostgreSQL database. For more information, see the <a href=":pgsql">online documentation for the PostgreSQL module</a>.', [':pgsql' => 'https://www.drupal.org/docs/core-modules-and-themes/core-modules/postgresql-module']) . '</p>';
return $output;
}
}

View File

@@ -0,0 +1,547 @@
<?php
namespace Drupal\pgsql\Driver\Database\pgsql;
use Drupal\Core\Database\Connection as DatabaseConnection;
use Drupal\Core\Database\Database;
use Drupal\Core\Database\DatabaseAccessDeniedException;
use Drupal\Core\Database\DatabaseNotFoundException;
use Drupal\Core\Database\ExceptionHandler;
use Drupal\Core\Database\Query\Condition;
use Drupal\Core\Database\StatementInterface;
use Drupal\Core\Database\StatementWrapperIterator;
use Drupal\Core\Database\SupportsTemporaryTablesInterface;
use Drupal\Core\Database\Transaction\TransactionManagerInterface;
// cSpell:ignore ilike nextval
/**
* @addtogroup database
* @{
*/
/**
* PostgreSQL implementation of \Drupal\Core\Database\Connection.
*/
class Connection extends DatabaseConnection implements SupportsTemporaryTablesInterface {
/**
* The name by which to obtain a lock for retrieve the next insert id.
*/
const POSTGRESQL_NEXTID_LOCK = 1000;
/**
* Error code for "Unknown database" error.
*/
const DATABASE_NOT_FOUND = 7;
/**
* Error code for "Connection failure" errors.
*
* Technically this is an internal error code that will only be shown in the
* PDOException message. It will need to get extracted.
*/
const CONNECTION_FAILURE = '08006';
/**
* {@inheritdoc}
*/
protected $statementWrapperClass = StatementWrapperIterator::class;
/**
* A map of condition operators to PostgreSQL operators.
*
* In PostgreSQL, 'LIKE' is case-sensitive. ILIKE should be used for
* case-insensitive statements.
*/
protected static $postgresqlConditionOperatorMap = [
'LIKE' => ['operator' => 'ILIKE'],
'LIKE BINARY' => ['operator' => 'LIKE'],
'NOT LIKE' => ['operator' => 'NOT ILIKE'],
'REGEXP' => ['operator' => '~*'],
'NOT REGEXP' => ['operator' => '!~*'],
];
/**
* {@inheritdoc}
*/
protected $transactionalDDLSupport = TRUE;
/**
* {@inheritdoc}
*/
protected $identifierQuotes = ['"', '"'];
/**
* An array of transaction savepoints.
*
* The main use for this array is to store information about transaction
* savepoints opened to to mimic MySql's InnoDB functionality, which provides
* an inherent savepoint before any query in a transaction.
*
* @see ::addSavepoint()
* @see ::releaseSavepoint()
* @see ::rollbackSavepoint()
*
* @var array<string,Transaction>
*/
protected array $savepoints = [];
/**
* Constructs a connection object.
*/
public function __construct(\PDO $connection, array $connection_options) {
// Sanitize the schema name here, so we do not have to do it in other
// functions.
if (isset($connection_options['schema']) && ($connection_options['schema'] !== 'public')) {
$connection_options['schema'] = preg_replace('/[^A-Za-z0-9_]+/', '', $connection_options['schema']);
}
// We need to set the connectionOptions before the parent, because setPrefix
// needs this.
$this->connectionOptions = $connection_options;
parent::__construct($connection, $connection_options);
// Force PostgreSQL to use the UTF-8 character set by default.
$this->connection->exec("SET NAMES 'UTF8'");
// Execute PostgreSQL init_commands.
if (isset($connection_options['init_commands'])) {
$this->connection->exec(implode('; ', $connection_options['init_commands']));
}
}
/**
* {@inheritdoc}
*/
protected function setPrefix($prefix) {
assert(is_string($prefix), 'The \'$prefix\' argument to ' . __METHOD__ . '() must be a string');
$this->prefix = $prefix;
// Add the schema name if it is not set to public, otherwise it will use the
// default schema name.
$quoted_schema = '';
if (isset($this->connectionOptions['schema']) && ($this->connectionOptions['schema'] !== 'public')) {
$quoted_schema = $this->identifierQuotes[0] . $this->connectionOptions['schema'] . $this->identifierQuotes[1] . '.';
}
$this->tablePlaceholderReplacements = [
$quoted_schema . $this->identifierQuotes[0] . str_replace('.', $this->identifierQuotes[1] . '.' . $this->identifierQuotes[0], $prefix),
$this->identifierQuotes[1],
];
}
/**
* {@inheritdoc}
*/
public static function open(array &$connection_options = []) {
// Default to TCP connection on port 5432.
if (empty($connection_options['port'])) {
$connection_options['port'] = 5432;
}
// PostgreSQL in trust mode doesn't require a password to be supplied.
if (empty($connection_options['password'])) {
$connection_options['password'] = NULL;
}
// If the password contains a backslash it is treated as an escape character
// http://bugs.php.net/bug.php?id=53217
// so backslashes in the password need to be doubled up.
// The bug was reported against pdo_pgsql 1.0.2, backslashes in passwords
// will break on this doubling up when the bug is fixed, so check the version
// elseif (phpversion('pdo_pgsql') < 'version_this_was_fixed_in') {
else {
$connection_options['password'] = str_replace('\\', '\\\\', $connection_options['password']);
}
$connection_options['database'] = (!empty($connection_options['database']) ? $connection_options['database'] : 'template1');
$dsn = 'pgsql:host=' . $connection_options['host'] . ' dbname=' . $connection_options['database'] . ' port=' . $connection_options['port'];
// Allow PDO options to be overridden.
$connection_options += [
'pdo' => [],
];
$connection_options['pdo'] += [
\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
// Prepared statements are most effective for performance when queries
// are recycled (used several times). However, if they are not re-used,
// prepared statements become inefficient. Since most of Drupal's
// prepared queries are not re-used, it should be faster to emulate
// the preparation than to actually ready statements for re-use. If in
// doubt, reset to FALSE and measure performance.
\PDO::ATTR_EMULATE_PREPARES => TRUE,
// Convert numeric values to strings when fetching.
\PDO::ATTR_STRINGIFY_FETCHES => TRUE,
];
try {
$pdo = new \PDO($dsn, $connection_options['username'], $connection_options['password'], $connection_options['pdo']);
}
catch (\PDOException $e) {
if (static::getSQLState($e) == static::CONNECTION_FAILURE) {
if (str_contains($e->getMessage(), 'password authentication failed for user')) {
throw new DatabaseAccessDeniedException($e->getMessage(), $e->getCode(), $e);
}
elseif (str_contains($e->getMessage(), 'database') && str_contains($e->getMessage(), 'does not exist')) {
throw new DatabaseNotFoundException($e->getMessage(), $e->getCode(), $e);
}
}
throw $e;
}
return $pdo;
}
/**
* {@inheritdoc}
*/
public function query($query, array $args = [], $options = []) {
$options += $this->defaultOptions();
// The PDO PostgreSQL driver has a bug which doesn't type cast booleans
// correctly when parameters are bound using associative arrays.
// @see http://bugs.php.net/bug.php?id=48383
foreach ($args as &$value) {
if (is_bool($value)) {
$value = (int) $value;
}
}
// We need to wrap queries with a savepoint if:
// - Currently in a transaction.
// - A 'mimic_implicit_commit' does not exist already.
// - The query is not a savepoint query.
$wrap_with_savepoint = $this->inTransaction() &&
!$this->transactionManager()->has('mimic_implicit_commit') &&
!(is_string($query) && (
stripos($query, 'ROLLBACK TO SAVEPOINT ') === 0 ||
stripos($query, 'RELEASE SAVEPOINT ') === 0 ||
stripos($query, 'SAVEPOINT ') === 0
)
);
if ($wrap_with_savepoint) {
// Create a savepoint so we can rollback a failed query. This is so we can
// mimic MySQL and SQLite transactions which don't fail if a single query
// fails. This is important for tables that are created on demand. For
// example, \Drupal\Core\Cache\DatabaseBackend.
$this->addSavepoint();
try {
$return = parent::query($query, $args, $options);
$this->releaseSavepoint();
}
catch (\Exception $e) {
$this->rollbackSavepoint();
throw $e;
}
}
else {
$return = parent::query($query, $args, $options);
}
return $return;
}
/**
* {@inheritdoc}
*/
public function prepareStatement(string $query, array $options, bool $allow_row_count = FALSE): StatementInterface {
// 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.
$query = preg_replace('/ ([^ ]+) +(I*LIKE|NOT +I*LIKE|~\*|!~\*) /i', ' ${1}::text ${2} ', $query);
return parent::prepareStatement($query, $options, $allow_row_count);
}
public function queryRange($query, $from, $count, array $args = [], array $options = []) {
return $this->query($query . ' LIMIT ' . (int) $count . ' OFFSET ' . (int) $from, $args, $options);
}
/**
* {@inheritdoc}
*/
public function queryTemporary($query, array $args = [], array $options = []) {
$tablename = 'db_temporary_' . uniqid();
$this->query('CREATE TEMPORARY TABLE {' . $tablename . '} AS ' . $query, $args, $options);
return $tablename;
}
public function driver() {
return 'pgsql';
}
public function databaseType() {
return 'pgsql';
}
/**
* Overrides \Drupal\Core\Database\Connection::createDatabase().
*
* @param string $database
* The name of the database to create.
*
* @throws \Drupal\Core\Database\DatabaseNotFoundException
*/
public function createDatabase($database) {
// Escape the database name.
$database = Database::getConnection()->escapeDatabase($database);
$db_created = FALSE;
// Try to determine the proper locales for character classification and
// collation. If we could determine locales other than 'en_US', try creating
// the database with these first.
$ctype = setlocale(LC_CTYPE, 0);
$collate = setlocale(LC_COLLATE, 0);
if ($ctype && $collate) {
try {
$this->connection->exec("CREATE DATABASE $database WITH TEMPLATE template0 ENCODING='UTF8' LC_CTYPE='$ctype.UTF-8' LC_COLLATE='$collate.UTF-8'");
$db_created = TRUE;
}
catch (\Exception $e) {
// It might be that the server is remote and does not support the
// locale and collation of the webserver, so we will try again.
}
}
// Otherwise fall back to creating the database using the 'en_US' locales.
if (!$db_created) {
try {
$this->connection->exec("CREATE DATABASE $database WITH TEMPLATE template0 ENCODING='UTF8' LC_CTYPE='en_US.UTF-8' LC_COLLATE='en_US.UTF-8'");
}
catch (\Exception $e) {
// If the database can't be created with the 'en_US' locale either,
// we're finally throwing an exception.
throw new DatabaseNotFoundException($e->getMessage());
}
}
}
public function mapConditionOperator($operator) {
return static::$postgresqlConditionOperatorMap[$operator] ?? NULL;
}
/**
* Creates the appropriate sequence name for a given table and serial field.
*
* This method should only be called by the driver's code.
*
* @param string $table
* The table name to use for the sequence.
* @param string $field
* The field name to use for the sequence.
*
* @return string
* A table prefix-parsed string for the sequence name.
*
* @internal
*/
public function makeSequenceName($table, $field) {
$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->identifierQuotes, '', $sequence_name);
}
/**
* Retrieve a the next id in a sequence.
*
* PostgreSQL has built in sequences. We'll use these instead of inserting
* and updating a sequences table.
*/
public function nextId($existing = 0) {
@trigger_error('Drupal\Core\Database\Connection::nextId() is deprecated in drupal:10.2.0 and is removed from drupal:11.0.0. Modules should use instead the keyvalue storage for the last used id. See https://www.drupal.org/node/3349345', E_USER_DEPRECATED);
// Retrieve the name of the sequence. This information cannot be cached
// because the prefix may change, for example, like it does in tests.
$sequence_name = $this->makeSequenceName('sequences', 'value');
// When PostgreSQL gets a value too small then it will lock the table,
// retry the INSERT and if it's still too small then alter the sequence.
$id = $this->query("SELECT nextval('" . $sequence_name . "')")->fetchField();
if ($id > $existing) {
return $id;
}
// PostgreSQL advisory locks are simply locks to be used by an
// application such as Drupal. This will prevent other Drupal processes
// from altering the sequence while we are.
$this->query("SELECT pg_advisory_lock(" . self::POSTGRESQL_NEXTID_LOCK . ")");
// While waiting to obtain the lock, the sequence may have been altered
// so lets try again to obtain an adequate value.
$id = $this->query("SELECT nextval('" . $sequence_name . "')")->fetchField();
if ($id > $existing) {
$this->query("SELECT pg_advisory_unlock(" . self::POSTGRESQL_NEXTID_LOCK . ")");
return $id;
}
// Reset the sequence to a higher value than the existing id.
$this->query("ALTER SEQUENCE " . $sequence_name . " RESTART WITH " . ($existing + 1));
// Retrieve the next id. We know this will be as high as we want it.
$id = $this->query("SELECT nextval('" . $sequence_name . "')")->fetchField();
$this->query("SELECT pg_advisory_unlock(" . self::POSTGRESQL_NEXTID_LOCK . ")");
return $id;
}
/**
* {@inheritdoc}
*/
public function getFullQualifiedTableName($table) {
$options = $this->getConnectionOptions();
$schema = $options['schema'] ?? 'public';
// The fully qualified table name in PostgreSQL is in the form of
// <database>.<schema>.<table>.
return $options['database'] . '.' . $schema . '.' . $this->getPrefix() . $table;
}
/**
* Add a new savepoint with a unique name.
*
* The main use for this method is to mimic InnoDB functionality, which
* provides an inherent savepoint before any query in a transaction.
*
* @param $savepoint_name
* A string representing the savepoint name. By default,
* "mimic_implicit_commit" is used.
*/
public function addSavepoint($savepoint_name = 'mimic_implicit_commit') {
if ($this->inTransaction()) {
$this->savepoints[$savepoint_name] = $this->startTransaction($savepoint_name);
}
}
/**
* Release a savepoint by name.
*
* @param $savepoint_name
* A string representing the savepoint name. By default,
* "mimic_implicit_commit" is used.
*/
public function releaseSavepoint($savepoint_name = 'mimic_implicit_commit') {
if ($this->inTransaction() && $this->transactionManager()->has($savepoint_name)) {
unset($this->savepoints[$savepoint_name]);
}
}
/**
* Rollback a savepoint by name if it exists.
*
* @param $savepoint_name
* A string representing the savepoint name. By default,
* "mimic_implicit_commit" is used.
*/
public function rollbackSavepoint($savepoint_name = 'mimic_implicit_commit') {
if ($this->inTransaction() && $this->transactionManager()->has($savepoint_name)) {
$this->savepoints[$savepoint_name]->rollBack();
unset($this->savepoints[$savepoint_name]);
}
}
/**
* {@inheritdoc}
*/
public function hasJson(): bool {
try {
return (bool) $this->query('SELECT JSON_TYPEOF(\'1\')');
}
catch (\Exception $e) {
return FALSE;
}
}
/**
* {@inheritdoc}
*/
public function exceptionHandler() {
return new ExceptionHandler();
}
/**
* {@inheritdoc}
*/
public function select($table, $alias = NULL, array $options = []) {
return new Select($this, $table, $alias, $options);
}
/**
* {@inheritdoc}
*/
public function insert($table, array $options = []) {
return new Insert($this, $table, $options);
}
/**
* {@inheritdoc}
*/
public function merge($table, array $options = []) {
return new Merge($this, $table, $options);
}
/**
* {@inheritdoc}
*/
public function upsert($table, array $options = []) {
return new Upsert($this, $table, $options);
}
/**
* {@inheritdoc}
*/
public function update($table, array $options = []) {
return new Update($this, $table, $options);
}
/**
* {@inheritdoc}
*/
public function delete($table, array $options = []) {
return new Delete($this, $table, $options);
}
/**
* {@inheritdoc}
*/
public function truncate($table, array $options = []) {
return new Truncate($this, $table, $options);
}
/**
* {@inheritdoc}
*/
public function schema() {
if (empty($this->schema)) {
$this->schema = new Schema($this);
}
return $this->schema;
}
/**
* {@inheritdoc}
*/
public function condition($conjunction) {
return new Condition($conjunction);
}
/**
* {@inheritdoc}
*/
protected function driverTransactionManager(): TransactionManagerInterface {
return new TransactionManager($this);
}
/**
* {@inheritdoc}
*/
public function startTransaction($name = '') {
return $this->transactionManager()->push($name);
}
}
/**
* @} End of "addtogroup database".
*/

View File

@@ -0,0 +1,39 @@
<?php
namespace Drupal\pgsql\Driver\Database\pgsql;
use Drupal\Core\Database\Query\Delete as QueryDelete;
/**
* PostgreSQL implementation of \Drupal\Core\Database\Query\Delete.
*/
class Delete extends QueryDelete {
/**
* {@inheritdoc}
*/
public function __construct(Connection $connection, string $table, array $options = []) {
// @todo Remove the __construct in Drupal 11.
// @see https://www.drupal.org/project/drupal/issues/3256524
parent::__construct($connection, $table, $options);
unset($this->queryOptions['return']);
}
/**
* {@inheritdoc}
*/
public function execute() {
$this->connection->addSavepoint();
try {
$result = parent::execute();
}
catch (\Exception $e) {
$this->connection->rollbackSavepoint();
throw $e;
}
$this->connection->releaseSavepoint();
return $result;
}
}

View File

@@ -0,0 +1,156 @@
<?php
namespace Drupal\pgsql\Driver\Database\pgsql;
use Drupal\Core\Database\DatabaseExceptionWrapper;
use Drupal\Core\Database\Query\Insert as QueryInsert;
// cSpell:ignore nextval setval
/**
* @ingroup database
* @{
*/
/**
* PostgreSQL implementation of \Drupal\Core\Database\Query\Insert.
*/
class Insert extends QueryInsert {
/**
* {@inheritdoc}
*/
public function __construct(Connection $connection, string $table, array $options = []) {
// @todo Remove the __construct in Drupal 11.
// @see https://www.drupal.org/project/drupal/issues/3256524
parent::__construct($connection, $table, $options);
unset($this->queryOptions['return']);
}
public function execute() {
if (!$this->preExecute()) {
return NULL;
}
$stmt = $this->connection->prepareStatement((string) $this, $this->queryOptions);
// 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]) && $insert_values[$idx] !== NULL) {
$blobs[$blob_count] = fopen('php://memory', 'a');
fwrite($blobs[$blob_count], $insert_values[$idx]);
rewind($blobs[$blob_count]);
$stmt->getClientStatement()->bindParam(':db_insert_placeholder_' . $max_placeholder++, $blobs[$blob_count], \PDO::PARAM_LOB);
// Pre-increment is faster in PHP than increment.
++$blob_count;
}
else {
$stmt->getClientStatement()->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 . "}", [':serial_value' => (int) $serial_value]);
}
}
}
}
if (!empty($this->fromQuery)) {
// bindParam stores only a reference to the variable that is followed when
// the statement is executed. We pass $arguments[$key] instead of $value
// because the second argument to bindParam is passed by reference and
// the foreach statement assigns the element to the existing reference.
$arguments = $this->fromQuery->getArguments();
foreach ($arguments as $key => $value) {
$stmt->getClientStatement()->bindParam($key, $arguments[$key]);
}
}
// Create a savepoint so we can rollback a failed query. This is so we can
// mimic MySQL and SQLite transactions which don't fail if a single query
// fails. This is important for tables that are created on demand. For
// example, \Drupal\Core\Cache\DatabaseBackend.
$this->connection->addSavepoint();
try {
$stmt->execute(NULL, $this->queryOptions);
if (isset($table_information->serial_fields[0])) {
$last_insert_id = $stmt->fetchField();
}
$this->connection->releaseSavepoint();
}
catch (\Exception $e) {
$this->connection->rollbackSavepoint();
$this->connection->exceptionHandler()->handleExecutionException($e, $stmt, [], $this->queryOptions);
}
// Re-initialize the values array so that we can re-use this query.
$this->insertValues = [];
return $last_insert_id ?? NULL;
}
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);
// If we're selecting from a SelectQuery, finish building the query and
// pass it back, as any remaining options are irrelevant.
if (!empty($this->fromQuery)) {
$insert_fields_string = $insert_fields ? ' (' . implode(', ', $insert_fields) . ') ' : ' ';
$query = $comments . 'INSERT INTO {' . $this->table . '}' . $insert_fields_string . $this->fromQuery;
}
else {
$query = $comments . 'INSERT INTO {' . $this->table . '} (' . implode(', ', $insert_fields) . ') VALUES ';
$values = $this->getInsertPlaceholderFragment($this->insertValues, $this->defaultFields);
$query .= implode(', ', $values);
}
try {
// Fetch the list of blobs and sequences used on that table.
$table_information = $this->connection->schema()->queryTableInformation($this->table);
if (isset($table_information->serial_fields[0])) {
// Use RETURNING syntax to get the last insert ID in the same INSERT
// query, see https://www.postgresql.org/docs/12/dml-returning.html.
$query .= ' RETURNING ' . $table_information->serial_fields[0];
}
}
catch (DatabaseExceptionWrapper $e) {
// If we fail to get the table information it is probably because the
// table does not exist yet so adding the returning statement is pointless
// because the query will fail. This happens for tables created on demand,
// for example, cache tables.
}
return $query;
}
}

View File

@@ -0,0 +1,328 @@
<?php
namespace Drupal\pgsql\Driver\Database\pgsql\Install;
use Drupal\Core\Database\Database;
use Drupal\Core\Database\Install\Tasks as InstallTasks;
use Drupal\Core\Database\DatabaseNotFoundException;
// cspell:ignore trgm
/**
* Specifies installation tasks for PostgreSQL databases.
*/
class Tasks extends InstallTasks {
/**
* Minimum required PostgreSQL version.
*
* The contrib extension pg_trgm is supposed to be installed.
*
* @see https://www.postgresql.org/docs/12/pgtrgm.html
*/
const PGSQL_MINIMUM_VERSION = '12';
/**
* {@inheritdoc}
*/
protected $pdoDriver = 'pgsql';
/**
* Constructs a \Drupal\pgsql\Driver\Database\pgsql\Install\Tasks object.
*/
public function __construct() {
$this->tasks[] = [
'function' => 'checkEncoding',
'arguments' => [],
];
$this->tasks[] = [
'function' => 'checkBinaryOutput',
'arguments' => [],
];
$this->tasks[] = [
'function' => 'checkStandardConformingStrings',
'arguments' => [],
];
$this->tasks[] = [
'function' => 'checkExtensions',
'arguments' => [],
];
$this->tasks[] = [
'function' => 'initializeDatabase',
'arguments' => [],
];
}
/**
* {@inheritdoc}
*/
public function name() {
return t('PostgreSQL');
}
/**
* {@inheritdoc}
*/
public function minimumVersion() {
return static::PGSQL_MINIMUM_VERSION;
}
/**
* {@inheritdoc}
*/
protected function connect() {
try {
// This doesn't actually test the connection.
Database::setActiveConnection();
// Now actually do a check.
Database::getConnection();
$this->pass('Drupal can CONNECT to the database ok.');
}
catch (\Exception $e) {
// Attempt to create the database if it is not found.
if ($e instanceof DatabaseNotFoundException) {
// Remove the database string from connection info.
$connection_info = Database::getConnectionInfo();
$database = $connection_info['default']['database'];
unset($connection_info['default']['database']);
// In order to change the Database::$databaseInfo array, need to remove
// the active connection, then re-add it with the new info.
Database::removeConnection('default');
Database::addConnectionInfo('default', 'default', $connection_info['default']);
try {
// Now, attempt the connection again; if it's successful, attempt to
// create the database.
Database::getConnection()->createDatabase($database);
Database::closeConnection();
// Now, restore the database config.
Database::removeConnection('default');
$connection_info['default']['database'] = $database;
Database::addConnectionInfo('default', 'default', $connection_info['default']);
// Check the database connection.
Database::getConnection();
$this->pass('Drupal can CONNECT to the database ok.');
}
catch (DatabaseNotFoundException $e) {
// Still no dice; probably a permission issue. Raise the error to the
// installer.
$this->fail(t('Database %database not found. The server reports the following message when attempting to create the database: %error.', ['%database' => $database, '%error' => $e->getMessage()]));
}
}
else {
// Database connection failed for some other reason than a non-existent
// database.
$this->fail(t('Failed to connect to your database server. The server reports the following message: %error.<ul><li>Is the database server running?</li><li>Does the database exist, and have you entered the correct database name?</li><li>Have you entered the correct username and password?</li><li>Have you entered the correct database hostname and port number?</li></ul>', ['%error' => $e->getMessage()]));
return FALSE;
}
}
return TRUE;
}
/**
* Check encoding is UTF8.
*/
protected function checkEncoding() {
try {
if (Database::getConnection()->query('SHOW server_encoding')->fetchField() == 'UTF8') {
$this->pass(t('Database is encoded in UTF-8'));
}
else {
$this->fail(t('The %driver database must use %encoding encoding to work with Drupal. Recreate the database with %encoding encoding. See <a href="INSTALL.pgsql.txt">INSTALL.pgsql.txt</a> for more details.', [
'%encoding' => 'UTF8',
'%driver' => $this->name(),
]));
}
}
catch (\Exception $e) {
$this->fail(t('Drupal could not determine the encoding of the database was set to UTF-8'));
}
}
/**
* Check Binary Output.
*
* Unserializing does not work on Postgresql 9 when bytea_output is 'hex'.
*/
public function checkBinaryOutput() {
$database_connection = Database::getConnection();
if (!$this->checkBinaryOutputSuccess()) {
// First try to alter the database. If it fails, raise an error telling
// the user to do it themselves.
$connection_options = $database_connection->getConnectionOptions();
// It is safe to include the database name directly here, because this
// code is only called when a connection to the database is already
// established, thus the database name is guaranteed to be a correct
// value.
$query = "ALTER DATABASE \"{$connection_options['database']}\" SET bytea_output = 'escape';";
try {
$database_connection->query($query);
}
catch (\Exception $e) {
// Ignore possible errors when the user doesn't have the necessary
// privileges to ALTER the database.
}
// Close the database connection so that the configuration parameter
// is applied to the current connection.
Database::closeConnection();
// Recheck, if it fails, finally just rely on the end user to do the
// right thing.
if (!$this->checkBinaryOutputSuccess()) {
$replacements = [
'%setting' => 'bytea_output',
'%current_value' => 'hex',
'%needed_value' => 'escape',
'@query' => $query,
];
$this->fail(t("The %setting setting is currently set to '%current_value', but needs to be '%needed_value'. Change this by running the following query: <code>@query</code>", $replacements));
}
}
}
/**
* Verify that a binary data roundtrip returns the original string.
*/
protected function checkBinaryOutputSuccess() {
$bytea_output = Database::getConnection()->query("SHOW bytea_output")->fetchField();
return ($bytea_output == 'escape');
}
/**
* Ensures standard_conforming_strings setting is 'on'.
*
* When standard_conforming_strings setting is 'on' string literals ('...')
* treat backslashes literally, as specified in the SQL standard. This allows
* Drupal to convert between bytea, text and varchar columns.
*/
public function checkStandardConformingStrings() {
$database_connection = Database::getConnection();
if (!$this->checkStandardConformingStringsSuccess()) {
// First try to alter the database. If it fails, raise an error telling
// the user to do it themselves.
$connection_options = $database_connection->getConnectionOptions();
// It is safe to include the database name directly here, because this
// code is only called when a connection to the database is already
// established, thus the database name is guaranteed to be a correct
// value.
$query = "ALTER DATABASE \"" . $connection_options['database'] . "\" SET standard_conforming_strings = 'on';";
try {
$database_connection->query($query);
}
catch (\Exception $e) {
// Ignore possible errors when the user doesn't have the necessary
// privileges to ALTER the database.
}
// Close the database connection so that the configuration parameter
// is applied to the current connection.
Database::closeConnection();
// Recheck, if it fails, finally just rely on the end user to do the
// right thing.
if (!$this->checkStandardConformingStringsSuccess()) {
$replacements = [
'%setting' => 'standard_conforming_strings',
'%current_value' => 'off',
'%needed_value' => 'on',
'@query' => $query,
];
$this->fail(t("The %setting setting is currently set to '%current_value', but needs to be '%needed_value'. Change this by running the following query: <code>@query</code>", $replacements));
}
}
}
/**
* Verifies the standard_conforming_strings setting.
*/
protected function checkStandardConformingStringsSuccess() {
$standard_conforming_strings = Database::getConnection()->query("SHOW standard_conforming_strings")->fetchField();
return ($standard_conforming_strings == 'on');
}
/**
* Generic function to check postgresql extensions.
*/
public function checkExtensions() {
$connection = Database::getConnection();
try {
// Enable pg_trgm for PostgreSQL 13 or higher.
// @todo Remove this if-statement in D11 when the minimum required version
// for PostgreSQL becomes 13 or higher. https://www.drupal.org/i/3357409
if (version_compare($connection->version(), '13.0', '>=')) {
$connection->query('CREATE EXTENSION IF NOT EXISTS pg_trgm');
}
if ($connection->schema()->extensionExists('pg_trgm')) {
$this->pass(t('PostgreSQL has the pg_trgm extension enabled.'));
}
else {
$this->fail(t('The <a href=":pg_trgm">pg_trgm</a> PostgreSQL extension is not present. The extension is required by Drupal 10 to improve performance when using PostgreSQL. See <a href=":requirements">Drupal database server requirements</a> for more information.', [
':pg_trgm' => 'https://www.postgresql.org/docs/current/pgtrgm.html',
':requirements' => 'https://www.drupal.org/docs/system-requirements/database-server-requirements',
]));
}
}
catch (\Exception $e) {
$this->fail(t('Drupal could not check for the pg_trgm extension: @error.', ['@error' => $e->getMessage()]));
}
}
/**
* Make PostgreSQL Drupal friendly.
*/
public function initializeDatabase() {
// We create some functions using global names instead of prefixing them
// 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 {
$connection = Database::getConnection();
// When testing, two installs might try to run the CREATE FUNCTION queries
// at the same time. Do not let that happen.
$connection->query('SELECT pg_advisory_lock(1)');
// Don't use {} around pg_proc table.
if (!$connection->query("SELECT COUNT(*) FROM pg_proc WHERE proname = 'rand'")->fetchField()) {
$connection->query('CREATE OR REPLACE FUNCTION "rand"() RETURNS float AS
\'SELECT random();\'
LANGUAGE \'sql\'',
[],
['allow_delimiter_in_query' => TRUE]
);
}
if (!$connection->query("SELECT COUNT(*) FROM pg_proc WHERE proname = 'substring_index'")->fetchField()) {
$connection->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\'',
[],
['allow_delimiter_in_query' => TRUE, 'allow_square_brackets' => TRUE]
);
}
$connection->query('SELECT pg_advisory_unlock(1)');
$this->pass(t('PostgreSQL has initialized itself.'));
}
catch (\Exception $e) {
$this->fail(t('Drupal could not be correctly setup with the existing database due to the following error: @error.', ['@error' => $e->getMessage()]));
}
}
/**
* {@inheritdoc}
*/
public function getFormOptions(array $database) {
$form = parent::getFormOptions($database);
if (empty($form['advanced_options']['port']['#default_value'])) {
$form['advanced_options']['port']['#default_value'] = '5432';
}
return $form;
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace Drupal\pgsql\Driver\Database\pgsql;
use Drupal\Core\Database\Query\Merge as QueryMerge;
/**
* PostgreSQL implementation of \Drupal\Core\Database\Query\Merge.
*/
class Merge extends QueryMerge {
/**
* {@inheritdoc}
*/
public function __construct(Connection $connection, string $table, array $options = []) {
// @todo Remove the __construct in Drupal 11.
// @see https://www.drupal.org/project/drupal/issues/3256524
parent::__construct($connection, $table, $options);
unset($this->queryOptions['return']);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,170 @@
<?php
namespace Drupal\pgsql\Driver\Database\pgsql;
use Drupal\Core\Database\Query\Select as QuerySelect;
/**
* @addtogroup database
* @{
*/
/**
* PostgreSQL implementation of \Drupal\Core\Database\Query\Select.
*/
class Select extends QuerySelect {
/**
* {@inheritdoc}
*/
public function __construct(Connection $connection, $table, $alias = NULL, array $options = []) {
// @todo Remove the __construct in Drupal 11.
// @see https://www.drupal.org/project/drupal/issues/3256524
parent::__construct($connection, $table, $alias, $options);
unset($this->queryOptions['return']);
}
public function orderRandom() {
$alias = $this->addExpression('RANDOM()', 'random_field');
$this->orderBy($alias);
return $this;
}
/**
* Overrides SelectQuery::orderBy().
*
* PostgreSQL adheres strictly to the SQL-92 standard and requires that when
* using DISTINCT or GROUP BY conditions, fields and expressions that are
* ordered on also need to be selected. This is a best effort implementation
* to handle the cases that can be automated by adding the field if it is not
* yet selected.
*
* @code
* $query = \Drupal::database()->select('example', 'e');
* $query->join('example_revision', 'er', '[e].[vid] = [er].[vid]');
* $query
* ->distinct()
* ->fields('e')
* ->orderBy('timestamp');
* @endcode
*
* In this query, it is not possible (without relying on the schema) to know
* whether timestamp belongs to example_revision and needs to be added or
* belongs to node and is already selected. Queries like this will need to be
* corrected in the original query by adding an explicit call to
* SelectQuery::addField() or SelectQuery::fields().
*
* Since this has a small performance impact, both by the additional
* processing in this function and in the database that needs to return the
* additional fields, this is done as an override instead of implementing it
* directly in SelectQuery::orderBy().
*/
public function orderBy($field, $direction = 'ASC') {
// Only allow ASC and DESC, default to ASC.
// Emulate MySQL default behavior to sort NULL values first for ascending,
// and last for descending.
// @see http://www.postgresql.org/docs/9.3/static/queries-order.html
$direction = strtoupper($direction) == 'DESC' ? 'DESC NULLS LAST' : 'ASC NULLS FIRST';
$this->order[$field] = $direction;
if ($this->hasTag('entity_query')) {
return $this;
}
// If there is a table alias specified, split it up.
if (str_contains($field, '.')) {
[$table, $table_field] = explode('.', $field);
}
// Figure out if the field has already been added.
foreach ($this->fields as $existing_field) {
if (!empty($table)) {
// If table alias is given, check if field and table exists.
if ($existing_field['table'] == $table && $existing_field['field'] == $table_field) {
return $this;
}
}
else {
// If there is no table, simply check if the field exists as a field or
// an aliased field.
if ($existing_field['alias'] == $field) {
return $this;
}
}
}
// Also check expression aliases.
foreach ($this->expressions as $expression) {
if ($expression['alias'] == $this->connection->escapeAlias($field)) {
return $this;
}
}
// If a table loads all fields, it can not be added again. It would
// result in an ambiguous alias error because that field would be loaded
// twice: Once through table_alias.* and once directly. If the field
// actually belongs to a different table, it must be added manually.
foreach ($this->tables as $table) {
if (!empty($table['all_fields'])) {
return $this;
}
}
// If $field contains characters which are not allowed in a field name
// it is considered an expression, these can't be handled automatically
// either.
if ($this->connection->escapeField($field) != $field) {
return $this;
}
// This is a case that can be handled automatically, add the field.
$this->addField(NULL, $field);
return $this;
}
/**
* {@inheritdoc}
*/
public function addExpression($expression, $alias = NULL, $arguments = []) {
if (empty($alias)) {
$alias = 'expression';
}
// This implements counting in the same manner as the parent method.
$alias_candidate = $alias;
$count = 2;
while (!empty($this->expressions[$alias_candidate])) {
$alias_candidate = $alias . '_' . $count++;
}
$alias = $alias_candidate;
$this->expressions[$alias] = [
'expression' => $expression,
'alias' => $this->connection->escapeAlias($alias_candidate),
'arguments' => $arguments,
];
return $alias;
}
/**
* {@inheritdoc}
*/
public function execute() {
$this->connection->addSavepoint();
try {
$result = parent::execute();
}
catch (\Exception $e) {
$this->connection->rollbackSavepoint();
throw $e;
}
$this->connection->releaseSavepoint();
return $result;
}
}
/**
* @} End of "addtogroup database".
*/

View File

@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace Drupal\pgsql\Driver\Database\pgsql;
use Drupal\Core\Database\Transaction\ClientConnectionTransactionState;
use Drupal\Core\Database\Transaction\TransactionManagerBase;
/**
* PostgreSql implementation of TransactionManagerInterface.
*/
class TransactionManager extends TransactionManagerBase {
/**
* {@inheritdoc}
*/
protected function beginClientTransaction(): bool {
return $this->connection->getClientConnection()->beginTransaction();
}
/**
* {@inheritdoc}
*/
protected function rollbackClientTransaction(): bool {
$clientRollback = $this->connection->getClientConnection()->rollBack();
$this->setConnectionTransactionState($clientRollback ?
ClientConnectionTransactionState::RolledBack :
ClientConnectionTransactionState::RollbackFailed
);
return $clientRollback;
}
/**
* {@inheritdoc}
*/
protected function commitClientTransaction(): bool {
$clientCommit = $this->connection->getClientConnection()->commit();
$this->setConnectionTransactionState($clientCommit ?
ClientConnectionTransactionState::Committed :
ClientConnectionTransactionState::CommitFailed
);
return $clientCommit;
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace Drupal\pgsql\Driver\Database\pgsql;
use Drupal\Core\Database\Query\Truncate as QueryTruncate;
/**
* PostgreSQL implementation of \Drupal\Core\Database\Query\Truncate.
*/
class Truncate extends QueryTruncate {
/**
* {@inheritdoc}
*/
public function __construct(Connection $connection, string $table, array $options = []) {
// @todo Remove the __construct in Drupal 11.
// @see https://www.drupal.org/project/drupal/issues/3256524
parent::__construct($connection, $table, $options);
unset($this->queryOptions['return']);
}
/**
* {@inheritdoc}
*/
public function execute() {
$this->connection->addSavepoint();
try {
$result = parent::execute();
}
catch (\Exception $e) {
$this->connection->rollbackSavepoint();
throw $e;
}
$this->connection->releaseSavepoint();
return $result;
}
}

View File

@@ -0,0 +1,93 @@
<?php
namespace Drupal\pgsql\Driver\Database\pgsql;
use Drupal\Core\Database\Query\Update as QueryUpdate;
use Drupal\Core\Database\Query\SelectInterface;
/**
* PostgreSQL implementation of \Drupal\Core\Database\Query\Update.
*/
class Update extends QueryUpdate {
/**
* {@inheritdoc}
*/
public function __construct(Connection $connection, string $table, array $options = []) {
// @todo Remove the __construct in Drupal 11.
// @see https://www.drupal.org/project/drupal/issues/3256524
parent::__construct($connection, $table, $options);
unset($this->queryOptions['return']);
}
public function execute() {
$max_placeholder = 0;
$blobs = [];
$blob_count = 0;
// Because we filter $fields the same way here and in __toString(), the
// placeholders will all match up properly.
$stmt = $this->connection->prepareStatement((string) $this, $this->queryOptions, TRUE);
// Fetch the list of blobs and sequences used on that table.
$table_information = $this->connection->schema()->queryTableInformation($this->table);
// Expressions take priority over literal fields, so we process those first
// and remove any literal fields that conflict.
$fields = $this->fields;
foreach ($this->expressionFields as $field => $data) {
if (!empty($data['arguments'])) {
foreach ($data['arguments'] as $placeholder => $argument) {
// We assume that an expression will never happen on a BLOB field,
// which is a fairly safe assumption to make since in most cases
// it would be an invalid query anyway.
$stmt->getClientStatement()->bindParam($placeholder, $data['arguments'][$placeholder]);
}
}
if ($data['expression'] instanceof SelectInterface) {
$data['expression']->compile($this->connection, $this);
$select_query_arguments = $data['expression']->arguments();
foreach ($select_query_arguments as $placeholder => $argument) {
$stmt->getClientStatement()->bindParam($placeholder, $select_query_arguments[$placeholder]);
}
}
unset($fields[$field]);
}
foreach ($fields as $field => $value) {
$placeholder = ':db_update_placeholder_' . ($max_placeholder++);
if (isset($table_information->blob_fields[$field]) && $value !== NULL) {
$blobs[$blob_count] = fopen('php://memory', 'a');
fwrite($blobs[$blob_count], $value);
rewind($blobs[$blob_count]);
$stmt->getClientStatement()->bindParam($placeholder, $blobs[$blob_count], \PDO::PARAM_LOB);
++$blob_count;
}
else {
$stmt->getClientStatement()->bindParam($placeholder, $fields[$field]);
}
}
if (count($this->condition)) {
$this->condition->compile($this->connection, $this);
$arguments = $this->condition->arguments();
foreach ($arguments as $placeholder => $value) {
$stmt->getClientStatement()->bindParam($placeholder, $arguments[$placeholder]);
}
}
$this->connection->addSavepoint();
try {
$stmt->execute(NULL, $this->queryOptions);
$this->connection->releaseSavepoint();
return $stmt->rowCount();
}
catch (\Exception $e) {
$this->connection->rollbackSavepoint();
$this->connection->exceptionHandler()->handleExecutionException($e, $stmt, [], $this->queryOptions);
}
}
}

View File

@@ -0,0 +1,136 @@
<?php
namespace Drupal\pgsql\Driver\Database\pgsql;
use Drupal\Core\Database\Query\Upsert as QueryUpsert;
// cSpell:ignore nextval setval
/**
* PostgreSQL implementation of \Drupal\Core\Database\Query\Upsert.
*/
class Upsert extends QueryUpsert {
/**
* {@inheritdoc}
*/
public function __construct(Connection $connection, string $table, array $options = []) {
// @todo Remove the __construct in Drupal 11.
// @see https://www.drupal.org/project/drupal/issues/3256524
parent::__construct($connection, $table, $options);
unset($this->queryOptions['return']);
}
/**
* {@inheritdoc}
*/
public function execute() {
if (!$this->preExecute()) {
return NULL;
}
$stmt = $this->connection->prepareStatement((string) $this, $this->queryOptions, TRUE);
// 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]) && $insert_values[$idx] !== NULL) {
$blobs[$blob_count] = fopen('php://memory', 'a');
fwrite($blobs[$blob_count], $insert_values[$idx]);
rewind($blobs[$blob_count]);
$stmt->getClientStatement()->bindParam(':db_insert_placeholder_' . $max_placeholder++, $blobs[$blob_count], \PDO::PARAM_LOB);
// Pre-increment is faster in PHP than increment.
++$blob_count;
}
else {
$stmt->getClientStatement()->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 . "}", [':serial_value' => (int) $serial_value]);
}
}
}
}
$options = $this->queryOptions;
if (!empty($table_information->sequences)) {
$options['sequence_name'] = $table_information->sequences[0];
}
// Re-initialize the values array so that we can re-use this query.
$this->insertValues = [];
// Create a savepoint so we can rollback a failed query. This is so we can
// mimic MySQL and SQLite transactions which don't fail if a single query
// fails. This is important for tables that are created on demand. For
// example, \Drupal\Core\Cache\DatabaseBackend.
$this->connection->addSavepoint();
try {
$stmt->execute(NULL, $options);
$this->connection->releaseSavepoint();
return $stmt->rowCount();
}
catch (\Exception $e) {
$this->connection->rollbackSavepoint();
$this->connection->exceptionHandler()->handleExecutionException($e, $stmt, [], $options);
}
}
/**
* {@inheritdoc}
*/
public function __toString() {
// Create a sanitized comment string to prepend to the query.
$comments = $this->connection->makeComment($this->comments);
// Default fields are always placed first for consistency.
$insert_fields = array_merge($this->defaultFields, $this->insertFields);
$insert_fields = array_map(function ($field) {
return $this->connection->escapeField($field);
}, $insert_fields);
$query = $comments . 'INSERT INTO {' . $this->table . '} (' . implode(', ', $insert_fields) . ') VALUES ';
$values = $this->getInsertPlaceholderFragment($this->insertValues, $this->defaultFields);
$query .= implode(', ', $values);
// Updating the unique / primary key is not necessary.
unset($insert_fields[$this->key]);
$update = [];
foreach ($insert_fields as $field) {
// The "excluded." prefix causes the field to refer to the value for field
// that would have been inserted had there been no conflict.
$update[] = "$field = EXCLUDED.$field";
}
$query .= ' ON CONFLICT (' . $this->connection->escapeField($this->key) . ') DO UPDATE SET ' . implode(', ', $update);
return $query;
}
}

View File

@@ -0,0 +1,234 @@
<?php
namespace Drupal\pgsql;
use Drupal\Core\Database\Connection;
use Drupal\Core\Database\DatabaseExceptionWrapper;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Entity\EntityLastInstalledSchemaRepositoryInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\Sql\SqlContentEntityStorage;
use Drupal\Core\Extension\ModuleExtensionList;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\StringTranslation\PluralTranslatableMarkup;
use Symfony\Component\DependencyInjection\ContainerInterface;
// cSpell:ignore relkind objid regclass
/**
* An update class for sequence ownership.
* @see https://www.drupal.org/i/3028706
*
* @internal
*/
class Update10101 implements ContainerInjectionInterface {
/**
* Sequence owner update constructor.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
* The entity type manager.
* @param \Drupal\Core\Entity\EntityLastInstalledSchemaRepositoryInterface $entityLastInstalledSchemaRepository
* The last installed schema repository service.
* @param \Drupal\Core\Database\Connection $connection
* The database connection.
* @param \Drupal\Core\Extension\ModuleExtensionList $moduleExtensionList
* The module extension list.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $moduleHandler
* The module handler service.
*/
public function __construct(
protected EntityTypeManagerInterface $entityTypeManager,
protected EntityLastInstalledSchemaRepositoryInterface $entityLastInstalledSchemaRepository,
protected Connection $connection,
protected ModuleExtensionList $moduleExtensionList,
protected ModuleHandlerInterface $moduleHandler,
) {
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('entity_type.manager'),
$container->get('entity.last_installed_schema.repository'),
$container->get('database'),
$container->get('extension.list.module'),
$container->get('module_handler')
);
}
/**
* Update *all* existing sequences to include the owner tables.
*
* @param array $sandbox
* Stores information for batch updates.
*
* @return \Drupal\Core\StringTranslation\PluralTranslatableMarkup|null
* Returns the amount of orphaned sequences fixed.
*/
public function update(array &$sandbox): ?PluralTranslatableMarkup {
if ($this->connection->databaseType() !== 'pgsql') {
// This database update is a no-op for all other core database drivers.
$sandbox['#finished'] = 1;
return NULL;
}
if (!isset($sandbox['progress'])) {
$sandbox['fixed'] = 0;
$sandbox['progress'] = 0;
$sandbox['tables'] = [];
// Discovers all tables defined with hook_schema().
// @todo We need to add logic to do the same for on-demand tables. See
// https://www.drupal.org/i/3358777
$modules = $this->moduleExtensionList->getList();
foreach ($modules as $extension) {
$module = $extension->getName();
$this->moduleHandler->loadInclude($module, 'install');
$schema = $this->moduleHandler->invoke($module, 'schema');
if (!empty($schema)) {
foreach ($schema as $table_name => $table_info) {
foreach ($table_info['fields'] as $column_name => $column_info) {
if (str_starts_with($column_info['type'], 'serial')) {
$sandbox['tables'][] = [
'table' => $table_name,
'column' => $column_name,
];
}
}
}
}
}
// Discovers all content entity types with integer entity keys that are
// most likely serial columns.
$entity_types = $this->entityTypeManager->getDefinitions();
/** @var \Drupal\Core\Entity\EntityTypeInterface $entity_type */
foreach ($entity_types as $entity_type) {
$storage_class = $entity_type->getStorageClass();
if (is_subclass_of($storage_class, SqlContentEntityStorage::class)) {
$id_key = $entity_type->getKey('id');
$revision_key = $entity_type->getKey('revision');
$original_storage_definitions = $this->entityLastInstalledSchemaRepository->getLastInstalledFieldStorageDefinitions($entity_type->id());
if ($original_storage_definitions[$id_key]->getType() === 'integer') {
$sandbox['tables'][] = [
'table' => $entity_type->getBaseTable(),
'column' => $id_key,
];
}
if ($entity_type->isRevisionable() &&
$original_storage_definitions[$revision_key]->getType() === 'integer') {
$sandbox['tables'][] = [
'table' => $entity_type->getRevisionTable(),
'column' => $revision_key,
];
}
}
}
$sandbox['max'] = count($sandbox['tables']);
}
else {
// Adds ownership of orphan sequences to tables.
$to_process = array_slice($sandbox['tables'], $sandbox['progress'], 50);
// Ensures that a sequence is not owned first, then ensures that the a
// sequence exists at all before trying to alter it.
foreach ($to_process as $table_info) {
if ($this->connection->schema()->tableExists($table_info['table'])) {
$owned = (bool) $this->getSequenceName($table_info['table'], $table_info['column']);
if (!$owned) {
$sequence_name = $this->connection
->makeSequenceName($table_info['table'], $table_info['column']);
$exists = $this->sequenceExists($sequence_name);
if ($exists) {
$transaction = $this->connection->startTransaction($sequence_name);
try {
$this->updateSequenceOwnership($sequence_name, $table_info['table'], $table_info['column']);
$sandbox['fixed']++;
}
catch (DatabaseExceptionWrapper $e) {
$transaction->rollBack();
}
}
}
}
$sandbox['progress']++;
}
}
if ($sandbox['max'] && $sandbox['progress'] < $sandbox['max']) {
$sandbox['#finished'] = $sandbox['progress'] / $sandbox['max'];
return NULL;
}
else {
$sandbox['#finished'] = 1;
return new PluralTranslatableMarkup(
$sandbox['fixed'],
'1 orphaned sequence fixed.',
'@count orphaned sequences fixed'
);
}
}
/**
* Alters the ownership of a sequence.
*
* This is used for updating orphaned sequences.
*
* @param string $sequence_name
* The appropriate sequence name for a given table and serial field.
* @param string $table
* The unquoted or prefixed table name.
* @param string $column
* The column name for the sequence.
*
* @see https://www.drupal.org/i/3028706
*/
private function updateSequenceOwnership(string $sequence_name, string $table, string $column): void {
$this->connection->query('ALTER SEQUENCE IF EXISTS ' . $sequence_name . ' OWNED BY {' . $table . '}.[' . $column . ']');
}
/**
* Retrieves a sequence name that is owned by the table and column.
*
* @param string $table
* A table name that is not prefixed or quoted.
* @param string $column
* The column name.
*
* @return string|null
* The name of the sequence or NULL if it does not exist.
*/
public function getSequenceName(string $table, string $column): ?string {
return $this->connection
->query("SELECT pg_get_serial_sequence(:table, :column)", [
':table' => $this->connection->getPrefix() . $table,
':column' => $column,
])
->fetchField();
}
/**
* Checks if a sequence exists.
*
* @param string $name
* The fully-qualified sequence name.
*
* @return bool
* TRUE if the sequence exists by the name.
*
* @see \Drupal\pgsql\Driver\Database\pgsql\Connection::makeSequenceName()
*/
private function sequenceExists(string $name): bool {
return (bool) \Drupal::database()
->query("SELECT c.relname FROM pg_class as c WHERE c.relkind = 'S' AND c.relname = :name", [':name' => $name])
->fetchField();
}
}

View File

@@ -0,0 +1,43 @@
<?php
// @codingStandardsIgnoreFile
use Drupal\Core\Database\Database;
$connection = Database::getConnection();
$db_type = $connection->databaseType();
// Creates a table, then adds a sequence without ownership to simulate tables
// that were altered from integer to serial columns.
$connection
->schema()
->createTable('pgsql_sequence_test', [
'fields' => [
'sequence_field' => [
'type' => 'int',
'not null' => TRUE,
'unsigned' => TRUE,
],
],
'primary key' => ['sequence_field'],
]);
$seq = $connection
->makeSequenceName('pgsql_sequence_test', 'sequence_field');
$connection->query('CREATE SEQUENCE ' . $seq);
// Enables the pgsql_test module so that the pgsql_sequence_test schema will
// be available.
$extensions = $connection
->query("SELECT data FROM {config} where name = 'core.extension'")
->fetchField();
$extensions = unserialize($extensions);
$extensions['module']['pgsql_test'] = 1;
$connection
->update('config')
->fields(['data' => serialize($extensions)])
->condition('name', 'core.extension')
->execute();
$connection
->delete('cache_config')
->condition('cid', 'core.extension')
->execute();

View File

@@ -0,0 +1,81 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\pgsql\Functional\Database;
use Drupal\FunctionalTests\Update\UpdatePathTestBase;
use Drupal\Core\Database\Database;
use Drupal\pgsql\Update10101;
// cSpell:ignore objid refobjid regclass attname attrelid attnum refobjsubid
/**
* Tests that any unowned sequences created previously have a table owner.
*
* The update path only applies to Drupal sites using the pgsql driver.
*
* @group Database
*/
class PostgreSqlSequenceUpdateTest extends UpdatePathTestBase {
/**
* The database connection to use.
*
* @var \Drupal\Core\Database\Connection
*/
protected $connection;
/**
* {@inheritdoc}
*/
protected function runDbTasks() {
parent::runDbTasks();
$this->connection = Database::getConnection();
if ($this->connection->driver() !== 'pgsql') {
$this->markTestSkipped('This test only works with the pgsql driver');
}
}
/**
* {@inheritdoc}
*/
protected function setDatabaseDumpFiles() {
$this->databaseDumpFiles = [
__DIR__ . '/../../../../../system/tests/fixtures/update/drupal-9.4.0.bare.standard.php.gz',
__DIR__ . '/../../../fixtures/update/drupal-9.pgsql-orphan-sequence.php',
];
}
/**
* Asserts that a newly created sequence has the correct ownership.
*/
public function testPostgreSqlSequenceUpdate(): void {
$this->assertFalse($this->getSequenceOwner('pgsql_sequence_test', 'sequence_field'));
// Run the updates.
$this->runUpdates();
$seq_owner = $this->getSequenceOwner('pgsql_sequence_test', 'sequence_field');
$this->assertEquals($this->connection->getPrefix() . 'pgsql_sequence_test', $seq_owner->table_name);
$this->assertEquals('sequence_field', $seq_owner->field_name, 'Sequence is owned by the table and column.');
}
/**
* Retrieves the sequence owner object.
*
* @return object|bool
* Returns the sequence owner object or bool if it does not exist.
*/
protected function getSequenceOwner(string $table, string $field): object|bool {
$update_sequence = \Drupal::classResolver(Update10101::class);
$seq_name = $update_sequence->getSequenceName($table, $field);
return \Drupal::database()->query("SELECT d.refobjid::regclass as table_name, a.attname as field_name
FROM pg_depend d
JOIN pg_attribute a ON a.attrelid = d.refobjid AND a.attnum = d.refobjsubid
WHERE d.objid = :seq_name::regclass
AND d.refobjsubid > 0
AND d.classid = 'pg_class'::regclass", [':seq_name' => $seq_name])->fetchObject();
}
}

View File

@@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\pgsql\Functional;
use Drupal\Tests\system\Functional\Module\GenericModuleTestBase;
/**
* Generic module test for pgsql.
*
* @group pgsql
*/
class GenericTest extends GenericModuleTestBase {}

View File

@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\pgsql\Kernel\pgsql;
use Drupal\KernelTests\Core\Database\DriverSpecificConnectionUnitTestBase;
/**
* PostgreSQL-specific connection unit tests.
*
* @group Database
*/
class ConnectionUnitTest extends DriverSpecificConnectionUnitTestBase {
/**
* Returns a set of queries specific for PostgreSQL.
*/
protected function getQuery(): array {
return [
'connection_id' => 'SELECT pg_backend_pid()',
'processlist' => 'SELECT pid FROM pg_stat_activity',
'show_tables' => 'SELECT * FROM pg_catalog.pg_tables',
];
}
}

View File

@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\pgsql\Kernel\pgsql;
use Drupal\KernelTests\Core\Database\DriverSpecificKernelTestBase;
/**
* Tests exceptions thrown by queries.
*
* @group Database
*/
class DatabaseExceptionWrapperTest extends DriverSpecificKernelTestBase {
/**
* Tests Connection::prepareStatement exception on execution.
*/
public function testPrepareStatementFailOnExecution(): void {
$this->expectException(\PDOException::class);
$stmt = $this->connection->prepareStatement('bananas', []);
$stmt->execute();
}
}

View File

@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\pgsql\Kernel\pgsql;
use Drupal\KernelTests\Core\Database\DriverSpecificKernelTestBase;
/**
* @coversDefaultClass \Drupal\KernelTests\KernelTestBase
*
* @group KernelTests
* @group Database
*/
class KernelTestBaseTest extends DriverSpecificKernelTestBase {
/**
* @covers ::setUp
*/
public function testSetUp(): void {
// Ensure that the database tasks have been run during set up.
$this->assertSame('on', $this->connection->query("SHOW standard_conforming_strings")->fetchField());
$this->assertSame('escape', $this->connection->query("SHOW bytea_output")->fetchField());
}
}

View File

@@ -0,0 +1,363 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\pgsql\Kernel\pgsql;
use Drupal\Core\Database\Database;
use Drupal\Core\Database\Connection;
use Drupal\KernelTests\Core\Database\DatabaseTestSchemaDataTrait;
use Drupal\KernelTests\Core\Database\DatabaseTestSchemaInstallTrait;
use Drupal\KernelTests\Core\Database\DriverSpecificKernelTestBase;
// cSpell:ignore nspname schemaname upserting indexdef
/**
* Tests schema API for non-public schema for the PostgreSQL driver.
*
* @group Database
* @coversDefaultClass \Drupal\pgsql\Driver\Database\pgsql\Schema
*/
class NonPublicSchemaTest extends DriverSpecificKernelTestBase {
use DatabaseTestSchemaDataTrait;
use DatabaseTestSchemaInstallTrait;
/**
* The database connection for testing.
*
* @var \Drupal\Core\Database\Connection
*/
protected Connection $testingFakeConnection;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Create a connection to the non-public schema.
$info = Database::getConnectionInfo('default');
$info['default']['schema'] = 'testing_fake';
Database::getConnection()->query('CREATE SCHEMA IF NOT EXISTS testing_fake');
Database::addConnectionInfo('default', 'testing_fake', $info['default']);
$this->testingFakeConnection = Database::getConnection('testing_fake', 'default');
$table_specification = [
'description' => 'Schema table description may contain "quotes" and could be long—very long indeed.',
'fields' => [
'id' => [
'type' => 'serial',
'not null' => TRUE,
],
'test_field' => [
'type' => 'int',
'not null' => TRUE,
],
],
'primary key' => ['id'],
];
$this->testingFakeConnection->schema()->createTable('faking_table', $table_specification);
$this->testingFakeConnection->insert('faking_table')
->fields(
[
'id' => '1',
'test_field' => '123',
]
)->execute();
$this->testingFakeConnection->insert('faking_table')
->fields(
[
'id' => '2',
'test_field' => '456',
]
)->execute();
$this->testingFakeConnection->insert('faking_table')
->fields(
[
'id' => '3',
'test_field' => '789',
]
)->execute();
}
/**
* {@inheritdoc}
*/
protected function tearDown(): void {
// We overwrite this function because the regular teardown will not drop the
// tables from a specified schema.
$tables = $this->testingFakeConnection->schema()->findTables('%');
foreach ($tables as $table) {
if ($this->testingFakeConnection->schema()->dropTable($table)) {
unset($tables[$table]);
}
}
$this->assertEmpty($this->testingFakeConnection->schema()->findTables('%'));
Database::removeConnection('testing_fake');
parent::tearDown();
}
/**
* @covers ::extensionExists
* @covers ::tableExists
*/
public function testExtensionExists(): void {
// Check if PG_trgm extension is present.
$this->assertTrue($this->testingFakeConnection->schema()->extensionExists('pg_trgm'));
// Asserting that the Schema testing_fake exist in the database.
$this->assertCount(1, \Drupal::database()->query("SELECT * FROM pg_catalog.pg_namespace WHERE nspname = 'testing_fake'")->fetchAll());
$this->assertTrue($this->testingFakeConnection->schema()->tableExists('faking_table'));
// Hardcoded assertion that we created the table in the non-public schema.
$this->assertCount(1, $this->testingFakeConnection->query("SELECT * FROM pg_tables WHERE schemaname = 'testing_fake' AND tablename = :prefixedTable", [':prefixedTable' => $this->testingFakeConnection->getPrefix() . "faking_table"])->fetchAll());
}
/**
* @covers ::addField
* @covers ::fieldExists
* @covers ::dropField
* @covers ::changeField
*/
public function testField(): void {
$this->testingFakeConnection->schema()->addField('faking_table', 'added_field', ['type' => 'int', 'not null' => FALSE]);
$this->assertTrue($this->testingFakeConnection->schema()->fieldExists('faking_table', 'added_field'));
$this->testingFakeConnection->schema()->changeField('faking_table', 'added_field', 'changed_field', ['type' => 'int', 'not null' => FALSE]);
$this->assertFalse($this->testingFakeConnection->schema()->fieldExists('faking_table', 'added_field'));
$this->assertTrue($this->testingFakeConnection->schema()->fieldExists('faking_table', 'changed_field'));
$this->testingFakeConnection->schema()->dropField('faking_table', 'changed_field');
$this->assertFalse($this->testingFakeConnection->schema()->fieldExists('faking_table', 'changed_field'));
}
/**
* @covers \Drupal\Core\Database\Connection::insert
* @covers \Drupal\Core\Database\Connection::select
*/
public function testInsert(): void {
$num_records_before = $this->testingFakeConnection->query('SELECT COUNT(*) FROM {faking_table}')->fetchField();
$this->testingFakeConnection->insert('faking_table')
->fields([
'id' => '123',
'test_field' => '55',
])->execute();
// Testing that the insert is correct.
$results = $this->testingFakeConnection->select('faking_table')->fields('faking_table')->condition('id', '123')->execute()->fetchAll();
$this->assertIsArray($results);
$num_records_after = $this->testingFakeConnection->query('SELECT COUNT(*) FROM {faking_table}')->fetchField();
$this->assertEquals($num_records_before + 1, $num_records_after, 'Merge inserted properly.');
$this->assertSame('123', $results[0]->id);
$this->assertSame('55', $results[0]->test_field);
}
/**
* @covers \Drupal\Core\Database\Connection::update
*/
public function testUpdate(): void {
$updated_record = $this->testingFakeConnection->update('faking_table')
->fields(['test_field' => 321])
->condition('id', 1)
->execute();
$this->assertSame(1, $updated_record, 'Updated 1 record.');
$updated_results = $this->testingFakeConnection->select('faking_table')->fields('faking_table')->condition('id', '1')->execute()->fetchAll();
$this->assertSame('321', $updated_results[0]->test_field);
}
/**
* @covers \Drupal\Core\Database\Connection::upsert
*/
public function testUpsert(): void {
$num_records_before = $this->testingFakeConnection->query('SELECT COUNT(*) FROM {faking_table}')->fetchField();
$upsert = $this->testingFakeConnection->upsert('faking_table')
->key('id')
->fields(['id', 'test_field']);
// Upserting a new row.
$upsert->values([
'id' => '456',
'test_field' => '444',
]);
// Upserting an existing row.
$upsert->values([
'id' => '1',
'test_field' => '898',
]);
$result = $upsert->execute();
$this->assertSame(2, $result, 'The result of the upsert operation should report that at exactly two rows were affected.');
$num_records_after = $this->testingFakeConnection->query('SELECT COUNT(*) FROM {faking_table}')->fetchField();
$this->assertEquals($num_records_before + 1, $num_records_after, 'Merge inserted properly.');
// Check if new row has been added with upsert.
$result = $this->testingFakeConnection->query('SELECT * FROM {faking_table} WHERE [id] = :id', [':id' => '456'])->fetch();
$this->assertEquals('456', $result->id, 'ID set correctly.');
$this->assertEquals('444', $result->test_field, 'test_field set correctly.');
// Check if new row has been edited with upsert.
$result = $this->testingFakeConnection->query('SELECT * FROM {faking_table} WHERE [id] = :id', [':id' => '1'])->fetch();
$this->assertEquals('1', $result->id, 'ID set correctly.');
$this->assertEquals('898', $result->test_field, 'test_field set correctly.');
}
/**
* @covers \Drupal\Core\Database\Connection::merge
*/
public function testMerge(): void {
$num_records_before = $this->testingFakeConnection->query('SELECT COUNT(*) FROM {faking_table}')->fetchField();
$this->testingFakeConnection->merge('faking_table')
->key('id', '789')
->fields([
'test_field' => 343,
])
->execute();
$num_records_after = $this->testingFakeConnection->query('SELECT COUNT(*) FROM {faking_table}')->fetchField();
$this->assertEquals($num_records_before + 1, $num_records_after, 'Merge inserted properly.');
$merge_results = $this->testingFakeConnection->select('faking_table')->fields('faking_table')->condition('id', '789')->execute()->fetchAll();
$this->assertSame('789', $merge_results[0]->id);
$this->assertSame('343', $merge_results[0]->test_field);
}
/**
* @covers \Drupal\Core\Database\Connection::delete
* @covers \Drupal\Core\Database\Connection::truncate
*/
public function testDelete(): void {
$num_records_before = $this->testingFakeConnection->query('SELECT COUNT(*) FROM {faking_table}')->fetchField();
$num_deleted = $this->testingFakeConnection->delete('faking_table')
->condition('id', 3)
->execute();
$this->assertSame(1, $num_deleted, 'Deleted 1 record.');
$num_records_after = $this->testingFakeConnection->query('SELECT COUNT(*) FROM {faking_table}')->fetchField();
$this->assertEquals($num_records_before, $num_records_after + $num_deleted, 'Deletion adds up.');
$num_records_before = $this->testingFakeConnection->query("SELECT COUNT(*) FROM {faking_table}")->fetchField();
$this->assertNotEmpty($num_records_before);
$this->testingFakeConnection->truncate('faking_table')->execute();
$num_records_after = $this->testingFakeConnection->query("SELECT COUNT(*) FROM {faking_table}")->fetchField();
$this->assertEquals(0, $num_records_after, 'Truncate really deletes everything.');
}
/**
* @covers ::addIndex
* @covers ::indexExists
* @covers ::dropIndex
*/
public function testIndex(): void {
$this->testingFakeConnection->schema()->addIndex('faking_table', 'test_field', ['test_field'], []);
$this->assertTrue($this->testingFakeConnection->schema()->indexExists('faking_table', 'test_field'));
$results = $this->testingFakeConnection->query("SELECT * FROM pg_indexes WHERE indexname = :indexname", [':indexname' => $this->testingFakeConnection->getPrefix() . 'faking_table__test_field__idx'])->fetchAll();
$this->assertCount(1, $results);
$this->assertSame('testing_fake', $results[0]->schemaname);
$this->assertSame($this->testingFakeConnection->getPrefix() . 'faking_table', $results[0]->tablename);
$this->assertStringContainsString('USING btree (test_field)', $results[0]->indexdef);
$this->testingFakeConnection->schema()->dropIndex('faking_table', 'test_field');
$this->assertFalse($this->testingFakeConnection->schema()->indexExists('faking_table', 'test_field'));
}
/**
* @covers ::addUniqueKey
* @covers ::indexExists
* @covers ::dropUniqueKey
*/
public function testUniqueKey(): void {
$this->testingFakeConnection->schema()->addUniqueKey('faking_table', 'test_field', ['test_field']);
// This should work, but currently indexExist() only searches for keys that end with idx.
// @todo remove comments when: https://www.drupal.org/project/drupal/issues/3325358 is committed.
// $this->assertTrue($this->testingFakeConnection->schema()->indexExists('faking_table', 'test_field'));
$results = $this->testingFakeConnection->query("SELECT * FROM pg_indexes WHERE indexname = :indexname", [':indexname' => $this->testingFakeConnection->getPrefix() . 'faking_table__test_field__key'])->fetchAll();
// Check the unique key columns.
$this->assertCount(1, $results);
$this->assertSame('testing_fake', $results[0]->schemaname);
$this->assertSame($this->testingFakeConnection->getPrefix() . 'faking_table', $results[0]->tablename);
$this->assertStringContainsString('USING btree (test_field)', $results[0]->indexdef);
$this->testingFakeConnection->schema()->dropUniqueKey('faking_table', 'test_field');
// This function will not work due to a the fact that indexExist() does not search for keys without idx tag.
// @todo remove comments when: https://www.drupal.org/project/drupal/issues/3325358 is committed.
// $this->assertFalse($this->testingFakeConnection->schema()->indexExists('faking_table', 'test_field'));
}
/**
* @covers ::addPrimaryKey
* @covers ::dropPrimaryKey
*/
public function testPrimaryKey(): void {
$this->testingFakeConnection->schema()->dropPrimaryKey('faking_table');
$results = $this->testingFakeConnection->query("SELECT * FROM pg_indexes WHERE schemaname = 'testing_fake'")->fetchAll();
$this->assertCount(0, $results);
$this->testingFakeConnection->schema()->addPrimaryKey('faking_table', ['id']);
$results = $this->testingFakeConnection->query("SELECT * FROM pg_indexes WHERE schemaname = 'testing_fake'")->fetchAll();
$this->assertCount(1, $results);
$this->assertSame('testing_fake', $results[0]->schemaname);
$this->assertSame($this->testingFakeConnection->getPrefix() . 'faking_table', $results[0]->tablename);
$this->assertStringContainsString('USING btree (id)', $results[0]->indexdef);
$find_primary_keys_columns = new \ReflectionMethod(get_class($this->testingFakeConnection->schema()), 'findPrimaryKeyColumns');
$results = $find_primary_keys_columns->invoke($this->testingFakeConnection->schema(), 'faking_table');
$this->assertCount(1, $results);
$this->assertSame('id', $results[0]);
}
/**
* @covers ::renameTable
* @covers ::tableExists
* @covers ::findTables
* @covers ::dropTable
*/
public function testTable(): void {
$this->testingFakeConnection->schema()->renameTable('faking_table', 'new_faking_table');
$tables = $this->testingFakeConnection->schema()->findTables('%');
$result = $this->testingFakeConnection->query("SELECT * FROM information_schema.tables WHERE table_schema = 'testing_fake'")->fetchAll();
$this->assertFalse($this->testingFakeConnection->schema()->tableExists('faking_table'));
$this->assertTrue($this->testingFakeConnection->schema()->tableExists('new_faking_table'));
$this->assertEquals($this->testingFakeConnection->getPrefix() . 'new_faking_table', $result[0]->table_name);
$this->assertEquals('testing_fake', $result[0]->table_schema);
sort($tables);
$this->assertEquals(['new_faking_table'], $tables);
$this->testingFakeConnection->schema()->dropTable('new_faking_table');
$this->assertFalse($this->testingFakeConnection->schema()->tableExists('new_faking_table'));
$this->assertCount(0, $this->testingFakeConnection->query("SELECT * FROM information_schema.tables WHERE table_schema = 'testing_fake'")->fetchAll());
}
}

View File

@@ -0,0 +1,108 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\pgsql\Kernel\pgsql;
use Drupal\Core\Database\Driver\pgsql\Connection;
use Drupal\Core\Database\Driver\pgsql\Delete;
use Drupal\Core\Database\Driver\pgsql\Install\Tasks;
use Drupal\Core\Database\Driver\pgsql\Insert;
use Drupal\Core\Database\Driver\pgsql\Schema;
use Drupal\Core\Database\Driver\pgsql\Select;
use Drupal\Core\Database\Driver\pgsql\Truncate;
use Drupal\Core\Database\Driver\pgsql\Update;
use Drupal\Core\Database\Driver\pgsql\Upsert;
use Drupal\KernelTests\Core\Database\DriverSpecificDatabaseTestBase;
use Drupal\Tests\Core\Database\Stub\StubPDO;
/**
* Tests the deprecations of the PostgreSQL database driver classes in Core.
*
* @group legacy
* @group Database
*/
class PgsqlDriverLegacyTest extends DriverSpecificDatabaseTestBase {
/**
* @covers Drupal\Core\Database\Driver\pgsql\Install\Tasks
*/
public function testDeprecationInstallTasks(): void {
$this->expectDeprecation('\Drupal\Core\Database\Driver\pgsql\Install\Tasks is deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The PostgreSQL database driver has been moved to the pgsql module. See https://www.drupal.org/node/3129492');
$tasks = new Tasks();
$this->assertInstanceOf(Tasks::class, $tasks);
}
/**
* @covers Drupal\Core\Database\Driver\pgsql\Connection
*/
public function testDeprecationConnection(): void {
$this->expectDeprecation('\Drupal\Core\Database\Driver\pgsql\Connection is deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The PostgreSQL database driver has been moved to the pgsql module. See https://www.drupal.org/node/3129492');
$connection = new Connection($this->createMock(StubPDO::class), []);
$this->assertInstanceOf(Connection::class, $connection);
}
/**
* @covers Drupal\Core\Database\Driver\pgsql\Delete
*/
public function testDeprecationDelete(): void {
$this->expectDeprecation('\Drupal\Core\Database\Driver\pgsql\Delete is deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The PostgreSQL database driver has been moved to the pgsql module. See https://www.drupal.org/node/3129492');
$delete = new Delete($this->connection, 'test');
$this->assertInstanceOf(Delete::class, $delete);
}
/**
* @covers Drupal\Core\Database\Driver\pgsql\Insert
*/
public function testDeprecationInsert(): void {
$this->expectDeprecation('\Drupal\Core\Database\Driver\pgsql\Insert is deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The PostgreSQL database driver has been moved to the pgsql module. See https://www.drupal.org/node/3129492');
$insert = new Insert($this->connection, 'test');
$this->assertInstanceOf(Insert::class, $insert);
}
/**
* @covers Drupal\Core\Database\Driver\pgsql\Schema
*/
public function testDeprecationSchema(): void {
$this->expectDeprecation('\Drupal\Core\Database\Driver\pgsql\Schema is deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The PostgreSQL database driver has been moved to the pgsql module. See https://www.drupal.org/node/3129492');
$schema = new Schema($this->connection);
$this->assertInstanceOf(Schema::class, $schema);
}
/**
* @covers Drupal\Core\Database\Driver\pgsql\Select
*/
public function testDeprecationSelect(): void {
$this->expectDeprecation('\Drupal\Core\Database\Driver\pgsql\Select is deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The PostgreSQL database driver has been moved to the pgsql module. See https://www.drupal.org/node/3129492');
$select = new Select($this->connection, 'test');
$this->assertInstanceOf(Select::class, $select);
}
/**
* @covers Drupal\Core\Database\Driver\pgsql\Truncate
*/
public function testDeprecationTruncate(): void {
$this->expectDeprecation('\Drupal\Core\Database\Driver\pgsql\Truncate is deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The PostgreSQL database driver has been moved to the pgsql module. See https://www.drupal.org/node/3129492');
$truncate = new Truncate($this->connection, 'test');
$this->assertInstanceOf(Truncate::class, $truncate);
}
/**
* @covers Drupal\Core\Database\Driver\pgsql\Update
*/
public function testDeprecationUpdate(): void {
$this->expectDeprecation('\Drupal\Core\Database\Driver\pgsql\Update is deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The PostgreSQL database driver has been moved to the pgsql module. See https://www.drupal.org/node/3129492');
$update = new Update($this->connection, 'test');
$this->assertInstanceOf(Update::class, $update);
}
/**
* @covers Drupal\Core\Database\Driver\pgsql\Upsert
*/
public function testDeprecationUpsert(): void {
$this->expectDeprecation('\Drupal\Core\Database\Driver\pgsql\Upsert is deprecated in drupal:9.4.0 and is removed from drupal:11.0.0. The PostgreSQL database driver has been moved to the pgsql module. See https://www.drupal.org/node/3129492');
$upsert = new Upsert($this->connection, 'test');
$this->assertInstanceOf(Upsert::class, $upsert);
}
}

View File

@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\pgsql\Kernel\pgsql\Plugin\views;
use Drupal\Tests\views\Kernel\Plugin\CastedIntFieldJoinTestBase;
/**
* Tests PostgreSQL specific cast handling.
*
* @group Database
*/
class PgsqlCastedIntFieldJoinTest extends CastedIntFieldJoinTestBase {
/**
* The db type that should be used for casting fields as integers.
*/
protected string $castingType = 'INTEGER';
}

View File

@@ -0,0 +1,386 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\pgsql\Kernel\pgsql;
use Drupal\KernelTests\Core\Database\DriverSpecificSchemaTestBase;
// cSpell:ignore relkind objid refobjid regclass attname attrelid attnum
// cSpell:ignore refobjsubid
/**
* Tests schema API for the PostgreSQL driver.
*
* @group Database
*/
class SchemaTest extends DriverSpecificSchemaTestBase {
/**
* {@inheritdoc}
*/
public function checkSchemaComment(string $description, string $table, ?string $column = NULL): void {
$this->assertSame($description, $this->schema->getComment($table, $column), 'The comment matches the schema description.');
}
/**
* {@inheritdoc}
*/
protected function checkSequenceRenaming(string $tableName): void {
// For PostgreSQL, we also need to check that the sequence has been renamed.
// The initial name of the sequence has been generated automatically by
// PostgreSQL when the table was created, however, on subsequent table
// renames the name is generated by Drupal and can not be easily
// re-constructed. Hence we can only check that we still have a sequence on
// the new table name.
$sequenceExists = (bool) $this->connection->query("SELECT pg_get_serial_sequence('{" . $tableName . "}', 'id')")->fetchField();
$this->assertTrue($sequenceExists, 'Sequence was renamed.');
// Rename the table again and repeat the check.
$anotherTableName = strtolower($this->getRandomGenerator()->name(63 - strlen($this->getDatabasePrefix())));
$this->schema->renameTable($tableName, $anotherTableName);
$sequenceExists = (bool) $this->connection->query("SELECT pg_get_serial_sequence('{" . $anotherTableName . "}', 'id')")->fetchField();
$this->assertTrue($sequenceExists, 'Sequence was renamed.');
}
/**
* {@inheritdoc}
*/
public function testTableWithSpecificDataType(): void {
$table_specification = [
'description' => 'Schema table description.',
'fields' => [
'timestamp' => [
'pgsql_type' => 'timestamp',
'not null' => FALSE,
'default' => NULL,
],
],
];
$this->schema->createTable('test_timestamp', $table_specification);
$this->assertTrue($this->schema->tableExists('test_timestamp'));
}
/**
* @covers \Drupal\pgsql\Driver\Database\pgsql\Schema::introspectIndexSchema
*/
public function testIntrospectIndexSchema(): void {
$table_specification = [
'fields' => [
'id' => [
'type' => 'int',
'not null' => TRUE,
'default' => 0,
],
'test_field_1' => [
'type' => 'int',
'not null' => TRUE,
'default' => 0,
],
'test_field_2' => [
'type' => 'int',
'default' => 0,
],
'test_field_3' => [
'type' => 'int',
'default' => 0,
],
'test_field_4' => [
'type' => 'int',
'default' => 0,
],
'test_field_5' => [
'type' => 'int',
'default' => 0,
],
],
'primary key' => ['id', 'test_field_1'],
'unique keys' => [
'test_field_2' => ['test_field_2'],
'test_field_3_test_field_4' => ['test_field_3', 'test_field_4'],
],
'indexes' => [
'test_field_4' => ['test_field_4'],
'test_field_4_test_field_5' => ['test_field_4', 'test_field_5'],
],
];
$table_name = strtolower($this->getRandomGenerator()->name());
$this->schema->createTable($table_name, $table_specification);
unset($table_specification['fields']);
$introspect_index_schema = new \ReflectionMethod(get_class($this->schema), 'introspectIndexSchema');
$index_schema = $introspect_index_schema->invoke($this->schema, $table_name);
// The PostgreSQL driver is using a custom naming scheme for its indexes, so
// we need to adjust the initial table specification.
$ensure_identifier_length = new \ReflectionMethod(get_class($this->schema), 'ensureIdentifiersLength');
foreach ($table_specification['unique keys'] as $original_index_name => $columns) {
unset($table_specification['unique keys'][$original_index_name]);
$new_index_name = $ensure_identifier_length->invoke($this->schema, $table_name, $original_index_name, 'key');
$table_specification['unique keys'][$new_index_name] = $columns;
}
foreach ($table_specification['indexes'] as $original_index_name => $columns) {
unset($table_specification['indexes'][$original_index_name]);
$new_index_name = $ensure_identifier_length->invoke($this->schema, $table_name, $original_index_name, 'idx');
$table_specification['indexes'][$new_index_name] = $columns;
}
$this->assertEquals($table_specification, $index_schema);
}
/**
* {@inheritdoc}
*/
public function testReservedKeywordsForNaming(): void {
$table_specification = [
'description' => 'A test table with an ANSI reserved keywords for naming.',
'fields' => [
'primary' => [
'description' => 'Simple unique ID.',
'type' => 'int',
'not null' => TRUE,
],
'update' => [
'description' => 'A column with reserved name.',
'type' => 'varchar',
'length' => 255,
],
],
'primary key' => ['primary'],
'unique keys' => [
'having' => ['update'],
],
'indexes' => [
'in' => ['primary', 'update'],
],
];
// Creating a table.
$table_name = 'select';
$this->schema->createTable($table_name, $table_specification);
$this->assertTrue($this->schema->tableExists($table_name));
// Finding all tables.
$tables = $this->schema->findTables('%');
sort($tables);
$this->assertEquals(['config', 'select'], $tables);
// Renaming a table.
$table_name_new = 'from';
$this->schema->renameTable($table_name, $table_name_new);
$this->assertFalse($this->schema->tableExists($table_name));
$this->assertTrue($this->schema->tableExists($table_name_new));
// Adding a field.
$field_name = 'delete';
$this->schema->addField($table_name_new, $field_name, ['type' => 'int', 'not null' => TRUE]);
$this->assertTrue($this->schema->fieldExists($table_name_new, $field_name));
// Dropping a primary key.
$this->schema->dropPrimaryKey($table_name_new);
// Adding a primary key.
$this->schema->addPrimaryKey($table_name_new, [$field_name]);
// Check the primary key columns.
$find_primary_key_columns = new \ReflectionMethod(get_class($this->schema), 'findPrimaryKeyColumns');
$this->assertEquals([$field_name], $find_primary_key_columns->invoke($this->schema, $table_name_new));
// Dropping a primary key.
$this->schema->dropPrimaryKey($table_name_new);
// Changing a field.
$field_name_new = 'where';
$this->schema->changeField($table_name_new, $field_name, $field_name_new, ['type' => 'int', 'not null' => FALSE]);
$this->assertFalse($this->schema->fieldExists($table_name_new, $field_name));
$this->assertTrue($this->schema->fieldExists($table_name_new, $field_name_new));
// Adding an unique key
$unique_key_name = $unique_key_introspect_name = 'unique';
$this->schema->addUniqueKey($table_name_new, $unique_key_name, [$field_name_new]);
// Check the unique key columns.
$introspect_index_schema = new \ReflectionMethod(get_class($this->schema), 'introspectIndexSchema');
$ensure_identifiers_length = new \ReflectionMethod(get_class($this->schema), 'ensureIdentifiersLength');
$unique_key_introspect_name = $ensure_identifiers_length->invoke($this->schema, $table_name_new, $unique_key_name, 'key');
$this->assertEquals([$field_name_new], $introspect_index_schema->invoke($this->schema, $table_name_new)['unique keys'][$unique_key_introspect_name]);
// Dropping an unique key
$this->schema->dropUniqueKey($table_name_new, $unique_key_name);
// Dropping a field.
$this->schema->dropField($table_name_new, $field_name_new);
$this->assertFalse($this->schema->fieldExists($table_name_new, $field_name_new));
// Adding an index.
$index_name = $index_introspect_name = 'index';
$this->schema->addIndex($table_name_new, $index_name, ['update'], $table_specification);
$this->assertTrue($this->schema->indexExists($table_name_new, $index_name));
// Check the index columns.
$index_introspect_name = $ensure_identifiers_length->invoke($this->schema, $table_name_new, $index_name, 'idx');
$this->assertEquals(['update'], $introspect_index_schema->invoke($this->schema, $table_name_new)['indexes'][$index_introspect_name]);
// Dropping an index.
$this->schema->dropIndex($table_name_new, $index_name);
$this->assertFalse($this->schema->indexExists($table_name_new, $index_name));
// Dropping a table.
$this->schema->dropTable($table_name_new);
$this->assertFalse($this->schema->tableExists($table_name_new));
}
/**
* @covers \Drupal\Core\Database\Driver\pgsql\Schema::extensionExists
*/
public function testPgsqlExtensionExists(): void {
// Test the method for a non existing extension.
$this->assertFalse($this->schema->extensionExists('non_existing_extension'));
// Test the method for an existing extension.
$this->assertTrue($this->schema->extensionExists('pg_trgm'));
}
/**
* Tests if the new sequences get the right ownership.
*/
public function testPgsqlSequences(): void {
$table_specification = [
'description' => 'A test table with an ANSI reserved keywords for naming.',
'fields' => [
'uid' => [
'description' => 'Simple unique ID.',
'type' => 'serial',
'not null' => TRUE,
],
'update' => [
'description' => 'A column with reserved name.',
'type' => 'varchar',
'length' => 255,
],
],
'primary key' => ['uid'],
'unique keys' => [
'having' => ['update'],
],
'indexes' => [
'in' => ['uid', 'update'],
],
];
// Creating a table.
$table_name = 'sequence_test';
$this->schema->createTable($table_name, $table_specification);
$this->assertTrue($this->schema->tableExists($table_name));
// Retrieves a sequence name that is owned by the table and column.
$sequence_name = $this->connection
->query("SELECT pg_get_serial_sequence(:table, :column)", [
':table' => $this->connection->getPrefix() . 'sequence_test',
':column' => 'uid',
])
->fetchField();
$schema = $this->connection->getConnectionOptions()['schema'] ?? 'public';
$this->assertEquals($schema . '.' . $this->connection->getPrefix() . 'sequence_test_uid_seq', $sequence_name);
// Checks if the sequence exists.
$this->assertTrue((bool) \Drupal::database()
->query("SELECT c.relname FROM pg_class as c WHERE c.relkind = 'S' AND c.relname = :name", [
':name' => $this->connection->getPrefix() . 'sequence_test_uid_seq',
])
->fetchField());
// Retrieves the sequence owner object.
$sequence_owner = \Drupal::database()->query("SELECT d.refobjid::regclass as table_name, a.attname as field_name
FROM pg_depend d
JOIN pg_attribute a ON a.attrelid = d.refobjid AND a.attnum = d.refobjsubid
WHERE d.objid = :seq_name::regclass
AND d.refobjsubid > 0
AND d.classid = 'pg_class'::regclass", [':seq_name' => $sequence_name])->fetchObject();
$this->assertEquals($this->connection->getPrefix() . 'sequence_test', $sequence_owner->table_name);
$this->assertEquals('uid', $sequence_owner->field_name, 'New sequence is owned by its table.');
}
/**
* Tests the method tableExists().
*/
public function testTableExists(): void {
$table_name = 'test_table';
$table_specification = [
'fields' => [
'id' => [
'type' => 'int',
'default' => NULL,
],
],
];
$this->schema->createTable($table_name, $table_specification);
$prefixed_table_name = $this->connection->getPrefix($table_name) . $table_name;
// Three different calls to the method Schema::tableExists() with an
// unprefixed table name.
$this->assertTrue($this->schema->tableExists($table_name));
$this->assertTrue($this->schema->tableExists($table_name, TRUE));
$this->assertFalse($this->schema->tableExists($table_name, FALSE));
// Three different calls to the method Schema::tableExists() with a
// prefixed table name.
$this->assertFalse($this->schema->tableExists($prefixed_table_name));
$this->assertFalse($this->schema->tableExists($prefixed_table_name, TRUE));
$this->assertTrue($this->schema->tableExists($prefixed_table_name, FALSE));
}
/**
* Tests renaming a table where the new index name is equal to the table name.
*/
public function testRenameTableWithNewIndexNameEqualsTableName(): void {
// Special table names for colliding with the PostgreSQL new index name.
$table_name_old = 'some_new_table_name__id__idx';
$table_name_new = 'some_new_table_name';
$table_specification = [
'fields' => [
'id' => [
'type' => 'int',
'default' => NULL,
],
],
'indexes' => [
'id' => ['id'],
],
];
$this->schema->createTable($table_name_old, $table_specification);
// Renaming the table can fail for PostgreSQL, when a new index name is
// equal to the old table name.
$this->schema->renameTable($table_name_old, $table_name_new);
$this->assertTrue($this->schema->tableExists($table_name_new));
}
/**
* Tests column name escaping in field constraints.
*/
public function testUnsignedField(): void {
$table_name = 'unsigned_table';
$table_spec = [
'fields' => [
'order' => [
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
],
],
'primary key' => ['order'],
];
$this->schema->createTable($table_name, $table_spec);
}
}

View File

@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\pgsql\Kernel\pgsql;
use Drupal\KernelTests\Core\Database\SchemaUniquePrefixedKeysIndexTestBase;
/**
* Tests adding UNIQUE keys to tables.
*
* @group Database
*/
class SchemaUniquePrefixedKeysIndexTest extends SchemaUniquePrefixedKeysIndexTestBase {
/**
* {@inheritdoc}
*/
protected string $columnValue = '1234567890 foo';
}

View File

@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\pgsql\Kernel\pgsql;
use Drupal\KernelTests\Core\Database\DriverSpecificSyntaxTestBase;
/**
* Tests PostgreSQL syntax interpretation.
*
* @group Database
*/
class SyntaxTest extends DriverSpecificSyntaxTestBase {
}

View File

@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\pgsql\Kernel\pgsql;
use Drupal\KernelTests\Core\Database\TemporaryQueryTestBase;
/**
* Tests the temporary query functionality.
*
* @group Database
*/
class TemporaryQueryTest extends TemporaryQueryTestBase {
/**
* Confirms that temporary tables work.
*/
public function testTemporaryQuery(): void {
parent::testTemporaryQuery();
$connection = $this->getConnection();
$table_name_test = $connection->queryTemporary('SELECT [name] FROM {test}', []);
// Assert that the table is indeed a temporary one.
$temporary_table_info = $connection->query("SELECT * FROM pg_class WHERE relname LIKE '%$table_name_test%'")->fetch();
$this->assertEquals("t", $temporary_table_info->relpersistence);
// Assert that both have the same field names.
$normal_table_fields = $connection->query("SELECT * FROM {test}")->fetch();
$temp_table_name = $connection->queryTemporary('SELECT * FROM {test}');
$temp_table_fields = $connection->query("SELECT * FROM {" . $temp_table_name . "}")->fetch();
$normal_table_fields = array_keys(get_object_vars($normal_table_fields));
$temp_table_fields = array_keys(get_object_vars($temp_table_fields));
$this->assertEmpty(array_diff($normal_table_fields, $temp_table_fields));
}
}

View File

@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\pgsql\Kernel\pgsql;
use Drupal\KernelTests\Core\Database\DriverSpecificTransactionTestBase;
/**
* Tests transaction for the PostgreSQL driver.
*
* @group Database
*/
class TransactionTest extends DriverSpecificTransactionTestBase {
}

View File

@@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\pgsql\Unit;
use Drupal\pgsql\Driver\Database\pgsql\Schema;
use Drupal\Tests\UnitTestCase;
use Prophecy\Argument;
/**
* @coversDefaultClass \Drupal\pgsql\Driver\Database\pgsql\Schema
* @group Database
*/
class SchemaTest extends UnitTestCase {
/**
* Tests whether the actual constraint name is correctly computed.
*
* @param string $table_name
* The table name the constrained column belongs to.
* @param string $name
* The constraint name.
* @param string $expected
* The expected computed constraint name.
*
* @covers ::constraintExists
* @dataProvider providerComputedConstraintName
*/
public function testComputedConstraintName($table_name, $name, $expected): void {
$max_identifier_length = 63;
$connection = $this->prophesize('\Drupal\pgsql\Driver\Database\pgsql\Connection');
$connection->getConnectionOptions()->willReturn([]);
$connection->getPrefix()->willReturn('');
$statement = $this->prophesize('\Drupal\Core\Database\StatementInterface');
$statement->fetchField()->willReturn($max_identifier_length);
$connection->query('SHOW max_identifier_length')->willReturn($statement->reveal());
$connection->query(Argument::containingString($expected))
->willReturn($this->prophesize('\Drupal\Core\Database\StatementInterface')->reveal())
->shouldBeCalled();
$schema = new Schema($connection->reveal());
$schema->constraintExists($table_name, $name);
}
/**
* Data provider for ::testComputedConstraintName().
*/
public static function providerComputedConstraintName() {
return [
['user_field_data', 'pkey', 'user_field_data____pkey'],
['user_field_data', 'name__key', 'user_field_data__name__key'],
['user_field_data', 'a_very_very_very_very_super_long_field_name__key', 'drupal_WW_a8TlbZ3UQi20UTtRlJFaIeSa6FEtQS5h4NRA3UeU_key'],
];
}
}