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
3 changes: 0 additions & 3 deletions build/psalm-baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3417,9 +3417,6 @@
<NullableReturnStatement>
<code><![CDATA[$alias]]></code>
</NullableReturnStatement>
<ParamNameMismatch>
<code><![CDATA[$selects]]></code>
</ParamNameMismatch>
</file>
<file src="lib/private/DB/QueryBuilder/QuoteHelper.php">
<InvalidNullableReturnType>
Expand Down
37 changes: 37 additions & 0 deletions build/psalm/ITypedQueryBuilderTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

declare(strict_types=1);

use OCP\IDBConnection;
use OCP\Server;

/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

$qb = Server::get(IDBConnection::class)->getTypedQueryBuilder();

$qb->selectColumns('a', 'b');
$qb->selectColumns('c');

$qb->selectColumnDistinct('d');
$qb->selectColumnDistinct('e');

$qb->selectAlias('f', 'g');
$qb->selectAlias($qb->func()->lower('h'), 'i');

/** @psalm-check-type-exact $result = \OCP\DB\IResult<'a'|'b'|'c'|'d'|'e'|'g'|'i'> */
$result = $qb->executeQuery();

/** @psalm-check-type-exact $rows = array<'a'|'b'|'c'|'d'|'e'|'g'|'i', mixed>|false */
$rows = $result->fetch(\PDO::FETCH_ASSOC);

/** @psalm-check-type-exact $rows = array<'a'|'b'|'c'|'d'|'e'|'g'|'i', mixed>|false */
$rows = $result->fetchAssociative();

/** @psalm-check-type-exact $rows = list<array<'a'|'b'|'c'|'d'|'e'|'g'|'i', mixed>> */
$rows = $result->fetchAll(\PDO::FETCH_ASSOC);

/** @psalm-check-type-exact $rows = list<array<'a'|'b'|'c'|'d'|'e'|'g'|'i', mixed>> */
$rows = $result->fetchAllAssociative();
3 changes: 3 additions & 0 deletions build/rector-strict.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
$nextcloudDir . '/build/rector-strict.php',
$nextcloudDir . '/core/BackgroundJobs/ExpirePreviewsJob.php',
$nextcloudDir . '/lib/public/IContainer.php',
$nextcloudDir . '/build/psalm/ITypedQueryBuilderTest.php',
$nextcloudDir . '/lib/private/DB/QueryBuilder/TypedQueryBuilder.php',
$nextcloudDir . '/lib/public/DB/QueryBuilder/ITypedQueryBuilder.php',
])
->withPreparedSets(
deadCode: true,
Expand Down
2 changes: 2 additions & 0 deletions lib/composer/composer/autoload_classmap.php
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,7 @@
'OCP\\DB\\QueryBuilder\\IParameter' => $baseDir . '/lib/public/DB/QueryBuilder/IParameter.php',
'OCP\\DB\\QueryBuilder\\IQueryBuilder' => $baseDir . '/lib/public/DB/QueryBuilder/IQueryBuilder.php',
'OCP\\DB\\QueryBuilder\\IQueryFunction' => $baseDir . '/lib/public/DB/QueryBuilder/IQueryFunction.php',
'OCP\\DB\\QueryBuilder\\ITypedQueryBuilder' => $baseDir . '/lib/public/DB/QueryBuilder/ITypedQueryBuilder.php',
'OCP\\DB\\QueryBuilder\\Sharded\\IShardMapper' => $baseDir . '/lib/public/DB/QueryBuilder/Sharded/IShardMapper.php',
'OCP\\DB\\Types' => $baseDir . '/lib/public/DB/Types.php',
'OCP\\Dashboard\\IAPIWidget' => $baseDir . '/lib/public/Dashboard/IAPIWidget.php',
Expand Down Expand Up @@ -1657,6 +1658,7 @@
'OC\\DB\\QueryBuilder\\Sharded\\ShardDefinition' => $baseDir . '/lib/private/DB/QueryBuilder/Sharded/ShardDefinition.php',
'OC\\DB\\QueryBuilder\\Sharded\\ShardQueryRunner' => $baseDir . '/lib/private/DB/QueryBuilder/Sharded/ShardQueryRunner.php',
'OC\\DB\\QueryBuilder\\Sharded\\ShardedQueryBuilder' => $baseDir . '/lib/private/DB/QueryBuilder/Sharded/ShardedQueryBuilder.php',
'OC\\DB\\QueryBuilder\\TypedQueryBuilder' => $baseDir . '/lib/private/DB/QueryBuilder/TypedQueryBuilder.php',
'OC\\DB\\ResultAdapter' => $baseDir . '/lib/private/DB/ResultAdapter.php',
'OC\\DB\\SQLiteMigrator' => $baseDir . '/lib/private/DB/SQLiteMigrator.php',
'OC\\DB\\SQLiteSessionInit' => $baseDir . '/lib/private/DB/SQLiteSessionInit.php',
Expand Down
2 changes: 2 additions & 0 deletions lib/composer/composer/autoload_static.php
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
'OCP\\DB\\QueryBuilder\\IParameter' => __DIR__ . '/../../..' . '/lib/public/DB/QueryBuilder/IParameter.php',
'OCP\\DB\\QueryBuilder\\IQueryBuilder' => __DIR__ . '/../../..' . '/lib/public/DB/QueryBuilder/IQueryBuilder.php',
'OCP\\DB\\QueryBuilder\\IQueryFunction' => __DIR__ . '/../../..' . '/lib/public/DB/QueryBuilder/IQueryFunction.php',
'OCP\\DB\\QueryBuilder\\ITypedQueryBuilder' => __DIR__ . '/../../..' . '/lib/public/DB/QueryBuilder/ITypedQueryBuilder.php',
'OCP\\DB\\QueryBuilder\\Sharded\\IShardMapper' => __DIR__ . '/../../..' . '/lib/public/DB/QueryBuilder/Sharded/IShardMapper.php',
'OCP\\DB\\Types' => __DIR__ . '/../../..' . '/lib/public/DB/Types.php',
'OCP\\Dashboard\\IAPIWidget' => __DIR__ . '/../../..' . '/lib/public/Dashboard/IAPIWidget.php',
Expand Down Expand Up @@ -1698,6 +1699,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
'OC\\DB\\QueryBuilder\\Sharded\\ShardDefinition' => __DIR__ . '/../../..' . '/lib/private/DB/QueryBuilder/Sharded/ShardDefinition.php',
'OC\\DB\\QueryBuilder\\Sharded\\ShardQueryRunner' => __DIR__ . '/../../..' . '/lib/private/DB/QueryBuilder/Sharded/ShardQueryRunner.php',
'OC\\DB\\QueryBuilder\\Sharded\\ShardedQueryBuilder' => __DIR__ . '/../../..' . '/lib/private/DB/QueryBuilder/Sharded/ShardedQueryBuilder.php',
'OC\\DB\\QueryBuilder\\TypedQueryBuilder' => __DIR__ . '/../../..' . '/lib/private/DB/QueryBuilder/TypedQueryBuilder.php',
'OC\\DB\\ResultAdapter' => __DIR__ . '/../../..' . '/lib/private/DB/ResultAdapter.php',
'OC\\DB\\SQLiteMigrator' => __DIR__ . '/../../..' . '/lib/private/DB/SQLiteMigrator.php',
'OC\\DB\\SQLiteSessionInit' => __DIR__ . '/../../..' . '/lib/private/DB/SQLiteSessionInit.php',
Expand Down
2 changes: 2 additions & 0 deletions lib/private/DB/ArrayResult.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@

/**
* Wrap an array or rows into a result interface
*
* @template-implements IResult<string>
*/
class ArrayResult implements IResult {
protected int $count;
Expand Down
9 changes: 9 additions & 0 deletions lib/private/DB/Connection.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
use OC\DB\QueryBuilder\Sharded\ShardDefinition;
use OC\SystemConfig;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\DB\QueryBuilder\ITypedQueryBuilder;
use OCP\DB\QueryBuilder\Sharded\IShardMapper;
use OCP\Diagnostics\IEventLogger;
use OCP\EventDispatcher\IEventDispatcher;
Expand Down Expand Up @@ -247,6 +248,14 @@ public function getStats(): array {
* Returns a QueryBuilder for the connection.
*/
public function getQueryBuilder(): IQueryBuilder {
return $this->getInnerQueryBuilder();
}

public function getTypedQueryBuilder(): ITypedQueryBuilder {
return $this->getInnerQueryBuilder();
}

private function getInnerQueryBuilder(): IQueryBuilder&ITypedQueryBuilder {
$this->queriesBuilt++;

$builder = new QueryBuilder(
Expand Down
5 changes: 5 additions & 0 deletions lib/private/DB/ConnectionAdapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
use OCP\DB\IPreparedStatement;
use OCP\DB\IResult;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\DB\QueryBuilder\ITypedQueryBuilder;
use OCP\IDBConnection;

/**
Expand All @@ -32,6 +33,10 @@ public function getQueryBuilder(): IQueryBuilder {
return $this->inner->getQueryBuilder();
}

public function getTypedQueryBuilder(): ITypedQueryBuilder {
return $this->inner->getTypedQueryBuilder();
}

public function prepare($sql, $limit = null, $offset = null): IPreparedStatement {
try {
return new PreparedStatement(
Expand Down
4 changes: 2 additions & 2 deletions lib/private/DB/QueryBuilder/ExtendedQueryBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
/**
* Base class for creating classes that extend the builtin query builder
*/
abstract class ExtendedQueryBuilder implements IQueryBuilder {
abstract class ExtendedQueryBuilder extends TypedQueryBuilder {
public function __construct(
protected IQueryBuilder $builder,
) {
Expand Down Expand Up @@ -100,7 +100,7 @@ public function select(...$selects) {
return $this;
}

public function selectAlias($select, $alias) {
public function selectAlias($select, $alias): self {
$this->builder->selectAlias($select, $alias);
return $this;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ public function addSelect(...$select) {
return $this;
}

public function selectAlias($select, $alias) {
public function selectAlias($select, $alias): self {
$this->selects[] = ['select' => $select, 'alias' => $alias];
return $this;
}
Expand Down
21 changes: 11 additions & 10 deletions lib/private/DB/QueryBuilder/QueryBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,9 @@
use OCP\IDBConnection;
use Override;
use Psr\Log\LoggerInterface;
use RuntimeException;

class QueryBuilder implements IQueryBuilder {
class QueryBuilder extends TypedQueryBuilder {
private \Doctrine\DBAL\Query\QueryBuilder $queryBuilder;
private QuoteHelper $helper;
private bool $automaticTablePrefix = true;
Expand Down Expand Up @@ -242,7 +243,7 @@ private function prepareForExecute() {

public function executeQuery(?IDBConnection $connection = null): IResult {
if ($this->getType() !== \Doctrine\DBAL\Query\QueryBuilder::SELECT) {
throw new \RuntimeException('Invalid query type, expected SELECT query');
throw new RuntimeException('Invalid query type, expected SELECT query');
}

$this->prepareForExecute();
Expand All @@ -259,7 +260,7 @@ public function executeQuery(?IDBConnection $connection = null): IResult {

public function executeStatement(?IDBConnection $connection = null): int {
if ($this->getType() === \Doctrine\DBAL\Query\QueryBuilder::SELECT) {
throw new \RuntimeException('Invalid query type, expected INSERT, DELETE or UPDATE statement');
throw new RuntimeException('Invalid query type, expected INSERT, DELETE or UPDATE statement');
}

$this->prepareForExecute();
Expand Down Expand Up @@ -476,7 +477,7 @@ public function select(...$selects) {
*
* @return $this This QueryBuilder instance.
*/
public function selectAlias($select, $alias) {
public function selectAlias($select, $alias): self {
$this->queryBuilder->addSelect(
$this->helper->quoteColumnName($select) . ' AS ' . $this->helper->quoteColumnName($alias)
);
Expand Down Expand Up @@ -524,18 +525,18 @@ public function selectDistinct($select) {
* ->leftJoin('u', 'phonenumbers', 'u.id = p.user_id');
* </code>
*
* @param mixed ...$selects The selection expression.
* @param mixed ...$select The selection expression.
*
* @return $this This QueryBuilder instance.
*/
public function addSelect(...$selects) {
if (count($selects) === 1 && is_array($selects[0])) {
$selects = $selects[0];
public function addSelect(...$select) {
if (count($select) === 1 && is_array($select[0])) {
$select = $select[0];
}
$this->addOutputColumns($selects);
$this->addOutputColumns($select);

$this->queryBuilder->addSelect(
$this->helper->quoteColumnNames($selects)
$this->helper->quoteColumnNames($select)
);

return $this;
Expand Down
40 changes: 40 additions & 0 deletions lib/private/DB/QueryBuilder/TypedQueryBuilder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OC\DB\QueryBuilder;

use OCP\DB\QueryBuilder\ITypedQueryBuilder;
use RuntimeException;

/**
* @psalm-suppress InvalidTemplateParam
* @template-implements ITypedQueryBuilder<string>
*/
abstract class TypedQueryBuilder implements ITypedQueryBuilder {
private function validateColumn(string $column): void {
if (str_contains($column, '.') || trim($column) === '*') {
throw new RuntimeException('Only column names are allowed, got: ' . $column);
}
}

public function selectColumns(string ...$columns): static {
foreach ($columns as $column) {
$this->validateColumn($column);
}

/** @psalm-suppress InternalMethod */
return $this->select(...$columns);
}

public function selectColumnDistinct(string $column): static {
$this->validateColumn($column);

/** @psalm-suppress InternalMethod */
return $this->selectDistinct($column);
}
}
2 changes: 2 additions & 0 deletions lib/private/DB/ResultAdapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@

/**
* Adapts DBAL 2.6 API for DBAL 3.x for backwards compatibility of a leaked type
*
* @template-implements IResult<string>
*/
class ResultAdapter implements IResult {
public function __construct(
Expand Down
11 changes: 6 additions & 5 deletions lib/public/DB/IResult.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
* }
* ```
*
* @template-covariant S of string
* @since 21.0.0
*/
#[Consumable(since: '21.0.0')]
Expand All @@ -41,7 +42,7 @@ public function closeCursor(): bool;
/**
* @param PDO::FETCH_* $fetchMode
*
* @return ($fetchMode is PDO::FETCH_ASSOC ? array<string, mixed> : ($fetchMode is PDO::FETCH_NUM ? list<mixed> : mixed))|false
* @return ($fetchMode is PDO::FETCH_ASSOC ? array<S, mixed> : ($fetchMode is PDO::FETCH_NUM ? list<mixed> : mixed))|false
*
* @since 21.0.0
* @note Since 33.0.0, prefer using fetchAssociative/fetchNumeric/fetchOne or iterateAssociate/iterateNumeric instead.
Expand All @@ -51,7 +52,7 @@ public function fetch(int $fetchMode = PDO::FETCH_ASSOC);
/**
* Returns the next row of the result as an associative array or FALSE if there are no more rows.
*
* @return array<string, mixed>|false
* @return array<S, mixed>|false
*
* @since 33.0.0
*/
Expand All @@ -78,7 +79,7 @@ public function fetchOne();
/**
* @param PDO::FETCH_* $fetchMode
*
* @return list<($fetchMode is PDO::FETCH_ASSOC ? array<string, mixed> : ($fetchMode is PDO::FETCH_NUM ? list<mixed> : mixed))>
* @return list<($fetchMode is PDO::FETCH_ASSOC ? array<S, mixed> : ($fetchMode is PDO::FETCH_NUM ? list<mixed> : mixed))>
*
* @since 21.0.0
* @note Since 33.0.0, prefer using fetchAllAssociative/fetchAllNumeric/fetchFirstColumn or iterateAssociate/iterateNumeric instead.
Expand All @@ -88,7 +89,7 @@ public function fetchAll(int $fetchMode = PDO::FETCH_ASSOC): array;
/**
* Returns an array containing all the result rows represented as associative arrays.
*
* @return list<array<string,mixed>>
* @return list<array<S, mixed>>
* @since 33.0.0
*/
public function fetchAllAssociative(): array;
Expand Down Expand Up @@ -136,7 +137,7 @@ public function iterateNumeric(): Traversable;
/**
* Returns an iterator over rows represented as associative arrays.
*
* @return Traversable<array<string,mixed>>
* @return Traversable<array<S, mixed>>
*
* @since 33.0.0
*/
Expand Down
2 changes: 1 addition & 1 deletion lib/public/DB/QueryBuilder/IQueryBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -386,7 +386,7 @@ public function select(...$selects);
* @psalm-taint-sink sql $select
* @psalm-taint-sink sql $alias
*/
public function selectAlias($select, $alias);
public function selectAlias($select, $alias): self;

/**
* Specifies an item that is to be returned uniquely in the query result.
Expand Down
Loading
Loading