diff --git a/build/psalm-baseline.xml b/build/psalm-baseline.xml
index 6fc98ad77a3..9936d3543d9 100644
--- a/build/psalm-baseline.xml
+++ b/build/psalm-baseline.xml
@@ -3404,6 +3404,12 @@
+
+
+
+
+
+
l($type, $timestamp, [
diff --git a/build/psalm/ITypedQueryBuilderTest.php b/build/psalm/ITypedQueryBuilderTest.php
new file mode 100644
index 00000000000..7bd2a7a3f80
--- /dev/null
+++ b/build/psalm/ITypedQueryBuilderTest.php
@@ -0,0 +1,37 @@
+getTypedQueryBuilder();
+
+$qb->selectColumns('a', 'b');
+$qb->selectColumns('c');
+
+$qb->selectColumnsDistinct('d', 'e');
+$qb->selectColumnsDistinct('f');
+
+$qb->selectAlias('g', 'h');
+$qb->selectAlias($qb->func()->lower('i'), 'j');
+
+/** @psalm-check-type-exact $result = \OCP\DB\IResult<'a'|'b'|'c'|'d'|'e'|'f'|'h'|'j'> */
+$result = $qb->executeQuery();
+
+/** @psalm-check-type-exact $rows = array<'a'|'b'|'c'|'d'|'e'|'f'|'h'|'j', mixed>|false */
+$rows = $result->fetch(\PDO::FETCH_ASSOC);
+
+/** @psalm-check-type-exact $rows = array<'a'|'b'|'c'|'d'|'e'|'f'|'h'|'j', mixed>|false */
+$rows = $result->fetchAssociative();
+
+/** @psalm-check-type-exact $rows = list> */
+$rows = $result->fetchAll(\PDO::FETCH_ASSOC);
+
+/** @psalm-check-type-exact $rows = list> */
+$rows = $result->fetchAllAssociative();
diff --git a/build/rector-strict.php b/build/rector-strict.php
index 768fe3cebe5..6df2c93be55 100644
--- a/build/rector-strict.php
+++ b/build/rector-strict.php
@@ -19,6 +19,9 @@ return (require __DIR__ . '/rector-shared.php')
$nextcloudDir . '/apps/settings/lib/Service/AuthorizedGroupService.php',
$nextcloudDir . '/lib/private/Files/Storage/Storage.php',
$nextcloudDir . '/lib/private/Files/Storage/Wrapper/Wrapper.php',
+ $nextcloudDir . '/build/psalm/ITypedQueryBuilderTest.php',
+ $nextcloudDir . '/lib/private/DB/QueryBuilder/TypedQueryBuilder.php',
+ $nextcloudDir . '/lib/public/DB/QueryBuilder/ITypedQueryBuilder.php',
])
->withPreparedSets(
deadCode: true,
diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php
index b2008b286ca..5b6de5ff356 100644
--- a/lib/composer/composer/autoload_classmap.php
+++ b/lib/composer/composer/autoload_classmap.php
@@ -327,6 +327,7 @@ return array(
'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',
@@ -1657,6 +1658,7 @@ return array(
'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',
diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php
index fb4a73103f5..fbee07dafc6 100644
--- a/lib/composer/composer/autoload_static.php
+++ b/lib/composer/composer/autoload_static.php
@@ -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',
@@ -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',
diff --git a/lib/private/DB/ArrayResult.php b/lib/private/DB/ArrayResult.php
index 614d4f77322..251000e29ae 100644
--- a/lib/private/DB/ArrayResult.php
+++ b/lib/private/DB/ArrayResult.php
@@ -14,6 +14,8 @@ use PDO;
/**
* Wrap an array or rows into a result interface
+ *
+ * @template-implements IResult
*/
class ArrayResult implements IResult {
protected int $count;
diff --git a/lib/private/DB/Connection.php b/lib/private/DB/Connection.php
index 081ef017aa3..3eef60c6daf 100644
--- a/lib/private/DB/Connection.php
+++ b/lib/private/DB/Connection.php
@@ -34,6 +34,7 @@ use OC\DB\QueryBuilder\Sharded\ShardConnectionManager;
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;
@@ -247,6 +248,14 @@ class Connection extends PrimaryReadReplicaConnection {
* 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(
diff --git a/lib/private/DB/ConnectionAdapter.php b/lib/private/DB/ConnectionAdapter.php
index 60b549dabcf..d9e3e7ec549 100644
--- a/lib/private/DB/ConnectionAdapter.php
+++ b/lib/private/DB/ConnectionAdapter.php
@@ -17,6 +17,7 @@ use OC\DB\QueryBuilder\Sharded\ShardDefinition;
use OCP\DB\IPreparedStatement;
use OCP\DB\IResult;
use OCP\DB\QueryBuilder\IQueryBuilder;
+use OCP\DB\QueryBuilder\ITypedQueryBuilder;
use OCP\IDBConnection;
/**
@@ -32,6 +33,10 @@ class ConnectionAdapter implements IDBConnection {
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(
diff --git a/lib/private/DB/QueryBuilder/ExtendedQueryBuilder.php b/lib/private/DB/QueryBuilder/ExtendedQueryBuilder.php
index c7eda9b337c..33901ace1d4 100644
--- a/lib/private/DB/QueryBuilder/ExtendedQueryBuilder.php
+++ b/lib/private/DB/QueryBuilder/ExtendedQueryBuilder.php
@@ -16,7 +16,7 @@ use OCP\IDBConnection;
/**
* 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,
) {
@@ -100,7 +100,7 @@ abstract class ExtendedQueryBuilder implements IQueryBuilder {
return $this;
}
- public function selectAlias($select, $alias) {
+ public function selectAlias($select, $alias): self {
$this->builder->selectAlias($select, $alias);
return $this;
}
diff --git a/lib/private/DB/QueryBuilder/Partitioned/PartitionedQueryBuilder.php b/lib/private/DB/QueryBuilder/Partitioned/PartitionedQueryBuilder.php
index 7750ce5056a..2a9c2eb9124 100644
--- a/lib/private/DB/QueryBuilder/Partitioned/PartitionedQueryBuilder.php
+++ b/lib/private/DB/QueryBuilder/Partitioned/PartitionedQueryBuilder.php
@@ -92,7 +92,7 @@ class PartitionedQueryBuilder extends ShardedQueryBuilder {
return $this;
}
- public function selectAlias($select, $alias) {
+ public function selectAlias($select, $alias): self {
$this->selects[] = ['select' => $select, 'alias' => $alias];
return $this;
}
diff --git a/lib/private/DB/QueryBuilder/QueryBuilder.php b/lib/private/DB/QueryBuilder/QueryBuilder.php
index 227f6048264..1129970265c 100644
--- a/lib/private/DB/QueryBuilder/QueryBuilder.php
+++ b/lib/private/DB/QueryBuilder/QueryBuilder.php
@@ -30,8 +30,9 @@ use OCP\DB\QueryBuilder\IQueryFunction;
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;
@@ -242,7 +243,7 @@ class QueryBuilder implements IQueryBuilder {
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();
@@ -259,7 +260,7 @@ class QueryBuilder implements IQueryBuilder {
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();
@@ -476,7 +477,7 @@ class QueryBuilder implements IQueryBuilder {
*
* @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)
);
diff --git a/lib/private/DB/QueryBuilder/TypedQueryBuilder.php b/lib/private/DB/QueryBuilder/TypedQueryBuilder.php
new file mode 100644
index 00000000000..cd6a060fbab
--- /dev/null
+++ b/lib/private/DB/QueryBuilder/TypedQueryBuilder.php
@@ -0,0 +1,40 @@
+
+ */
+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);
+ }
+
+ return $this->select(...$columns);
+ }
+
+ public function selectColumnsDistinct(string ...$columns): static {
+ foreach ($columns as $column) {
+ $this->validateColumn($column);
+ }
+
+ return $this->selectDistinct($columns);
+ }
+}
diff --git a/lib/private/DB/ResultAdapter.php b/lib/private/DB/ResultAdapter.php
index 0f365b67321..b403e78673e 100644
--- a/lib/private/DB/ResultAdapter.php
+++ b/lib/private/DB/ResultAdapter.php
@@ -15,6 +15,8 @@ use PDO;
/**
* Adapts DBAL 2.6 API for DBAL 3.x for backwards compatibility of a leaked type
+ *
+ * @template-implements IResult
*/
class ResultAdapter implements IResult {
public function __construct(
diff --git a/lib/public/DB/IResult.php b/lib/public/DB/IResult.php
index 4f7e048907c..daca3be31ba 100644
--- a/lib/public/DB/IResult.php
+++ b/lib/public/DB/IResult.php
@@ -27,6 +27,7 @@ use Traversable;
* }
* ```
*
+ * @template-covariant S of string
* @since 21.0.0
*/
#[Consumable(since: '21.0.0')]
@@ -41,7 +42,7 @@ interface IResult {
/**
* @param PDO::FETCH_* $fetchMode
*
- * @return ($fetchMode is PDO::FETCH_ASSOC ? array : ($fetchMode is PDO::FETCH_NUM ? list : mixed))|false
+ * @return ($fetchMode is PDO::FETCH_ASSOC ? array : ($fetchMode is PDO::FETCH_NUM ? list : mixed))|false
*
* @since 21.0.0
* @note Since 33.0.0, prefer using fetchAssociative/fetchNumeric/fetchOne or iterateAssociate/iterateNumeric instead.
@@ -51,7 +52,7 @@ interface IResult {
/**
* Returns the next row of the result as an associative array or FALSE if there are no more rows.
*
- * @return array|false
+ * @return array|false
*
* @since 33.0.0
*/
@@ -78,7 +79,7 @@ interface IResult {
/**
* @param PDO::FETCH_* $fetchMode
*
- * @return list<($fetchMode is PDO::FETCH_ASSOC ? array : ($fetchMode is PDO::FETCH_NUM ? list : mixed))>
+ * @return list<($fetchMode is PDO::FETCH_ASSOC ? array : ($fetchMode is PDO::FETCH_NUM ? list : mixed))>
*
* @since 21.0.0
* @note Since 33.0.0, prefer using fetchAllAssociative/fetchAllNumeric/fetchFirstColumn or iterateAssociate/iterateNumeric instead.
@@ -88,7 +89,7 @@ interface IResult {
/**
* Returns an array containing all the result rows represented as associative arrays.
*
- * @return list>
+ * @return list>
* @since 33.0.0
*/
public function fetchAllAssociative(): array;
@@ -136,7 +137,7 @@ interface IResult {
/**
* Returns an iterator over rows represented as associative arrays.
*
- * @return Traversable>
+ * @return Traversable>
*
* @since 33.0.0
*/
diff --git a/lib/public/DB/QueryBuilder/IQueryBuilder.php b/lib/public/DB/QueryBuilder/IQueryBuilder.php
index 34e23b17ed2..32a505292c2 100644
--- a/lib/public/DB/QueryBuilder/IQueryBuilder.php
+++ b/lib/public/DB/QueryBuilder/IQueryBuilder.php
@@ -386,7 +386,7 @@ interface IQueryBuilder {
* @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.
diff --git a/lib/public/DB/QueryBuilder/ITypedQueryBuilder.php b/lib/public/DB/QueryBuilder/ITypedQueryBuilder.php
new file mode 100644
index 00000000000..ea5daa2a71c
--- /dev/null
+++ b/lib/public/DB/QueryBuilder/ITypedQueryBuilder.php
@@ -0,0 +1,76 @@
+
+ */
+ #[Override]
+ public function executeQuery(?IDBConnection $connection = null): IResult;
+
+ /**
+ * @inheritDoc
+ * @internal This method does not work with {@see self}. Use {@see self::selectColumns()} or {@see self::selectAlias()} instead.
+ */
+ #[Override]
+ public function select(...$selects);
+
+ /**
+ * @template NewS of string
+ * @param NewS ...$columns The columns to select. They are not allowed to contain table names or aliases, or asterisks. Use {@see self::selectAlias()} for that.
+ * @psalm-this-out self
+ * @since 34.0.0
+ */
+ public function selectColumns(string ...$columns): self;
+
+ /**
+ * @inheritDoc
+ * @internal This method does not work with {@see self}. Use {@see self::selectColumnDistinct()} or {@see self::selectAlias()} instead.
+ */
+ #[Override]
+ public function selectDistinct($select);
+
+ /**
+ * @template NewS of string
+ * @param NewS ...$columns The columns to select distinct. They are not allowed to contain table names or aliases, or asterisks. Use {@see self::selectAlias()} for that.
+ * @psalm-this-out self
+ * @since 34.0.0
+ */
+ public function selectColumnsDistinct(string ...$columns): self;
+
+ /**
+ * @inheritDoc
+ * @internal This method does not work with {@see self}. Use {@see self::selectColumns()} or {@see self::selectAlias()} instead.
+ */
+ #[Override]
+ public function addSelect(...$select);
+
+ /**
+ * @inheritDoc
+ * @param mixed $select
+ * @template NewS of string
+ * @param NewS $alias
+ * @psalm-this-out self
+ * @psalm-suppress LessSpecificImplementedReturnType
+ */
+ #[Override]
+ public function selectAlias($select, $alias): self;
+}
diff --git a/lib/public/IDBConnection.php b/lib/public/IDBConnection.php
index 05ac0da2d7a..4526f71ceeb 100644
--- a/lib/public/IDBConnection.php
+++ b/lib/public/IDBConnection.php
@@ -17,6 +17,7 @@ use OCP\DB\Exception;
use OCP\DB\IPreparedStatement;
use OCP\DB\IResult;
use OCP\DB\QueryBuilder\IQueryBuilder;
+use OCP\DB\QueryBuilder\ITypedQueryBuilder;
/**
* Interface IDBConnection
@@ -54,9 +55,17 @@ interface IDBConnection {
*
* @return \OCP\DB\QueryBuilder\IQueryBuilder
* @since 8.2.0
+ * @note Since 34.0.0 prefer using {@see self::getTypedQueryBuilder()} instead.
*/
public function getQueryBuilder();
+ /**
+ * Gets the ITypedQueryBuilder for the connection.
+ *
+ * @since 34.0.0
+ */
+ public function getTypedQueryBuilder(): ITypedQueryBuilder;
+
/**
* Used to abstract the Nextcloud database access away
* @param string $sql the sql query with ? placeholder for params
diff --git a/psalm-strict.xml b/psalm-strict.xml
index 07be9fddeea..85bfce79a31 100644
--- a/psalm-strict.xml
+++ b/psalm-strict.xml
@@ -25,6 +25,9 @@
+
+
+
@@ -58,5 +61,10 @@
+
+
+
+
+