From b8d555f56a52664f3b87016cfa0c2ff9147602b5 Mon Sep 17 00:00:00 2001
From: Tristan Darricau <tristan.darricau@gmail.com>
Date: Tue, 9 Nov 2021 03:53:52 +0100
Subject: [PATCH] [ticket/16741] Specific DBs fixes

MSSQL:
- Fix bool type
- Fix comparator
- Drop Default constraint before deleting column
- Rename Default constraint to use phpBB's names
- Re-create the indices when changing the type of one column
- Uses varchar instead of varbinary

PostgreSQL:
- Creates auto increment sequences by hand instead of using serial
  in order to use phpBB's names
- Drop constraint on unique / primary indices

Oracle:
- Rename indices to use phpBB's names
- Fix string not null behaviour
- Fix broken regex in Oracle driver
- Handle to long indices on Oracle
- Rename auto_increment trigger and sequence
- Automatically lowercase keys in assoc results

PHPBB3-16741
---
 .../config/default/container/services_db.yml  |   2 +-
 .../install/convert/controller/convertor.php  |   2 +-
 phpBB/phpbb/db/doctrine/comparator.php        |  64 +++++++
 phpBB/phpbb/db/doctrine/oci8/connection.php   |  99 ++++++++++
 phpBB/phpbb/db/doctrine/oci8/driver.php       |  65 +++++++
 phpBB/phpbb/db/doctrine/oci8/result.php       | 109 +++++++++++
 .../phpbb/db/doctrine/oci8/schema_manager.php |  45 +++++
 phpBB/phpbb/db/doctrine/oci8/statement.php    |  58 ++++++
 phpBB/phpbb/db/doctrine/oracle_platform.php   | 132 +++++++++++++
 .../phpbb/db/doctrine/postgresql_platform.php | 178 ++++++++++++++++++
 phpBB/phpbb/db/doctrine/sqlsrv_platform.php   | 139 ++++++++++++++
 phpBB/phpbb/db/driver/oracle.php              |  16 +-
 phpBB/phpbb/db/tools/mssql.php                |   2 +-
 phpBB/phpbb/db/tools/postgres.php             |   2 +-
 14 files changed, 901 insertions(+), 12 deletions(-)
 create mode 100644 phpBB/phpbb/db/doctrine/comparator.php
 create mode 100644 phpBB/phpbb/db/doctrine/oci8/connection.php
 create mode 100644 phpBB/phpbb/db/doctrine/oci8/driver.php
 create mode 100644 phpBB/phpbb/db/doctrine/oci8/result.php
 create mode 100644 phpBB/phpbb/db/doctrine/oci8/schema_manager.php
 create mode 100644 phpBB/phpbb/db/doctrine/oci8/statement.php
 create mode 100644 phpBB/phpbb/db/doctrine/postgresql_platform.php
 create mode 100644 phpBB/phpbb/db/doctrine/sqlsrv_platform.php

diff --git a/phpBB/config/default/container/services_db.yml b/phpBB/config/default/container/services_db.yml
index 4b0e49dddb..bad99b7d87 100644
--- a/phpBB/config/default/container/services_db.yml
+++ b/phpBB/config/default/container/services_db.yml
@@ -10,7 +10,7 @@ services:
     dbal.conn.doctrine:
         synthetic: true
 
-    # ----- DB Tools -----
+# ----- DB Tools -----
     dbal.tools.factory:
         class: phpbb\db\tools\factory
 
diff --git a/phpBB/install/convert/controller/convertor.php b/phpBB/install/convert/controller/convertor.php
index aa388204cd..6c95c7dc1a 100644
--- a/phpBB/install/convert/controller/convertor.php
+++ b/phpBB/install/convert/controller/convertor.php
@@ -13,6 +13,7 @@
 
 namespace phpbb\convert\controller;
 
+use Doctrine\DBAL\Connection;
 use phpbb\cache\driver\driver_interface;
 use phpbb\db\doctrine\connection_factory;
 use phpbb\exception\http_exception;
@@ -26,7 +27,6 @@ use phpbb\install\helper\navigation\navigation_provider;
 use phpbb\language\language;
 use phpbb\request\request_interface;
 use phpbb\template\template;
-use PHPUnit\DbUnit\Database\Connection;
 use Symfony\Component\HttpFoundation\StreamedResponse;
 
 /**
diff --git a/phpBB/phpbb/db/doctrine/comparator.php b/phpBB/phpbb/db/doctrine/comparator.php
new file mode 100644
index 0000000000..27390e3da0
--- /dev/null
+++ b/phpBB/phpbb/db/doctrine/comparator.php
@@ -0,0 +1,64 @@
+<?php
+/**
+ *
+ * This file is part of the phpBB Forum Software package.
+ *
+ * @copyright (c) phpBB Limited <https://www.phpbb.com>
+ * @license       GNU General Public License, version 2 (GPL-2.0)
+ *
+ * For full copyright and license information, please see
+ * the docs/CREDITS.txt file.
+ *
+ */
+
+namespace phpbb\db\doctrine;
+
+use Doctrine\DBAL\Schema\Table;
+
+class comparator extends \Doctrine\DBAL\Schema\Comparator
+{
+	/**
+	 * {@inerhitDoc}
+	 */
+	public function diffTable(Table $fromTable, Table $toTable)
+	{
+		$diff = parent::diffTable($fromTable, $toTable);
+
+		if ($diff === false)
+		{
+			return $diff;
+		}
+
+		if (!is_array($diff->changedColumns))
+		{
+			return $diff;
+		}
+
+		// When the type of a column changes, re-create the associated indices
+		foreach ($diff->changedColumns as $columnName => $changedColumn)
+		{
+			if (!$changedColumn->hasChanged('type'))
+			{
+				continue;
+			}
+
+			foreach ($toTable->getIndexes() as $index_name => $index)
+			{
+				if (array_key_exists($index_name, $diff->addedIndexes) || array_key_exists($index_name, $diff->changedIndexes))
+				{
+					continue;
+				}
+
+				$index_columns = array_map('strtolower', $index->getUnquotedColumns());
+				if (array_search($columnName, $index_columns, true) === false)
+				{
+					continue;
+				}
+
+				$diff->changedIndexes[$index_name] = $index;
+			}
+		}
+
+		return $diff;
+	}
+}
diff --git a/phpBB/phpbb/db/doctrine/oci8/connection.php b/phpBB/phpbb/db/doctrine/oci8/connection.php
new file mode 100644
index 0000000000..98d11c2fbe
--- /dev/null
+++ b/phpBB/phpbb/db/doctrine/oci8/connection.php
@@ -0,0 +1,99 @@
+<?php
+/**
+ *
+ * This file is part of the phpBB Forum Software package.
+ *
+ * @copyright (c) phpBB Limited <https://www.phpbb.com>
+ * @license       GNU General Public License, version 2 (GPL-2.0)
+ *
+ * For full copyright and license information, please see
+ * the docs/CREDITS.txt file.
+ *
+ */
+
+namespace phpbb\db\doctrine\oci8;
+
+use Doctrine\DBAL\Driver\Connection as DriverConnection;
+use Doctrine\DBAL\Driver\Result as DriverResult;
+use Doctrine\DBAL\Driver\Statement as DriverStatement;
+use Doctrine\DBAL\ParameterType;
+
+class Connection implements DriverConnection
+{
+	/**
+	 * @var DriverConnection
+	 */
+	private $wrapped;
+
+	/**
+	 * @param DriverConnection $wrapped
+	 */
+	public function __construct(DriverConnection $wrapped)
+	{
+		$this->wrapped = $wrapped;
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	public function prepare(string $sql): DriverStatement
+	{
+		return new statement($this->wrapped->prepare($sql));
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	public function query(string $sql): DriverResult
+	{
+		return new result($this->wrapped->query($sql));
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	public function quote($value, $type = ParameterType::STRING)
+	{
+		return $this->wrapped->quote($value, $type);
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	public function exec(string $sql): int
+	{
+		return $this->wrapped->exec($sql);
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	public function lastInsertId($name = null)
+	{
+		return $this->wrapped->lastInsertId($name);
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	public function beginTransaction()
+	{
+		return $this->wrapped->beginTransaction();
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	public function commit()
+	{
+		return $this->wrapped->commit();
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	public function rollBack()
+	{
+		return $this->wrapped->rollBack();
+	}
+}
diff --git a/phpBB/phpbb/db/doctrine/oci8/driver.php b/phpBB/phpbb/db/doctrine/oci8/driver.php
new file mode 100644
index 0000000000..0a5300092c
--- /dev/null
+++ b/phpBB/phpbb/db/doctrine/oci8/driver.php
@@ -0,0 +1,65 @@
+<?php
+/**
+ *
+ * This file is part of the phpBB Forum Software package.
+ *
+ * @copyright (c) phpBB Limited <https://www.phpbb.com>
+ * @license       GNU General Public License, version 2 (GPL-2.0)
+ *
+ * For full copyright and license information, please see
+ * the docs/CREDITS.txt file.
+ *
+ */
+
+namespace phpbb\db\doctrine\oci8;
+
+use Doctrine\DBAL\Connection as DoctrineConnection;
+use Doctrine\DBAL\Driver\API\ExceptionConverter;
+use Doctrine\DBAL\Platforms\AbstractPlatform;
+use Doctrine\DBAL\Driver as DoctrineDriver;
+use Doctrine\DBAL\Driver\OCI8\Driver as OCI8Driver;
+
+class driver implements DoctrineDriver
+{
+	/**
+	 * @var DoctrineDriver
+	 */
+	private $wrapped;
+
+	public function __construct()
+	{
+		$this->wrapped = new OCI8Driver();
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	public function connect(array $params)
+	{
+		return new connection($this->wrapped->connect($params));
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	public function getDatabasePlatform()
+	{
+		return $this->wrapped->getDatabasePlatform();
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	public function getSchemaManager(DoctrineConnection $conn, AbstractPlatform $platform)
+	{
+		return new schema_manager($conn, $platform);
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	public function getExceptionConverter(): ExceptionConverter
+	{
+		return $this->wrapped->getExceptionConverter();
+	}
+}
diff --git a/phpBB/phpbb/db/doctrine/oci8/result.php b/phpBB/phpbb/db/doctrine/oci8/result.php
new file mode 100644
index 0000000000..60072bfe9f
--- /dev/null
+++ b/phpBB/phpbb/db/doctrine/oci8/result.php
@@ -0,0 +1,109 @@
+<?php
+/**
+ *
+ * This file is part of the phpBB Forum Software package.
+ *
+ * @copyright (c) phpBB Limited <https://www.phpbb.com>
+ * @license       GNU General Public License, version 2 (GPL-2.0)
+ *
+ * For full copyright and license information, please see
+ * the docs/CREDITS.txt file.
+ *
+ */
+
+namespace phpbb\db\doctrine\oci8;
+
+use Doctrine\DBAL\Driver\Result as DriverResult;
+
+class result implements DriverResult
+{
+	/**
+	 * @var DriverResult
+	 */
+	private $wrapped;
+
+	/**
+	 * @param DriverResult $wrapped
+	 */
+	public function __construct(DriverResult $wrapped)
+	{
+		$this->wrapped = $wrapped;
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	public function fetchNumeric()
+	{
+		return $this->wrapped->fetchNumeric();
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	public function fetchAssociative()
+	{
+		return array_change_key_case($this->wrapped->fetchAssociative(), CASE_LOWER);
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	public function fetchOne()
+	{
+		return $this->wrapped->fetchOne();
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	public function fetchAllNumeric(): array
+	{
+		return $this->wrapped->fetchAllNumeric();
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	public function fetchAllAssociative(): array
+	{
+		$rows = [];
+		foreach ($this->wrapped->fetchAllAssociative() as $row)
+		{
+			$rows[] = array_change_key_case($row, CASE_LOWER);
+		}
+		return $rows;
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	public function fetchFirstColumn(): array
+	{
+		return $this->wrapped->fetchFirstColumn();
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	public function rowCount(): int
+	{
+		return $this->wrapped->rowCount();
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	public function columnCount(): int
+	{
+		return $this->wrapped->columnCount();
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	public function free(): void
+	{
+		$this->wrapped->free();
+	}
+}
diff --git a/phpBB/phpbb/db/doctrine/oci8/schema_manager.php b/phpBB/phpbb/db/doctrine/oci8/schema_manager.php
new file mode 100644
index 0000000000..aeb1120e12
--- /dev/null
+++ b/phpBB/phpbb/db/doctrine/oci8/schema_manager.php
@@ -0,0 +1,45 @@
+<?php
+/**
+ *
+ * This file is part of the phpBB Forum Software package.
+ *
+ * @copyright (c) phpBB Limited <https://www.phpbb.com>
+ * @license       GNU General Public License, version 2 (GPL-2.0)
+ *
+ * For full copyright and license information, please see
+ * the docs/CREDITS.txt file.
+ *
+ */
+
+namespace phpbb\db\doctrine\oci8;
+
+use Doctrine\DBAL\Platforms\OraclePlatform;
+use Doctrine\DBAL\Schema\AbstractSchemaManager;
+use Doctrine\DBAL\Schema\OracleSchemaManager;
+use Doctrine\DBAL\Schema\Table;
+
+class schema_manager extends OracleSchemaManager
+{
+	/**
+	 * {@inheritdoc}
+	 *
+	 * Copied from upstream to lowercase 'COMMENTS'
+	 */
+	public function listTableDetails($name): Table
+	{
+		$table = AbstractSchemaManager::listTableDetails($name);
+
+		$platform = $this->_platform;
+		assert($platform instanceof OraclePlatform);
+		$sql = $platform->getListTableCommentsSQL($name);
+
+		$tableOptions = $this->_conn->fetchAssociative($sql);
+
+		if ($tableOptions !== false)
+		{
+			$table->addOption('comment', $tableOptions['comments']);
+		}
+
+		return $table;
+	}
+}
diff --git a/phpBB/phpbb/db/doctrine/oci8/statement.php b/phpBB/phpbb/db/doctrine/oci8/statement.php
new file mode 100644
index 0000000000..332c3fab32
--- /dev/null
+++ b/phpBB/phpbb/db/doctrine/oci8/statement.php
@@ -0,0 +1,58 @@
+<?php
+/**
+ *
+ * This file is part of the phpBB Forum Software package.
+ *
+ * @copyright (c) phpBB Limited <https://www.phpbb.com>
+ * @license       GNU General Public License, version 2 (GPL-2.0)
+ *
+ * For full copyright and license information, please see
+ * the docs/CREDITS.txt file.
+ *
+ */
+
+namespace phpbb\db\doctrine\oci8;
+
+use Doctrine\DBAL\Driver\Result as DriverResult;
+use Doctrine\DBAL\Driver\Statement as DriverStatement;
+use Doctrine\DBAL\ParameterType;
+
+class statement implements DriverStatement
+{
+	/**
+	 * @var DriverStatement
+	 */
+	private $wrapped;
+
+	/**
+	 * @param DriverStatement $wrapped
+	 */
+	public function __construct(DriverStatement $wrapped)
+	{
+		$this->wrapped = $wrapped;
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	public function bindValue($param, $value, $type = ParameterType::STRING)
+	{
+		return $this->wrapped->bindValue($param, $value, $type);
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	public function bindParam($param, &$variable, $type = ParameterType::STRING, $length = null)
+	{
+		return $this->wrapped->bindParam($param, $variable, $type, $length);
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	public function execute($params = null): DriverResult
+	{
+		return new result($this->wrapped->execute($params));
+	}
+}
diff --git a/phpBB/phpbb/db/doctrine/oracle_platform.php b/phpBB/phpbb/db/doctrine/oracle_platform.php
index 6c8de23e8a..43ace17795 100644
--- a/phpBB/phpbb/db/doctrine/oracle_platform.php
+++ b/phpBB/phpbb/db/doctrine/oracle_platform.php
@@ -14,6 +14,9 @@
 namespace phpbb\db\doctrine;
 
 use Doctrine\DBAL\Platforms\OraclePlatform;
+use Doctrine\DBAL\Schema\Identifier;
+use Doctrine\DBAL\Schema\Index;
+use Doctrine\DBAL\Schema\Table;
 
 /**
  * Oracle specific schema restrictions for BC.
@@ -40,4 +43,133 @@ class oracle_platform extends OraclePlatform
 	{
 		return parent::getVarcharTypeDeclarationSQL($column);
 	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	public function getCreateIndexSQL(Index $index, $table)
+	{
+		if ($table instanceof Table)
+		{
+			$table_name = $table->getName();
+		}
+		else
+		{
+			$table_name = $table;
+		}
+
+		$index_name = $index->getName();
+		if (strpos($index->getName(), $table_name) !== 0)
+		{
+			$index_name = $table_name . '_' . $index->getName();
+		}
+
+		$index = new Index(
+			$this->check_index_name_length($table_name, $index_name),
+			$index->getColumns(),
+			$index->isUnique(),
+			$index->isPrimary(),
+			$index->getFlags(),
+			$index->getOptions()
+		);
+
+		return parent::getCreateIndexSQL($index, $table);
+	}
+
+	/**
+	 * Check whether the index name is too long
+	 *
+	 * @param string $table_name
+	 * @param string $index_name
+	 * @param bool $throw_error
+	 * @return string	The index name, shortened if too long
+	 */
+	protected function check_index_name_length($table_name, $index_name, $throw_error = true)
+	{
+		$max_index_name_length = $this->getMaxIdentifierLength();
+		if (strlen($index_name) > $max_index_name_length)
+		{
+			// Try removing the table prefix if it's at the beginning
+			$table_prefix = substr(CONFIG_TABLE, 0, -6); // strlen(config)
+			if (strpos($index_name, $table_prefix) === 0)
+			{
+				$index_name = substr($index_name, strlen($table_prefix));
+				return $this->check_index_name_length($table_name, $index_name, $throw_error);
+			}
+
+			// Try removing the remaining suffix part of table name then
+			$table_suffix = substr($table_name, strlen($table_prefix));
+			if (strpos($index_name, $table_suffix) === 0)
+			{
+				// Remove the suffix and underscore separator between table_name and index_name
+				$index_name = substr($index_name, strlen($table_suffix) + 1);
+				return $this->check_index_name_length($table_name, $index_name, $throw_error);
+			}
+
+			if ($throw_error)
+			{
+				trigger_error("Index name '$index_name' on table '$table_name' is too long. The maximum is $max_index_name_length characters.", E_USER_ERROR);
+			}
+		}
+
+		return $index_name;
+	}
+
+	/**
+	 * {@inheritdoc}
+	 */
+	public function getIdentitySequenceName($tableName, $columnName)
+	{
+		return $tableName.'_SEQ';
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	public function getCreateAutoincrementSql($name, $table, $start = 1)
+	{
+		$sql = parent::getCreateAutoincrementSql($name, $table, $start);
+
+		return str_replace(
+			$this->get_doctrine_autoincrement_identifier_name($this->doctrine_normalize_identifier($table)),
+			'T_'.$table,
+			$sql
+		);
+	}
+
+	/**
+	 * @see OraclePlatform::normalizeIdentifier()
+	 */
+	private function doctrine_normalize_identifier($name)
+	{
+		$identifier = new Identifier($name);
+
+		return $identifier->isQuoted() ? $identifier : new Identifier(strtoupper($name));
+	}
+
+	/**
+	 * @see OraclePlatform::getAutoincrementIdentifierName()
+	 */
+	private function get_doctrine_autoincrement_identifier_name(Identifier $table)
+	{
+		$identifierName = $this->add_doctrine_Suffix($table->getName(), '_AI_PK');
+
+		return $table->isQuoted()
+			? $this->quoteSingleIdentifier($identifierName)
+			: $identifierName;
+	}
+
+	/**
+	 * @see OraclePlatform::addSuffix()
+	 */
+	private function add_doctrine_Suffix(string $identifier, string $suffix): string
+	{
+		$maxPossibleLengthWithoutSuffix = $this->getMaxIdentifierLength() - strlen($suffix);
+		if (strlen($identifier) > $maxPossibleLengthWithoutSuffix)
+		{
+			$identifier = substr($identifier, 0, $maxPossibleLengthWithoutSuffix);
+		}
+
+		return $identifier . $suffix;
+	}
 }
diff --git a/phpBB/phpbb/db/doctrine/postgresql_platform.php b/phpBB/phpbb/db/doctrine/postgresql_platform.php
new file mode 100644
index 0000000000..197f9ac386
--- /dev/null
+++ b/phpBB/phpbb/db/doctrine/postgresql_platform.php
@@ -0,0 +1,178 @@
+<?php
+/**
+ *
+ * This file is part of the phpBB Forum Software package.
+ *
+ * @copyright (c) phpBB Limited <https://www.phpbb.com>
+ * @license GNU General Public License, version 2 (GPL-2.0)
+ *
+ * For full copyright and license information, please see
+ * the docs/CREDITS.txt file.
+ *
+ */
+
+namespace phpbb\db\doctrine;
+
+use Doctrine\DBAL\Platforms\AbstractPlatform;
+use Doctrine\DBAL\Platforms\PostgreSQL94Platform;
+use Doctrine\DBAL\Schema\Index;
+use Doctrine\DBAL\Schema\Sequence;
+use Doctrine\DBAL\Schema\Table;
+use Doctrine\DBAL\Types\BigIntType;
+use Doctrine\DBAL\Types\IntegerType;
+use Doctrine\DBAL\Types\SmallIntType;
+use Doctrine\DBAL\Types\Type;
+
+/**
+ * PostgreSQL specific schema restrictions for BC.
+ *
+ * Doctrine is using SERIAL which auto creates the sequences with
+ * a name different from the one our driver is using. So in order
+ * to stay compatible with the existing DB we have to change its
+ * naming and not ours.
+ */
+class postgresql_platform extends PostgreSQL94Platform
+{
+	/**
+	 * {@inheritdoc}
+	 */
+	public function getIdentitySequenceName($tableName, $columnName)
+	{
+		return $tableName . '_seq';
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	public function getIntegerTypeDeclarationSQL(array $column)
+	{
+		return 'INT';
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	public function getBigIntTypeDeclarationSQL(array $column)
+	{
+		return 'BIGINT';
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	public function getSmallIntTypeDeclarationSQL(array $column)
+	{
+		return 'SMALLINT';
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	public function getDefaultValueDeclarationSQL($column)
+	{
+		if ($this->isSerialColumn($column))
+		{
+			return ' DEFAULT {{placeholder_sequence}}';
+		}
+
+		return AbstractPlatform::getDefaultValueDeclarationSQL($column);
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	public function supportsIdentityColumns()
+	{
+		return false;
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	protected function _getCreateTableSQL($name, array $columns, array $options = [])
+	{
+		$sql = [];
+		$post_sql = [];
+		foreach ($columns as $column_name => $column)
+		{
+			if (! empty($column['autoincrement']))
+			{
+				$sequence = new Sequence($this->getIdentitySequenceName($name, $column_name));
+				$sql[] = $this->getCreateSequenceSQL($sequence);
+				$post_sql[] = 'ALTER SEQUENCE '.$sequence->getName().' OWNED BY '.$name.'.'.$column_name;
+			}
+		}
+		$sql = array_merge($sql, parent::_getCreateTableSQL($name, $columns, $options), $post_sql);
+
+		foreach ($sql as $i => $query)
+		{
+			$sql[$i] = str_replace('{{placeholder_sequence}}', "nextval('{$name}_seq')", $query);
+		}
+
+		return $sql;
+	}
+
+	/**
+	 * @param mixed[] $column
+	 */
+	private function isSerialColumn(array $column): bool
+	{
+		return isset($column['type'], $column['autoincrement'])
+			&& $column['autoincrement'] === true
+			&& $this->isNumericType($column['type']);
+	}
+
+	private function isNumericType(Type $type): bool
+	{
+		return $type instanceof IntegerType || $type instanceof BigIntType || $type instanceof SmallIntType;
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	public function getListSequencesSQL($database)
+	{
+		return "SELECT sequence_name AS relname,
+                       sequence_schema AS schemaname,
+                       1 AS min_value,
+                       1 AS increment_by
+                FROM   information_schema.sequences
+                WHERE  sequence_schema NOT LIKE 'pg\_%'
+                AND    sequence_schema != 'information_schema'";
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	public function getDropIndexSQL($index, $table = null)
+	{
+		// If we have a primary or a unique index, we need to drop the constraint
+		// instead of the index itself or postgreSQL will reject the query.
+		if ($index instanceof Index)
+		{
+			if ($index->isPrimary())
+			{
+				if ($table instanceof Table)
+				{
+					$table = $table->getQuotedName($this);
+				}
+				else if (!is_string($table))
+				{
+					throw new \InvalidArgumentException(
+						__METHOD__ . '() expects $table parameter to be string or ' . Table::class . '.'
+					);
+				}
+
+				return 'ALTER TABLE '.$table.' DROP CONSTRAINT '.$index->getQuotedName($this);
+			}
+		}
+		else if (! is_string($index))
+		{
+			throw new \InvalidArgumentException(
+				__METHOD__ . '() expects $index parameter to be string or ' . Index::class . '.'
+			);
+		}
+
+		return parent::getDropIndexSQL($index, $table);
+	}
+}
diff --git a/phpBB/phpbb/db/doctrine/sqlsrv_platform.php b/phpBB/phpbb/db/doctrine/sqlsrv_platform.php
new file mode 100644
index 0000000000..58deed2423
--- /dev/null
+++ b/phpBB/phpbb/db/doctrine/sqlsrv_platform.php
@@ -0,0 +1,139 @@
+<?php
+/**
+ *
+ * This file is part of the phpBB Forum Software package.
+ *
+ * @copyright (c) phpBB Limited <https://www.phpbb.com>
+ * @license       GNU General Public License, version 2 (GPL-2.0)
+ *
+ * For full copyright and license information, please see
+ * the docs/CREDITS.txt file.
+ *
+ */
+
+namespace phpbb\db\doctrine;
+
+use Doctrine\DBAL\Platforms\SQLServer2012Platform;
+use Doctrine\DBAL\Schema\Identifier;
+use Doctrine\DBAL\Schema\TableDiff;
+
+/**
+ * Oracle specific schema restrictions for BC.
+ */
+class sqlsrv_platform extends SQLServer2012Platform
+{
+	/**
+	 * {@inheritDoc}
+	 *
+	 * Renames the default constraints to use the classic phpBB's names
+	 */
+	public function getDefaultConstraintDeclarationSQL($table, array $column)
+	{
+		$sql = parent::getDefaultConstraintDeclarationSQL($table, $column);
+
+		return str_replace(
+			[
+				$this->generate_doctrine_identifier_name($table),
+				$this->generate_doctrine_identifier_name($column['name']),
+			], [
+				$table,
+				$column['name'].'_1',
+			],
+			$sql);
+	}
+
+	/**
+	 * {@inheritDoc}
+	 *
+	 * Renames the default constraints to use the classic phpBB's names
+	 */
+	public function getAlterTableSQL(TableDiff $diff)
+	{
+		$sql = [];
+
+		// When dropping a column, if it has a default we need to drop the default constraint first
+		foreach ($diff->removedColumns as $column)
+		{
+			if (!$column->getAutoincrement())
+			{
+				$sql[] = $this->getDropConstraintSQL($this->generate_doctrine_default_constraint_name($diff->name, $column->getQuotedName($this)), $diff->name);
+			}
+		}
+
+		$sql = array_merge($sql, parent::getAlterTableSQL($diff));
+
+		$doctrine_names = [];
+		$phpbb_names = [];
+
+		// OLD Table name
+		$doctrine_names[] = $this->generate_doctrine_identifier_name($diff->name);
+		$phpbb_names[] = $diff->name;
+
+		// NEW Table name if relevant
+		if ($diff->getNewName() != null)
+		{
+			$doctrine_names[] = $this->generate_doctrine_identifier_name($diff->getNewName()->getName());
+			$phpbb_names[] = $diff->getNewName()->getName();
+		}
+
+		foreach ($diff->addedColumns as $column)
+		{
+			$doctrine_names[] = $this->generate_doctrine_identifier_name($column->getQuotedName($this));
+			$phpbb_names[] = $column->getQuotedName($this).'_1';
+		}
+
+		foreach ($diff->removedColumns as $column)
+		{
+			$doctrine_names[] = $this->generate_doctrine_identifier_name($column->getQuotedName($this));
+			$phpbb_names[] = $column->getQuotedName($this).'_1';
+		}
+
+		foreach ($diff->renamedColumns as $column)
+		{
+			$doctrine_names[] = $this->generate_doctrine_identifier_name($column->getQuotedName($this));
+			$phpbb_names[] = $column->getQuotedName($this).'_1';
+		}
+
+		foreach ($diff->changedColumns as $column)
+		{
+			$doctrine_names[] = $this->generate_doctrine_identifier_name($column->column->getQuotedName($this));
+			$phpbb_names[] = $column->column->getQuotedName($this).'_1';
+
+			if ($column->oldColumnName != $column->column->getQuotedName($this))
+			{
+				$doctrine_names[] = $this->generate_doctrine_identifier_name($column->oldColumnName);
+				$phpbb_names[] = $column->oldColumnName.'_1';
+			}
+		}
+
+		return str_replace($doctrine_names, $phpbb_names, $sql);
+	}
+
+	/**
+	 * Returns a hash value for a given identifier.
+	 *
+	 * @param string $identifier Identifier to generate a hash value for.
+	 *
+	 * @return string
+	 */
+	private function generate_doctrine_identifier_name($identifier)
+	{
+		// Always generate name for unquoted identifiers to ensure consistency.
+		$identifier = new Identifier($identifier);
+
+		return strtoupper(dechex(crc32($identifier->getName())));
+	}
+
+	/**
+	 * Returns a unique default constraint name for a table and column.
+	 *
+	 * @param string $table  Name of the table to generate the unique default constraint name for.
+	 * @param string $column Name of the column in the table to generate the unique default constraint name for.
+	 *
+	 * @return string
+	 */
+	private function generate_doctrine_default_constraint_name($table, $column)
+	{
+		return 'DF_' . $this->generate_doctrine_identifier_name($table) . '_' . $this->generate_doctrine_identifier_name($column);
+	}
+}
diff --git a/phpBB/phpbb/db/driver/oracle.php b/phpBB/phpbb/db/driver/oracle.php
index 3f6bc49b35..04af0a0a9c 100644
--- a/phpBB/phpbb/db/driver/oracle.php
+++ b/phpBB/phpbb/db/driver/oracle.php
@@ -160,7 +160,7 @@ class oracle extends \phpbb\db\driver\driver
 	*/
 	function _rewrite_where($where_clause)
 	{
-		preg_match_all('/\s*(AND|OR)?\s*([\w_.()]++)\s*(?:(=|<[=>]?|>=?|LIKE)\s*((?>\'(?>[^\']++|\'\')*+\'|[\d-.()]+))|((NOT )?IN\s*\((?>\'(?>[^\']++|\'\')*+\',? ?|[\d-.]+,? ?)*+\)))/', $where_clause, $result, PREG_SET_ORDER);
+		preg_match_all('/\s*(AND|OR)?\s*([\w_.()]++)\s*(?:(=|<[=>]?|>=?|LIKE)\s*((?>\'(?>[^\']++|\'\')*+\'|[\d\-.()]+))|((NOT )?IN\s*\((?>\'(?>[^\']++|\'\')*+\',? ?|[\d\-.]+,? ?)*+\)))/', $where_clause, $result, PREG_SET_ORDER);
 		$out = '';
 		foreach ($result as $val)
 		{
@@ -188,7 +188,7 @@ class oracle extends \phpbb\db\driver\driver
 				$in_clause = array();
 				$sub_exp = substr($val[5], strpos($val[5], '(') + 1, -1);
 				$extra = false;
-				preg_match_all('/\'(?>[^\']++|\'\')*+\'|[\d-.]++/', $sub_exp, $sub_vals, PREG_PATTERN_ORDER);
+				preg_match_all('/\'(?>[^\']++|\'\')*+\'|[\d\-.]++/', $sub_exp, $sub_vals, PREG_PATTERN_ORDER);
 				$i = 0;
 				foreach ($sub_vals[0] as $sub_val)
 				{
@@ -282,7 +282,7 @@ class oracle extends \phpbb\db\driver\driver
 						{
 							$cols = explode(', ', $regs[2]);
 
-							preg_match_all('/\'(?:[^\']++|\'\')*+\'|[\d-.]+/', $regs[3], $vals, PREG_PATTERN_ORDER);
+							preg_match_all('/\'(?:[^\']++|\'\')*+\'|[\d\-.]+/', $regs[3], $vals, PREG_PATTERN_ORDER);
 
 /*						The code inside this comment block breaks clob handling, but does allow the
 						database restore script to work.  If you want to allow no posts longer than 4KB
@@ -353,13 +353,13 @@ class oracle extends \phpbb\db\driver\driver
 							$query = $regs[1] . '(' . $regs[2] . ') VALUES (' . implode(', ', $inserts) . ')';
 						}
 					}
-					else if (preg_match_all('/^(UPDATE [\\w_]++\\s+SET )([\\w_]++\\s*=\\s*(?:\'(?:[^\']++|\'\')*+\'|[\d-.]+)(?:,\\s*[\\w_]++\\s*=\\s*(?:\'(?:[^\']++|\'\')*+\'|[\d-.]+))*+)\\s+(WHERE.*)$/s', $query, $data, PREG_SET_ORDER))
+					else if (preg_match_all('/^(UPDATE [\\w_]++\\s+SET )([\\w_]++\\s*=\\s*(?:\'(?:[^\']++|\'\')*+\'|[\d\-.]+)(?:,\\s*[\\w_]++\\s*=\\s*(?:\'(?:[^\']++|\'\')*+\'|[\d\-.]+))*+)\\s+(WHERE.*)$/s', $query, $data, PREG_SET_ORDER))
 					{
 						if (strlen($data[0][2]) > 4000)
 						{
 							$update = $data[0][1];
 							$where = $data[0][3];
-							preg_match_all('/([\\w_]++)\\s*=\\s*(\'(?:[^\']++|\'\')*+\'|[\d-.]++)/', $data[0][2], $temp, PREG_SET_ORDER);
+							preg_match_all('/([\\w_]++)\\s*=\\s*(\'(?:[^\']++|\'\')*+\'|[\d\-.]++)/', $data[0][2], $temp, PREG_SET_ORDER);
 							unset($data);
 
 							$cols = array();
@@ -385,7 +385,7 @@ class oracle extends \phpbb\db\driver\driver
 				switch (substr($query, 0, 6))
 				{
 					case 'DELETE':
-						if (preg_match('/^(DELETE FROM [\w_]++ WHERE)((?:\s*(?:AND|OR)?\s*[\w_]+\s*(?:(?:=|<>)\s*(?>\'(?>[^\']++|\'\')*+\'|[\d-.]+)|(?:NOT )?IN\s*\((?>\'(?>[^\']++|\'\')*+\',? ?|[\d-.]+,? ?)*+\)))*+)$/', $query, $regs))
+						if (preg_match('/^(DELETE FROM [\w_]++ WHERE)((?:\s*(?:AND|OR)?\s*[\w_]+\s*(?:(?:=|<>)\s*(?>\'(?>[^\']++|\'\')*+\'|[\d\-.]+)|(?:NOT )?IN\s*\((?>\'(?>[^\']++|\'\')*+\',? ?|[\d\-.]+,? ?)*+\)))*+)$/', $query, $regs))
 						{
 							$query = $regs[1] . $this->_rewrite_where($regs[2]);
 							unset($regs);
@@ -393,7 +393,7 @@ class oracle extends \phpbb\db\driver\driver
 					break;
 
 					case 'UPDATE':
-						if (preg_match('/^(UPDATE [\\w_]++\\s+SET [\\w_]+\s*=\s*(?:\'(?:[^\']++|\'\')*+\'|[\d-.]++|:\w++)(?:, [\\w_]+\s*=\s*(?:\'(?:[^\']++|\'\')*+\'|[\d-.]++|:\w++))*+\\s+WHERE)(.*)$/s',  $query, $regs))
+						if (preg_match('/^(UPDATE [\\w_]++\\s+SET [\\w_]+\s*=\s*(?:\'(?:[^\']++|\'\')*+\'|[\d\-.]++|:\w++)(?:, [\\w_]+\s*=\s*(?:\'(?:[^\']++|\'\')*+\'|[\d\-.]++|:\w++))*+\\s+WHERE)(.*)$/s',  $query, $regs))
 						{
 							$query = $regs[1] . $this->_rewrite_where($regs[2]);
 							unset($regs);
@@ -401,7 +401,7 @@ class oracle extends \phpbb\db\driver\driver
 					break;
 
 					case 'SELECT':
-						$query = preg_replace_callback('/([\w_.]++)\s*(?:(=|<>)\s*(?>\'(?>[^\']++|\'\')*+\'|[\d-.]++|([\w_.]++))|(?:NOT )?IN\s*\((?>\'(?>[^\']++|\'\')*+\',? ?|[\d-.]++,? ?)*+\))/', array($this, '_rewrite_col_compare'), $query);
+						$query = preg_replace_callback('/([\w_.]++)\s*(?:(=|<>)\s*(?>\'(?>[^\']++|\'\')*+\'|[\d\-.]++|([\w_.]++))|(?:NOT )?IN\s*\((?>\'(?>[^\']++|\'\')*+\',? ?|[\d\-.]++,? ?)*+\))/', array($this, '_rewrite_col_compare'), $query);
 					break;
 				}
 
diff --git a/phpBB/phpbb/db/tools/mssql.php b/phpBB/phpbb/db/tools/mssql.php
index b638e9faaf..58d2dde045 100644
--- a/phpBB/phpbb/db/tools/mssql.php
+++ b/phpBB/phpbb/db/tools/mssql.php
@@ -19,6 +19,6 @@ namespace phpbb\db\tools;
  *
  * @deprecated 4.0.0-a1
  */
-class mssql extends tools
+class mssql extends doctrine
 {
 }
diff --git a/phpBB/phpbb/db/tools/postgres.php b/phpBB/phpbb/db/tools/postgres.php
index 1beac0bb6b..611c3ebf0f 100644
--- a/phpBB/phpbb/db/tools/postgres.php
+++ b/phpBB/phpbb/db/tools/postgres.php
@@ -19,6 +19,6 @@ namespace phpbb\db\tools;
  *
  * @deprecated 4.0.0-a1
  */
-class postgres extends tools
+class postgres extends doctrine
 {
 }