Initial Drupal 11 with DDEV setup
This commit is contained in:
5
web/core/modules/pgsql/pgsql.info.yml
Normal file
5
web/core/modules/pgsql/pgsql.info.yml
Normal file
@ -0,0 +1,5 @@
|
||||
name: PostgreSQL
|
||||
type: module
|
||||
description: 'Provides the PostgreSQL database driver.'
|
||||
package: Core
|
||||
version: VERSION
|
||||
13
web/core/modules/pgsql/pgsql.install
Normal file
13
web/core/modules/pgsql/pgsql.install
Normal file
@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Install, update and uninstall functions for the pgsql module.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Implements hook_update_last_removed().
|
||||
*/
|
||||
function pgsql_update_last_removed(): int {
|
||||
return 10101;
|
||||
}
|
||||
4
web/core/modules/pgsql/pgsql.services.yml
Normal file
4
web/core/modules/pgsql/pgsql.services.yml
Normal file
@ -0,0 +1,4 @@
|
||||
services:
|
||||
pgsql.entity.query.sql:
|
||||
class: Drupal\pgsql\EntityQuery\QueryFactory
|
||||
arguments: ['@database']
|
||||
497
web/core/modules/pgsql/src/Driver/Database/pgsql/Connection.php
Normal file
497
web/core/modules/pgsql/src/Driver/Database/pgsql/Connection.php
Normal file
@ -0,0 +1,497 @@
|
||||
<?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\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.
|
||||
*
|
||||
* @var string[][]
|
||||
*/
|
||||
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.
|
||||
*
|
||||
* @var array<string,Transaction>
|
||||
*
|
||||
* @see ::addSavepoint()
|
||||
* @see ::releaseSavepoint()
|
||||
* @see ::rollbackSavepoint()
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function driver() {
|
||||
return 'pgsql';
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@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 string $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 string $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 string $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) {
|
||||
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 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}
|
||||
*/
|
||||
protected function driverTransactionManager(): TransactionManagerInterface {
|
||||
return new TransactionManager($this);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @} End of "addtogroup database".
|
||||
*/
|
||||
29
web/core/modules/pgsql/src/Driver/Database/pgsql/Delete.php
Normal file
29
web/core/modules/pgsql/src/Driver/Database/pgsql/Delete.php
Normal file
@ -0,0 +1,29 @@
|
||||
<?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 execute() {
|
||||
$this->connection->addSavepoint();
|
||||
try {
|
||||
$result = parent::execute();
|
||||
}
|
||||
catch (\Exception $e) {
|
||||
$this->connection->rollbackSavepoint();
|
||||
throw $e;
|
||||
}
|
||||
$this->connection->releaseSavepoint();
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
}
|
||||
152
web/core/modules/pgsql/src/Driver/Database/pgsql/Insert.php
Normal file
152
web/core/modules/pgsql/src/Driver/Database/pgsql/Insert.php
Normal file
@ -0,0 +1,152 @@
|
||||
<?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 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function __toString() {
|
||||
// Create a sanitized comment string to prepend to the query.
|
||||
$comments = $this->connection->makeComment($this->comments);
|
||||
|
||||
// Default fields are always placed first for consistency.
|
||||
$insert_fields = array_merge($this->defaultFields, $this->insertFields);
|
||||
|
||||
$insert_fields = array_map(function ($f) {
|
||||
return $this->connection->escapeField($f);
|
||||
}, $insert_fields);
|
||||
|
||||
// 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) {
|
||||
// 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;
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,327 @@
|
||||
<?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;
|
||||
use Drupal\Core\StringTranslation\StringTranslationTrait;
|
||||
|
||||
// cspell:ignore proname trgm
|
||||
|
||||
/**
|
||||
* Specifies installation tasks for PostgreSQL databases.
|
||||
*/
|
||||
class Tasks extends InstallTasks {
|
||||
|
||||
use StringTranslationTrait;
|
||||
|
||||
/**
|
||||
* Minimum required PostgreSQL version.
|
||||
*
|
||||
* The contrib extension pg_trgm is supposed to be installed.
|
||||
*
|
||||
* @see https://www.postgresql.org/docs/16/pgtrgm.html
|
||||
*/
|
||||
const PGSQL_MINIMUM_VERSION = '16';
|
||||
|
||||
/**
|
||||
* {@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 $this->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($this->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($this->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($this->t('Database is encoded in UTF-8'));
|
||||
}
|
||||
else {
|
||||
$this->fail($this->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) {
|
||||
$this->fail($this->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) {
|
||||
// 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($this->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) {
|
||||
// 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($this->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.
|
||||
$connection->query('CREATE EXTENSION IF NOT EXISTS pg_trgm');
|
||||
|
||||
if ($connection->schema()->extensionExists('pg_trgm')) {
|
||||
$this->pass($this->t('PostgreSQL has the pg_trgm extension enabled.'));
|
||||
}
|
||||
else {
|
||||
$this->fail($this->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($this->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($this->t('PostgreSQL has initialized itself.'));
|
||||
}
|
||||
catch (\Exception $e) {
|
||||
$this->fail($this->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;
|
||||
}
|
||||
|
||||
}
|
||||
13
web/core/modules/pgsql/src/Driver/Database/pgsql/Merge.php
Normal file
13
web/core/modules/pgsql/src/Driver/Database/pgsql/Merge.php
Normal file
@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\pgsql\Driver\Database\pgsql;
|
||||
|
||||
use Drupal\Core\Database\Query\Merge as QueryMerge;
|
||||
|
||||
@trigger_error('Extending from \Drupal\pgsql\Driver\Database\pgsql\Merge is deprecated in drupal:11.0.0 and is removed from drupal:12.0.0. Extend from the base class instead. See https://www.drupal.org/node/3256524', E_USER_DEPRECATED);
|
||||
|
||||
/**
|
||||
* PostgreSQL implementation of \Drupal\Core\Database\Query\Merge.
|
||||
*/
|
||||
class Merge extends QueryMerge {
|
||||
}
|
||||
1132
web/core/modules/pgsql/src/Driver/Database/pgsql/Schema.php
Normal file
1132
web/core/modules/pgsql/src/Driver/Database/pgsql/Schema.php
Normal file
File diff suppressed because it is too large
Load Diff
163
web/core/modules/pgsql/src/Driver/Database/pgsql/Select.php
Normal file
163
web/core/modules/pgsql/src/Driver/Database/pgsql/Select.php
Normal file
@ -0,0 +1,163 @@
|
||||
<?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 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".
|
||||
*/
|
||||
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,29 @@
|
||||
<?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 execute() {
|
||||
$this->connection->addSavepoint();
|
||||
try {
|
||||
$result = parent::execute();
|
||||
}
|
||||
catch (\Exception $e) {
|
||||
$this->connection->rollbackSavepoint();
|
||||
throw $e;
|
||||
}
|
||||
$this->connection->releaseSavepoint();
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
}
|
||||
86
web/core/modules/pgsql/src/Driver/Database/pgsql/Update.php
Normal file
86
web/core/modules/pgsql/src/Driver/Database/pgsql/Update.php
Normal file
@ -0,0 +1,86 @@
|
||||
<?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 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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
126
web/core/modules/pgsql/src/Driver/Database/pgsql/Upsert.php
Normal file
126
web/core/modules/pgsql/src/Driver/Database/pgsql/Upsert.php
Normal file
@ -0,0 +1,126 @@
|
||||
<?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 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;
|
||||
}
|
||||
|
||||
}
|
||||
36
web/core/modules/pgsql/src/EntityQuery/Condition.php
Normal file
36
web/core/modules/pgsql/src/EntityQuery/Condition.php
Normal file
@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\pgsql\EntityQuery;
|
||||
|
||||
use Drupal\Core\Database\Query\SelectInterface;
|
||||
use Drupal\Core\Entity\Query\Sql\Condition as BaseCondition;
|
||||
|
||||
/**
|
||||
* Implements entity query conditions for PostgreSQL databases.
|
||||
*/
|
||||
class Condition extends BaseCondition {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function translateCondition(&$condition, SelectInterface $sql_query, $case_sensitive): void {
|
||||
if (is_array($condition['value']) && $case_sensitive === FALSE) {
|
||||
$condition['where'] = 'LOWER(' . $sql_query->escapeField($condition['real_field']) . ') ' . $condition['operator'] . ' (';
|
||||
$condition['where_args'] = [];
|
||||
|
||||
// Only use the array values in case an associative array is passed as an
|
||||
// argument following similar pattern in
|
||||
// \Drupal\Core\Database\Connection::expandArguments().
|
||||
$where_prefix = str_replace('.', '_', $condition['real_field']);
|
||||
foreach ($condition['value'] as $key => $value) {
|
||||
$where_id = $where_prefix . $key;
|
||||
$condition['where'] .= 'LOWER(:' . $where_id . '),';
|
||||
$condition['where_args'][':' . $where_id] = $value;
|
||||
}
|
||||
$condition['where'] = trim($condition['where'], ',');
|
||||
$condition['where'] .= ')';
|
||||
}
|
||||
parent::translateCondition($condition, $sql_query, $case_sensitive);
|
||||
}
|
||||
|
||||
}
|
||||
24
web/core/modules/pgsql/src/EntityQuery/QueryFactory.php
Normal file
24
web/core/modules/pgsql/src/EntityQuery/QueryFactory.php
Normal file
@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\pgsql\EntityQuery;
|
||||
|
||||
use Drupal\Core\Entity\Query\Sql\QueryFactory as BaseQueryFactory;
|
||||
|
||||
/**
|
||||
* PostgreSQL specific entity query implementation.
|
||||
*
|
||||
* To add a new query implementation extending the default SQL one, add
|
||||
* a service definition like pgsql.entity.query.sql and a factory class like
|
||||
* this. The system will automatically find the relevant Query, QueryAggregate,
|
||||
* Condition, ConditionAggregate, Tables classes in this namespace, in the
|
||||
* namespace of the parent class and so on. So after creating an empty query
|
||||
* factory class like this, it is possible to just drop in a class extending
|
||||
* the base class in this namespace and it will be used automatically but it
|
||||
* is optional: if a class is not extended the relevant default is used.
|
||||
*
|
||||
* @see \Drupal\Core\Entity\Query\QueryBase::getNamespaces()
|
||||
* @see \Drupal\Core\Entity\Query\QueryBase::getClass()
|
||||
*/
|
||||
class QueryFactory extends BaseQueryFactory {
|
||||
|
||||
}
|
||||
33
web/core/modules/pgsql/src/Hook/PgsqlHooks.php
Normal file
33
web/core/modules/pgsql/src/Hook/PgsqlHooks.php
Normal file
@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\pgsql\Hook;
|
||||
|
||||
use Drupal\Core\Routing\RouteMatchInterface;
|
||||
use Drupal\Core\Hook\Attribute\Hook;
|
||||
use Drupal\Core\StringTranslation\StringTranslationTrait;
|
||||
|
||||
/**
|
||||
* Hook implementations for pgsql.
|
||||
*/
|
||||
class PgsqlHooks {
|
||||
|
||||
use StringTranslationTrait;
|
||||
|
||||
/**
|
||||
* Implements hook_help().
|
||||
*/
|
||||
#[Hook('help')]
|
||||
public function help($route_name, RouteMatchInterface $route_match): ?string {
|
||||
switch ($route_name) {
|
||||
case 'help.page.pgsql':
|
||||
$output = '';
|
||||
$output .= '<h2>' . $this->t('About') . '</h2>';
|
||||
$output .= '<p>' . $this->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;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
}
|
||||
55
web/core/modules/pgsql/src/Hook/PgsqlRequirementsHooks.php
Normal file
55
web/core/modules/pgsql/src/Hook/PgsqlRequirementsHooks.php
Normal file
@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\pgsql\Hook;
|
||||
|
||||
use Drupal\Core\Database\Database;
|
||||
use Drupal\Core\Extension\Requirement\RequirementSeverity;
|
||||
use Drupal\Core\StringTranslation\StringTranslationTrait;
|
||||
use Drupal\Core\Hook\Attribute\Hook;
|
||||
|
||||
/**
|
||||
* Hook implementations for pgsql module.
|
||||
*/
|
||||
class PgsqlRequirementsHooks {
|
||||
|
||||
use StringTranslationTrait;
|
||||
|
||||
/**
|
||||
* Implements hook_update_requirements().
|
||||
*
|
||||
* Implements hook_runtime_requirements().
|
||||
*/
|
||||
#[Hook('update_requirements')]
|
||||
#[Hook('runtime_requirements')]
|
||||
public function checkRequirements(): array {
|
||||
$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' => RequirementSeverity::OK,
|
||||
'title' => $this->t('PostgreSQL pg_trgm extension'),
|
||||
'value' => $this->t('Available'),
|
||||
'description' => $this->t('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'] = RequirementSeverity::Error;
|
||||
$requirements['pgsql_extension_pg_trgm']['value'] = $this->t('Not created');
|
||||
$requirements['pgsql_extension_pg_trgm']['description'] = $this->t('The <a href=":pg_trgm">pg_trgm</a> PostgreSQL extension is not present. The extension is required by Drupal 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;
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\pgsql\Install\Requirements;
|
||||
|
||||
use Drupal\Core\Database\Database;
|
||||
use Drupal\Core\Extension\InstallRequirementsInterface;
|
||||
use Drupal\Core\Extension\Requirement\RequirementSeverity;
|
||||
|
||||
/**
|
||||
* Install time requirements for the pgsql module.
|
||||
*/
|
||||
class PgsqlRequirements implements InstallRequirementsInterface {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function getRequirements(): array {
|
||||
$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' => RequirementSeverity::OK,
|
||||
'title' => t('PostgreSQL pg_trgm extension'),
|
||||
'value' => t('Available'),
|
||||
'description' => t('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'] = RequirementSeverity::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 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;
|
||||
}
|
||||
|
||||
}
|
||||
14
web/core/modules/pgsql/tests/src/Functional/GenericTest.php
Normal file
14
web/core/modules/pgsql/tests/src/Functional/GenericTest.php
Normal 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 {}
|
||||
@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\pgsql\Kernel;
|
||||
|
||||
use Drupal\Core\Entity\Query\Sql\QueryFactory as BaseQueryFactory;
|
||||
use Drupal\Core\Entity\Query\Sql\pgsql\QueryFactory as DeprecatedQueryFactory;
|
||||
use Drupal\KernelTests\KernelTestBase;
|
||||
use Drupal\pgsql\EntityQuery\QueryFactory;
|
||||
use PHPUnit\Framework\Attributes\Group;
|
||||
use PHPUnit\Framework\Attributes\IgnoreDeprecations;
|
||||
|
||||
/**
|
||||
* Tests the move of the 'pgsql.entity.query.sql' service.
|
||||
*/
|
||||
#[Group('Database')]
|
||||
#[Group('pgsql')]
|
||||
class EntityQueryServiceDeprecationTest extends KernelTestBase {
|
||||
|
||||
/**
|
||||
* Tests that the core provided service is deprecated.
|
||||
*/
|
||||
#[IgnoreDeprecations]
|
||||
public function testPostgresServiceDeprecated(): void {
|
||||
$running_driver = $this->container->get('database')->driver();
|
||||
if ($running_driver === 'pgsql') {
|
||||
$this->markTestSkipped('The service is not deprecated for pgsql database driver.');
|
||||
}
|
||||
$this->expectDeprecation('The "pgsql.entity.query.sql" service is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Install the pgsql module to replace this service. See https://www.drupal.org/node/3488580');
|
||||
$this->expectDeprecation('\Drupal\Core\Entity\Query\Sql\pgsql\QueryFactory is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. The PostgreSQL override of the entity query has been moved to the pgsql module. See https://www.drupal.org/node/3488580');
|
||||
$service = $this->container->get('pgsql.entity.query.sql');
|
||||
$this->assertInstanceOf(DeprecatedQueryFactory::class, $service);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that the pgsql provided service is not deprecated.
|
||||
*/
|
||||
public function testPostgresServiceNotDeprecated(): void {
|
||||
$running_driver = $this->container->get('database')->driver();
|
||||
if ($running_driver !== 'pgsql') {
|
||||
$this->markTestSkipped('The service is deprecated for database drivers other than pgsql.');
|
||||
}
|
||||
$service = $this->container->get('pgsql.entity.query.sql');
|
||||
$this->assertInstanceOf(QueryFactory::class, $service);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests getting the backend overridden service does not trigger deprecations.
|
||||
*/
|
||||
public function testFactoryOverriddenService(): void {
|
||||
$service = $this->container->get('entity.query.sql');
|
||||
$this->assertInstanceOf(BaseQueryFactory::class, $service);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\pgsql\Kernel\pgsql;
|
||||
|
||||
use Drupal\KernelTests\Core\Database\DriverSpecificConnectionUnitTestBase;
|
||||
|
||||
// cspell:ignore processlist
|
||||
|
||||
/**
|
||||
* 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',
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,384 @@
|
||||
<?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.
|
||||
// phpcs:ignore
|
||||
// $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.
|
||||
// phpcs:ignore
|
||||
// $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());
|
||||
}
|
||||
|
||||
}
|
||||
@ -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';
|
||||
|
||||
}
|
||||
416
web/core/modules/pgsql/tests/src/Kernel/pgsql/SchemaTest.php
Normal file
416
web/core/modules/pgsql/tests/src/Kernel/pgsql/SchemaTest.php
Normal file
@ -0,0 +1,416 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\pgsql\Kernel\pgsql;
|
||||
|
||||
use Drupal\KernelTests\Core\Database\DriverSpecificSchemaTestBase;
|
||||
|
||||
// cSpell:ignore attname attnum attrelid objid refobjid refobjsubid regclass
|
||||
// cspell:ignore relkind relname
|
||||
|
||||
/**
|
||||
* 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\pgsql\Driver\Database\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 renaming a table which name contains drupal_ with multiple indexes.
|
||||
*/
|
||||
public function testRenameTableWithNameContainingDrupalUnderscoreAndMultipleIndexes(): void {
|
||||
$table_name_old = 'field_drupal_foo';
|
||||
$table_name_new = 'field_drupal_bar';
|
||||
$table_specification = [
|
||||
'fields' => [
|
||||
'one' => [
|
||||
'type' => 'int',
|
||||
'default' => NULL,
|
||||
],
|
||||
'two' => [
|
||||
'type' => 'int',
|
||||
'default' => NULL,
|
||||
],
|
||||
],
|
||||
'indexes' => [
|
||||
'one' => ['one'],
|
||||
'two' => ['two'],
|
||||
],
|
||||
];
|
||||
$this->schema->createTable($table_name_old, $table_specification);
|
||||
|
||||
$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);
|
||||
$this->assertTrue($this->schema->tableExists($table_name));
|
||||
}
|
||||
|
||||
}
|
||||
@ -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';
|
||||
|
||||
}
|
||||
15
web/core/modules/pgsql/tests/src/Kernel/pgsql/SyntaxTest.php
Normal file
15
web/core/modules/pgsql/tests/src/Kernel/pgsql/SyntaxTest.php
Normal 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 {
|
||||
}
|
||||
@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\Tests\pgsql\Kernel\pgsql;
|
||||
|
||||
use Drupal\KernelTests\Core\Database\TemporaryQueryTestBase;
|
||||
|
||||
// cspell:ignore relname relpersistence
|
||||
|
||||
/**
|
||||
* 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));
|
||||
}
|
||||
|
||||
}
|
||||
@ -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 {
|
||||
}
|
||||
64
web/core/modules/pgsql/tests/src/Unit/SchemaTest.php
Normal file
64
web/core/modules/pgsql/tests/src/Unit/SchemaTest.php
Normal file
@ -0,0 +1,64 @@
|
||||
<?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',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user