diff --git a/lib/private/DB/Adapter.php b/lib/private/DB/Adapter.php index 6ebcfdc34f242..ea4f5cd771327 100644 --- a/lib/private/DB/Adapter.php +++ b/lib/private/DB/Adapter.php @@ -111,13 +111,16 @@ public function insertIfNotExist($table, $input, ?array $compare = null) { /** * @throws \OCP\DB\Exception */ - public function insertIgnoreConflict(string $table, array $values) : int { + public function insertIgnoreConflict(string $table, array $values, array $hintShardKey = []) : int { try { $builder = $this->conn->getQueryBuilder(); $builder->insert($table); foreach ($values as $key => $value) { $builder->setValue($key, $builder->createNamedParameter($value)); } + if (isset($hintShardKey['column'], $hintShardKey['value'])) { + $builder->hintShardKey($hintShardKey['column'], $hintShardKey['value'], $hintShardKey['overwrite'] ?? false); + } return $builder->executeStatement(); } catch (DbalException $e) { if ($e->getReason() === \OCP\DB\Exception::REASON_UNIQUE_CONSTRAINT_VIOLATION) { @@ -126,4 +129,8 @@ public function insertIgnoreConflict(string $table, array $values) : int { throw $e; } } + + public function getInsertIgnoreSqlTransformer(): ?callable { + return null; + } } diff --git a/lib/private/DB/AdapterMySQL.php b/lib/private/DB/AdapterMySQL.php index 63c75607379a3..57a794b790fd1 100644 --- a/lib/private/DB/AdapterMySQL.php +++ b/lib/private/DB/AdapterMySQL.php @@ -36,31 +36,33 @@ protected function getCollation(): string { return $this->collation; } - public function insertIgnoreConflict(string $table, array $values): int { + public function insertIgnoreConflict(string $table, array $values, array $hintShardKey = []): int { $builder = $this->conn->getQueryBuilder(); $builder->insert($table); - $updates = []; foreach ($values as $key => $value) { $builder->setValue($key, $builder->createNamedParameter($value)); } - /* - * We can't use ON DUPLICATE KEY UPDATE here because Nextcloud use the CLIENT_FOUND_ROWS flag - * With this flag the MySQL returns the number of selected rows - * instead of the number of affected/modified rows - * It's impossible to change this behaviour at runtime or for a single query - * Then, the result is 1 if a row is inserted and also 1 if a row is updated with same or different values - * - * With INSERT IGNORE, the result is 1 when a row is inserted, 0 otherwise - * - * Risk: it can also ignore other errors like type mismatch or truncated data… - */ - $res = $this->conn->executeStatement( - preg_replace('/^INSERT/i', 'INSERT IGNORE', $builder->getSQL()), - $builder->getParameters(), - $builder->getParameterTypes() - ); + if (isset($hintShardKey['column'], $hintShardKey['value'])) { + $builder->hintShardKey($hintShardKey['column'], $hintShardKey['value'], $hintShardKey['overwrite'] ?? false); + } + + $builder->ignoreConflictsOnInsert(); + return $builder->executeStatement(); + } - return $res; + /** + * We can't use ON DUPLICATE KEY UPDATE here because Nextcloud use the CLIENT_FOUND_ROWS flag + * With this flag the MySQL returns the number of selected rows + * instead of the number of affected/modified rows + * It's impossible to change this behaviour at runtime or for a single query + * Then, the result is 1 if a row is inserted and also 1 if a row is updated with same or different values + * + * With INSERT IGNORE, the result is 1 when a row is inserted, 0 otherwise + * + * Risk: it can also ignore other errors like type mismatch or truncated data… + */ + public function getInsertIgnoreSqlTransformer(): callable { + return fn (string $sql) => preg_replace('/^INSERT/i', 'INSERT IGNORE', $sql); } } diff --git a/lib/private/DB/AdapterPgSql.php b/lib/private/DB/AdapterPgSql.php index db48c81c2c54c..af1e0a2f089f1 100644 --- a/lib/private/DB/AdapterPgSql.php +++ b/lib/private/DB/AdapterPgSql.php @@ -23,7 +23,7 @@ public function fixupStatement($statement) { return $statement; } - public function insertIgnoreConflict(string $table, array $values) : int { + public function insertIgnoreConflict(string $table, array $values, array $hintShardKey = []) : int { // "upsert" is only available since PgSQL 9.5, but the generic way // would leave error logs in the DB. $builder = $this->conn->getQueryBuilder(); @@ -31,7 +31,14 @@ public function insertIgnoreConflict(string $table, array $values) : int { foreach ($values as $key => $value) { $builder->setValue($key, $builder->createNamedParameter($value)); } - $queryString = $builder->getSQL() . ' ON CONFLICT DO NOTHING'; - return $this->conn->executeUpdate($queryString, $builder->getParameters(), $builder->getParameterTypes()); + if (isset($hintShardKey['column'], $hintShardKey['value'])) { + $builder->hintShardKey($hintShardKey['column'], $hintShardKey['value'], $hintShardKey['overwrite'] ?? false); + } + $builder->ignoreConflictsOnInsert(); + return $builder->executeStatement(); + } + + public function getInsertIgnoreSqlTransformer(): callable { + return fn (string $sql) => $sql . ' ON CONFLICT DO NOTHING'; } } diff --git a/lib/private/DB/AdapterSqlite.php b/lib/private/DB/AdapterSqlite.php index aeadf55ecf7b5..d9768572924cf 100644 --- a/lib/private/DB/AdapterSqlite.php +++ b/lib/private/DB/AdapterSqlite.php @@ -77,18 +77,22 @@ public function insertIfNotExist($table, $input, ?array $compare = null) { } } - public function insertIgnoreConflict(string $table, array $values): int { + public function insertIgnoreConflict(string $table, array $values, array $hintShardKey = []): int { $builder = $this->conn->getQueryBuilder(); $builder->insert($table); - $updates = []; foreach ($values as $key => $value) { $builder->setValue($key, $builder->createNamedParameter($value)); } - return $this->conn->executeStatement( - $builder->getSQL() . ' ON CONFLICT DO NOTHING', - $builder->getParameters(), - $builder->getParameterTypes() - ); + if (isset($hintShardKey['column'], $hintShardKey['value'])) { + $builder->hintShardKey($hintShardKey['column'], $hintShardKey['value'], $hintShardKey['overwrite'] ?? false); + } + + $builder->ignoreConflictsOnInsert(); + return $builder->executeStatement(); + } + + public function getInsertIgnoreSqlTransformer(): callable { + return fn (string $sql) => $sql . ' ON CONFLICT DO NOTHING'; } } diff --git a/lib/private/DB/Connection.php b/lib/private/DB/Connection.php index 3eef60c6dafb7..c817a11bd4d53 100644 --- a/lib/private/DB/Connection.php +++ b/lib/private/DB/Connection.php @@ -548,9 +548,9 @@ public function insertIfNotExist($table, $input, ?array $compare = null) { } } - public function insertIgnoreConflict(string $table, array $values) : int { + public function insertIgnoreConflict(string $table, array $values, array $hintShardKey = []) : int { try { - return $this->adapter->insertIgnoreConflict($table, $values); + return $this->adapter->insertIgnoreConflict($table, $values, $hintShardKey); } catch (\Exception $e) { $this->logDatabaseException($e); throw $e; @@ -961,4 +961,8 @@ public function getShardDefinition(string $name): ?ShardDefinition { public function getCrossShardMoveHelper(): CrossShardMoveHelper { return new CrossShardMoveHelper($this->shardConnectionManager); } + + public function getInsertIgnoreSqlTransformer(): ?callable { + return $this->adapter->getInsertIgnoreSqlTransformer(); + } } diff --git a/lib/private/DB/ConnectionAdapter.php b/lib/private/DB/ConnectionAdapter.php index d9e3e7ec5490b..d8af700de69c1 100644 --- a/lib/private/DB/ConnectionAdapter.php +++ b/lib/private/DB/ConnectionAdapter.php @@ -89,9 +89,9 @@ public function insertIfNotExist(string $table, array $input, ?array $compare = } } - public function insertIgnoreConflict(string $table, array $values): int { + public function insertIgnoreConflict(string $table, array $values, array $hintShardKey = []): int { try { - return $this->inner->insertIgnoreConflict($table, $values); + return $this->inner->insertIgnoreConflict($table, $values, $hintShardKey); } catch (Exception $e) { throw DbalException::wrap($e); } @@ -265,4 +265,8 @@ public function getShardDefinition(string $name): ?ShardDefinition { public function getCrossShardMoveHelper(): CrossShardMoveHelper { return $this->inner->getCrossShardMoveHelper(); } + + public function getInsertIgnoreSqlTransformer(): ?callable { + return $this->inner->getInsertIgnoreSqlTransformer(); + } } diff --git a/lib/private/DB/QueryBuilder/ExtendedQueryBuilder.php b/lib/private/DB/QueryBuilder/ExtendedQueryBuilder.php index 33901ace1d4ee..dcb8bc1f9c792 100644 --- a/lib/private/DB/QueryBuilder/ExtendedQueryBuilder.php +++ b/lib/private/DB/QueryBuilder/ExtendedQueryBuilder.php @@ -47,6 +47,11 @@ public function getState() { return $this->builder->getState(); } + public function ignoreConflictsOnInsert(): self { + $this->builder->ignoreConflictsOnInsert(); + return $this; + } + public function getSQL() { return $this->builder->getSQL(); } diff --git a/lib/private/DB/QueryBuilder/QueryBuilder.php b/lib/private/DB/QueryBuilder/QueryBuilder.php index 1129970265c66..bf8854ad6d314 100644 --- a/lib/private/DB/QueryBuilder/QueryBuilder.php +++ b/lib/private/DB/QueryBuilder/QueryBuilder.php @@ -39,6 +39,7 @@ class QueryBuilder extends TypedQueryBuilder { private bool $nonEmptyWhere = false; protected ?string $lastInsertedTable = null; private array $selectedColumns = []; + private bool $insertIgnoreConflicts = false; /** * Initializes a new QueryBuilder. @@ -275,6 +276,13 @@ public function executeStatement(?IDBConnection $connection = null): int { ); } + public function ignoreConflictsOnInsert(): self { + if ($this->getType() !== \Doctrine\DBAL\Query\QueryBuilder::INSERT) { + throw new \LogicException('ignoreConflictsOnInsert() can only be used on INSERT queries'); + } + $this->insertIgnoreConflicts = true; + return $this; + } /** * Gets the complete SQL string formed by the current specifications of this QueryBuilder. @@ -289,7 +297,11 @@ public function executeStatement(?IDBConnection $connection = null): int { * @return string The SQL query string. */ public function getSQL() { - return $this->queryBuilder->getSQL(); + $sql = $this->queryBuilder->getSQL(); + if ($this->insertIgnoreConflicts && $this->connection->getInsertIgnoreSqlTransformer() !== null) { + return ($this->connection->getInsertIgnoreSqlTransformer())($sql); + } + return $sql; } /** diff --git a/lib/private/Files/Cache/Cache.php b/lib/private/Files/Cache/Cache.php index 96b0c413e4db5..5875511e9eb89 100644 --- a/lib/private/Files/Cache/Cache.php +++ b/lib/private/Files/Cache/Cache.php @@ -394,21 +394,8 @@ public function update($id, array $data) { } if (count($extensionValues)) { - try { - $query = $this->getQueryBuilder(); - $query->insert('filecache_extended'); - $query->hintShardKey('storage', $this->getNumericStorageId()); - - $query->setValue('fileid', $query->createNamedParameter($id, IQueryBuilder::PARAM_INT)); - foreach ($extensionValues as $column => $value) { - $query->setValue($column, $query->createNamedParameter($value)); - } - - $query->executeStatement(); - } catch (Exception $e) { - if ($e->getReason() !== Exception::REASON_UNIQUE_CONSTRAINT_VIOLATION) { - throw $e; - } + $insertCount = $this->connection->insertIgnoreConflict('filecache_extended', array_merge(['fileid' => $id], $extensionValues), ['column' => 'storage', 'value' => $this->getNumericStorageId()]); + if ($insertCount === 0) { $query = $this->getQueryBuilder(); $query->update('filecache_extended') ->whereFileId($id) diff --git a/lib/public/DB/QueryBuilder/IQueryBuilder.php b/lib/public/DB/QueryBuilder/IQueryBuilder.php index 32a505292c20e..bb5dcc2f27d1a 100644 --- a/lib/public/DB/QueryBuilder/IQueryBuilder.php +++ b/lib/public/DB/QueryBuilder/IQueryBuilder.php @@ -216,6 +216,14 @@ public function executeQuery(?IDBConnection $connection = null): IResult; */ public function executeStatement(?IDBConnection $connection = null): int; + /** + * Set to ignore conflicts on insert + * + * @since 34.0.0 + * @return self + */ + public function ignoreConflictsOnInsert(): self; + /** * Gets the complete SQL string formed by the current specifications of this QueryBuilder. * diff --git a/lib/public/IDBConnection.php b/lib/public/IDBConnection.php index 4526f71ceeb47..108821e2f2f06 100644 --- a/lib/public/IDBConnection.php +++ b/lib/public/IDBConnection.php @@ -168,10 +168,12 @@ public function insertIfNotExist(string $table, array $input, ?array $compare = * * @param string $table The table name (will replace *PREFIX* with the actual prefix) * @param array $values data that should be inserted into the table (column name => value) + * @param array{column: string, value: mixed, overwrite?: bool}|array{} $hintShardKey An array representing the shard key to hint * @return int number of inserted rows + * @since 34.0.0 Parameter $hintShardKey was added * @since 16.0.0 */ - public function insertIgnoreConflict(string $table, array $values) : int; + public function insertIgnoreConflict(string $table, array $values, array $hintShardKey = []) : int; /** * Insert or update a row value