diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php
index dc5f9dc9e8e05ebe69fa8e12e208d17bf41077c0..b1068d1a4b2deca6ecd746f3397b9d1f0bf57a66 100644
--- a/lib/composer/composer/autoload_classmap.php
+++ b/lib/composer/composer/autoload_classmap.php
@@ -161,6 +161,7 @@ return array(
     'OCP\\Contacts\\ContactsMenu\\IProvider' => $baseDir . '/lib/public/Contacts/ContactsMenu/IProvider.php',
     'OCP\\Contacts\\Events\\ContactInteractedWithEvent' => $baseDir . '/lib/public/Contacts/Events/ContactInteractedWithEvent.php',
     'OCP\\Contacts\\IManager' => $baseDir . '/lib/public/Contacts/IManager.php',
+    'OCP\\DB\\Exception' => $baseDir . '/lib/public/DB/Exception.php',
     'OCP\\DB\\IPreparedStatement' => $baseDir . '/lib/public/DB/IPreparedStatement.php',
     'OCP\\DB\\IResult' => $baseDir . '/lib/public/DB/IResult.php',
     'OCP\\DB\\ISchemaWrapper' => $baseDir . '/lib/public/DB/ISchemaWrapper.php',
@@ -953,6 +954,7 @@ return array(
     'OC\\DB\\Connection' => $baseDir . '/lib/private/DB/Connection.php',
     'OC\\DB\\ConnectionAdapter' => $baseDir . '/lib/private/DB/ConnectionAdapter.php',
     'OC\\DB\\ConnectionFactory' => $baseDir . '/lib/private/DB/ConnectionFactory.php',
+    'OC\\DB\\Exceptions\\DbalException' => $baseDir . '/lib/private/DB/Exceptions/DbalException.php',
     'OC\\DB\\MDB2SchemaManager' => $baseDir . '/lib/private/DB/MDB2SchemaManager.php',
     'OC\\DB\\MDB2SchemaReader' => $baseDir . '/lib/private/DB/MDB2SchemaReader.php',
     'OC\\DB\\MigrationException' => $baseDir . '/lib/private/DB/MigrationException.php',
diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php
index 455fb01b18eeee6e3693e7d7cf22701e1c852963..d1e11bbb09a40af55fae1452710ab3e8ea2ba4fb 100644
--- a/lib/composer/composer/autoload_static.php
+++ b/lib/composer/composer/autoload_static.php
@@ -190,6 +190,7 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c
         'OCP\\Contacts\\ContactsMenu\\IProvider' => __DIR__ . '/../../..' . '/lib/public/Contacts/ContactsMenu/IProvider.php',
         'OCP\\Contacts\\Events\\ContactInteractedWithEvent' => __DIR__ . '/../../..' . '/lib/public/Contacts/Events/ContactInteractedWithEvent.php',
         'OCP\\Contacts\\IManager' => __DIR__ . '/../../..' . '/lib/public/Contacts/IManager.php',
+        'OCP\\DB\\Exception' => __DIR__ . '/../../..' . '/lib/public/DB/Exception.php',
         'OCP\\DB\\IPreparedStatement' => __DIR__ . '/../../..' . '/lib/public/DB/IPreparedStatement.php',
         'OCP\\DB\\IResult' => __DIR__ . '/../../..' . '/lib/public/DB/IResult.php',
         'OCP\\DB\\ISchemaWrapper' => __DIR__ . '/../../..' . '/lib/public/DB/ISchemaWrapper.php',
@@ -982,6 +983,7 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c
         'OC\\DB\\Connection' => __DIR__ . '/../../..' . '/lib/private/DB/Connection.php',
         'OC\\DB\\ConnectionAdapter' => __DIR__ . '/../../..' . '/lib/private/DB/ConnectionAdapter.php',
         'OC\\DB\\ConnectionFactory' => __DIR__ . '/../../..' . '/lib/private/DB/ConnectionFactory.php',
+        'OC\\DB\\Exceptions\\DbalException' => __DIR__ . '/../../..' . '/lib/private/DB/Exceptions/DbalException.php',
         'OC\\DB\\MDB2SchemaManager' => __DIR__ . '/../../..' . '/lib/private/DB/MDB2SchemaManager.php',
         'OC\\DB\\MDB2SchemaReader' => __DIR__ . '/../../..' . '/lib/private/DB/MDB2SchemaReader.php',
         'OC\\DB\\MigrationException' => __DIR__ . '/../../..' . '/lib/private/DB/MigrationException.php',
diff --git a/lib/private/DB/Adapter.php b/lib/private/DB/Adapter.php
index 49b831301be2ae9cf8e3fc770bd611fdfebbe8e8..62514e7d83a17ecdcf383a52d1aa4f817a93be31 100644
--- a/lib/private/DB/Adapter.php
+++ b/lib/private/DB/Adapter.php
@@ -30,6 +30,7 @@
 
 namespace OC\DB;
 
+use Doctrine\DBAL\Exception;
 use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
 
 /**
@@ -49,7 +50,9 @@ class Adapter {
 
 	/**
 	 * @param string $table name
+	 *
 	 * @return int id of last insert statement
+	 * @throws Exception
 	 */
 	public function lastInsertId($table) {
 		return (int) $this->conn->realLastInsertId($table);
@@ -67,6 +70,7 @@ class Adapter {
 	 * Create an exclusive read+write lock on a table
 	 *
 	 * @param string $tableName
+	 * @throws Exception
 	 * @since 9.1.0
 	 */
 	public function lockTable($tableName) {
@@ -77,6 +81,7 @@ class Adapter {
 	/**
 	 * Release a previous acquired lock again
 	 *
+	 * @throws Exception
 	 * @since 9.1.0
 	 */
 	public function unlockTable() {
@@ -94,7 +99,7 @@ class Adapter {
 	 *				If this is null or an empty array, all keys of $input will be compared
 	 *				Please note: text fields (clob) must not be used in the compare array
 	 * @return int number of inserted rows
-	 * @throws \Doctrine\DBAL\Exception
+	 * @throws Exception
 	 * @deprecated 15.0.0 - use unique index and "try { $db->insert() } catch (UniqueConstraintViolationException $e) {}" instead, because it is more reliable and does not have the risk for deadlocks - see https://github.com/nextcloud/server/pull/12371
 	 */
 	public function insertIfNotExist($table, $input, array $compare = null) {
@@ -130,6 +135,9 @@ class Adapter {
 		}
 	}
 
+	/**
+	 * @throws \OCP\DB\Exception
+	 */
 	public function insertIgnoreConflict(string $table,array $values) : int {
 		try {
 			$builder = $this->conn->getQueryBuilder();
diff --git a/lib/private/DB/Connection.php b/lib/private/DB/Connection.php
index c67c6df0826119fa61fcbccbcc496d0e21e51216..cb7af4d51e29bc0d3654fd869a0043ccb96efd56 100644
--- a/lib/private/DB/Connection.php
+++ b/lib/private/DB/Connection.php
@@ -74,6 +74,9 @@ class Connection extends ReconnectWrapper {
 	/** @var int */
 	protected $queriesExecuted = 0;
 
+	/**
+	 * @throws Exception
+	 */
 	public function connect() {
 		try {
 			return parent::connect();
@@ -183,7 +186,9 @@ class Connection extends ReconnectWrapper {
 	 * @param string $statement The SQL statement to prepare.
 	 * @param int $limit
 	 * @param int $offset
+	 *
 	 * @return Statement The prepared statement.
+	 * @throws Exception
 	 */
 	public function prepare($statement, $limit = null, $offset = null): Statement {
 		if ($limit === -1) {
@@ -221,6 +226,9 @@ class Connection extends ReconnectWrapper {
 		return parent::executeQuery($sql, $params, $types, $qcp);
 	}
 
+	/**
+	 * @throws Exception
+	 */
 	public function executeUpdate(string $sql, array $params = [], array $types = []): int {
 		$sql = $this->replaceTablePrefix($sql);
 		$sql = $this->adapter->fixupStatement($sql);
@@ -258,7 +266,9 @@ class Connection extends ReconnectWrapper {
 	 * columns or sequences.
 	 *
 	 * @param string $seqName Name of the sequence object from which the ID should be returned.
+	 *
 	 * @return string the last inserted ID.
+	 * @throws Exception
 	 */
 	public function lastInsertId($seqName = null) {
 		if ($seqName) {
@@ -267,7 +277,10 @@ class Connection extends ReconnectWrapper {
 		return $this->adapter->lastInsertId($seqName);
 	}
 
-	// internal use
+	/**
+	 * @internal
+	 * @throws Exception
+	 */
 	public function realLastInsertId($seqName = null) {
 		return parent::lastInsertId($seqName);
 	}
@@ -364,7 +377,9 @@ class Connection extends ReconnectWrapper {
 	 * Create an exclusive read+write lock on a table
 	 *
 	 * @param string $tableName
+	 *
 	 * @throws \BadMethodCallException When trying to acquire a second lock
+	 * @throws Exception
 	 * @since 9.1.0
 	 */
 	public function lockTable($tableName) {
@@ -380,6 +395,7 @@ class Connection extends ReconnectWrapper {
 	/**
 	 * Release a previous acquired lock again
 	 *
+	 * @throws Exception
 	 * @since 9.1.0
 	 */
 	public function unlockTable() {
@@ -415,6 +431,8 @@ class Connection extends ReconnectWrapper {
 	 * Drop a table from the database if it exists
 	 *
 	 * @param string $table table name without the prefix
+	 *
+	 * @throws Exception
 	 */
 	public function dropTable($table) {
 		$table = $this->tablePrefix . trim($table);
@@ -428,7 +446,9 @@ class Connection extends ReconnectWrapper {
 	 * Check if a table exists
 	 *
 	 * @param string $table table name without the prefix
+	 *
 	 * @return bool
+	 * @throws Exception
 	 */
 	public function tableExists($table) {
 		$table = $this->tablePrefix . trim($table);
@@ -483,6 +503,7 @@ class Connection extends ReconnectWrapper {
 	 * Create the schema of the connected database
 	 *
 	 * @return Schema
+	 * @throws Exception
 	 */
 	public function createSchema() {
 		$schemaManager = new MDB2SchemaManager($this);
@@ -494,6 +515,8 @@ class Connection extends ReconnectWrapper {
 	 * Migrate the database to the given schema
 	 *
 	 * @param Schema $toSchema
+	 *
+	 * @throws Exception
 	 */
 	public function migrateToSchema(Schema $toSchema) {
 		$schemaManager = new MDB2SchemaManager($this);
diff --git a/lib/private/DB/ConnectionAdapter.php b/lib/private/DB/ConnectionAdapter.php
index 97a0b60044d4d1950f3f3f73f14d5224fd47b2b2..62c013e4dcf2d7a29fb2e1d8bac3787e7e459d8d 100644
--- a/lib/private/DB/ConnectionAdapter.php
+++ b/lib/private/DB/ConnectionAdapter.php
@@ -25,8 +25,10 @@ declare(strict_types=1);
 
 namespace OC\DB;
 
+use Doctrine\DBAL\Exception;
 use Doctrine\DBAL\Platforms\AbstractPlatform;
 use Doctrine\DBAL\Schema\Schema;
+use OC\DB\Exceptions\DbalException;
 use OCP\DB\IPreparedStatement;
 use OCP\DB\IResult;
 use OCP\DB\QueryBuilder\IQueryBuilder;
@@ -49,51 +51,95 @@ class ConnectionAdapter implements IDBConnection {
 	}
 
 	public function prepare($sql, $limit = null, $offset = null): IPreparedStatement {
-		return new PreparedStatement(
-			$this->inner->prepare($sql, $limit, $offset)
-		);
+		try {
+			return new PreparedStatement(
+				$this->inner->prepare($sql, $limit, $offset)
+			);
+		} catch (Exception $e) {
+			throw DbalException::wrap($e);
+		}
 	}
 
 	public function executeQuery(string $sql, array $params = [], $types = []): IResult {
-		return new ResultAdapter(
-			$this->inner->executeQuery($sql, $params, $types)
-		);
+		try {
+			return new ResultAdapter(
+				$this->inner->executeQuery($sql, $params, $types)
+			);
+		} catch (Exception $e) {
+			throw DbalException::wrap($e);
+		}
 	}
 
 	public function executeUpdate(string $sql, array $params = [], array $types = []): int {
-		return $this->inner->executeUpdate($sql, $params, $types);
+		try {
+			return $this->inner->executeUpdate($sql, $params, $types);
+		} catch (Exception $e) {
+			throw DbalException::wrap($e);
+		}
 	}
 
 	public function executeStatement($sql, array $params = [], array $types = []): int {
-		return $this->inner->executeStatement($sql, $params, $types);
+		try {
+			return $this->inner->executeStatement($sql, $params, $types);
+		} catch (Exception $e) {
+			throw DbalException::wrap($e);
+		}
 	}
 
 	public function lastInsertId(string $table): int {
-		return (int) $this->inner->lastInsertId($table);
+		try {
+			return (int)$this->inner->lastInsertId($table);
+		} catch (Exception $e) {
+			throw DbalException::wrap($e);
+		}
 	}
 
 	public function insertIfNotExist(string $table, array $input, array $compare = null) {
-		return $this->inner->insertIfNotExist($table, $input, $compare);
+		try {
+			return $this->inner->insertIfNotExist($table, $input, $compare);
+		} catch (Exception $e) {
+			throw DbalException::wrap($e);
+		}
 	}
 
 	public function insertIgnoreConflict(string $table, array $values): int {
-		return $this->inner->insertIgnoreConflict($table, $values);
+		try {
+			return $this->inner->insertIgnoreConflict($table, $values);
+		} catch (Exception $e) {
+			throw DbalException::wrap($e);
+		}
 	}
 
 	public function setValues($table, array $keys, array $values, array $updatePreconditionValues = []): int {
-		return $this->inner->setValues($table, $keys, $values, $updatePreconditionValues);
+		try {
+			return $this->inner->setValues($table, $keys, $values, $updatePreconditionValues);
+		} catch (Exception $e) {
+			throw DbalException::wrap($e);
+		}
 	}
 
 	public function lockTable($tableName): void {
-		$this->inner->lockTable($tableName);
+		try {
+			$this->inner->lockTable($tableName);
+		} catch (Exception $e) {
+			throw DbalException::wrap($e);
+		}
 	}
 
 	public function unlockTable(): void {
-		$this->inner->unlockTable();
+		try {
+			$this->inner->unlockTable();
+		} catch (Exception $e) {
+			throw DbalException::wrap($e);
+		}
 	}
 
 	public function beginTransaction(): void {
-		$this->inner->beginTransaction();
+		try {
+			$this->inner->beginTransaction();
+		} catch (Exception $e) {
+			throw DbalException::wrap($e);
+		}
 	}
 
 	public function inTransaction(): bool {
@@ -101,11 +147,19 @@ class ConnectionAdapter implements IDBConnection {
 	}
 
 	public function commit(): void {
-		$this->inner->commit();
+		try {
+			$this->inner->commit();
+		} catch (Exception $e) {
+			throw DbalException::wrap($e);
+		}
 	}
 
 	public function rollBack(): void {
-		$this->inner->rollBack();
+		try {
+			$this->inner->rollBack();
+		} catch (Exception $e) {
+			throw DbalException::wrap($e);
+		}
 	}
 
 	public function getError(): string {
@@ -121,7 +175,11 @@ class ConnectionAdapter implements IDBConnection {
 	}
 
 	public function connect(): bool {
-		return $this->inner->connect();
+		try {
+			return $this->inner->connect();
+		} catch (Exception $e) {
+			throw DbalException::wrap($e);
+		}
 	}
 
 	public function close(): void {
@@ -140,11 +198,19 @@ class ConnectionAdapter implements IDBConnection {
 	}
 
 	public function dropTable(string $table): void {
-		$this->inner->dropTable($table);
+		try {
+			$this->inner->dropTable($table);
+		} catch (Exception $e) {
+			throw DbalException::wrap($e);
+		}
 	}
 
 	public function tableExists(string $table): bool {
-		return $this->inner->tableExists($table);
+		try {
+			return $this->inner->tableExists($table);
+		} catch (Exception $e) {
+			throw DbalException::wrap($e);
+		}
 	}
 
 	public function escapeLikeParameter(string $param): string {
@@ -159,11 +225,19 @@ class ConnectionAdapter implements IDBConnection {
 	 * @todo leaks a 3rdparty type
 	 */
 	public function createSchema(): Schema {
-		return $this->inner->createSchema();
+		try {
+			return $this->inner->createSchema();
+		} catch (Exception $e) {
+			throw DbalException::wrap($e);
+		}
 	}
 
 	public function migrateToSchema(Schema $toSchema): void {
-		$this->inner->migrateToSchema($toSchema);
+		try {
+			$this->inner->migrateToSchema($toSchema);
+		} catch (Exception $e) {
+			throw DbalException::wrap($e);
+		}
 	}
 
 	public function getInner(): Connection {
diff --git a/lib/private/DB/Exceptions/DbalException.php b/lib/private/DB/Exceptions/DbalException.php
new file mode 100644
index 0000000000000000000000000000000000000000..4e0e151704860a84f7a178d98b17445a5c6fa832
--- /dev/null
+++ b/lib/private/DB/Exceptions/DbalException.php
@@ -0,0 +1,136 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * @copyright 2021 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @author 2021 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+namespace OC\DB\Exceptions;
+
+use Doctrine\DBAL\ConnectionException;
+use Doctrine\DBAL\Exception\ConstraintViolationException;
+use Doctrine\DBAL\Exception\DatabaseObjectExistsException;
+use Doctrine\DBAL\Exception\DatabaseObjectNotFoundException;
+use Doctrine\DBAL\Exception\DeadlockException;
+use Doctrine\DBAL\Exception\DriverException;
+use Doctrine\DBAL\Exception\ForeignKeyConstraintViolationException;
+use Doctrine\DBAL\Exception\InvalidArgumentException;
+use Doctrine\DBAL\Exception\InvalidFieldNameException;
+use Doctrine\DBAL\Exception\NonUniqueFieldNameException;
+use Doctrine\DBAL\Exception\NotNullConstraintViolationException;
+use Doctrine\DBAL\Exception\ServerException;
+use Doctrine\DBAL\Exception\SyntaxErrorException;
+use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
+use OCP\DB\Exception;
+
+/**
+ * Wrapper around the raw dbal exception, so we can pass it to apps that catch
+ * our OCP db exception
+ *
+ * @psalm-immutable
+ */
+class DbalException extends Exception {
+
+	/** @var \Doctrine\DBAL\Exception */
+	private $original;
+
+	/**
+	 * @param \Doctrine\DBAL\Exception $original
+	 * @param int $code
+	 * @param string $message
+	 */
+	private function __construct(\Doctrine\DBAL\Exception $original, int $code, string $message) {
+		parent::__construct(
+			$message,
+			$code,
+			$original
+		);
+		$this->original = $original;
+	}
+
+	public static function wrap(\Doctrine\DBAL\Exception $original, string $message = ''): self {
+		return new self(
+			$original,
+			is_int($original->getCode()) ? $original->getCode() : 0,
+			empty($message) ? $original->getMessage() : $message
+		);
+	}
+
+	public function getReason(): ?int {
+		/**
+		 * Generic errors
+		 */
+		if ($this->original instanceof ConnectionException) {
+			return parent::REASON_CONNECTION_LOST;
+		}
+		if ($this->original instanceof DriverException) {
+			return parent::REASON_DRIVER;
+		}
+		if ($this->original instanceof InvalidArgumentException) {
+			return parent::REASON_INVALID_ARGUMENT;
+		}
+
+		/**
+		 * Constraint errors
+		 */
+		if ($this->original instanceof ForeignKeyConstraintViolationException) {
+			return parent::REASON_FOREIGN_KEY_VIOLATION;
+		}
+		if ($this->original instanceof NotNullConstraintViolationException) {
+			return parent::REASON_NOT_NULL_CONSTRAINT_VIOLATION;
+		}
+		if ($this->original instanceof UniqueConstraintViolationException) {
+			return parent::REASON_UNIQUE_CONSTRAINT_VIOLATION;
+		}
+		// The base exception comes last
+		if ($this->original instanceof ConstraintViolationException) {
+			return parent::REASON_CONSTRAINT_VIOLATION;
+		}
+
+		/**
+		 * Other server errors
+		 */
+		if ($this->original instanceof DatabaseObjectExistsException) {
+			return parent::REASON_DATABASE_OBJECT_EXISTS;
+		}
+		if ($this->original instanceof DatabaseObjectNotFoundException) {
+			return parent::REASON_DATABASE_OBJECT_NOT_FOUND;
+		}
+		if ($this->original instanceof DeadlockException) {
+			return parent::REASON_DEADLOCK;
+		}
+		if ($this->original instanceof InvalidFieldNameException) {
+			return parent::REASON_INVALID_FIELD_NAME;
+		}
+		if ($this->original instanceof NonUniqueFieldNameException) {
+			return parent::REASON_NON_UNIQUE_FIELD_NAME;
+		}
+		if ($this->original instanceof SyntaxErrorException) {
+			return parent::REASON_SYNTAX_ERROR;
+		}
+		// The base server exception class comes last
+		if ($this->original instanceof ServerException) {
+			return parent::REASON_SERVER;
+		}
+
+		return null;
+	}
+}
diff --git a/lib/private/DB/Migrator.php b/lib/private/DB/Migrator.php
index e50927f620bf162f3851c81ed98f7bd004726c59..dcf0db89f72476d01c96557e9712e6fc6cacf9d8 100644
--- a/lib/private/DB/Migrator.php
+++ b/lib/private/DB/Migrator.php
@@ -82,6 +82,8 @@ class Migrator {
 
 	/**
 	 * @param \Doctrine\DBAL\Schema\Schema $targetSchema
+	 *
+	 * @throws Exception
 	 */
 	public function migrate(Schema $targetSchema) {
 		$this->noEmit = true;
@@ -171,6 +173,9 @@ class Migrator {
 		return new Table($newName, $table->getColumns(), $newIndexes, [], [], $table->getOptions());
 	}
 
+	/**
+	 * @throws Exception
+	 */
 	public function createSchema() {
 		$this->connection->getConfiguration()->setSchemaAssetsFilter(function ($asset) {
 			/** @var string|AbstractAsset $asset */
@@ -231,6 +236,8 @@ class Migrator {
 	/**
 	 * @param \Doctrine\DBAL\Schema\Schema $targetSchema
 	 * @param \Doctrine\DBAL\Connection $connection
+	 *
+	 * @throws Exception
 	 */
 	protected function applySchema(Schema $targetSchema, \Doctrine\DBAL\Connection $connection = null) {
 		if (is_null($connection)) {
diff --git a/lib/public/DB/Exception.php b/lib/public/DB/Exception.php
new file mode 100644
index 0000000000000000000000000000000000000000..1154530e85d4d58e0bb1f833b8df23800b775ca6
--- /dev/null
+++ b/lib/public/DB/Exception.php
@@ -0,0 +1,149 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * @copyright 2021 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @author 2021 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+namespace OCP\DB;
+
+use Exception as BaseException;
+
+/**
+ * Database exception
+ *
+ * Thrown by Nextcloud's database abstraction layer. This is the base class that
+ * any specific exception will extend. Use this class in your try-catch to catch
+ * *any* error related to the database. Use any of the subclasses in the same
+ * namespace if you are only interested in specific errors.
+ *
+ * @psalm-immutable
+ * @since 21.0.0
+ */
+class Exception extends BaseException {
+
+	/**
+	 * Nextcloud lost connection to the database
+	 *
+	 * @since 21.0.0
+	 */
+	public const REASON_CONNECTION_LOST = 1;
+
+	/**
+	 * A database constraint was violated
+	 *
+	 * @since 21.0.0
+	 */
+	public const REASON_CONSTRAINT_VIOLATION = 2;
+
+	/**
+	 * A database object (table, column, index) already exists
+	 *
+	 * @since 21.0.0
+	 */
+	public const REASON_DATABASE_OBJECT_EXISTS = 3;
+
+	/**
+	 * A database object (table, column, index) can't be found
+	 *
+	 * @since 21.0.0
+	 */
+	public const REASON_DATABASE_OBJECT_NOT_FOUND = 4;
+
+	/**
+	 * The database ran into a deadlock
+	 *
+	 * @since 21.0.0
+	 */
+	public const REASON_DEADLOCK = 5;
+
+	/**
+	 * The database driver encountered an issue
+	 *
+	 * @since 21.0.0
+	 */
+	public const REASON_DRIVER = 6;
+
+	/**
+	 * A foreign key constraint was violated
+	 *
+	 * @since 21.0.0
+	 */
+	public const REASON_FOREIGN_KEY_VIOLATION = 7;
+
+	/**
+	 * An invalid argument was passed to the database abstraction
+	 *
+	 * @since 21.0.0
+	 */
+	public const REASON_INVALID_ARGUMENT = 8;
+
+	/**
+	 * A field name was invalid
+	 *
+	 * @since 21.0.0
+	 */
+	public const REASON_INVALID_FIELD_NAME = 9;
+
+	/**
+	 * A name in the query was ambiguous
+	 *
+	 * @since 21.0.0
+	 */
+	public const REASON_NON_UNIQUE_FIELD_NAME = 10;
+
+	/**
+	 * A not null contraint was violated
+	 *
+	 * @since 21.0.0
+	 */
+	public const REASON_NOT_NULL_CONSTRAINT_VIOLATION = 11;
+
+	/**
+	 * A generic server error was encountered
+	 *
+	 * @since 21.0.0
+	 */
+	public const REASON_SERVER = 12;
+
+	/**
+	 * A syntax error was reported by the server
+	 *
+	 * @since 21.0.0
+	 */
+	public const REASON_SYNTAX_ERROR = 13;
+
+	/**
+	 * A unique constraint was violated
+	 *
+	 * @since 21.0.0
+	 */
+	public const REASON_UNIQUE_CONSTRAINT_VIOLATION = 14;
+
+	/**
+	 * @return int|null
+	 * @psalm-return Exception::REASON_*
+	 * @since 21.0.0
+	 */
+	public function getReason(): ?int {
+		return null;
+	}
+}
diff --git a/lib/public/DB/QueryBuilder/IQueryBuilder.php b/lib/public/DB/QueryBuilder/IQueryBuilder.php
index 8fcbd6c32761cbe11c30ff7646cb4eff55eaf5d3..24de7b4ce609211fcc512462d97d7dc47899fec5 100644
--- a/lib/public/DB/QueryBuilder/IQueryBuilder.php
+++ b/lib/public/DB/QueryBuilder/IQueryBuilder.php
@@ -29,6 +29,7 @@
 namespace OCP\DB\QueryBuilder;
 
 use Doctrine\DBAL\Connection;
+use OCP\DB\Exception;
 use OCP\DB\IResult;
 
 /**
@@ -154,6 +155,7 @@ interface IQueryBuilder {
 	 *          to bridge old code to the new API
 	 *
 	 * @return IResult|int
+	 * @throws Exception since 21.0.0
 	 * @since 8.2.0
 	 */
 	public function execute();
diff --git a/lib/public/IDBConnection.php b/lib/public/IDBConnection.php
index 16a5f998fde7fafd459cdf90aa8cab17b2e5fa21..5618e3ec40b7a399898c5e4711f9c54472c3af85 100644
--- a/lib/public/IDBConnection.php
+++ b/lib/public/IDBConnection.php
@@ -39,8 +39,8 @@
 
 namespace OCP;
 
-use Doctrine\DBAL\Exception;
 use Doctrine\DBAL\Schema\Schema;
+use OCP\DB\Exception;
 use OCP\DB\IPreparedStatement;
 use OCP\DB\IResult;
 use OCP\DB\QueryBuilder\IQueryBuilder;
@@ -103,6 +103,7 @@ interface IDBConnection {
 	 * @param array $types The parameter types.
 	 * @return int The number of affected rows.
 	 * @since 8.0.0
+	 * @throws Exception since 21.0.0
 	 *
 	 * @deprecated 21.0.0 use executeStatement
 	 */
@@ -119,6 +120,7 @@ interface IDBConnection {
 	 * @param array $types The parameter types.
 	 * @return int The number of affected rows.
 	 * @since 21.0.0
+	 * @throws Exception since 21.0.0
 	 */
 	public function executeStatement($sql, array $params = [], array $types = []): int;
 
@@ -143,7 +145,7 @@ interface IDBConnection {
 	 *				If this is null or an empty array, all keys of $input will be compared
 	 *				Please note: text fields (clob) must not be used in the compare array
 	 * @return int number of inserted rows
-	 * @throws Exception
+	 * @throws Exception used to be the removed dbal exception, since 21.0.0 it's \OCP\DB\Exception
 	 * @since 6.0.0 - parameter $compare was added in 8.1.0, return type changed from boolean in 8.1.0
 	 * @deprecated 15.0.0 - use unique index and "try { $db->insert() } catch (UniqueConstraintViolationException $e) {}" instead, because it is more reliable and does not have the risk for deadlocks - see https://github.com/nextcloud/server/pull/12371
 	 */
@@ -171,7 +173,7 @@ interface IDBConnection {
 	 * @param array $values (column name => value)
 	 * @param array $updatePreconditionValues ensure values match preconditions (column name => value)
 	 * @return int number of new rows
-	 * @throws Exception
+	 * @throws Exception used to be the removed dbal exception, since 21.0.0 it's \OCP\DB\Exception
 	 * @throws PreconditionNotMetException
 	 * @since 9.0.0
 	 */
@@ -185,6 +187,7 @@ interface IDBConnection {
 	 * transaction while holding a lock.
 	 *
 	 * @param string $tableName
+	 * @throws Exception since 21.0.0
 	 * @since 9.1.0
 	 */
 	public function lockTable($tableName): void;
@@ -192,6 +195,7 @@ interface IDBConnection {
 	/**
 	 * Release a previous acquired lock again
 	 *
+	 * @throws Exception since 21.0.0
 	 * @since 9.1.0
 	 */
 	public function unlockTable(): void;
@@ -255,6 +259,7 @@ interface IDBConnection {
 	 * Establishes the connection with the database.
 	 *
 	 * @return bool
+	 * @throws Exception since 21.0.0
 	 * @since 8.0.0
 	 */
 	public function connect(): bool;
@@ -288,6 +293,7 @@ interface IDBConnection {
 	 * Drop a table from the database if it exists
 	 *
 	 * @param string $table table name without the prefix
+	 * @throws Exception since 21.0.0
 	 * @since 8.0.0
 	 */
 	public function dropTable(string $table): void;
@@ -297,6 +303,7 @@ interface IDBConnection {
 	 *
 	 * @param string $table table name without the prefix
 	 * @return bool
+	 * @throws Exception since 21.0.0
 	 * @since 8.0.0
 	 */
 	public function tableExists(string $table): bool;
@@ -322,6 +329,7 @@ interface IDBConnection {
 	 * Create the schema of the connected database
 	 *
 	 * @return Schema
+	 * @throws Exception since 21.0.0
 	 * @since 13.0.0
 	 */
 	public function createSchema(): Schema;
@@ -330,6 +338,7 @@ interface IDBConnection {
 	 * Migrate the database to the given schema
 	 *
 	 * @param Schema $toSchema
+	 * @throws Exception since 21.0.0
 	 * @since 13.0.0
 	 */
 	public function migrateToSchema(Schema $toSchema): void;