From a785e3388e00b26dccee05c3c86de36234f564af Mon Sep 17 00:00:00 2001 From: David Grudl Date: Mon, 29 Dec 2025 00:57:48 +0100 Subject: [PATCH 1/9] github actions: use composer tester script --- .github/workflows/tests.yml | 4 ++-- composer.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 03ce917d6..875732cde 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -103,7 +103,7 @@ jobs: run: docker exec -i mssql /opt/mssql-tools18/bin/sqlcmd -S localhost -U SA -P 'YourStrong!Passw0rd' -Q 'CREATE DATABASE nette_test' -N -C - run: composer install --no-progress --prefer-dist - - run: vendor/bin/tester tests -s -C + - run: composer tester - if: failure() uses: actions/upload-artifact@v4 with: @@ -125,4 +125,4 @@ jobs: run: cp ./tests/databases.sqlite.ini ./tests/Database/databases.ini - run: composer update --no-progress --prefer-dist --prefer-lowest --prefer-stable - - run: vendor/bin/tester tests -s -C + - run: composer tester diff --git a/composer.json b/composer.json index 5c926e4d0..e40e8baa4 100644 --- a/composer.json +++ b/composer.json @@ -21,7 +21,7 @@ "nette/utils": "^4.0" }, "require-dev": { - "nette/tester": "^2.5", + "nette/tester": "^2.6", "nette/di": "^3.1", "mockery/mockery": "^1.6@stable", "tracy/tracy": "^2.9", From 2d50deb2b37885b55fda8ca1c7922ff6e5e93441 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Tue, 13 Jan 2026 15:38:05 +0100 Subject: [PATCH 2/9] Connection, ResultSet: $rowNormalizer is normalized to Closure --- src/Database/Connection.php | 12 ++++++------ src/Database/ResultSet.php | 7 ++----- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/src/Database/Connection.php b/src/Database/Connection.php index 6f9d81eee..1f45aa12b 100644 --- a/src/Database/Connection.php +++ b/src/Database/Connection.php @@ -28,8 +28,8 @@ class Connection private SqlPreprocessor $preprocessor; private ?PDO $pdo = null; - /** @var callable(array, ResultSet): array */ - private $rowNormalizer = [Helpers::class, 'normalizeRow']; + /** @var ?\Closure(array, ResultSet): array */ + private ?\Closure $rowNormalizer; private ?string $sql = null; private int $transactionDepth = 0; @@ -42,9 +42,9 @@ public function __construct( private readonly ?string $password = null, private readonly array $options = [], ) { - if (!empty($options['newDateTime'])) { - $this->rowNormalizer = fn($row, $resultSet) => Helpers::normalizeRow($row, $resultSet, DateTime::class); - } + $this->rowNormalizer = !empty($options['newDateTime']) + ? fn(array $row, ResultSet $resultSet): array => Helpers::normalizeRow($row, $resultSet, DateTime::class) + : Helpers::normalizeRow(...); if (empty($options['lazy'])) { $this->connect(); } @@ -134,7 +134,7 @@ public function getReflection(): Reflection */ public function setRowNormalizer(?callable $normalizer): static { - $this->rowNormalizer = $normalizer; + $this->rowNormalizer = $normalizer ? $normalizer(...) : null; return $this; } diff --git a/src/Database/ResultSet.php b/src/Database/ResultSet.php index d6aac809a..b2157b8d8 100644 --- a/src/Database/ResultSet.php +++ b/src/Database/ResultSet.php @@ -19,9 +19,6 @@ class ResultSet implements \Iterator, IRowContainer { private ?\PDOStatement $pdoStatement = null; - - /** @var callable(array, ResultSet): array */ - private readonly mixed $normalizer; private Row|false|null $lastRow = null; private int $lastRowKey = -1; @@ -35,10 +32,10 @@ public function __construct( private readonly Connection $connection, private readonly string $queryString, private readonly array $params, - ?callable $normalizer = null, + /** @var ?\Closure(array, self): array */ + private readonly ?\Closure $normalizer = null, ) { $time = microtime(true); - $this->normalizer = $normalizer; $types = ['boolean' => PDO::PARAM_BOOL, 'integer' => PDO::PARAM_INT, 'resource' => PDO::PARAM_LOB, 'NULL' => PDO::PARAM_NULL]; try { From 587def9600c0e6d3994b5c5bb1591d2b0e897b1d Mon Sep 17 00:00:00 2001 From: David Grudl Date: Fri, 9 Jan 2026 01:07:12 +0100 Subject: [PATCH 3/9] improved phpDoc types --- src/Bridges/DatabaseTracy/ConnectionPanel.php | 4 ++ src/Database/Connection.php | 16 ++++- src/Database/Conventions.php | 5 +- src/Database/Driver.php | 3 +- src/Database/DriverException.php | 3 + src/Database/Explorer.php | 22 +++++- src/Database/Helpers.php | 16 ++++- src/Database/IStructure.php | 6 +- src/Database/Reflection.php | 2 +- src/Database/Reflection/Column.php | 1 + src/Database/Reflection/ForeignKey.php | 4 +- src/Database/Reflection/Index.php | 2 +- src/Database/ResultSet.php | 21 +++++- src/Database/Row.php | 1 + src/Database/SqlLiteral.php | 2 + src/Database/SqlPreprocessor.php | 14 +++- src/Database/Structure.php | 13 +++- src/Database/Table/ActiveRow.php | 19 ++++- src/Database/Table/GroupedSelection.php | 3 + src/Database/Table/Selection.php | 67 ++++++++++-------- src/Database/Table/SqlBuilder.php | 70 ++++++++++++++++++- 21 files changed, 240 insertions(+), 54 deletions(-) diff --git a/src/Bridges/DatabaseTracy/ConnectionPanel.php b/src/Bridges/DatabaseTracy/ConnectionPanel.php index cf6d66817..026655d17 100644 --- a/src/Bridges/DatabaseTracy/ConnectionPanel.php +++ b/src/Bridges/DatabaseTracy/ConnectionPanel.php @@ -26,6 +26,8 @@ class ConnectionPanel implements Tracy\IBarPanel public float $performanceScale = 0.25; private float $totalTime = 0; private int $count = 0; + + /** @var list, list>, ?float, ?int, ?string}> */ private array $queries = []; private Tracy\BlueScreen $blueScreen; @@ -61,6 +63,7 @@ public function __construct(Connection $connection, Tracy\BlueScreen $blueScreen } + /** @param Nette\Database\ResultSet|\PDOException $result */ private function logQuery(Connection $connection, $result): void { if ($this->disabled) { @@ -91,6 +94,7 @@ private function logQuery(Connection $connection, $result): void } + /** @return array{tab: string, panel: string}|null */ public static function renderException(?\Throwable $e): ?array { if (!$e instanceof \PDOException) { diff --git a/src/Database/Connection.php b/src/Database/Connection.php index 1f45aa12b..31f6a19b5 100644 --- a/src/Database/Connection.php +++ b/src/Database/Connection.php @@ -34,6 +34,7 @@ class Connection private int $transactionDepth = 0; + /** @param array $options */ public function __construct( private readonly string $dsn, #[\SensitiveParameter] @@ -131,6 +132,7 @@ public function getReflection(): Reflection /** * Sets callback for row preprocessing. + * @param ?(callable(array, ResultSet): array) $normalizer */ public function setRowNormalizer(?callable $normalizer): static { @@ -210,6 +212,7 @@ public function rollBack(): void /** * Executes callback inside a transaction. + * @param callable(static): mixed $callback */ public function transaction(callable $callback): mixed { @@ -257,7 +260,11 @@ public function query(#[Language('SQL')] string $sql, #[Language('GenericSQL')] } - /** @deprecated use query() */ + /** + * @deprecated use query() + * @param literal-string $sql + * @param array $params + */ public function queryArgs(string $sql, array $params): ResultSet { return $this->query($sql, ...$params); @@ -266,7 +273,7 @@ public function queryArgs(string $sql, array $params): ResultSet /** * @param literal-string $sql - * @return array{string, array} + * @return array{string, array} */ public function preprocess(string $sql, ...$params): array { @@ -299,6 +306,7 @@ public function fetch(#[Language('SQL')] string $sql, #[Language('GenericSQL')] /** * Shortcut for query()->fetchAssoc() * @param literal-string $sql + * @return ?array */ public function fetchAssoc(#[Language('SQL')] string $sql, #[Language('GenericSQL')] ...$params): ?array { @@ -319,6 +327,7 @@ public function fetchField(#[Language('SQL')] string $sql, #[Language('GenericSQ /** * Shortcut for query()->fetchList() * @param literal-string $sql + * @return ?list */ public function fetchList(#[Language('SQL')] string $sql, #[Language('GenericSQL')] ...$params): ?array { @@ -329,6 +338,7 @@ public function fetchList(#[Language('SQL')] string $sql, #[Language('GenericSQL /** * Shortcut for query()->fetchList() * @param literal-string $sql + * @return ?list */ public function fetchFields(#[Language('SQL')] string $sql, #[Language('GenericSQL')] ...$params): ?array { @@ -339,6 +349,7 @@ public function fetchFields(#[Language('SQL')] string $sql, #[Language('GenericS /** * Shortcut for query()->fetchPairs() * @param literal-string $sql + * @return array */ public function fetchPairs(#[Language('SQL')] string $sql, #[Language('GenericSQL')] ...$params): array { @@ -349,6 +360,7 @@ public function fetchPairs(#[Language('SQL')] string $sql, #[Language('GenericSQ /** * Shortcut for query()->fetchAll() * @param literal-string $sql + * @return list */ public function fetchAll(#[Language('SQL')] string $sql, #[Language('GenericSQL')] ...$params): array { diff --git a/src/Database/Conventions.php b/src/Database/Conventions.php index 31cd5db54..459b9df43 100644 --- a/src/Database/Conventions.php +++ b/src/Database/Conventions.php @@ -17,6 +17,7 @@ interface Conventions { /** * Returns primary key for table. + * @return string|list|null */ function getPrimary(string $table): string|array|null; @@ -25,7 +26,7 @@ function getPrimary(string $table): string|array|null; * Example: * (author, book) returns [book, author_id] * - * @return array|null [referenced table, referenced column] + * @return ?array{string, string} * @throws AmbiguousReferenceKeyException */ function getHasManyReference(string $table, string $key): ?array; @@ -36,7 +37,7 @@ function getHasManyReference(string $table, string $key): ?array; * (book, author) returns [author, author_id] * (book, translator) returns [author, translator_id] * - * @return array|null [referenced table, referencing column] + * @return ?array{string, string} */ function getBelongsToReference(string $table, string $key): ?array; } diff --git a/src/Database/Driver.php b/src/Database/Driver.php index 0c1dfad4f..20f6ec341 100644 --- a/src/Database/Driver.php +++ b/src/Database/Driver.php @@ -38,6 +38,7 @@ function isSupported(string $feature): bool; /** * Initializes connection. + * @param array $options */ function initialize(Connection $connection, array $options): void; @@ -73,7 +74,7 @@ function getTables(): array; /** * Returns metadata for all columns in a table. - * @return list + * @return list}> */ function getColumns(string $table): array; diff --git a/src/Database/DriverException.php b/src/Database/DriverException.php index a0d7f7f94..45e726e86 100644 --- a/src/Database/DriverException.php +++ b/src/Database/DriverException.php @@ -16,6 +16,8 @@ class DriverException extends \PDOException { public ?string $queryString = null; + + /** @var array|null */ public ?array $params = null; @@ -56,6 +58,7 @@ public function getQueryString(): ?string } + /** @return array|null */ public function getParameters(): ?array { return $this->params; diff --git a/src/Database/Explorer.php b/src/Database/Explorer.php index 729a7fb15..771d9f569 100644 --- a/src/Database/Explorer.php +++ b/src/Database/Explorer.php @@ -49,6 +49,7 @@ public function rollBack(): void } + /** @param callable(static): mixed $callback */ public function transaction(callable $callback): mixed { return $this->connection->transaction(fn() => $callback($this)); @@ -71,7 +72,11 @@ public function query(#[Language('SQL')] string $sql, #[Language('GenericSQL')] } - /** @deprecated use query() */ + /** + * @deprecated use query() + * @param literal-string $sql + * @param array $params + */ public function queryArgs(string $sql, array $params): ResultSet { return $this->connection->query($sql, ...$params); @@ -106,13 +111,21 @@ public function getConventions(): Conventions } + /** + * @param array $data + * @param Table\Selection $selection + */ public function createActiveRow(array $data, Table\Selection $selection): Table\ActiveRow { return new Table\ActiveRow($data, $selection); } - /** @internal */ + /** + * @internal + * @param Table\Selection $refSelection + * @return Table\GroupedSelection + */ public function createGroupedSelection( Table\Selection $refSelection, string $table, @@ -139,6 +152,7 @@ public function fetch(#[Language('SQL')] string $sql, #[Language('GenericSQL')] /** * Shortcut for query()->fetchAssoc() * @param literal-string $sql + * @return ?array */ public function fetchAssoc(#[Language('SQL')] string $sql, #[Language('GenericSQL')] ...$params): ?array { @@ -159,6 +173,7 @@ public function fetchField(#[Language('SQL')] string $sql, #[Language('GenericSQ /** * Shortcut for query()->fetchList() * @param literal-string $sql + * @return ?list */ public function fetchList(#[Language('SQL')] string $sql, #[Language('GenericSQL')] ...$params): ?array { @@ -169,6 +184,7 @@ public function fetchList(#[Language('SQL')] string $sql, #[Language('GenericSQL /** * Shortcut for query()->fetchList() * @param literal-string $sql + * @return ?list */ public function fetchFields(#[Language('SQL')] string $sql, #[Language('GenericSQL')] ...$params): ?array { @@ -179,6 +195,7 @@ public function fetchFields(#[Language('SQL')] string $sql, #[Language('GenericS /** * Shortcut for query()->fetchPairs() * @param literal-string $sql + * @return array */ public function fetchPairs(#[Language('SQL')] string $sql, #[Language('GenericSQL')] ...$params): array { @@ -189,6 +206,7 @@ public function fetchPairs(#[Language('SQL')] string $sql, #[Language('GenericSQ /** * Shortcut for query()->fetchAll() * @param literal-string $sql + * @return list */ public function fetchAll(#[Language('SQL')] string $sql, #[Language('GenericSQL')] ...$params): array { diff --git a/src/Database/Helpers.php b/src/Database/Helpers.php index 3429545b1..9561d8bda 100644 --- a/src/Database/Helpers.php +++ b/src/Database/Helpers.php @@ -23,6 +23,7 @@ class Helpers /** maximum SQL length */ public static int $maxLength = 100; + /** @var array */ public static array $typePatterns = [ '^_' => IStructure::FIELD_TEXT, // PostgreSQL arrays '(TINY|SMALL|SHORT|MEDIUM|BIG|LONG)(INT)?|INT(EGER|\d+| IDENTITY| UNSIGNED)?|(SMALL|BIG|)SERIAL\d*|COUNTER|YEAR|BYTE|LONGLONG|UNSIGNED BIG INT' => IStructure::FIELD_INTEGER, @@ -85,6 +86,7 @@ public static function dumpResult(ResultSet $result): void /** * Returns syntax highlighted SQL command. + * @param ?array $params */ public static function dumpSql(string $sql, ?array $params = null, ?Connection $connection = null): string { @@ -164,6 +166,7 @@ public static function dumpSql(string $sql, ?array $params = null, ?Connection $ /** * Returns column types from result set. + * @return array column name => type */ public static function detectTypes(\PDOStatement $statement): array { @@ -200,7 +203,11 @@ public static function detectType(string $type): string } - /** @internal */ + /** + * @internal + * @param array $row + * @return array + */ public static function normalizeRow( array $row, ResultSet $resultSet, @@ -245,7 +252,7 @@ public static function normalizeRow( /** * Imports SQL dump from file. - * @param ?array $onProgress Called after each query + * @param ?(callable(int, ?float): void) $onProgress Called after each query * @return int Number of executed commands * @throws Nette\FileNotFoundException */ @@ -323,6 +330,9 @@ public static function initializeTracy( /** * Converts rows to key-value pairs. + * @param array> $rows + * @param string|int|(\Closure(Row|Table\ActiveRow|array): array{0: mixed, 1?: mixed})|null $key + * @return array */ public static function toPairs(array $rows, string|int|\Closure|null $key, string|int|null $value): array { @@ -388,7 +398,7 @@ public static function findDuplicates(\PDOStatement $statement): string } - /** @return array{type: ?string, length: ?null, scale: ?null, parameters: ?string} */ + /** @return array{type: ?string, length: ?int, scale: ?int, parameters: ?string} */ public static function parseColumnType(string $type): array { preg_match('/^([^(]+)(?:\((?:(\d+)(?:,(\d+))?|([^)]+))\))?/', $type, $m, PREG_UNMATCHED_AS_NULL); diff --git a/src/Database/IStructure.php b/src/Database/IStructure.php index 2a8e781e5..3cb403a5e 100644 --- a/src/Database/IStructure.php +++ b/src/Database/IStructure.php @@ -28,17 +28,19 @@ interface IStructure /** * Returns tables list. + * @return list */ function getTables(): array; /** * Returns table columns list. + * @return list}> */ function getColumns(string $table): array; /** * Returns table primary key. - * @return string|string[]|null + * @return string|list|null */ function getPrimaryKey(string $table): string|array|null; @@ -55,12 +57,14 @@ function getPrimaryKeySequence(string $table): ?string; /** * Returns hasMany reference. * If a targetTable is not provided, returns references for all tables. + * @return array>|null table name => list of referencing columns */ function getHasManyReference(string $table): ?array; /** * Returns belongsTo reference. * If a column is not provided, returns references for all columns. + * @return ?array column name => referenced table name */ function getBelongsToReference(string $table): ?array; diff --git a/src/Database/Reflection.php b/src/Database/Reflection.php index 7b1d8afd2..036f0e9be 100644 --- a/src/Database/Reflection.php +++ b/src/Database/Reflection.php @@ -27,7 +27,7 @@ public function __construct( } - /** @return Table[] */ + /** @return list */ public function getTables(): array { return array_values($this->tables); diff --git a/src/Database/Reflection/Column.php b/src/Database/Reflection/Column.php index 96c09c0c3..16a6dba13 100644 --- a/src/Database/Reflection/Column.php +++ b/src/Database/Reflection/Column.php @@ -24,6 +24,7 @@ public function __construct( public readonly bool $autoIncrement = false, public readonly bool $primary = false, public readonly ?string $comment = null, + /** @var array */ public readonly array $vendor = [], ) { } diff --git a/src/Database/Reflection/ForeignKey.php b/src/Database/Reflection/ForeignKey.php index 6a2f00501..245677d9e 100644 --- a/src/Database/Reflection/ForeignKey.php +++ b/src/Database/Reflection/ForeignKey.php @@ -16,9 +16,9 @@ final class ForeignKey /** @internal */ public function __construct( public readonly Table $foreignTable, - /** @var Column[] */ + /** @var list */ public readonly array $localColumns, - /** @var Column[] */ + /** @var list */ public readonly array $foreignColumns, public readonly ?string $name = null, ) { diff --git a/src/Database/Reflection/Index.php b/src/Database/Reflection/Index.php index d490f14d3..cc089d846 100644 --- a/src/Database/Reflection/Index.php +++ b/src/Database/Reflection/Index.php @@ -15,7 +15,7 @@ final class Index { /** @internal */ public function __construct( - /** @var Column[] */ + /** @var list */ public readonly array $columns, public readonly bool $unique = false, public readonly bool $primary = false, diff --git a/src/Database/ResultSet.php b/src/Database/ResultSet.php index b2157b8d8..0f6289426 100644 --- a/src/Database/ResultSet.php +++ b/src/Database/ResultSet.php @@ -15,6 +15,7 @@ /** * Represents a database result set. + * @implements \Iterator */ class ResultSet implements \Iterator, IRowContainer { @@ -22,15 +23,18 @@ class ResultSet implements \Iterator, IRowContainer private Row|false|null $lastRow = null; private int $lastRowKey = -1; - /** @var Row[] */ + /** @var list */ private array $rows; private float $time; + + /** @var array column name => type */ private array $types; public function __construct( private readonly Connection $connection, private readonly string $queryString, + /** @var mixed[] */ private readonly array $params, /** @var ?\Closure(array, self): array */ private readonly ?\Closure $normalizer = null, @@ -82,6 +86,7 @@ public function getQueryString(): string } + /** @return mixed[] */ public function getParameters(): array { return $this->params; @@ -100,6 +105,7 @@ public function getRowCount(): ?int } + /** @return array */ public function getColumnTypes(): array { $this->types ??= $this->connection->getDriver()->getColumnTypes($this->pdoStatement); @@ -113,7 +119,11 @@ public function getTime(): float } - /** @internal */ + /** + * @internal + * @param array $row + * @return array + */ public function normalizeRow(array $row): array { return $this->normalizer @@ -178,6 +188,7 @@ public function valid(): bool /** * Returns the next row as an associative array or null if there are no more rows. + * @return ?array */ public function fetchAssoc(?string $path = null): ?array { @@ -226,6 +237,7 @@ public function fetchField(): mixed /** * Returns the next row as indexed array or null if there are no more rows. + * @return ?list */ public function fetchList(): ?array { @@ -236,6 +248,7 @@ public function fetchList(): ?array /** * Alias for fetchList(). + * @return ?list */ public function fetchFields(): ?array { @@ -247,6 +260,8 @@ public function fetchFields(): ?array * Returns all rows as associative array, where first argument specifies key column and second value column. * For duplicate keys, the last value is used. When using null as key, array is indexed from zero. * Alternatively accepts callback returning value or key-value pairs. + * @param string|int|(\Closure(Row|Table\ActiveRow|array): array{0: mixed, 1?: mixed})|null $keyOrCallback + * @return array */ public function fetchPairs(string|int|\Closure|null $keyOrCallback = null, string|int|null $value = null): array { @@ -256,7 +271,7 @@ public function fetchPairs(string|int|\Closure|null $keyOrCallback = null, strin /** * Returns all remaining rows as array of Row objects. - * @return Row[] + * @return list */ public function fetchAll(): array { diff --git a/src/Database/Row.php b/src/Database/Row.php index 8e34b920f..9a66d71d8 100644 --- a/src/Database/Row.php +++ b/src/Database/Row.php @@ -13,6 +13,7 @@ /** * Represents a single database table row. + * @extends Nette\Utils\ArrayHash */ class Row extends Nette\Utils\ArrayHash implements IRow { diff --git a/src/Database/SqlLiteral.php b/src/Database/SqlLiteral.php index e110f53cd..07070a799 100644 --- a/src/Database/SqlLiteral.php +++ b/src/Database/SqlLiteral.php @@ -15,6 +15,7 @@ class SqlLiteral { public function __construct( private readonly string $value, + /** @var mixed[] */ private readonly array $parameters = [], ) { } @@ -26,6 +27,7 @@ public function getSql(): string } + /** @return mixed[] */ public function getParameters(): array { return $this->parameters; diff --git a/src/Database/SqlPreprocessor.php b/src/Database/SqlPreprocessor.php index 9495dc143..c94508be0 100644 --- a/src/Database/SqlPreprocessor.php +++ b/src/Database/SqlPreprocessor.php @@ -48,7 +48,11 @@ class SqlPreprocessor private readonly Connection $connection; private readonly Driver $driver; + + /** @var list */ private array $params; + + /** @var list */ private array $remaining; private int $counter; private bool $useParams; @@ -66,7 +70,8 @@ public function __construct(Connection $connection) /** * Processes SQL query with parameter substitution. - * @return array{string, array} + * @param mixed[] $params + * @return array{string, list} */ public function process(array $params, bool $useParams = false): array { @@ -114,6 +119,7 @@ public function process(array $params, bool $useParams = false): array /** * Handles SQL placeholders and skips string literals and comments. + * @param string[] $match */ private function parsePart(array $match): string { @@ -200,6 +206,7 @@ private function formatValue(mixed $value): string /** * Output: value, value, ... | (tuple), (tuple), ... + * @param mixed[] $values */ private function formatList(array $values): string { @@ -218,6 +225,7 @@ private function formatList(array $values): string /** * Output format: (key, key, ...) VALUES (value, value, ...) + * @param array $items */ private function formatInsert(array $items): string { @@ -233,6 +241,7 @@ private function formatInsert(array $items): string /** * Output format: (key, key, ...) VALUES (value, value, ...), (value, value, ...), ... + * @param list|Row> $groups */ private function formatMultiInsert(array $groups): string { @@ -261,6 +270,7 @@ private function formatMultiInsert(array $groups): string /** * Output format: key=value, key=value, ... + * @param mixed[] $items */ private function formatSet(array $items): string { @@ -282,6 +292,7 @@ private function formatSet(array $items): string /** * Output format: (key [operator] value) AND/OR ... + * @param mixed[] $items */ private function formatWhere(array $items, string $mode): string { @@ -322,6 +333,7 @@ private function formatWhere(array $items, string $mode): string /** * Output format: key, key DESC, ... + * @param array $items column => direction (positive = ASC, negative = DESC) */ private function formatOrderBy(array $items): string { diff --git a/src/Database/Structure.php b/src/Database/Structure.php index 891433f09..a03c6f14e 100644 --- a/src/Database/Structure.php +++ b/src/Database/Structure.php @@ -18,7 +18,7 @@ class Structure implements IStructure { protected readonly Nette\Caching\Cache $cache; - /** @var array{tables: array, columns: array, primary: array, aliases: array, hasMany: array, belongsTo: array} */ + /** @var array{tables: list, columns: array>>, primary: array|null>, aliases: array, hasMany: array>>, belongsTo: array>} */ protected array $structure; protected bool $isRebuilt = false; @@ -31,6 +31,7 @@ public function __construct( } + /** @return list */ public function getTables(): array { $this->needStructure(); @@ -48,7 +49,7 @@ public function getColumns(string $table): array /** - * @return string|string[]|null + * @return string|list|null */ public function getPrimaryKey(string $table): string|array|null { @@ -113,6 +114,7 @@ public function getPrimaryKeySequence(string $table): ?string } + /** @return array> table name => list of referencing columns */ public function getHasManyReference(string $table): array { $this->needStructure(); @@ -121,6 +123,7 @@ public function getHasManyReference(string $table): array } + /** @return array column name => referenced table name */ public function getBelongsToReference(string $table): array { $this->needStructure(); @@ -157,6 +160,7 @@ protected function needStructure(): void /** * Loads complete structure from database. + * @return array{tables: list, columns: array>>, primary: array|null>, aliases: array, hasMany: array>>, belongsTo: array>} */ protected function loadStructure(): array { @@ -193,6 +197,10 @@ protected function loadStructure(): array } + /** + * @param list $columns + * @return string|list|null + */ protected function analyzePrimaryKey(array $columns): string|array|null { $primary = []; @@ -212,6 +220,7 @@ protected function analyzePrimaryKey(array $columns): string|array|null } + /** @param array $structure */ protected function analyzeForeignKeys(array &$structure, string $table): void { $lowerTable = strtolower($table); diff --git a/src/Database/Table/ActiveRow.php b/src/Database/Table/ActiveRow.php index 083a4995e..6bb3349f5 100644 --- a/src/Database/Table/ActiveRow.php +++ b/src/Database/Table/ActiveRow.php @@ -14,6 +14,7 @@ /** * Represents database row with support for relations. * ActiveRow is based on the great library NotORM http://www.notorm.com written by Jakub Vrana. + * @implements \IteratorAggregate */ class ActiveRow implements \IteratorAggregate, IRow { @@ -21,20 +22,28 @@ class ActiveRow implements \IteratorAggregate, IRow public function __construct( + /** @var array */ private array $data, + /** @var Selection */ private Selection $table, ) { } - /** @internal */ + /** + * @internal + * @param Selection $table + */ public function setTable(Selection $table): void { $this->table = $table; } - /** @internal */ + /** + * @internal + * @return Selection + */ public function getTable(): Selection { return $this->table; @@ -47,6 +56,7 @@ public function __toString(): string } + /** @return array */ public function toArray(): array { $this->accessColumn(null); @@ -102,7 +112,7 @@ public function getSignature(bool $throw = true): string /** * Returns referenced row. - * @return self|null if the row does not exist + * @return ?self if the row does not exist */ public function ref(string $key, ?string $throughColumn = null): ?self { @@ -117,6 +127,7 @@ public function ref(string $key, ?string $throughColumn = null): ?self /** * Returns referencing rows collection. + * @return GroupedSelection */ public function related(string $key, ?string $throughColumn = null): GroupedSelection { @@ -131,6 +142,7 @@ public function related(string $key, ?string $throughColumn = null): GroupedSele /** * Updates row data. + * @param iterable $data */ public function update(iterable $data): bool { @@ -186,6 +198,7 @@ public function delete(): int /********************* interface IteratorAggregate ****************d*g**/ + /** @return \ArrayIterator */ public function getIterator(): \Iterator { $this->accessColumn(null); diff --git a/src/Database/Table/GroupedSelection.php b/src/Database/Table/GroupedSelection.php index f58b589cb..48c384889 100644 --- a/src/Database/Table/GroupedSelection.php +++ b/src/Database/Table/GroupedSelection.php @@ -16,6 +16,8 @@ /** * Represents filtered table grouped by referencing table. * GroupedSelection is based on the great library NotORM http://www.notorm.com written by Jakub Vrana. + * @template T of ActiveRow + * @extends Selection */ class GroupedSelection extends Selection { @@ -28,6 +30,7 @@ class GroupedSelection extends Selection /** * Creates filtered and grouped table representation. + * @param Selection $refTable */ public function __construct( Explorer $explorer, diff --git a/src/Database/Table/Selection.php b/src/Database/Table/Selection.php index a09193c75..f3289289c 100644 --- a/src/Database/Table/Selection.php +++ b/src/Database/Table/Selection.php @@ -17,8 +17,8 @@ * Represents filtered table result. * Selection is based on the great library NotORM http://www.notorm.com written by Jakub Vrana. * @template T of ActiveRow - * @implements \Iterator - * @implements \ArrayAccess + * @implements \Iterator + * @implements \ArrayAccess */ class Selection implements \Iterator, IRowContainer, \ArrayAccess, \Countable { @@ -39,10 +39,10 @@ class Selection implements \Iterator, IRowContainer, \ArrayAccess, \Countable /** primary column sequence name, false for autodetection */ protected string|bool|null $primarySequence = false; - /** @var array|null data read from database in [primary key => ActiveRow] format */ + /** @var ?array data read from database in [primary key => ActiveRow] format */ protected ?array $rows = null; - /** @var array|null modifiable data in [primary key => ActiveRow] format */ + /** @var ?array modifiable data in [primary key => ActiveRow] format */ protected ?array $data = null; protected bool $dataRefreshed = false; @@ -54,15 +54,19 @@ class Selection implements \Iterator, IRowContainer, \ArrayAccess, \Countable protected ?string $generalCacheKey = null; protected ?string $specificCacheKey = null; - /** of [conditions => [key => ActiveRow]]; used by GroupedSelection */ + /** @var array> of [conditions => [key => ActiveRow]]; used by GroupedSelection */ protected array $aggregation = []; + + /** @var array|false|null column => selected */ protected array|false|null $accessedColumns = null; + + /** @var array|false|null */ protected array|false|null $previousAccessedColumns = null; - /** should instance observe accessed columns caching */ + /** @var ?static should instance observe accessed columns caching */ protected ?self $observeCache = null; - /** of primary key values */ + /** @var list of primary key values */ protected array $keys = []; @@ -130,7 +134,6 @@ public function getPrimarySequence(): ?string } - /** @return static */ public function setPrimarySequence(string $sequence): static { $this->primarySequence = $sequence; @@ -147,6 +150,7 @@ public function getSql(): string /** * Loads cache of previous accessed columns and returns it. * @internal + * @return list */ public function getPreviousAccessedColumns(): array|bool { @@ -173,7 +177,7 @@ public function getSqlBuilder(): SqlBuilder /** * Returns row specified by primary key. - * @return T|null + * @return ?T */ public function get(mixed $key): ?ActiveRow { @@ -184,7 +188,7 @@ public function get(mixed $key): ?ActiveRow /** * Returns the next row or null if there are no more rows. - * @return T|null + * @return ?T */ public function fetch(): ?ActiveRow { @@ -218,6 +222,7 @@ public function fetchField(?string $column = null): mixed * Returns all rows as associative array, where first argument specifies key column and second value column. * For duplicate keys, the last value is used. When using null as key, array is indexed from zero. * Alternatively accepts callback returning value or key-value pairs. + * @param string|int|\Closure(T): array{0: mixed, 1?: mixed}|null $keyOrCallback * @return array */ public function fetchPairs(string|int|\Closure|null $keyOrCallback = null, string|int|null $value = null): array @@ -239,6 +244,7 @@ public function fetchAll(): array /** * Returns all rows as associative tree. * @deprecated + * @return array */ public function fetchAssoc(string $path): array { @@ -253,7 +259,6 @@ public function fetchAssoc(string $path): array /** * Adds select clause, more calls append to the end. * @param string $columns for example "column, MD5(column) AS column_md5" - * @return static */ public function select(string $columns, ...$params): static { @@ -265,7 +270,6 @@ public function select(string $columns, ...$params): static /** * Adds condition for primary key. - * @return static */ public function wherePrimary(mixed $key): static { @@ -289,8 +293,7 @@ public function wherePrimary(mixed $key): static /** * Adds where condition, more calls append with AND. - * @param string|array $condition possibly containing ? - * @return static + * @param string|array $condition possibly containing ? */ public function where(string|array $condition, ...$params): static { @@ -303,7 +306,6 @@ public function where(string|array $condition, ...$params): static * Adds ON condition when joining specified table, more calls appends with AND. * @param string $tableChain table chain or table alias for which you need additional left join condition * @param string $condition possibly containing ? - * @return static */ public function joinWhere(string $tableChain, string $condition, ...$params): static { @@ -315,6 +317,7 @@ public function joinWhere(string $tableChain, string $condition, ...$params): st /** * Adds condition, more calls appends with AND. * @param string|string[] $condition possibly containing ? + * @param mixed[] $params */ protected function condition(string|array $condition, array $params, ?string $tableChain = null): void { @@ -338,9 +341,8 @@ protected function condition(string|array $condition, array $params, ?string $ta /** * Adds where condition using the OR operator between parameters. * More calls appends with AND. - * @param array $parameters ['column1' => 1, 'column2 > ?' => 2, 'full condition'] + * @param array $parameters ['column1' => 1, 'column2 > ?' => 2, 'full condition'] * @throws Nette\InvalidArgumentException - * @return static */ public function whereOr(array $parameters): static { @@ -375,7 +377,6 @@ public function whereOr(array $parameters): static /** * Adds ORDER BY clause, more calls appends to the end. * @param string $columns for example 'column1, column2 DESC' - * @return static */ public function order(string $columns, ...$params): static { @@ -387,7 +388,6 @@ public function order(string $columns, ...$params): static /** * Sets LIMIT clause, more calls rewrite old values. - * @return static */ public function limit(?int $limit, ?int $offset = null): static { @@ -399,7 +399,6 @@ public function limit(?int $limit, ?int $offset = null): static /** * Sets OFFSET using page number, more calls rewrite old values. - * @return static */ public function page(int $page, int $itemsPerPage, &$numOfPages = null): static { @@ -417,7 +416,6 @@ public function page(int $page, int $itemsPerPage, &$numOfPages = null): static /** * Sets GROUP BY clause, more calls rewrite old value. - * @return static */ public function group(string $columns, ...$params): static { @@ -429,7 +427,6 @@ public function group(string $columns, ...$params): static /** * Sets HAVING clause, more calls rewrite old value. - * @return static */ public function having(string $having, ...$params): static { @@ -441,7 +438,6 @@ public function having(string $having, ...$params): static /** * Aliases table. Example ':book:book_tag.tag', 'tg' - * @return static */ public function alias(string $tableChain, string $alias): static { @@ -563,21 +559,30 @@ protected function execute(): void } - /** @deprecated */ + /** + * @deprecated + * @param array $row + */ protected function createRow(array $row): ActiveRow { return $this->explorer->createActiveRow($row, $this); } - /** @deprecated */ + /** + * @deprecated + * @return static + */ public function createSelectionInstance(?string $table = null): self { return $this->explorer->table($table ?: $this->name); } - /** @deprecated */ + /** + * @deprecated + * @return GroupedSelection + */ protected function createGroupedSelectionInstance(string $table, string $column): GroupedSelection { return $this->explorer->createGroupedSelection($this, $table, $column); @@ -639,6 +644,8 @@ protected function saveCacheState(): void /** * Returns Selection parent for caching. + * @param-out string $refPath + * @return static */ protected function getRefTable(&$refPath): self { @@ -694,7 +701,7 @@ protected function getSpecificCacheKey(): string /** * @internal - * @param string|null column name or null to reload all columns + * @param ?string $key column name or null to reload all columns * @return bool if selection requeried for more columns. */ public function accessColumn(?string $key, bool $selectColumn = true): bool @@ -785,8 +792,8 @@ public function getDataRefreshed(): bool /** * Inserts row in a table. Returns ActiveRow or number of affected rows for Selection or table without primary key. - * @param iterable|Selection $data - * @return T|array|int|bool + * @param iterable|self $data + * @return ($data is array ? T|array : int) */ public function insert(iterable $data): ActiveRow|array|int|bool { @@ -867,6 +874,7 @@ public function insert(iterable $data): ActiveRow|array|int|bool /** * Updates all rows in result set. * Joins in UPDATE are supported only in MySQL + * @param iterable $data * @return int number of affected rows */ public function update(iterable $data): int @@ -949,6 +957,7 @@ public function getReferencedTable(ActiveRow $row, ?string $table, ?string $colu /** * Returns referencing rows. + * @return GroupedSelection|null */ public function getReferencingTable( string $table, diff --git a/src/Database/Table/SqlBuilder.php b/src/Database/Table/SqlBuilder.php index 454739119..0b6fccb9d 100644 --- a/src/Database/Table/SqlBuilder.php +++ b/src/Database/Table/SqlBuilder.php @@ -25,10 +25,20 @@ class SqlBuilder protected readonly string $tableName; protected readonly Conventions $conventions; protected readonly string $delimitedTable; + + /** @var string[] */ protected array $select = []; + + /** @var string[] */ protected array $where = []; + + /** @var array table chain => conditions */ protected array $joinCondition = []; + + /** @var array condition hash => condition */ protected array $conditions = []; + + /** @var array> */ protected array $parameters = [ 'select' => [], 'joinCondition' => [], @@ -37,17 +47,27 @@ class SqlBuilder 'having' => [], 'order' => [], ]; + + /** @var string[] */ protected array $order = []; protected ?int $limit = null; protected ?int $offset = null; protected string $group = ''; protected string $having = ''; + + /** @var array table name => chain */ protected array $reservedTableNames = []; + + /** @var array alias => chain */ protected array $aliases = []; protected string $currentAlias = ''; private readonly Driver $driver; private readonly IStructure $structure; + + /** @var array table fullName => exists */ private array $cacheTableList = []; + + /** @var array tables being expanded (cycle detection) */ private array $expandingJoins = []; @@ -104,6 +124,7 @@ public function buildDeleteQuery(): string /** * Returns select query hash for caching. + * @param string[]|null $columns */ public function getSelectQueryHash(?array $columns = null): string { @@ -137,7 +158,7 @@ public function getSelectQueryHash(?array $columns = null): string /** * Returns SQL query. - * @param string[] $columns + * @param list|null $columns */ public function buildSelectQuery(?array $columns = null): string { @@ -188,6 +209,7 @@ public function buildSelectQuery(?array $columns = null): string } + /** @return list */ public function getParameters(): array { if (!isset($this->parameters['joinConditionSorted'])) { @@ -244,6 +266,7 @@ public function addSelect(string $columns, ...$params): void } + /** @return string[] */ public function getSelect(): array { return $this->select; @@ -259,6 +282,7 @@ public function resetSelect(): void /** * Adds WHERE condition, more calls append with AND. + * @param array|string $condition */ public function addWhere(string|array $condition, ...$params): bool { @@ -268,6 +292,7 @@ public function addWhere(string|array $condition, ...$params): bool /** * Adds JOIN condition. + * @param array|string $condition */ public function addJoinCondition(string $tableChain, string|array $condition, ...$params): bool { @@ -280,6 +305,12 @@ public function addJoinCondition(string $tableChain, string|array $condition, .. } + /** + * @param array|string $condition + * @param array $params + * @param array $conditions + * @param array $conditionsParameters + */ protected function addCondition( string|array $condition, array $params, @@ -417,6 +448,7 @@ protected function addCondition( } + /** @return list */ public function getConditions(): array { return array_values($this->conditions); @@ -465,6 +497,10 @@ public function addOrder(string|array $columns, ...$params): void } + /** + * @param string[] $columns + * @param mixed[] $parameters + */ public function setOrder(array $columns, array $parameters): void { $this->order = $columns; @@ -472,6 +508,7 @@ public function setOrder(array $columns, array $parameters): void } + /** @return string[] */ public function getOrder(): array { return $this->order; @@ -532,12 +569,18 @@ public function getHaving(): string /********************* SQL building ****************d*g**/ + /** @param string[] $columns */ protected function buildSelect(array $columns): string { return 'SELECT ' . implode(', ', $columns); } + /** + * @param array $joins + * @param array $joinConditions + * @return array + */ protected function parseJoinConditions(array &$joins, array $joinConditions): array { $tableJoins = $leftJoinDependency = $finalJoinConditions = []; @@ -574,6 +617,11 @@ protected function parseJoinConditions(array &$joins, array $joinConditions): ar } + /** + * @param array $leftJoinDependency + * @param array $tableJoins + * @param array $finalJoins + */ protected function getSortedJoins( string $table, array &$leftJoinDependency, @@ -627,6 +675,7 @@ protected function getSortedJoins( } + /** @param array $joins */ protected function parseJoins(array &$joins, string &$query): void { $query = preg_replace_callback($this->getColumnChainsRegxp(), function (array $match) use (&$joins): string { @@ -643,6 +692,10 @@ private function getColumnChainsRegxp(): string } + /** + * @param array $joins + * @param array $match + */ public function parseJoinsCb(array &$joins, array $match): string { $chain = $match['chain']; @@ -761,6 +814,10 @@ public function parseJoinsCb(array &$joins, array $match): string } + /** + * @param array $joins + * @param array $leftJoinConditions + */ protected function buildQueryJoins(array $joins, array $leftJoinConditions = []): string { $return = ''; @@ -775,6 +832,9 @@ protected function buildQueryJoins(array $joins, array $leftJoinConditions = []) } + /** + * @return array table chain => condition SQL + */ protected function buildJoinConditions(): array { $conditions = []; @@ -825,6 +885,12 @@ protected function tryDelimite(string $s): string } + /** + * @param string[] $columns + * @param list> $parameters + * @param array $conditions + * @param mixed[] $conditionsParameters + */ protected function addConditionComposition( array $columns, array $parameters, @@ -842,6 +908,7 @@ protected function addConditionComposition( } + /** @param mixed[] $parameters */ private function getConditionHash(string $condition, array $parameters): string { foreach ($parameters as $key => &$parameter) { @@ -860,6 +927,7 @@ private function getConditionHash(string $condition, array $parameters): string } + /** @return array */ private function getCachedTableList(): array { if (!$this->cacheTableList) { From f8941f552fb4edd37ca2a3732b6ca3ed2a010215 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Fri, 9 Jan 2026 01:07:12 +0100 Subject: [PATCH 4/9] improved native types --- src/Database/Connection.php | 20 ++++++++++---------- src/Database/Explorer.php | 18 +++++++++--------- src/Database/Table/GroupedSelection.php | 8 ++++---- src/Database/Table/Selection.php | 20 ++++++++++---------- src/Database/Table/SqlBuilder.php | 12 ++++++------ 5 files changed, 39 insertions(+), 39 deletions(-) diff --git a/src/Database/Connection.php b/src/Database/Connection.php index 31f6a19b5..f21005841 100644 --- a/src/Database/Connection.php +++ b/src/Database/Connection.php @@ -245,7 +245,7 @@ public function transaction(callable $callback): mixed * Generates and executes SQL query. * @param literal-string $sql */ - public function query(#[Language('SQL')] string $sql, #[Language('GenericSQL')] ...$params): ResultSet + public function query(#[Language('SQL')] string $sql, #[Language('GenericSQL')] mixed ...$params): ResultSet { [$this->sql, $params] = $this->preprocess($sql, ...$params); try { @@ -275,7 +275,7 @@ public function queryArgs(string $sql, array $params): ResultSet * @param literal-string $sql * @return array{string, array} */ - public function preprocess(string $sql, ...$params): array + public function preprocess(string $sql, mixed ...$params): array { $this->connect(); return $params @@ -297,7 +297,7 @@ public function getLastQueryString(): ?string * Shortcut for query()->fetch() * @param literal-string $sql */ - public function fetch(#[Language('SQL')] string $sql, #[Language('GenericSQL')] ...$params): ?Row + public function fetch(#[Language('SQL')] string $sql, #[Language('GenericSQL')] mixed ...$params): ?Row { return $this->query($sql, ...$params)->fetch(); } @@ -308,7 +308,7 @@ public function fetch(#[Language('SQL')] string $sql, #[Language('GenericSQL')] * @param literal-string $sql * @return ?array */ - public function fetchAssoc(#[Language('SQL')] string $sql, #[Language('GenericSQL')] ...$params): ?array + public function fetchAssoc(#[Language('SQL')] string $sql, #[Language('GenericSQL')] mixed ...$params): ?array { return $this->query($sql, ...$params)->fetchAssoc(); } @@ -318,7 +318,7 @@ public function fetchAssoc(#[Language('SQL')] string $sql, #[Language('GenericSQ * Shortcut for query()->fetchField() * @param literal-string $sql */ - public function fetchField(#[Language('SQL')] string $sql, #[Language('GenericSQL')] ...$params): mixed + public function fetchField(#[Language('SQL')] string $sql, #[Language('GenericSQL')] mixed ...$params): mixed { return $this->query($sql, ...$params)->fetchField(); } @@ -329,7 +329,7 @@ public function fetchField(#[Language('SQL')] string $sql, #[Language('GenericSQ * @param literal-string $sql * @return ?list */ - public function fetchList(#[Language('SQL')] string $sql, #[Language('GenericSQL')] ...$params): ?array + public function fetchList(#[Language('SQL')] string $sql, #[Language('GenericSQL')] mixed ...$params): ?array { return $this->query($sql, ...$params)->fetchList(); } @@ -340,7 +340,7 @@ public function fetchList(#[Language('SQL')] string $sql, #[Language('GenericSQL * @param literal-string $sql * @return ?list */ - public function fetchFields(#[Language('SQL')] string $sql, #[Language('GenericSQL')] ...$params): ?array + public function fetchFields(#[Language('SQL')] string $sql, #[Language('GenericSQL')] mixed ...$params): ?array { return $this->query($sql, ...$params)->fetchList(); } @@ -351,7 +351,7 @@ public function fetchFields(#[Language('SQL')] string $sql, #[Language('GenericS * @param literal-string $sql * @return array */ - public function fetchPairs(#[Language('SQL')] string $sql, #[Language('GenericSQL')] ...$params): array + public function fetchPairs(#[Language('SQL')] string $sql, #[Language('GenericSQL')] mixed ...$params): array { return $this->query($sql, ...$params)->fetchPairs(); } @@ -362,7 +362,7 @@ public function fetchPairs(#[Language('SQL')] string $sql, #[Language('GenericSQ * @param literal-string $sql * @return list */ - public function fetchAll(#[Language('SQL')] string $sql, #[Language('GenericSQL')] ...$params): array + public function fetchAll(#[Language('SQL')] string $sql, #[Language('GenericSQL')] mixed ...$params): array { return $this->query($sql, ...$params)->fetchAll(); } @@ -371,7 +371,7 @@ public function fetchAll(#[Language('SQL')] string $sql, #[Language('GenericSQL' /** * Creates SQL literal value. */ - public static function literal(string $value, ...$params): SqlLiteral + public static function literal(string $value, mixed ...$params): SqlLiteral { return new SqlLiteral($value, $params); } diff --git a/src/Database/Explorer.php b/src/Database/Explorer.php index 771d9f569..f38723f4c 100644 --- a/src/Database/Explorer.php +++ b/src/Database/Explorer.php @@ -66,7 +66,7 @@ public function getInsertId(?string $sequence = null): string * Generates and executes SQL query. * @param literal-string $sql */ - public function query(#[Language('SQL')] string $sql, #[Language('GenericSQL')] ...$params): ResultSet + public function query(#[Language('SQL')] string $sql, #[Language('GenericSQL')] mixed ...$params): ResultSet { return $this->connection->query($sql, ...$params); } @@ -143,7 +143,7 @@ public function createGroupedSelection( * Shortcut for query()->fetch() * @param literal-string $sql */ - public function fetch(#[Language('SQL')] string $sql, #[Language('GenericSQL')] ...$params): ?Row + public function fetch(#[Language('SQL')] string $sql, #[Language('GenericSQL')] mixed ...$params): ?Row { return $this->connection->query($sql, ...$params)->fetch(); } @@ -154,7 +154,7 @@ public function fetch(#[Language('SQL')] string $sql, #[Language('GenericSQL')] * @param literal-string $sql * @return ?array */ - public function fetchAssoc(#[Language('SQL')] string $sql, #[Language('GenericSQL')] ...$params): ?array + public function fetchAssoc(#[Language('SQL')] string $sql, #[Language('GenericSQL')] mixed ...$params): ?array { return $this->connection->query($sql, ...$params)->fetchAssoc(); } @@ -164,7 +164,7 @@ public function fetchAssoc(#[Language('SQL')] string $sql, #[Language('GenericSQ * Shortcut for query()->fetchField() * @param literal-string $sql */ - public function fetchField(#[Language('SQL')] string $sql, #[Language('GenericSQL')] ...$params): mixed + public function fetchField(#[Language('SQL')] string $sql, #[Language('GenericSQL')] mixed ...$params): mixed { return $this->connection->query($sql, ...$params)->fetchField(); } @@ -175,7 +175,7 @@ public function fetchField(#[Language('SQL')] string $sql, #[Language('GenericSQ * @param literal-string $sql * @return ?list */ - public function fetchList(#[Language('SQL')] string $sql, #[Language('GenericSQL')] ...$params): ?array + public function fetchList(#[Language('SQL')] string $sql, #[Language('GenericSQL')] mixed ...$params): ?array { return $this->connection->query($sql, ...$params)->fetchList(); } @@ -186,7 +186,7 @@ public function fetchList(#[Language('SQL')] string $sql, #[Language('GenericSQL * @param literal-string $sql * @return ?list */ - public function fetchFields(#[Language('SQL')] string $sql, #[Language('GenericSQL')] ...$params): ?array + public function fetchFields(#[Language('SQL')] string $sql, #[Language('GenericSQL')] mixed ...$params): ?array { return $this->connection->query($sql, ...$params)->fetchList(); } @@ -197,7 +197,7 @@ public function fetchFields(#[Language('SQL')] string $sql, #[Language('GenericS * @param literal-string $sql * @return array */ - public function fetchPairs(#[Language('SQL')] string $sql, #[Language('GenericSQL')] ...$params): array + public function fetchPairs(#[Language('SQL')] string $sql, #[Language('GenericSQL')] mixed ...$params): array { return $this->connection->query($sql, ...$params)->fetchPairs(); } @@ -208,7 +208,7 @@ public function fetchPairs(#[Language('SQL')] string $sql, #[Language('GenericSQ * @param literal-string $sql * @return list */ - public function fetchAll(#[Language('SQL')] string $sql, #[Language('GenericSQL')] ...$params): array + public function fetchAll(#[Language('SQL')] string $sql, #[Language('GenericSQL')] mixed ...$params): array { return $this->connection->query($sql, ...$params)->fetchAll(); } @@ -217,7 +217,7 @@ public function fetchAll(#[Language('SQL')] string $sql, #[Language('GenericSQL' /** * Creates SQL literal value. */ - public static function literal(string $value, ...$params): SqlLiteral + public static function literal(string $value, mixed ...$params): SqlLiteral { return new SqlLiteral($value, $params); } diff --git a/src/Database/Table/GroupedSelection.php b/src/Database/Table/GroupedSelection.php index 48c384889..1182e6692 100644 --- a/src/Database/Table/GroupedSelection.php +++ b/src/Database/Table/GroupedSelection.php @@ -58,7 +58,7 @@ public function setActive(int|string $active): static } - public function select(string $columns, ...$params): static + public function select(string $columns, mixed ...$params): static { if (!$this->sqlBuilder->getSelect()) { $this->sqlBuilder->addSelect("$this->name.$this->column"); @@ -68,7 +68,7 @@ public function select(string $columns, ...$params): static } - public function order(string $columns, ...$params): static + public function order(string $columns, mixed ...$params): static { if (!$this->sqlBuilder->getOrder()) { // improve index utilization @@ -198,7 +198,7 @@ protected function execute(): void } - protected function getRefTable(&$refPath): Selection + protected function getRefTable(mixed &$refPath): Selection { $refObj = $this->refTable; $refPath = $this->name . '.'; @@ -235,7 +235,7 @@ protected function emptyResultSet(bool $clearCache = true, bool $deleteReference /********************* manipulation ****************d*g**/ - public function insert(iterable $data): ActiveRow|array|int|bool + public function insert(iterable $data): ActiveRow|array|int { if ($data instanceof \Traversable && !$data instanceof Selection) { $data = iterator_to_array($data); diff --git a/src/Database/Table/Selection.php b/src/Database/Table/Selection.php index f3289289c..ec1731d39 100644 --- a/src/Database/Table/Selection.php +++ b/src/Database/Table/Selection.php @@ -152,7 +152,7 @@ public function getSql(): string * @internal * @return list */ - public function getPreviousAccessedColumns(): array|bool + public function getPreviousAccessedColumns(): array { if ($this->cache && $this->previousAccessedColumns === null) { $this->accessedColumns = $this->previousAccessedColumns = $this->cache->load($this->getGeneralCacheKey()); @@ -260,7 +260,7 @@ public function fetchAssoc(string $path): array * Adds select clause, more calls append to the end. * @param string $columns for example "column, MD5(column) AS column_md5" */ - public function select(string $columns, ...$params): static + public function select(string $columns, mixed ...$params): static { $this->emptyResultSet(); $this->sqlBuilder->addSelect($columns, ...$params); @@ -295,7 +295,7 @@ public function wherePrimary(mixed $key): static * Adds where condition, more calls append with AND. * @param string|array $condition possibly containing ? */ - public function where(string|array $condition, ...$params): static + public function where(string|array $condition, mixed ...$params): static { $this->condition($condition, $params); return $this; @@ -307,7 +307,7 @@ public function where(string|array $condition, ...$params): static * @param string $tableChain table chain or table alias for which you need additional left join condition * @param string $condition possibly containing ? */ - public function joinWhere(string $tableChain, string $condition, ...$params): static + public function joinWhere(string $tableChain, string $condition, mixed ...$params): static { $this->condition($condition, $params, $tableChain); return $this; @@ -378,7 +378,7 @@ public function whereOr(array $parameters): static * Adds ORDER BY clause, more calls appends to the end. * @param string $columns for example 'column1, column2 DESC' */ - public function order(string $columns, ...$params): static + public function order(string $columns, mixed ...$params): static { $this->emptyResultSet(); $this->sqlBuilder->addOrder($columns, ...$params); @@ -400,7 +400,7 @@ public function limit(?int $limit, ?int $offset = null): static /** * Sets OFFSET using page number, more calls rewrite old values. */ - public function page(int $page, int $itemsPerPage, &$numOfPages = null): static + public function page(int $page, int $itemsPerPage, ?int &$numOfPages = null): static { if (func_num_args() > 2) { $numOfPages = (int) ceil($this->count('*') / $itemsPerPage); @@ -417,7 +417,7 @@ public function page(int $page, int $itemsPerPage, &$numOfPages = null): static /** * Sets GROUP BY clause, more calls rewrite old value. */ - public function group(string $columns, ...$params): static + public function group(string $columns, mixed ...$params): static { $this->emptyResultSet(); $this->sqlBuilder->setGroup($columns, ...$params); @@ -428,7 +428,7 @@ public function group(string $columns, ...$params): static /** * Sets HAVING clause, more calls rewrite old value. */ - public function having(string $having, ...$params): static + public function having(string $having, mixed ...$params): static { $this->emptyResultSet(); $this->sqlBuilder->setHaving($having, ...$params); @@ -647,7 +647,7 @@ protected function saveCacheState(): void * @param-out string $refPath * @return static */ - protected function getRefTable(&$refPath): self + protected function getRefTable(mixed &$refPath): self { $refPath = ''; return $this; @@ -795,7 +795,7 @@ public function getDataRefreshed(): bool * @param iterable|self $data * @return ($data is array ? T|array : int) */ - public function insert(iterable $data): ActiveRow|array|int|bool + public function insert(iterable $data): ActiveRow|array|int { //should be called before query for not to spoil PDO::lastInsertId $primarySequenceName = $this->getPrimarySequence(); diff --git a/src/Database/Table/SqlBuilder.php b/src/Database/Table/SqlBuilder.php index 0b6fccb9d..40755ba32 100644 --- a/src/Database/Table/SqlBuilder.php +++ b/src/Database/Table/SqlBuilder.php @@ -259,7 +259,7 @@ public function importGroupConditions(self $builder): bool /** * Adds SELECT clause, more calls append to the end. */ - public function addSelect(string $columns, ...$params): void + public function addSelect(string $columns, mixed ...$params): void { $this->select[] = $columns; $this->parameters['select'] = array_merge($this->parameters['select'], $params); @@ -284,7 +284,7 @@ public function resetSelect(): void * Adds WHERE condition, more calls append with AND. * @param array|string $condition */ - public function addWhere(string|array $condition, ...$params): bool + public function addWhere(string|array $condition, mixed ...$params): bool { return $this->addCondition($condition, $params, $this->where, $this->parameters['where']); } @@ -294,7 +294,7 @@ public function addWhere(string|array $condition, ...$params): bool * Adds JOIN condition. * @param array|string $condition */ - public function addJoinCondition(string $tableChain, string|array $condition, ...$params): bool + public function addJoinCondition(string $tableChain, string|array $condition, mixed ...$params): bool { $this->parameters['joinConditionSorted'] = null; if (!isset($this->joinCondition[$tableChain])) { @@ -490,7 +490,7 @@ protected function checkUniqueTableName(string $tableName, string $chain): void /** * Adds ORDER BY clause, more calls append to the end. */ - public function addOrder(string|array $columns, ...$params): void + public function addOrder(string $columns, mixed ...$params): void { $this->order[] = $columns; $this->parameters['order'] = array_merge($this->parameters['order'], $params); @@ -540,7 +540,7 @@ public function getOffset(): ?int /** * Sets GROUP BY and HAVING clause. */ - public function setGroup(string|array $columns, ...$params): void + public function setGroup(string $columns, mixed ...$params): void { $this->group = $columns; $this->parameters['group'] = $params; @@ -553,7 +553,7 @@ public function getGroup(): string } - public function setHaving(string $having, ...$params): void + public function setHaving(string $having, mixed ...$params): void { $this->having = $having; $this->parameters['having'] = $params; From cafa44dacf92f8575dd349563569a494bcfd3f0e Mon Sep 17 00:00:00 2001 From: David Grudl Date: Mon, 9 Mar 2026 01:54:25 +0100 Subject: [PATCH 5/9] improved PHPDoc descriptions --- src/Bridges/DatabaseDI/DatabaseExtension.php | 2 +- src/Bridges/DatabaseTracy/ConnectionPanel.php | 5 ++- src/Database/Connection.php | 22 ++++----- src/Database/Conventions.php | 9 ++-- .../AmbiguousReferenceKeyException.php | 2 +- .../Conventions/DiscoveredConventions.php | 8 ++++ .../Conventions/StaticConventions.php | 1 - src/Database/DateTime.php | 2 +- src/Database/DriverException.php | 9 ++++ src/Database/Explorer.php | 20 +++++---- src/Database/Helpers.php | 18 +++++--- src/Database/IStructure.php | 16 +++---- src/Database/Reflection/Table.php | 2 +- src/Database/ResultSet.php | 3 +- src/Database/Row.php | 4 +- src/Database/SqlPreprocessor.php | 8 ++-- src/Database/Structure.php | 16 ++++++- src/Database/Table/ActiveRow.php | 15 +++---- src/Database/Table/GroupedSelection.php | 9 ++++ src/Database/Table/Selection.php | 45 ++++++++++--------- src/Database/Table/SqlBuilder.php | 18 +++++++- 21 files changed, 149 insertions(+), 85 deletions(-) diff --git a/src/Bridges/DatabaseDI/DatabaseExtension.php b/src/Bridges/DatabaseDI/DatabaseExtension.php index cda600eb2..4bdaed3b0 100644 --- a/src/Bridges/DatabaseDI/DatabaseExtension.php +++ b/src/Bridges/DatabaseDI/DatabaseExtension.php @@ -14,7 +14,7 @@ /** - * Nette Framework Database services. + * Registers database Connection, Structure, and Explorer services in the DI container. */ class DatabaseExtension extends Nette\DI\CompilerExtension { diff --git a/src/Bridges/DatabaseTracy/ConnectionPanel.php b/src/Bridges/DatabaseTracy/ConnectionPanel.php index 026655d17..bbe6ae6f4 100644 --- a/src/Bridges/DatabaseTracy/ConnectionPanel.php +++ b/src/Bridges/DatabaseTracy/ConnectionPanel.php @@ -15,7 +15,7 @@ /** - * Debug panel for Nette\Database. + * Tracy Bar panel showing executed SQL queries with timing and EXPLAIN support. */ class ConnectionPanel implements Tracy\IBarPanel { @@ -32,6 +32,9 @@ class ConnectionPanel implements Tracy\IBarPanel private Tracy\BlueScreen $blueScreen; + /** + * Registers the panel with Tracy. Optionally adds it to the Tracy Bar. + */ public static function initialize( Connection $connection, bool $addBarPanel = false, diff --git a/src/Database/Connection.php b/src/Database/Connection.php index f21005841..33bbabf6b 100644 --- a/src/Database/Connection.php +++ b/src/Database/Connection.php @@ -53,6 +53,7 @@ public function __construct( /** + * Connects to the database server if not already connected. * @throws ConnectionException */ public function connect(): void @@ -131,7 +132,7 @@ public function getReflection(): Reflection /** - * Sets callback for row preprocessing. + * Sets a callback for normalizing each result row (e.g., type conversion). Pass null to disable. * @param ?(callable(array, ResultSet): array) $normalizer */ public function setRowNormalizer(?callable $normalizer): static @@ -142,7 +143,7 @@ public function setRowNormalizer(?callable $normalizer): static /** - * Returns last inserted ID. + * Returns the ID of the last inserted row, or the last value from a sequence. */ public function getInsertId(?string $sequence = null): string { @@ -211,7 +212,7 @@ public function rollBack(): void /** - * Executes callback inside a transaction. + * Executes callback inside a transaction. Supports nesting. * @param callable(static): mixed $callback */ public function transaction(callable $callback): mixed @@ -272,6 +273,7 @@ public function queryArgs(string $sql, array $params): ResultSet /** + * Preprocesses SQL query with parameter substitution and returns the resulting SQL and bound parameters. * @param literal-string $sql * @return array{string, array} */ @@ -294,7 +296,7 @@ public function getLastQueryString(): ?string /** - * Shortcut for query()->fetch() + * Executes SQL query and returns the first row, or null if no rows were returned. * @param literal-string $sql */ public function fetch(#[Language('SQL')] string $sql, #[Language('GenericSQL')] mixed ...$params): ?Row @@ -304,7 +306,7 @@ public function fetch(#[Language('SQL')] string $sql, #[Language('GenericSQL')] /** - * Shortcut for query()->fetchAssoc() + * Executes SQL query and returns the first row as an associative array, or null. * @param literal-string $sql * @return ?array */ @@ -315,7 +317,7 @@ public function fetchAssoc(#[Language('SQL')] string $sql, #[Language('GenericSQ /** - * Shortcut for query()->fetchField() + * Executes SQL query and returns the first field of the first row, or null. * @param literal-string $sql */ public function fetchField(#[Language('SQL')] string $sql, #[Language('GenericSQL')] mixed ...$params): mixed @@ -325,7 +327,7 @@ public function fetchField(#[Language('SQL')] string $sql, #[Language('GenericSQ /** - * Shortcut for query()->fetchList() + * Executes SQL query and returns the first row as an indexed array, or null. * @param literal-string $sql * @return ?list */ @@ -336,7 +338,7 @@ public function fetchList(#[Language('SQL')] string $sql, #[Language('GenericSQL /** - * Shortcut for query()->fetchList() + * Executes SQL query and returns the first row as an indexed array, or null. * @param literal-string $sql * @return ?list */ @@ -347,7 +349,7 @@ public function fetchFields(#[Language('SQL')] string $sql, #[Language('GenericS /** - * Shortcut for query()->fetchPairs() + * Executes SQL query and returns rows as key-value pairs. * @param literal-string $sql * @return array */ @@ -358,7 +360,7 @@ public function fetchPairs(#[Language('SQL')] string $sql, #[Language('GenericSQ /** - * Shortcut for query()->fetchAll() + * Executes SQL query and returns all rows as an array of Row objects. * @param literal-string $sql * @return list */ diff --git a/src/Database/Conventions.php b/src/Database/Conventions.php index 459b9df43..8ed7aaf6c 100644 --- a/src/Database/Conventions.php +++ b/src/Database/Conventions.php @@ -22,9 +22,8 @@ interface Conventions function getPrimary(string $table): string|array|null; /** - * Returns referenced table & referenced column. - * Example: - * (author, book) returns [book, author_id] + * Returns the referencing table name and referencing column for a has-many relationship. + * Example: (author, book) returns [book, author_id] * * @return ?array{string, string} * @throws AmbiguousReferenceKeyException @@ -32,8 +31,8 @@ function getPrimary(string $table): string|array|null; function getHasManyReference(string $table, string $key): ?array; /** - * Returns referenced table & referencing column. - * Example + * Returns the referenced table name and local foreign key column for a belongs-to relationship. + * Example: * (book, author) returns [author, author_id] * (book, translator) returns [author, translator_id] * diff --git a/src/Database/Conventions/AmbiguousReferenceKeyException.php b/src/Database/Conventions/AmbiguousReferenceKeyException.php index 860370534..f2a1c7480 100644 --- a/src/Database/Conventions/AmbiguousReferenceKeyException.php +++ b/src/Database/Conventions/AmbiguousReferenceKeyException.php @@ -9,7 +9,7 @@ /** - * Ambiguous reference key exception. + * Multiple matching columns found for a relationship reference. */ class AmbiguousReferenceKeyException extends \RuntimeException { diff --git a/src/Database/Conventions/DiscoveredConventions.php b/src/Database/Conventions/DiscoveredConventions.php index ee50c0b19..1bdec3df5 100644 --- a/src/Database/Conventions/DiscoveredConventions.php +++ b/src/Database/Conventions/DiscoveredConventions.php @@ -28,6 +28,10 @@ public function getPrimary(string $table): string|array|null } + /** + * Finds the referencing table and column for a has-many relationship by searching structure metadata. + * Triggers structure rebuild if needed. Throws on ambiguous match. + */ public function getHasManyReference(string $nsTable, string $key): ?array { $candidates = $columnCandidates = []; @@ -77,6 +81,10 @@ public function getHasManyReference(string $nsTable, string $key): ?array } + /** + * Finds the referenced table and local foreign key column for a belongs-to relationship. + * Triggers structure rebuild if needed. + */ public function getBelongsToReference(string $table, string $key): ?array { $tableColumns = $this->structure->getBelongsToReference($table); diff --git a/src/Database/Conventions/StaticConventions.php b/src/Database/Conventions/StaticConventions.php index 32da714f9..094484c41 100644 --- a/src/Database/Conventions/StaticConventions.php +++ b/src/Database/Conventions/StaticConventions.php @@ -17,7 +17,6 @@ class StaticConventions implements Conventions { /** - * Create static conventional structure. * @param string $primary %s stands for table name * @param string $foreign %1$s stands for key used after ->, %2$s for table name * @param string $table %1$s stands for key used after ->, %2$s for table name diff --git a/src/Database/DateTime.php b/src/Database/DateTime.php index 37e46bc44..b4b2fca6c 100644 --- a/src/Database/DateTime.php +++ b/src/Database/DateTime.php @@ -9,7 +9,7 @@ /** - * Date Time. + * Immutable date-time value with JSON and string serialization support. */ final class DateTime extends \DateTimeImmutable implements \JsonSerializable { diff --git a/src/Database/DriverException.php b/src/Database/DriverException.php index 45e726e86..41a29d47d 100644 --- a/src/Database/DriverException.php +++ b/src/Database/DriverException.php @@ -21,6 +21,9 @@ class DriverException extends \PDOException public ?array $params = null; + /** + * Creates a DriverException from a PDOException, preserving error info and stack trace location. + */ public static function from(\PDOException $src): static { $e = new static($src->message, 0, $src); @@ -40,12 +43,18 @@ public static function from(\PDOException $src): static } + /** + * Returns the driver-specific error code, or null if not available. + */ public function getDriverCode(): int|string|null { return $this->errorInfo[1] ?? null; } + /** + * Returns the SQLSTATE error code, or null if not available. + */ public function getSqlState(): ?string { return $this->errorInfo[0] ?? null; diff --git a/src/Database/Explorer.php b/src/Database/Explorer.php index f38723f4c..f030813b4 100644 --- a/src/Database/Explorer.php +++ b/src/Database/Explorer.php @@ -49,7 +49,10 @@ public function rollBack(): void } - /** @param callable(static): mixed $callback */ + /** + * Executes callback inside a transaction. + * @param callable(static): mixed $callback + */ public function transaction(callable $callback): mixed { return $this->connection->transaction(fn() => $callback($this)); @@ -112,6 +115,7 @@ public function getConventions(): Conventions /** + * Creates an ActiveRow instance, using the configured row mapping class if available. * @param array $data * @param Table\Selection $selection */ @@ -140,7 +144,7 @@ public function createGroupedSelection( /** - * Shortcut for query()->fetch() + * Executes SQL query and returns the first row, or null if no rows were returned. * @param literal-string $sql */ public function fetch(#[Language('SQL')] string $sql, #[Language('GenericSQL')] mixed ...$params): ?Row @@ -150,7 +154,7 @@ public function fetch(#[Language('SQL')] string $sql, #[Language('GenericSQL')] /** - * Shortcut for query()->fetchAssoc() + * Executes SQL query and returns the first row as an associative array, or null. * @param literal-string $sql * @return ?array */ @@ -161,7 +165,7 @@ public function fetchAssoc(#[Language('SQL')] string $sql, #[Language('GenericSQ /** - * Shortcut for query()->fetchField() + * Executes SQL query and returns the first field of the first row, or null. * @param literal-string $sql */ public function fetchField(#[Language('SQL')] string $sql, #[Language('GenericSQL')] mixed ...$params): mixed @@ -171,7 +175,7 @@ public function fetchField(#[Language('SQL')] string $sql, #[Language('GenericSQ /** - * Shortcut for query()->fetchList() + * Executes SQL query and returns the first row as an indexed array, or null. * @param literal-string $sql * @return ?list */ @@ -182,7 +186,7 @@ public function fetchList(#[Language('SQL')] string $sql, #[Language('GenericSQL /** - * Shortcut for query()->fetchList() + * Executes SQL query and returns the first row as an indexed array, or null. * @param literal-string $sql * @return ?list */ @@ -193,7 +197,7 @@ public function fetchFields(#[Language('SQL')] string $sql, #[Language('GenericS /** - * Shortcut for query()->fetchPairs() + * Executes SQL query and returns rows as key-value pairs. * @param literal-string $sql * @return array */ @@ -204,7 +208,7 @@ public function fetchPairs(#[Language('SQL')] string $sql, #[Language('GenericSQ /** - * Shortcut for query()->fetchAll() + * Executes SQL query and returns all rows as an array of Row objects. * @param literal-string $sql * @return list */ diff --git a/src/Database/Helpers.php b/src/Database/Helpers.php index 9561d8bda..a86b56e98 100644 --- a/src/Database/Helpers.php +++ b/src/Database/Helpers.php @@ -85,7 +85,7 @@ public static function dumpResult(ResultSet $result): void /** - * Returns syntax highlighted SQL command. + * Returns syntax-highlighted SQL query as an HTML string. * @param ?array $params */ public static function dumpSql(string $sql, ?array $params = null, ?Connection $connection = null): string @@ -165,8 +165,8 @@ public static function dumpSql(string $sql, ?array $params = null, ?Connection $ /** - * Returns column types from result set. - * @return array column name => type + * Detects column types from a PDO statement using column metadata. + * @return array column name => IStructure::FIELD_* type */ public static function detectTypes(\PDOStatement $statement): array { @@ -184,7 +184,7 @@ public static function detectTypes(\PDOStatement $statement): array /** - * Detects column type from native type. + * Maps a native column type string to an IStructure::FIELD_* constant. * @internal */ public static function detectType(string $type): string @@ -204,6 +204,7 @@ public static function detectType(string $type): string /** + * Converts raw column values to PHP types based on column type metadata. * @internal * @param array $row * @return array @@ -329,7 +330,7 @@ public static function initializeTracy( /** - * Converts rows to key-value pairs. + * Transforms rows into an associative array using the specified key and value columns. * @param array> $rows * @param string|int|(\Closure(Row|Table\ActiveRow|array): array{0: mixed, 1?: mixed})|null $key * @return array @@ -376,7 +377,7 @@ public static function toPairs(array $rows, string|int|\Closure|null $key, strin /** - * Returns duplicate columns from result set. + * Returns a human-readable string listing duplicate column names in the result set. */ public static function findDuplicates(\PDOStatement $statement): string { @@ -398,7 +399,10 @@ public static function findDuplicates(\PDOStatement $statement): string } - /** @return array{type: ?string, length: ?int, scale: ?int, parameters: ?string} */ + /** + * Parses a SQL column type string into its components. + * @return array{type: ?string, length: ?int, scale: ?int, parameters: ?string} + */ public static function parseColumnType(string $type): array { preg_match('/^([^(]+)(?:\((?:(\d+)(?:,(\d+))?|([^)]+))\))?/', $type, $m, PREG_UNMATCHED_AS_NULL); diff --git a/src/Database/IStructure.php b/src/Database/IStructure.php index 3cb403a5e..48c5f7982 100644 --- a/src/Database/IStructure.php +++ b/src/Database/IStructure.php @@ -27,13 +27,13 @@ interface IStructure FIELD_TIME_INTERVAL = 'timeint'; /** - * Returns tables list. + * Returns all tables in the database. * @return list */ function getTables(): array; /** - * Returns table columns list. + * Returns all columns in a table. * @return list}> */ function getColumns(string $table): array; @@ -55,16 +55,14 @@ function getPrimaryAutoincrementKey(string $table): ?string; function getPrimaryKeySequence(string $table): ?string; /** - * Returns hasMany reference. - * If a targetTable is not provided, returns references for all tables. - * @return array>|null table name => list of referencing columns + * Returns tables referencing the given table via foreign key, or null if unknown. + * @return array>|null referencing table name => list of referencing columns */ function getHasManyReference(string $table): ?array; /** - * Returns belongsTo reference. - * If a column is not provided, returns references for all columns. - * @return ?array column name => referenced table name + * Returns foreign key columns in the given table mapped to their referenced tables, or null if unknown. + * @return ?array local column name => referenced table name */ function getBelongsToReference(string $table): ?array; @@ -74,7 +72,7 @@ function getBelongsToReference(string $table): ?array; function rebuild(): void; /** - * Returns true if database cached structure has been rebuilt. + * Checks whether the structure has been rebuilt from the database during this request. */ function isRebuilt(): bool; } diff --git a/src/Database/Reflection/Table.php b/src/Database/Reflection/Table.php index b863d58a0..165347702 100644 --- a/src/Database/Reflection/Table.php +++ b/src/Database/Reflection/Table.php @@ -12,7 +12,7 @@ /** - * Database table structure. + * Database table metadata including columns, indexes, and foreign keys. */ final class Table { diff --git a/src/Database/ResultSet.php b/src/Database/ResultSet.php index 0f6289426..d2a07878d 100644 --- a/src/Database/ResultSet.php +++ b/src/Database/ResultSet.php @@ -187,7 +187,8 @@ public function valid(): bool /** - * Returns the next row as an associative array or null if there are no more rows. + * Returns the next row as an associative array, or null if there are no more rows. + * When $path is given, fetches all rows and restructures them using Arrays::associate(). * @return ?array */ public function fetchAssoc(?string $path = null): ?array diff --git a/src/Database/Row.php b/src/Database/Row.php index 9a66d71d8..3143a19fd 100644 --- a/src/Database/Row.php +++ b/src/Database/Row.php @@ -31,7 +31,7 @@ public function __isset(string $key): bool /** - * Returns a item. + * Returns an item by key or numeric index. * @param string|int $key key or index */ public function offsetGet($key): mixed @@ -50,7 +50,7 @@ public function offsetGet($key): mixed /** - * Checks if $key exists. + * Checks if a key or numeric index exists. * @param string|int $key key or index */ public function offsetExists($key): bool diff --git a/src/Database/SqlPreprocessor.php b/src/Database/SqlPreprocessor.php index c94508be0..aae1c435f 100644 --- a/src/Database/SqlPreprocessor.php +++ b/src/Database/SqlPreprocessor.php @@ -71,6 +71,7 @@ public function __construct(Connection $connection) /** * Processes SQL query with parameter substitution. * @param mixed[] $params + * @param bool $useParams when true, scalar values are kept as bound parameters instead of being inlined * @return array{string, list} */ public function process(array $params, bool $useParams = false): array @@ -118,7 +119,8 @@ public function process(array $params, bool $useParams = false): array /** - * Handles SQL placeholders and skips string literals and comments. + * Processes a regex match from the SQL scan: skips string literals and comments, + * detects SQL command keywords to set array mode, and replaces ? placeholders. * @param string[] $match */ private function parsePart(array $match): string @@ -147,8 +149,8 @@ private function parsePart(array $match): string /** - * Formats a value for use in SQL query where ? placeholder is used. - * For arrays, the formatting is determined by $mode or last SQL keyword before the placeholder + * Formats a value for use in SQL at a ? placeholder. + * For arrays, the mode is taken from $mode or detected from the last SQL keyword before the placeholder. */ private function formatParameter(mixed $value, ?string $mode = null): string { diff --git a/src/Database/Structure.php b/src/Database/Structure.php index a03c6f14e..f3fbba88c 100644 --- a/src/Database/Structure.php +++ b/src/Database/Structure.php @@ -59,6 +59,9 @@ public function getPrimaryKey(string $table): string|array|null } + /** + * Returns the name of the autoincrement primary key column, or null if none exists. + */ public function getPrimaryAutoincrementKey(string $table): ?string { $primaryKey = $this->getPrimaryKey($table); @@ -89,6 +92,9 @@ public function getPrimaryAutoincrementKey(string $table): ?string } + /** + * Returns the sequence name for the primary key column, or null if not applicable. + */ public function getPrimaryKeySequence(string $table): ?string { $this->needStructure(); @@ -114,7 +120,7 @@ public function getPrimaryKeySequence(string $table): ?string } - /** @return array> table name => list of referencing columns */ + /** @return array> referencing table name => list of referencing columns */ public function getHasManyReference(string $table): array { $this->needStructure(); @@ -123,7 +129,7 @@ public function getHasManyReference(string $table): array } - /** @return array column name => referenced table name */ + /** @return array local column name => referenced table name */ public function getBelongsToReference(string $table): array { $this->needStructure(); @@ -142,12 +148,18 @@ public function rebuild(): void } + /** + * Checks whether the structure has been rebuilt from the database during this request. + */ public function isRebuilt(): bool { return $this->isRebuilt; } + /** + * Ensures the structure is loaded, from cache or from the database. + */ protected function needStructure(): void { if (isset($this->structure)) { diff --git a/src/Database/Table/ActiveRow.php b/src/Database/Table/ActiveRow.php index 6bb3349f5..8241abddf 100644 --- a/src/Database/Table/ActiveRow.php +++ b/src/Database/Table/ActiveRow.php @@ -65,8 +65,7 @@ public function toArray(): array /** - * Returns primary key value. - * @return mixed possible int, string, array, object (Nette\Utils\DateTime) + * Returns primary key value, or an array of values for composite primary keys. */ public function getPrimary(bool $throw = true): mixed { @@ -111,8 +110,7 @@ public function getSignature(bool $throw = true): string /** - * Returns referenced row. - * @return ?self if the row does not exist + * Returns referenced row, or null if the row does not exist. */ public function ref(string $key, ?string $throughColumn = null): ?self { @@ -141,7 +139,7 @@ public function related(string $key, ?string $throughColumn = null): GroupedSele /** - * Updates row data. + * Updates row data and refreshes the instance from database. Returns true if the row was changed. * @param iterable $data */ public function update(iterable $data): bool @@ -178,8 +176,8 @@ public function update(iterable $data): bool /** - * Deletes row from database. - * @return int number of affected rows + * Deletes the row from database. + * @return int number of affected rows */ public function delete(): int { @@ -240,8 +238,9 @@ public function __set(string $column, mixed $value): void /** + * Returns column value, or a referenced row if the key matches a relationship. * @return ActiveRow|mixed - * @throws Nette\MemberAccessException + * @throws Nette\MemberAccessException if the column does not exist and no relationship is found */ public function &__get(string $key): mixed { diff --git a/src/Database/Table/GroupedSelection.php b/src/Database/Table/GroupedSelection.php index 1182e6692..880479c37 100644 --- a/src/Database/Table/GroupedSelection.php +++ b/src/Database/Table/GroupedSelection.php @@ -58,6 +58,9 @@ public function setActive(int|string $active): static } + /** + * Adds a SELECT clause. Automatically prepends the grouping column if no select exists yet. + */ public function select(string $columns, mixed ...$params): static { if (!$this->sqlBuilder->getSelect()) { @@ -68,6 +71,9 @@ public function select(string $columns, mixed ...$params): static } + /** + * Adds an ORDER BY clause. Automatically prepends the grouping column (matching direction) to improve index utilization. + */ public function order(string $columns, mixed ...$params): static { if (!$this->sqlBuilder->getOrder()) { @@ -79,6 +85,9 @@ public function order(string $columns, mixed ...$params): static } + /** + * Invalidates cached data and forces reload on next access. + */ public function refreshData(): void { unset($this->refCache['referencing'][$this->getGeneralCacheKey()][$this->getSpecificCacheKey()]); diff --git a/src/Database/Table/Selection.php b/src/Database/Table/Selection.php index ec1731d39..6d3791d2a 100644 --- a/src/Database/Table/Selection.php +++ b/src/Database/Table/Selection.php @@ -232,7 +232,7 @@ public function fetchPairs(string|int|\Closure|null $keyOrCallback = null, strin /** - * Returns all rows. + * Returns all rows as an array indexed by primary key. * @return T[] */ public function fetchAll(): array @@ -315,7 +315,7 @@ public function joinWhere(string $tableChain, string $condition, mixed ...$param /** - * Adds condition, more calls appends with AND. + * Adds a WHERE or JOIN condition. When $tableChain is given, the condition is added to the JOIN ON clause. * @param string|string[] $condition possibly containing ? * @param mixed[] $params */ @@ -398,7 +398,7 @@ public function limit(?int $limit, ?int $offset = null): static /** - * Sets OFFSET using page number, more calls rewrite old values. + * Sets LIMIT and OFFSET for the given page number. Optionally calculates total number of pages. */ public function page(int $page, int $itemsPerPage, ?int &$numOfPages = null): static { @@ -472,7 +472,7 @@ public function aggregation(string $function, ?string $groupFunction = null): mi /** - * Counts number of rows. If column is not provided returns count of result rows, otherwise runs new sql counting query. + * Returns count of fetched rows, or runs COUNT($column) query when column is specified. */ public function count(?string $column = null): int { @@ -643,8 +643,8 @@ protected function saveCacheState(): void /** - * Returns Selection parent for caching. - * @param-out string $refPath + * Returns the root Selection used as the shared cache anchor for referenced rows. + * @param-out string $refPath * @return static */ protected function getRefTable(mixed &$refPath): self @@ -655,7 +655,7 @@ protected function getRefTable(mixed &$refPath): self /** - * Loads refCache references + * Initializes the reference cache for the current selection. Overridden by GroupedSelection. */ protected function loadRefCache(): void { @@ -663,8 +663,8 @@ protected function loadRefCache(): void /** - * Returns general cache key independent on query parameters or sql limit - * Used e.g. for previously accessed columns caching + * Returns general cache key independent of query parameters or SQL limit. + * Used e.g. for previously accessed columns caching. */ protected function getGeneralCacheKey(): string { @@ -686,8 +686,8 @@ protected function getGeneralCacheKey(): string /** - * Returns object specific cache key dependent on query parameters - * Used e.g. for reference memory caching + * Returns object-specific cache key dependent on query parameters. + * Used e.g. for reference memory caching. */ protected function getSpecificCacheKey(): string { @@ -779,7 +779,7 @@ public function removeAccessColumn(string $key): void /** - * Returns if selection requeried for more columns. + * Checks whether the selection re-queried for additional columns. */ public function getDataRefreshed(): bool { @@ -791,7 +791,8 @@ public function getDataRefreshed(): bool /** - * Inserts row in a table. Returns ActiveRow or number of affected rows for Selection or table without primary key. + * Inserts one or more rows into the table. + * Returns the inserted ActiveRow for single-row inserts, or the number of affected rows otherwise. * @param iterable|self $data * @return ($data is array ? T|array : int) */ @@ -872,10 +873,9 @@ public function insert(iterable $data): ActiveRow|array|int /** - * Updates all rows in result set. - * Joins in UPDATE are supported only in MySQL + * Updates all rows matching current conditions. JOINs in UPDATE are supported only by MySQL. * @param iterable $data - * @return int number of affected rows + * @return int number of affected rows */ public function update(iterable $data): int { @@ -895,8 +895,8 @@ public function update(iterable $data): int /** - * Deletes all rows in result set. - * @return int number of affected rows + * Deletes all rows matching current conditions. + * @return int number of affected rows */ public function delete(): int { @@ -908,8 +908,9 @@ public function delete(): int /** - * Returns referenced row. - * @return ActiveRow|false|null null if the row does not exist, false if the relationship does not exist + * Returns a referenced (parent) row for a belongs-to relationship. + * Returns null if the referenced row does not exist, false if the relationship is not defined. + * @return ActiveRow|false|null */ public function getReferencedTable(ActiveRow $row, ?string $table, ?string $column = null): ActiveRow|false|null { @@ -956,7 +957,7 @@ public function getReferencedTable(ActiveRow $row, ?string $table, ?string $colu /** - * Returns referencing rows. + * Returns a grouped selection of referencing (child) rows for a has-many relationship. * @return GroupedSelection|null */ public function getReferencingTable( @@ -1033,7 +1034,7 @@ public function valid(): bool /** - * Mimic row. + * Sets a row by primary key. * @param string $key * @param ActiveRow $value */ diff --git a/src/Database/Table/SqlBuilder.php b/src/Database/Table/SqlBuilder.php index 40755ba32..aca3c99e9 100644 --- a/src/Database/Table/SqlBuilder.php +++ b/src/Database/Table/SqlBuilder.php @@ -17,8 +17,7 @@ /** - * Builds SQL query. - * SqlBuilder is based on great library NotORM http://www.notorm.com written by Jakub Vrana. + * Builds SQL queries for the Explorer layer. */ class SqlBuilder { @@ -227,6 +226,9 @@ public function getParameters(): array } + /** + * Copies WHERE conditions and aliases from another builder. + */ public function importConditions(self $builder): void { $this->where = $builder->where; @@ -239,6 +241,9 @@ public function importConditions(self $builder): void } + /** + * Copies GROUP BY and HAVING clauses from another builder. Returns true if HAVING was present. + */ public function importGroupConditions(self $builder): bool { if ($builder->having) { @@ -306,6 +311,8 @@ public function addJoinCondition(string $tableChain, string|array $condition, mi /** + * Normalizes and appends a condition with its parameters. Deduplicates identical conditions. + * Returns true if the condition was added, false if it was a duplicate. * @param array|string $condition * @param array $params * @param array $conditions @@ -469,6 +476,9 @@ public function addAlias(string $chain, string $alias): void } + /** + * Ensures a table alias is not used for two different chains, throwing on conflict. + */ protected function checkUniqueTableName(string $tableName, string $chain): void { if (isset($this->aliases[$tableName]) && ($chain === '.' . $tableName)) { @@ -873,6 +883,9 @@ protected function buildQueryEnd(): string } + /** + * Delimits lowercase identifiers in a SQL fragment while leaving uppercase keywords untouched. + */ protected function tryDelimite(string $s): string { return preg_replace_callback( @@ -886,6 +899,7 @@ protected function tryDelimite(string $s): string /** + * Adds a multi-column IN condition using OR expansion or tuple syntax depending on driver support. * @param string[] $columns * @param list> $parameters * @param array $conditions From 37770de322ce196082af7f7a9b111d0a722913e2 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Wed, 21 Jan 2026 07:51:02 +0100 Subject: [PATCH 6/9] uses nette/phpstan-rules --- composer.json | 9 ++- phpstan.neon | 3 - tests/types/TypesTest.phpt | 7 ++ tests/types/database-types.php | 118 +++++++++++++++++++++++++++++++++ 4 files changed, 133 insertions(+), 4 deletions(-) create mode 100644 tests/types/TypesTest.phpt create mode 100644 tests/types/database-types.php diff --git a/composer.json b/composer.json index e40e8baa4..3b047bcbf 100644 --- a/composer.json +++ b/composer.json @@ -25,7 +25,9 @@ "nette/di": "^3.1", "mockery/mockery": "^1.6@stable", "tracy/tracy": "^2.9", - "phpstan/phpstan-nette": "^2.0@stable", + "phpstan/phpstan": "^2.1@stable", + "phpstan/extension-installer": "^1.4@stable", + "nette/phpstan-rules": "^1.0", "jetbrains/phpstorm-attributes": "^1.2" }, "autoload": { @@ -43,5 +45,10 @@ "branch-alias": { "dev-master": "3.2-dev" } + }, + "config": { + "allow-plugins": { + "phpstan/extension-installer": true + } } } diff --git a/phpstan.neon b/phpstan.neon index 8a634adc9..a08469300 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -3,6 +3,3 @@ parameters: paths: - src - -includes: - - vendor/phpstan/phpstan-nette/extension.neon diff --git a/tests/types/TypesTest.phpt b/tests/types/TypesTest.phpt new file mode 100644 index 000000000..1bd53e017 --- /dev/null +++ b/tests/types/TypesTest.phpt @@ -0,0 +1,7 @@ + $row) { + assertType('int', $key); + assertType(Row::class, $row); + } +} + + +function testActiveRowIterator(ActiveRow $activeRow): void +{ + foreach ($activeRow as $key => $value) { + assertType('string', $key); + assertType('mixed', $value); + } +} + + +/** + * @param Selection $selection + */ +function testSelectionIterator(Selection $selection): void +{ + foreach ($selection as $key => $row) { + assertType('string', $key); + assertType(ActiveRow::class, $row); + } +} + + +/** + * @param Selection $selection + */ +function testSelectionArrayAccess(Selection $selection): void +{ + $row = $selection['key']; + assertType(ActiveRow::class . '|null', $row); +} + + +/** + * @param Selection $selection + */ +function testSelectionFluentMethods(Selection $selection): void +{ + $result = $selection->where('id', 1); + assertType('Nette\Database\Table\Selection', $result); + + $result = $selection->limit(10); + assertType('Nette\Database\Table\Selection', $result); + + $result = $selection->page(1, 10); + assertType('Nette\Database\Table\Selection', $result); +} + + +function testParseColumnType(): void +{ + $result = Helpers::parseColumnType('varchar(255)'); + assertType('array{type: string|null, length: int|null, scale: int|null, parameters: string|null}', $result); +} + + +/** @param Selection $selection */ +function testSelectionFetch(Selection $selection): void +{ + $result = $selection->fetch(); + assertType(ActiveRow::class . '|null', $result); +} + + +/** @param Selection $selection */ +function testSelectionGet(Selection $selection): void +{ + $result = $selection->get(1); + assertType(ActiveRow::class . '|null', $result); +} + + +/** @param Selection $selection */ +function testSelectionFetchAll(Selection $selection): void +{ + $result = $selection->fetchAll(); + assertType('array', $result); +} + + +function testReflectionGetTables(Nette\Database\Reflection $reflection): void +{ + assertType('list', $reflection->getTables()); +} + + +function testResultSetFetchAll(ResultSet $resultSet): void +{ + assertType('list', $resultSet->fetchAll()); +} + + +function testResultSetFetchPairs(ResultSet $resultSet): void +{ + assertType('array', $resultSet->fetchPairs()); +} From 406dcf83f094c8efc7c6fba4880f1f6f7eaf9519 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Fri, 23 Jan 2026 00:20:11 +0100 Subject: [PATCH 7/9] made static analysis mandatory --- .github/workflows/static-analysis.yml | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml index 8af71c683..36cd4b85f 100644 --- a/.github/workflows/static-analysis.yml +++ b/.github/workflows/static-analysis.yml @@ -1,9 +1,6 @@ -name: Static Analysis (only informative) +name: Static Analysis -on: - push: - branches: - - master +on: [push, pull_request] jobs: phpstan: @@ -13,9 +10,8 @@ jobs: - uses: actions/checkout@v4 - uses: shivammathur/setup-php@v2 with: - php-version: 8.1 + php-version: 8.5 coverage: none - run: composer install --no-progress --prefer-dist - run: composer phpstan -- --no-progress - continue-on-error: true # is only informative From bd9887322ea4fb72faa1384e374a249eb4353426 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Mon, 9 Mar 2026 01:30:52 +0100 Subject: [PATCH 8/9] added row mapping support via setRowMapping() callback and `mapping` config option --- src/Bridges/DatabaseDI/DatabaseExtension.php | 37 ++++- src/Database/Explorer.php | 7 +- .../DatabaseExtension.rowMapping.phpt | 138 ++++++++++++++++++ 3 files changed, 180 insertions(+), 2 deletions(-) create mode 100644 tests/Database.DI/DatabaseExtension.rowMapping.phpt diff --git a/src/Bridges/DatabaseDI/DatabaseExtension.php b/src/Bridges/DatabaseDI/DatabaseExtension.php index 4bdaed3b0..745c13446 100644 --- a/src/Bridges/DatabaseDI/DatabaseExtension.php +++ b/src/Bridges/DatabaseDI/DatabaseExtension.php @@ -36,6 +36,10 @@ public function getConfigSchema(): Nette\Schema\Schema 'explain' => Expect::bool(true), 'reflection' => Expect::string(), // BC 'conventions' => Expect::string('discovered'), // Nette\Database\Conventions\DiscoveredConventions + 'mapping' => Expect::structure([ + 'convention' => Expect::string(''), + 'tables' => Expect::arrayOf('string', 'string'), + ])->before(fn($v) => is_string($v) ? ['convention' => $v] : $v), 'autowired' => Expect::bool(), ]), )->before(fn($val) => is_array(reset($val)) || reset($val) === null @@ -120,8 +124,15 @@ private function setupDatabase(\stdClass $config, string $name): void $conventions = Nette\DI\Helpers::filterArguments([$config->conventions])[0]; } + $rowMapping = ($config->mapping->convention || $config->mapping->tables) + ? new Nette\DI\Definitions\Statement([self::class, 'createRowMapping'], [ + $config->mapping->convention, + (array) $config->mapping->tables, + ]) + : null; + $builder->addDefinition($this->prefix("$name.explorer")) - ->setFactory(Nette\Database\Explorer::class, [$connection, $structure, $conventions]) + ->setFactory(Nette\Database\Explorer::class, [$connection, $structure, $conventions, null, $rowMapping]) ->setAutowired($config->autowired); $builder->addAlias($this->prefix("$name.context"), $this->prefix("$name.explorer")); @@ -132,4 +143,28 @@ private function setupDatabase(\stdClass $config, string $name): void $builder->addAlias("nette.database.$name.context", $this->prefix("$name.explorer")); } } + + + /** + * Creates a row mapping closure that resolves an ActiveRow subclass for each table name. + * @param array $tables + * @return \Closure(string): string + */ + public static function createRowMapping(string $convention, array $tables): \Closure + { + return static function (string $table) use ($convention, $tables): string { + if (isset($tables[$table])) { + return $tables[$table]; + } + + if ($convention !== '') { + $class = str_replace('*', str_replace(' ', '', ucwords(strtr($table, '_', ' '))), $convention); + if (class_exists($class)) { + return $class; + } + } + + return Nette\Database\Table\ActiveRow::class; + }; + } } diff --git a/src/Database/Explorer.php b/src/Database/Explorer.php index f030813b4..04b64a7bb 100644 --- a/src/Database/Explorer.php +++ b/src/Database/Explorer.php @@ -26,6 +26,8 @@ public function __construct( private readonly IStructure $structure, ?Conventions $conventions = null, private readonly ?Nette\Caching\Storage $cacheStorage = null, + /** @var ?\Closure(string): class-string */ + private readonly ?\Closure $rowMapping = null, ) { $this->conventions = $conventions ?: new StaticConventions; } @@ -121,7 +123,10 @@ public function getConventions(): Conventions */ public function createActiveRow(array $data, Table\Selection $selection): Table\ActiveRow { - return new Table\ActiveRow($data, $selection); + $class = $this->rowMapping + ? ($this->rowMapping)($selection->getName()) + : Table\ActiveRow::class; + return new $class($data, $selection); } diff --git a/tests/Database.DI/DatabaseExtension.rowMapping.phpt b/tests/Database.DI/DatabaseExtension.rowMapping.phpt new file mode 100644 index 000000000..91b48db95 --- /dev/null +++ b/tests/Database.DI/DatabaseExtension.rowMapping.phpt @@ -0,0 +1,138 @@ +load(Tester\FileMock::create(' + database: + dsn: "sqlite::memory:" + mapping: App\Entity\*Row + debugger: no + + services: + cache: Nette\Caching\Storages\DevNullStorage + ', 'neon')); + + $compiler = new DI\Compiler; + $compiler->addExtension('database', new DatabaseExtension(false)); + $code = $compiler->addConfig($config)->setClassName('Container1')->compile(); + eval($code); + + $container = new Container1; + $container->initialize(); + + $explorer = $container->getService('database.default.explorer'); + Assert::type(Nette\Database\Explorer::class, $explorer); + + // verify the mapping closure was set by checking generated code + Assert::contains('createRowMapping', $code); +}); + + +test('full mapping with convention and tables', function () { + $loader = new DI\Config\Loader; + $config = $loader->load(Tester\FileMock::create(' + database: + dsn: "sqlite::memory:" + mapping: + convention: App\Entity\*Row + tables: + special: App\Entity\SpecialRow + debugger: no + + services: + cache: Nette\Caching\Storages\DevNullStorage + ', 'neon')); + + $compiler = new DI\Compiler; + $compiler->addExtension('database', new DatabaseExtension(false)); + $code = $compiler->addConfig($config)->setClassName('Container2')->compile(); + eval($code); + + $container = new Container2; + $container->initialize(); + + $explorer = $container->getService('database.default.explorer'); + Assert::type(Nette\Database\Explorer::class, $explorer); + Assert::contains('createRowMapping', $code); +}); + + +test('no mapping by default', function () { + $loader = new DI\Config\Loader; + $config = $loader->load(Tester\FileMock::create(' + database: + dsn: "sqlite::memory:" + debugger: no + + services: + cache: Nette\Caching\Storages\DevNullStorage + ', 'neon')); + + $compiler = new DI\Compiler; + $compiler->addExtension('database', new DatabaseExtension(false)); + $code = $compiler->addConfig($config)->setClassName('Container3')->compile(); + eval($code); + + $container = new Container3; + $container->initialize(); + + $explorer = $container->getService('database.default.explorer'); + Assert::type(Nette\Database\Explorer::class, $explorer); + + // no mapping should be set + Assert::notContains('createRowMapping', $code); +}); + + +test('createRowMapping() with convention', function () { + $mapping = DatabaseExtension::createRowMapping('App\Entity\*Row', []); + + // unknown class -> fallback to ActiveRow + Assert::same(Nette\Database\Table\ActiveRow::class, $mapping('nonexistent')); +}); + + +test('createRowMapping() with explicit tables', function () { + $mapping = DatabaseExtension::createRowMapping('', [ + 'my_table' => 'Nette\Database\Table\ActiveRow', + ]); + + Assert::same(Nette\Database\Table\ActiveRow::class, $mapping('my_table')); + + // not in tables and no convention -> fallback + Assert::same(Nette\Database\Table\ActiveRow::class, $mapping('other')); +}); + + +test('createRowMapping() tables override convention', function () { + $mapping = DatabaseExtension::createRowMapping('Some\*Row', [ + 'special' => 'Nette\Database\Table\ActiveRow', + ]); + + // explicit override wins + Assert::same(Nette\Database\Table\ActiveRow::class, $mapping('special')); +}); + + +test('createRowMapping() snake_case to PascalCase', function () { + $mapping = DatabaseExtension::createRowMapping('*', []); + + // We can't test actual entity classes (they don't exist in test env), + // but we can verify the fallback for non-existent classes + Assert::same(Nette\Database\Table\ActiveRow::class, $mapping('some_table')); + + // Verify convention produces correct class names by using a class that exists + $mapping = DatabaseExtension::createRowMapping('Nette\Database\Table\*', []); + Assert::same('Nette\Database\Table\ActiveRow', $mapping('active_row')); +}); From 4114a27bff1f8bb2c8f33e035c92af28b63a03cc Mon Sep 17 00:00:00 2001 From: Simon Podlipsky Date: Fri, 3 Apr 2026 12:44:01 +0200 Subject: [PATCH 9/9] fix: use ad.adrelid instead of pg_attrdef OID in pg_get_expr MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes: SQLSTATE[XX000]: Internal error: 7 ERROR: invalid attnum 9 for relation "pg_attrdef" pg_get_expr requires the OID of the relation that owns the expression as its second argument. Using 'pg_catalog.pg_attrdef'::regclass passes the OID of the catalog table itself, which causes "invalid attnum" errors when tables have GENERATED ALWAYS AS stored columns. The correct relation OID is ad.adrelid (the table the default/generated expression belongs to). The autoincrement detection on the same line already uses ad.adrelid correctly — this aligns the other two calls. --- .github/workflows/tests.yml | 14 +++++++++++++ src/Database/Drivers/PgSqlDriver.php | 4 ++-- tests/Database/Reflection.postgre.phpt | 29 ++++++++++++++++++++++++++ tests/databases.github.ini | 6 ++++++ 4 files changed, 51 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 875732cde..37c8b1aaa 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -72,6 +72,20 @@ jobs: --health-timeout 5s --health-retries 5 + postgres15: + image: postgres:15 + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: nette_test + ports: + - 5434:5432 + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + mssql: image: mcr.microsoft.com/mssql/server:latest env: diff --git a/src/Database/Drivers/PgSqlDriver.php b/src/Database/Drivers/PgSqlDriver.php index deb3e927e..ec23f84e1 100644 --- a/src/Database/Drivers/PgSqlDriver.php +++ b/src/Database/Drivers/PgSqlDriver.php @@ -148,11 +148,11 @@ public function getColumns(string $table): array ELSE NULL END AS size, NOT (a.attnotnull OR t.typtype = 'd' AND t.typnotnull) AS nullable, - pg_catalog.pg_get_expr(ad.adbin, 'pg_catalog.pg_attrdef'::regclass)::varchar AS default, + pg_catalog.pg_get_expr(ad.adbin, ad.adrelid)::varchar AS default, coalesce(co.contype = 'p' AND (seq.relname IS NOT NULL OR strpos(pg_catalog.pg_get_expr(ad.adbin, ad.adrelid), 'nextval') = 1), FALSE) AS autoincrement, coalesce(co.contype = 'p', FALSE) AS primary, coalesce(col_description(c.oid, a.attnum)::varchar, '') AS comment, - coalesce(seq.relname, substring(pg_catalog.pg_get_expr(ad.adbin, 'pg_catalog.pg_attrdef'::regclass) from 'nextval[(]''"?([^''"]+)')) AS sequence + coalesce(seq.relname, substring(pg_catalog.pg_get_expr(ad.adbin, ad.adrelid) from 'nextval[(]''"?([^''"]+)')) AS sequence FROM pg_catalog.pg_attribute AS a JOIN pg_catalog.pg_class AS c ON a.attrelid = c.oid diff --git a/tests/Database/Reflection.postgre.phpt b/tests/Database/Reflection.postgre.phpt index b3788e82a..cbdde4f6b 100644 --- a/tests/Database/Reflection.postgre.phpt +++ b/tests/Database/Reflection.postgre.phpt @@ -76,3 +76,32 @@ test('Tables in schema', function () use ($connection) { $connection->query('SET search_path TO one, two'); Assert::same(['one_slave_fk', 'one_two_fk'], names($driver->getForeignKeys('slave'))); }); + + +test('Table with GENERATED ALWAYS AS stored columns', function () use ($connection) { + $ver = $connection->query('SHOW server_version')->fetchField(); + if (version_compare($ver, '12') < 0) { + Tester\Environment::skip("GENERATED ALWAYS AS requires PostgreSQL 12+, running $ver."); + } + + Nette\Database\Helpers::loadFromFile($connection, Tester\FileMock::create(' + DROP TABLE IF EXISTS "generated_test"; + + CREATE TABLE "generated_test" ( + "id" serial PRIMARY KEY, + "first_name" varchar(50) NOT NULL, + "last_name" varchar(50) NOT NULL, + "full_name" text GENERATED ALWAYS AS ("first_name" || \' \' || "last_name") STORED + ); + ')); + + $driver = $connection->getDriver(); + $columns = $driver->getColumns('generated_test'); + $columnNames = array_column($columns, 'name'); + + Assert::same(['id', 'first_name', 'last_name', 'full_name'], $columnNames); + + $fullNameCol = $columns[3]; + Assert::same('full_name', $fullNameCol['name']); + Assert::same('TEXT', $fullNameCol['nativetype']); +}); diff --git a/tests/databases.github.ini b/tests/databases.github.ini index d00f44271..ba5deb2c8 100644 --- a/tests/databases.github.ini +++ b/tests/databases.github.ini @@ -24,6 +24,12 @@ username = postgres password = postgres options[newDateTime] = yes +[postgresql 15] +dsn = "pgsql:host=127.0.0.1;port=5434;dbname=nette_test" +username = postgres +password = postgres +options[newDateTime] = yes + [sqlsrv] dsn = "sqlsrv:Server=localhost,1433;Database=nette_test" username = SA