Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion lib/private/DB/Adapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -126,4 +129,8 @@ public function insertIgnoreConflict(string $table, array $values) : int {
throw $e;
}
}

public function getInsertIgnoreSqlTransformer(): ?callable {
return null;
}
}
40 changes: 21 additions & 19 deletions lib/private/DB/AdapterMySQL.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
13 changes: 10 additions & 3 deletions lib/private/DB/AdapterPgSql.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,22 @@ 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();
$builder->insert($table);
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';
}
}
18 changes: 11 additions & 7 deletions lib/private/DB/AdapterSqlite.php
Original file line number Diff line number Diff line change
Expand Up @@ -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';
}
}
8 changes: 6 additions & 2 deletions lib/private/DB/Connection.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
}
}
8 changes: 6 additions & 2 deletions lib/private/DB/ConnectionAdapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down Expand Up @@ -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();
}
}
5 changes: 5 additions & 0 deletions lib/private/DB/QueryBuilder/ExtendedQueryBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down
14 changes: 13 additions & 1 deletion lib/private/DB/QueryBuilder/QueryBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand All @@ -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;
}

/**
Expand Down
17 changes: 2 additions & 15 deletions lib/private/Files/Cache/Cache.php
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
8 changes: 8 additions & 0 deletions lib/public/DB/QueryBuilder/IQueryBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down
4 changes: 3 additions & 1 deletion lib/public/IDBConnection.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading