diff --git a/.github/ISSUE_TEMPLATE/Bug_report.md b/.github/ISSUE_TEMPLATE/Bug_report.md deleted file mode 100644 index a4cd12634..000000000 --- a/.github/ISSUE_TEMPLATE/Bug_report.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -name: "🐛 Bug Report" -about: "If something isn't working as expected 🤔" - ---- - -Version: ?.?.? - -### Bug Description -... A clear and concise description of what the bug is. A good bug report shouldn't leave others needing to chase you up for more information. - -### Steps To Reproduce -... If possible a minimal demo of the problem ... - -### Expected Behavior -... A clear and concise description of what you expected to happen. - -### Possible Solution -... Only if you have suggestions on a fix for the bug diff --git a/.github/ISSUE_TEMPLATE/Feature_request.md b/.github/ISSUE_TEMPLATE/Feature_request.md deleted file mode 100644 index d2e219489..000000000 --- a/.github/ISSUE_TEMPLATE/Feature_request.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -name: "🚀 Feature Request" -about: "I have a suggestion (and may want to implement it) 🙂" - ---- - -- Is your feature request related to a problem? Please describe. -- Explain your intentions. -- It's up to you to make a strong case to convince the project's developers of the merits of this feature. diff --git a/.github/ISSUE_TEMPLATE/Support_question.md b/.github/ISSUE_TEMPLATE/Support_question.md deleted file mode 100644 index 75c48b6ed..000000000 --- a/.github/ISSUE_TEMPLATE/Support_question.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -name: "🤗 Support Question" -about: "If you have a question 💬, please check out our forum!" - ---- - ---------------^ Click "Preview" for a nicer view! -We primarily use GitHub as an issue tracker; for usage and support questions, please check out these resources below. Thanks! 😁. - -* Nette Forum: https://forum.nette.org -* Nette Gitter: https://gitter.im/nette/nette -* Slack (czech): https://pehapkari.slack.com/messages/C2R30BLKA diff --git a/.github/ISSUE_TEMPLATE/Support_us.md b/.github/ISSUE_TEMPLATE/Support_us.md deleted file mode 100644 index 92d8a4c3a..000000000 --- a/.github/ISSUE_TEMPLATE/Support_us.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -name: "❤️ Support us" -about: "If you would like to support our efforts in maintaining this project 🙌" - ---- - ---------------^ Click "Preview" for a nicer view! - -> https://nette.org/donate - -Help support Nette! - -We develop Nette Framework for more than 14 years. In order to make your life more comfortable. Nette cares about the safety of your sites. Nette saves you time. And gives job opportunities. - -Nette earns you money. And is absolutely free. - -To ensure future development and improving the documentation, we need your donation. - -Whether you are chief of IT company which benefits from Nette, or developer who goes for advice on our forum, if you like Nette, [please make a donation now](https://nette.org/donate). - -Thank you! diff --git a/.github/funding.yml b/.github/funding.yml deleted file mode 100644 index 25adc9520..000000000 --- a/.github/funding.yml +++ /dev/null @@ -1,2 +0,0 @@ -github: dg -custom: "https://nette.org/donate" diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md deleted file mode 100644 index f8aa3f408..000000000 --- a/.github/pull_request_template.md +++ /dev/null @@ -1,11 +0,0 @@ -- bug fix / new feature? -- BC break? yes/no -- doc PR: nette/docs#??? - - diff --git a/.github/workflows/coding-style.yml b/.github/workflows/coding-style.yml index 79c9babf1..875287f8f 100644 --- a/.github/workflows/coding-style.yml +++ b/.github/workflows/coding-style.yml @@ -10,7 +10,7 @@ jobs: - uses: actions/checkout@v3 - uses: shivammathur/setup-php@v2 with: - php-version: 7.2 + php-version: 8.0 coverage: none - run: composer create-project nette/code-checker temp/code-checker ^3 --no-progress diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml index 18e8ae5be..ba9869082 100644 --- a/.github/workflows/static-analysis.yml +++ b/.github/workflows/static-analysis.yml @@ -13,7 +13,7 @@ jobs: - uses: actions/checkout@v3 - uses: shivammathur/setup-php@v2 with: - php-version: 7.4 + php-version: 8.0 coverage: none - run: composer install --no-progress --prefer-dist diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index a2d9b0e86..888bec203 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - php: ['7.2', '7.3', '7.4', '8.0', '8.1', '8.2'] + php: ['8.0', '8.1', '8.2'] fail-fast: false diff --git a/composer.json b/composer.json index 6c2a56fd7..448bb09fa 100644 --- a/composer.json +++ b/composer.json @@ -15,20 +15,18 @@ } ], "require": { - "php": ">=7.2 <8.3", + "php": ">=8.0 <8.3", "ext-pdo": "*", - "nette/caching": "^3.0", - "nette/utils": "^3.2.1 || ~4.0.0" + "nette/caching": "^3.1 || ^4.0", + "nette/utils": "^4.0" }, "require-dev": { - "nette/tester": "^2.4", - "nette/di": "^v3.0", - "mockery/mockery": "^1.3.4", - "tracy/tracy": "^2.4", - "phpstan/phpstan-nette": "^0.12" - }, - "conflict": { - "nette/di": "<3.0-stable" + "nette/tester": "^2.5", + "nette/di": "^3.1 || ^4.0", + "mockery/mockery": "^1.4.3", + "tracy/tracy": "^2.8", + "phpstan/phpstan-nette": "^1.0", + "jetbrains/phpstorm-attributes": "dev-master" }, "autoload": { "classmap": ["src/"] @@ -40,7 +38,7 @@ }, "extra": { "branch-alias": { - "dev-master": "3.1-dev" + "dev-master": "4.0-dev" } } } diff --git a/contributing.md b/contributing.md deleted file mode 100644 index 184152c02..000000000 --- a/contributing.md +++ /dev/null @@ -1,33 +0,0 @@ -How to contribute & use the issue tracker -========================================= - -Nette welcomes your contributions. There are several ways to help out: - -* Create an issue on GitHub, if you have found a bug -* Write test cases for open bug issues -* Write fixes for open bug/feature issues, preferably with test cases included -* Contribute to the [documentation](https://nette.org/en/writing) - -Issues ------- - -Please **do not use the issue tracker to ask questions**. We will be happy to help you -on [Nette forum](https://forum.nette.org) or chat with us on [Gitter](https://gitter.im/nette/nette). - -A good bug report shouldn't leave others needing to chase you up for more -information. Please try to be as detailed as possible in your report. - -**Feature requests** are welcome. But take a moment to find out whether your idea -fits with the scope and aims of the project. It's up to *you* to make a strong -case to convince the project's developers of the merits of this feature. - -Contributing ------------- - -If you'd like to contribute, please take a moment to read [the contributing guide](https://nette.org/en/contributing). - -The best way to propose a feature is to discuss your ideas on [Nette forum](https://forum.nette.org) before implementing them. - -Please do not fix whitespace, format code, or make a purely cosmetic patch. - -Thanks! :heart: diff --git a/readme.md b/readme.md index 011991390..3e8e36168 100644 --- a/readme.md +++ b/readme.md @@ -41,7 +41,7 @@ The recommended way to install is via Composer: composer require nette/database ``` -It requires PHP version 7.2 and supports PHP up to 8.2. +It requires PHP version 8.0 and supports PHP up to 8.2. Usage diff --git a/src/Bridges/DatabaseDI/DatabaseExtension.php b/src/Bridges/DatabaseDI/DatabaseExtension.php index 6815b4b67..d33ecff29 100644 --- a/src/Bridges/DatabaseDI/DatabaseExtension.php +++ b/src/Bridges/DatabaseDI/DatabaseExtension.php @@ -19,8 +19,7 @@ */ class DatabaseExtension extends Nette\DI\CompilerExtension { - /** @var bool */ - private $debugMode; + private bool $debugMode; public function __construct(bool $debugMode = false) @@ -42,12 +41,10 @@ public function getConfigSchema(): Nette\Schema\Schema 'reflection' => Expect::string(), // BC 'conventions' => Expect::string('discovered'), // Nette\Database\Conventions\DiscoveredConventions 'autowired' => Expect::bool(), - ]) - )->before(function ($val) { - return is_array(reset($val)) || reset($val) === null + ]), + )->before(fn($val) => is_array(reset($val)) || reset($val) === null ? $val - : ['default' => $val]; - }); + : ['default' => $val]); } @@ -55,7 +52,7 @@ public function loadConfiguration() { $autowired = true; foreach ($this->config as $name => $config) { - $config->autowired = $config->autowired ?? $autowired; + $config->autowired ??= $autowired; $autowired = false; $this->setupDatabase($config, $name); } @@ -71,7 +68,7 @@ public function beforeCompile() $connection = $builder->getDefinition($this->prefix("$name.connection")); $connection->addSetup( [Nette\Bridges\DatabaseTracy\ConnectionPanel::class, 'initialize'], - [$connection, $this->debugMode, $name, !empty($config->explain)] + [$connection, $this->debugMode, $name, !empty($config->explain)], ); } } diff --git a/src/Bridges/DatabaseTracy/ConnectionPanel.php b/src/Bridges/DatabaseTracy/ConnectionPanel.php index 152fb2f95..f9e470e35 100644 --- a/src/Bridges/DatabaseTracy/ConnectionPanel.php +++ b/src/Bridges/DatabaseTracy/ConnectionPanel.php @@ -22,32 +22,23 @@ class ConnectionPanel implements Tracy\IBarPanel { use Nette\SmartObject; - /** @var int */ - public $maxQueries = 100; + public int $maxQueries = 100; - /** @var string */ - public $name; + public string $name; - /** @var bool|string explain queries? */ - public $explain = true; + public bool|string $explain = true; - /** @var bool */ - public $disabled = false; + public bool $disabled = false; - /** @var float */ - public $performanceScale = 0.25; + public float $performanceScale = 0.25; - /** @var float logged time */ - private $totalTime = 0; + private float $totalTime = 0; - /** @var int */ - private $count = 0; + private int $count = 0; - /** @var array */ - private $queries = []; + private array $queries = []; - /** @var Tracy\BlueScreen */ - private $blueScreen; + private Tracy\BlueScreen $blueScreen; public static function initialize( @@ -56,17 +47,17 @@ public static function initialize( string $name = '', bool $explain = true, ?Tracy\Bar $bar = null, - ?Tracy\BlueScreen $blueScreen = null + ?Tracy\BlueScreen $blueScreen = null, ): ?self { - $blueScreen = $blueScreen ?? Tracy\Debugger::getBlueScreen(); + $blueScreen ??= Tracy\Debugger::getBlueScreen(); $blueScreen->addPanel([self::class, 'renderException']); if ($addBarPanel) { $panel = new self($connection, $blueScreen); $panel->explain = $explain; $panel->name = $name; - $bar = $bar ?? Tracy\Debugger::getBar(); + $bar ??= Tracy\Debugger::getBar(); $bar->addPanel($panel); } diff --git a/src/Database/Connection.php b/src/Database/Connection.php index aec7d909f..acfc3d66e 100644 --- a/src/Database/Connection.php +++ b/src/Database/Connection.php @@ -9,10 +9,9 @@ namespace Nette\Database; +use JetBrains\PhpStorm\Language; use Nette; use Nette\Utils\Arrays; -use PDO; -use PDOException; /** @@ -23,34 +22,20 @@ class Connection use Nette\SmartObject; /** @var array Occurs after connection is established */ - public $onConnect = []; + public array $onConnect = []; /** @var array Occurs after query is executed */ - public $onQuery = []; + public array $onQuery = []; - /** @var array */ - private $params; - - /** @var array */ - private $options; - - /** @var Driver */ - private $driver; - - /** @var SqlPreprocessor */ - private $preprocessor; - - /** @var PDO|null */ - private $pdo; + private array $params; + private array $options; + private ?Driver $driver = null; + private SqlPreprocessor $preprocessor; /** @var callable(array, ResultSet): array */ - private $rowNormalizer = [Helpers::class, 'normalizeRow']; - - /** @var string|null */ - private $sql; - - /** @var int */ - private $transactionDepth = 0; + private $rowNormalizer; + private ?string $sql = null; + private int $transactionDepth = 0; public function __construct( @@ -59,10 +44,11 @@ public function __construct( ?string $user = null, #[\SensitiveParameter] ?string $password = null, - ?array $options = null + ?array $options = null, ) { $this->params = [$dsn, $user, $password]; $this->options = (array) $options; + $this->rowNormalizer = new RowNormalizer; if (empty($options['lazy'])) { $this->connect(); @@ -72,23 +58,21 @@ public function __construct( public function connect(): void { - if ($this->pdo) { + if ($this->driver) { return; } - try { - $this->pdo = new PDO($this->params[0], $this->params[1], $this->params[2], $this->options); - $this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); - } catch (PDOException $e) { - throw ConnectionException::from($e); - } - + $dsn = explode(':', $this->params[0])[0]; $class = empty($this->options['driverClass']) - ? 'Nette\Database\Drivers\\' . ucfirst(str_replace('sql', 'Sql', $this->pdo->getAttribute(PDO::ATTR_DRIVER_NAME))) . 'Driver' + ? 'Nette\Database\Drivers\\' . ucfirst(str_replace('sql', 'Sql', $dsn)) . 'Driver' : $this->options['driverClass']; + if (!class_exists($class)) { + throw new ConnectionException("Invalid data source '$dsn'."); + } + $this->driver = new $class; + $this->driver->connect($this->params[0], $this->params[1], $this->params[2], $this->options); $this->preprocessor = new SqlPreprocessor($this); - $this->driver->initialize($this, $this->options); Arrays::invoke($this->onConnect, $this); } @@ -102,7 +86,7 @@ public function reconnect(): void public function disconnect(): void { - $this->pdo = null; + $this->driver = null; } @@ -112,10 +96,11 @@ public function getDsn(): string } - public function getPdo(): PDO + /** deprecated use getDriver()->getPdo() */ + public function getPdo(): \PDO { $this->connect(); - return $this->pdo; + return $this->driver->getPdo(); } @@ -129,6 +114,7 @@ public function getDriver(): Driver /** @deprecated use getDriver() */ public function getSupplementalDriver(): Driver { + trigger_error(__METHOD__ . '() is deprecated, use getDriver()', E_USER_DEPRECATED); $this->connect(); return $this->driver; } @@ -143,22 +129,15 @@ public function setRowNormalizer(?callable $normalizer): self public function getInsertId(?string $sequence = null): string { - try { - $res = $this->getPdo()->lastInsertId($sequence); - return $res === false ? '0' : $res; - } catch (PDOException $e) { - throw $this->driver->convertException($e); - } + $this->connect(); + return $this->driver->getInsertId($sequence); } - public function quote(string $string, int $type = PDO::PARAM_STR): string + public function quote(string $string): string { - try { - return $this->getPdo()->quote($string, $type); - } catch (PDOException $e) { - throw DriverException::from($e); - } + $this->connect(); + return $this->driver->quote($string); } @@ -192,10 +171,7 @@ public function rollBack(): void } - /** - * @return mixed - */ - public function transaction(callable $callback) + public function transaction(callable $callback): mixed { if ($this->transactionDepth === 0) { $this->beginTransaction(); @@ -226,12 +202,12 @@ public function transaction(callable $callback) * Generates and executes SQL query. * @param literal-string $sql */ - public function query(string $sql, ...$params): ResultSet + public function query(#[Language('SQL')] string $sql, #[Language('GenericSQL')] ...$params): ResultSet { [$this->sql, $params] = $this->preprocess($sql, ...$params); try { $result = new ResultSet($this, $this->sql, $params, $this->rowNormalizer); - } catch (PDOException $e) { + } catch (DriverException $e) { Arrays::invoke($this->onQuery, $this, $e); throw $e; } @@ -244,6 +220,7 @@ public function query(string $sql, ...$params): ResultSet /** @deprecated use query() */ public function queryArgs(string $sql, array $params): ResultSet { + trigger_error(__METHOD__ . '() is deprecated, use query()', E_USER_DEPRECATED); return $this->query($sql, ...$params); } @@ -274,7 +251,7 @@ public function getLastQueryString(): ?string * Shortcut for query()->fetch() * @param literal-string $sql */ - public function fetch(string $sql, ...$params): ?Row + public function fetch(#[Language('SQL')] string $sql, #[Language('GenericSQL')] ...$params): ?Row { return $this->query($sql, ...$params)->fetch(); } @@ -283,9 +260,8 @@ public function fetch(string $sql, ...$params): ?Row /** * Shortcut for query()->fetchField() * @param literal-string $sql - * @return mixed */ - public function fetchField(string $sql, ...$params) + public function fetchField(#[Language('SQL')] string $sql, #[Language('GenericSQL')] ...$params): mixed { return $this->query($sql, ...$params)->fetchField(); } @@ -295,7 +271,7 @@ public function fetchField(string $sql, ...$params) * Shortcut for query()->fetchFields() * @param literal-string $sql */ - public function fetchFields(string $sql, ...$params): ?array + public function fetchFields(#[Language('SQL')] string $sql, #[Language('GenericSQL')] ...$params): ?array { return $this->query($sql, ...$params)->fetchFields(); } @@ -305,7 +281,7 @@ public function fetchFields(string $sql, ...$params): ?array * Shortcut for query()->fetchPairs() * @param literal-string $sql */ - public function fetchPairs(string $sql, ...$params): array + public function fetchPairs(#[Language('SQL')] string $sql, #[Language('GenericSQL')] ...$params): array { return $this->query($sql, ...$params)->fetchPairs(); } @@ -315,7 +291,7 @@ public function fetchPairs(string $sql, ...$params): array * Shortcut for query()->fetchAll() * @param literal-string $sql */ - public function fetchAll(string $sql, ...$params): array + public function fetchAll(#[Language('SQL')] string $sql, #[Language('GenericSQL')] ...$params): array { return $this->query($sql, ...$params)->fetchAll(); } diff --git a/src/Database/Conventions.php b/src/Database/Conventions.php index 19ab567af..3e7a68710 100644 --- a/src/Database/Conventions.php +++ b/src/Database/Conventions.php @@ -16,9 +16,8 @@ interface Conventions { /** * Returns primary key for table. - * @return string|string[]|null */ - function getPrimary(string $table); + function getPrimary(string $table): string|array|null; /** * Returns referenced table & referenced column. diff --git a/src/Database/Conventions/DiscoveredConventions.php b/src/Database/Conventions/DiscoveredConventions.php index 933731db9..6780b8f2e 100644 --- a/src/Database/Conventions/DiscoveredConventions.php +++ b/src/Database/Conventions/DiscoveredConventions.php @@ -18,8 +18,7 @@ */ class DiscoveredConventions implements Conventions { - /** @var IStructure */ - protected $structure; + protected IStructure $structure; public function __construct(IStructure $structure) @@ -28,7 +27,7 @@ public function __construct(IStructure $structure) } - public function getPrimary(string $table) + public function getPrimary(string $table): string|array|null { return $this->structure->getPrimaryKey($table); } diff --git a/src/Database/Conventions/StaticConventions.php b/src/Database/Conventions/StaticConventions.php index 2581039b6..0a562ed06 100644 --- a/src/Database/Conventions/StaticConventions.php +++ b/src/Database/Conventions/StaticConventions.php @@ -20,14 +20,11 @@ class StaticConventions implements Conventions { use Nette\SmartObject; - /** @var string */ - protected $primary; + protected string $primary; - /** @var string */ - protected $foreign; + protected string $foreign; - /** @var string */ - protected $table; + protected string $table; /** diff --git a/src/Database/DateTime.php b/src/Database/DateTime.php new file mode 100644 index 000000000..f3c60bbdc --- /dev/null +++ b/src/Database/DateTime.php @@ -0,0 +1,50 @@ +setTimezone(new \DateTimeZone(date_default_timezone_get())) + ->format('Y-m-d H:i:s.u'); + } + + parent::__construct($time); + } + + + /** + * Returns JSON representation in ISO 8601 (used by JavaScript). + */ + public function jsonSerialize(): string + { + return $this->format('c'); + } + + + /** + * Returns the date and time in the format 'Y-m-d H:i:s.u'. + */ + public function __toString(): string + { + return $this->format('Y-m-d H:i:s.u'); + } +} diff --git a/src/Database/Driver.php b/src/Database/Driver.php index 227c7ff62..b1d742afd 100644 --- a/src/Database/Driver.php +++ b/src/Database/Driver.php @@ -11,30 +11,44 @@ /** - * Supplemental PDO database driver. + * Supplemental database driver. */ interface Driver { public const - SUPPORT_SEQUENCE = 'sequence', - SUPPORT_SELECT_UNGROUPED_COLUMNS = 'ungrouped_cols', - SUPPORT_MULTI_INSERT_AS_SELECT = 'insert_as_select', - SUPPORT_MULTI_COLUMN_AS_OR_COND = 'multi_column_as_or', - SUPPORT_SUBSELECT = 'subselect', - SUPPORT_SCHEMA = 'schema'; + SupportSequence = 'sequence', + SupportSelectUngroupedColumns = 'ungrouped_cols', + SupportMultiInsertAsSelect = 'insert_as_select', + SupportMultiColumnAsOrCond = 'multi_column_as_or', + SupportSubselect = 'subselect', + SupportSchema = 'schema'; /** * Initializes connection. + * @throws ConnectionException */ - function initialize(Connection $connection, array $options): void; + function connect(string $dsn, ?string $user = null, ?string $password = null, ?array $options = null): void; + + function query(string $queryString, array $params): ResultDriver; + + function beginTransaction(): void; + + function commit(): void; + + function rollBack(): void; + + /** + * Returns the ID of the last inserted row or sequence value. + */ + function getInsertId(?string $sequence = null): string; /** - * Converts PDOException to DriverException or its descendant. + * Delimits string for use in SQL statement. */ - function convertException(\PDOException $e): DriverException; + function quote(string $string): string; /** - * Delimites identifier for use in a SQL statement. + * Delimits identifier for use in SQL statement. */ function delimite(string $name): string; @@ -55,40 +69,34 @@ function formatLike(string $value, int $pos): string; /** * Injects LIMIT/OFFSET to the SQL query. - * @param string $sql query that will be modified. */ function applyLimit(string &$sql, ?int $limit, ?int $offset): void; /********************* reflection ****************d*g**/ /** - * Returns list of tables as tuples [(string) name, (bool) view, [(string) fullName]] + * @return Reflection\Table[] */ function getTables(): array; /** * Returns metadata for all columns in a table. - * As tuples [(string) name, (string) table, (string) nativetype, (int) size, (bool) nullable, (mixed) default, (bool) autoincrement, (bool) primary, (array) vendor]] + * @return Reflection\Column[] */ function getColumns(string $table): array; /** * Returns metadata for all indexes in a table. - * As tuples [(string) name, (string[]) columns, (bool) unique, (bool) primary] + * @return Reflection\Index[] */ function getIndexes(string $table): array; /** * Returns metadata for all foreign keys in a table. - * As tuples [(string) name, (string) local, (string) table, (string) foreign] + * @return Reflection\ForeignKey[] */ function getForeignKeys(string $table): array; - /** - * Returns associative array of detected types (IStructure::FIELD_*) in result set. - */ - function getColumnTypes(\PDOStatement $statement): array; - /** * Cheks if driver supports specific property * @param string $item self::SUPPORT_* property diff --git a/src/Database/DriverException.php b/src/Database/DriverException.php index 82cbfa5ea..af9cee5b8 100644 --- a/src/Database/DriverException.php +++ b/src/Database/DriverException.php @@ -13,60 +13,39 @@ /** * Base class for all errors in the driver or SQL server. */ -class DriverException extends \PDOException +class DriverException extends \Exception { - /** @var string */ - public $queryString; + private int|string|null $driverCode = null; + private string|null $sqlState = null; - /** @var array */ - public $params; - - /** - * @return static - */ - public static function from(\PDOException $src) + public function __construct(string $message = '', $code = 0, ?\Throwable $previous = null) { - $e = new static($src->message, 0, $src); - $e->file = $src->file; - $e->line = $src->line; - if (!$src->errorInfo && preg_match('#SQLSTATE\[(.*?)\] \[(.*?)\] (.*)#A', $src->message, $m)) { - $m[2] = (int) $m[2]; - $e->errorInfo = array_slice($m, 1); - $e->code = $m[1]; - } else { - $e->errorInfo = $src->errorInfo; - $e->code = $src->code; - $e->code = $e->errorInfo[0] ?? $src->code; + parent::__construct($message, 0, $previous); + $this->code = $code; + if ($previous) { + $this->file = $previous->file; + $this->line = $previous->line; } - - return $e; - } - - - /** - * @return int|string|null Driver-specific error code - */ - public function getDriverCode() - { - return $this->errorInfo[1] ?? null; } - public function getSqlState(): ?string + /** @internal */ + public function setDriverCode(string $state, int|string $code): void { - return $this->errorInfo[0] ?? null; + $this->sqlState = $state; + $this->driverCode = $code; } - public function getQueryString(): ?string + public function getDriverCode(): int|string|null { - return $this->queryString; + return $this->driverCode; } - public function getParameters(): ?array + public function getSqlState(): ?string { - return $this->params; + return $this->sqlState; } } diff --git a/src/Database/Drivers/MsSqlDriver.php b/src/Database/Drivers/MsSqlDriver.php index 9e21e1964..13142cf8d 100644 --- a/src/Database/Drivers/MsSqlDriver.php +++ b/src/Database/Drivers/MsSqlDriver.php @@ -15,29 +15,8 @@ /** * Supplemental MS SQL database driver. */ -class MsSqlDriver implements Nette\Database\Driver +class MsSqlDriver extends PdoDriver { - use Nette\SmartObject; - - /** @var Nette\Database\Connection */ - private $connection; - - - public function initialize(Nette\Database\Connection $connection, array $options): void - { - $this->connection = $connection; - } - - - public function convertException(\PDOException $e): Nette\Database\DriverException - { - return Nette\Database\DriverException::from($e); - } - - - /********************* SQL ****************d*g**/ - - public function delimite(string $name): string { // @see https://msdn.microsoft.com/en-us/library/ms176027.aspx @@ -86,15 +65,10 @@ public function applyLimit(string &$sql, ?int $limit, ?int $offset): void public function getTables(): array { - $tables = []; - foreach ($this->connection->query('SELECT TABLE_SCHEMA, TABLE_NAME, TABLE_TYPE FROM INFORMATION_SCHEMA.TABLES') as $row) { - $tables[] = [ - 'name' => $row['TABLE_SCHEMA'] . '.' . $row['TABLE_NAME'], - 'view' => ($row['TABLE_TYPE'] ?? null) === 'VIEW', - ]; - } - - return $tables; + return $this->pdo->query('SELECT TABLE_SCHEMA, TABLE_NAME, TABLE_TYPE FROM INFORMATION_SCHEMA.TABLES')->fetchAll( + \PDO::FETCH_FUNC, + fn($schema, $name, $type) => new Nette\Database\Reflection\Table($schema . '.' . $name, $type === 'VIEW'), + ); } @@ -103,7 +77,7 @@ public function getColumns(string $table): array [$table_schema, $table_name] = explode('.', $table); $columns = []; - $query = " + $query = <<connection->quote($table_schema)} - AND TABLE_NAME = {$this->connection->quote($table_name)}"; - - foreach ($this->connection->query($query) as $row) { - $columns[] = [ - 'name' => $row['COLUMN_NAME'], - 'table' => $table, - 'nativetype' => strtoupper($row['DATA_TYPE']), - 'size' => $row['CHARACTER_MAXIMUM_LENGTH'] ?? ($row['NUMERIC_PRECISION'] ?? null), - 'unsigned' => false, - 'nullable' => $row['IS_NULLABLE'] === 'YES', - 'default' => $row['COLUMN_DEFAULT'], - 'autoincrement' => $row['DOMAIN_NAME'] === 'COUNTER', - 'primary' => $row['COLUMN_NAME'] === 'ID', - 'vendor' => (array) $row, - ]; + TABLE_SCHEMA = {$this->pdo->quote($table_schema)} + AND TABLE_NAME = {$this->pdo->quote($table_name)} + X; + + foreach ($this->pdo->query($query, \PDO::FETCH_ASSOC) as $row) { + $columns[] = new Nette\Database\Reflection\Column( + name: $row['COLUMN_NAME'], + table: $table, + nativeType: $row['DATA_TYPE'], + size: $row['CHARACTER_MAXIMUM_LENGTH'] ?? $row['NUMERIC_PRECISION'] ?? null, + nullable: $row['IS_NULLABLE'] === 'YES', + default: $row['COLUMN_DEFAULT'], + autoIncrement: $row['DOMAIN_NAME'] === 'COUNTER', + primary: $row['COLUMN_NAME'] === 'ID', + vendor: $row, + ); } return $columns; @@ -142,7 +116,7 @@ public function getIndexes(string $table): array [, $table_name] = explode('.', $table); $indexes = []; - $query = " + $query = <<connection->quote($table_name)} + t.name = {$this->pdo->quote($table_name)} ORDER BY - t.name, ind.name, ind.index_id, ic.index_column_id"; + t.name, ind.name, ind.index_id, ic.index_column_id + X; - foreach ($this->connection->query($query) as $row) { + foreach ($this->pdo->query($query) as $row) { $id = $row['name_index']; $indexes[$id]['name'] = $id; $indexes[$id]['unique'] = $row['is_unique'] !== 'False'; @@ -167,7 +142,7 @@ public function getIndexes(string $table): array $indexes[$id]['columns'][$row['id_column'] - 1] = $row['name_column']; } - return array_values($indexes); + return array_map(fn($data) => new Nette\Database\Reflection\Index(...$data), array_values($indexes)); } @@ -176,7 +151,7 @@ public function getForeignKeys(string $table): array [$table_schema, $table_name] = explode('.', $table); $keys = []; - $query = " + $query = <<connection->quote($table_name)}"; - - foreach ($this->connection->query($query) as $id => $row) { - $keys[$id]['name'] = $row['fk_name']; - $keys[$id]['local'] = $row['column']; - $keys[$id]['table'] = $table_schema . '.' . $row['referenced_table']; - $keys[$id]['foreign'] = $row['referenced_column']; + tab1.name = {$this->pdo->quote($table_name)} + X; + + foreach ($this->pdo->query($query) as $row) { + $id = $row['fk_name']; + $keys[$id]['name'] = $id; + $keys[$id]['columns'][] = $row['column']; + $keys[$id]['targetTable'] = $table_schema . '.' . $row['referenced_table']; + $keys[$id]['targetColumns'][] = $row['referenced_column']; } - return array_values($keys); + return array_map(fn($data) => new Nette\Database\Reflection\ForeignKey(...$data), array_values($keys)); } @@ -218,6 +195,6 @@ public function getColumnTypes(\PDOStatement $statement): array public function isSupported(string $item): bool { - return $item === self::SUPPORT_SUBSELECT; + return $item === self::SupportSubselect; } } diff --git a/src/Database/Drivers/MySqlDriver.php b/src/Database/Drivers/MySqlDriver.php index 84269d989..2615b24bc 100644 --- a/src/Database/Drivers/MySqlDriver.php +++ b/src/Database/Drivers/MySqlDriver.php @@ -15,56 +15,57 @@ /** * Supplemental MySQL database driver. */ -class MySqlDriver implements Nette\Database\Driver +class MySqlDriver extends PdoDriver { - use Nette\SmartObject; - public const ERROR_ACCESS_DENIED = 1045, ERROR_DUPLICATE_ENTRY = 1062, ERROR_DATA_TRUNCATED = 1265; - /** @var Nette\Database\Connection */ - private $connection; - /** * Driver options: * - charset => character encoding to set (default is utf8 or utf8mb4 since MySQL 5.5.3) * - sqlmode => see http://dev.mysql.com/doc/refman/5.0/en/server-sql-mode.html */ - public function initialize(Nette\Database\Connection $connection, array $options): void + public function connect( + string $dsn, + ?string $user = null, + #[\SensitiveParameter] + ?string $password = null, + ?array $options = null, + ): void { - $this->connection = $connection; + parent::connect($dsn, $user, $password, $options); $charset = $options['charset'] - ?? (version_compare($connection->getPdo()->getAttribute(\PDO::ATTR_SERVER_VERSION), '5.5.3', '>=') ? 'utf8mb4' : 'utf8'); + ?? (version_compare($this->pdo->getAttribute(\PDO::ATTR_SERVER_VERSION), '5.5.3', '>=') ? 'utf8mb4' : 'utf8'); if ($charset) { - $connection->query('SET NAMES ?', $charset); + $this->pdo->query('SET NAMES ' . $this->pdo->quote($charset)); } if (isset($options['sqlmode'])) { - $connection->query('SET sql_mode=?', $options['sqlmode']); + $this->pdo->query('SET sql_mode=' . $this->pdo->quote($options['sqlmode'])); } } - public function convertException(\PDOException $e): Nette\Database\DriverException + public function detectExceptionClass(\PDOException $e): ?string { $code = $e->errorInfo[1] ?? null; if (in_array($code, [1216, 1217, 1451, 1452, 1701], true)) { - return Nette\Database\ForeignKeyConstraintViolationException::from($e); + return Nette\Database\ForeignKeyConstraintViolationException::class; } elseif (in_array($code, [1062, 1557, 1569, 1586], true)) { - return Nette\Database\UniqueConstraintViolationException::from($e); + return Nette\Database\UniqueConstraintViolationException::class; } elseif ($code >= 2001 && $code <= 2028) { - return Nette\Database\ConnectionException::from($e); + return Nette\Database\ConnectionException::class; } elseif (in_array($code, [1048, 1121, 1138, 1171, 1252, 1263, 1566], true)) { - return Nette\Database\NotNullConstraintViolationException::from($e); + return Nette\Database\NotNullConstraintViolationException::class; } else { - return Nette\Database\DriverException::from($e); + return null; } } @@ -94,7 +95,7 @@ public function formatDateInterval(\DateInterval $value): string public function formatLike(string $value, int $pos): string { $value = str_replace('\\', '\\\\', $value); - $value = addcslashes(substr($this->connection->quote($value), 1, -1), '%_'); + $value = addcslashes(substr($this->pdo->quote($value), 1, -1), '%_'); return ($pos <= 0 ? "'%" : "'") . $value . ($pos >= 0 ? "%'" : "'"); } @@ -117,35 +118,29 @@ public function applyLimit(string &$sql, ?int $limit, ?int $offset): void public function getTables(): array { - $tables = []; - foreach ($this->connection->query('SHOW FULL TABLES') as $row) { - $tables[] = [ - 'name' => $row[0], - 'view' => ($row[1] ?? null) === 'VIEW', - ]; - } - - return $tables; + return $this->pdo->query('SHOW FULL TABLES')->fetchAll( + \PDO::FETCH_FUNC, + fn($name, $type) => new Nette\Database\Reflection\Table($name, $type === 'VIEW'), + ); } public function getColumns(string $table): array { $columns = []; - foreach ($this->connection->query('SHOW FULL COLUMNS FROM ' . $this->delimite($table)) as $row) { - $row = array_change_key_case((array) $row, CASE_LOWER); - $type = explode('(', $row['type']); - $columns[] = [ - 'name' => $row['field'], - 'table' => $table, - 'nativetype' => strtoupper($type[0]), - 'size' => isset($type[1]) ? (int) $type[1] : null, - 'nullable' => $row['null'] === 'YES', - 'default' => $row['default'], - 'autoincrement' => $row['extra'] === 'auto_increment', - 'primary' => $row['key'] === 'PRI', - 'vendor' => (array) $row, - ]; + foreach ($this->pdo->query('SHOW FULL COLUMNS FROM ' . $this->delimite($table), \PDO::FETCH_ASSOC) as $row) { + $type = explode('(', $row['Type']); + $columns[] = new Nette\Database\Reflection\Column( + name: $row['Field'], + table: $table, + nativeType: $type[0], + size: isset($type[1]) ? (int) $type[1] : null, + nullable: $row['Null'] === 'YES', + default: $row['Default'], + autoIncrement: $row['Extra'] === 'auto_increment', + primary: $row['Key'] === 'PRI', + vendor: $row, + ); } return $columns; @@ -155,7 +150,7 @@ public function getColumns(string $table): array public function getIndexes(string $table): array { $indexes = []; - foreach ($this->connection->query('SHOW INDEX FROM ' . $this->delimite($table)) as $row) { + foreach ($this->pdo->query('SHOW INDEX FROM ' . $this->delimite($table)) as $row) { $id = $row['Key_name']; $indexes[$id]['name'] = $id; $indexes[$id]['unique'] = !$row['Non_unique']; @@ -163,24 +158,28 @@ public function getIndexes(string $table): array $indexes[$id]['columns'][$row['Seq_in_index'] - 1] = $row['Column_name']; } - return array_values($indexes); + return array_map(fn($data) => new Nette\Database\Reflection\Index(...$data), array_values($indexes)); } public function getForeignKeys(string $table): array { $keys = []; - $query = 'SELECT CONSTRAINT_NAME, COLUMN_NAME, REFERENCED_TABLE_NAME, REFERENCED_COLUMN_NAME FROM information_schema.KEY_COLUMN_USAGE ' - . 'WHERE TABLE_SCHEMA = DATABASE() AND REFERENCED_TABLE_NAME IS NOT NULL AND TABLE_NAME = ' . $this->connection->quote($table); - - foreach ($this->connection->query($query) as $id => $row) { - $keys[$id]['name'] = $row['CONSTRAINT_NAME']; - $keys[$id]['local'] = $row['COLUMN_NAME']; - $keys[$id]['table'] = $row['REFERENCED_TABLE_NAME']; - $keys[$id]['foreign'] = $row['REFERENCED_COLUMN_NAME']; + foreach ($this->pdo->query(<<pdo->quote($table)} + X) as $row) { + $id = $row['CONSTRAINT_NAME']; + $keys[$id]['name'] = $id; + $keys[$id]['columns'][] = $row['COLUMN_NAME']; + $keys[$id]['targetTable'] = $row['REFERENCED_TABLE_NAME']; + $keys[$id]['targetColumns'][] = $row['REFERENCED_COLUMN_NAME']; } - return array_values($keys); + return array_map(fn($data) => new Nette\Database\Reflection\ForeignKey(...$data), array_values($keys)); } @@ -208,6 +207,6 @@ public function isSupported(string $item): bool // - http://bugs.mysql.com/bug.php?id=31188 // - http://bugs.mysql.com/bug.php?id=35819 // and more. - return $item === self::SUPPORT_SELECT_UNGROUPED_COLUMNS || $item === self::SUPPORT_MULTI_COLUMN_AS_OR_COND; + return $item === self::SupportSelectUngroupedColumns || $item === self::SupportMultiColumnAsOrCond; } } diff --git a/src/Database/Drivers/OciDriver.php b/src/Database/Drivers/OciDriver.php index e18308d02..4160eae41 100644 --- a/src/Database/Drivers/OciDriver.php +++ b/src/Database/Drivers/OciDriver.php @@ -15,38 +15,39 @@ /** * Supplemental Oracle database driver. */ -class OciDriver implements Nette\Database\Driver +class OciDriver extends PdoDriver { - use Nette\SmartObject; + /** Datetime format */ + private string $fmtDateTime; - /** @var Nette\Database\Connection */ - private $connection; - /** @var string Datetime format */ - private $fmtDateTime; - - - public function initialize(Nette\Database\Connection $connection, array $options): void + public function connect( + string $dsn, + ?string $user = null, + #[\SensitiveParameter] + ?string $password = null, + ?array $options = null, + ): void { - $this->connection = $connection; + parent::connect($dsn, $user, $password, $options); $this->fmtDateTime = $options['formatDateTime'] ?? 'U'; } - public function convertException(\PDOException $e): Nette\Database\DriverException + public function detectExceptionClass(\PDOException $e): ?string { $code = $e->errorInfo[1] ?? null; if (in_array($code, [1, 2299, 38911], true)) { - return Nette\Database\UniqueConstraintViolationException::from($e); + return Nette\Database\UniqueConstraintViolationException::class; } elseif (in_array($code, [1400], true)) { - return Nette\Database\NotNullConstraintViolationException::from($e); + return Nette\Database\NotNullConstraintViolationException::class; } elseif (in_array($code, [2266, 2291, 2292], true)) { - return Nette\Database\ForeignKeyConstraintViolationException::from($e); + return Nette\Database\ForeignKeyConstraintViolationException::class; } else { - return Nette\Database\DriverException::from($e); + return null; } } @@ -102,7 +103,7 @@ public function applyLimit(string &$sql, ?int $limit, ?int $offset): void public function getTables(): array { $tables = []; - foreach ($this->connection->query('SELECT * FROM cat') as $row) { + foreach ($this->pdo->query('SELECT * FROM cat') as $row) { if ($row[1] === 'TABLE' || $row[1] === 'VIEW') { $tables[] = [ 'name' => $row[0], @@ -141,6 +142,6 @@ public function getColumnTypes(\PDOStatement $statement): array public function isSupported(string $item): bool { - return $item === self::SUPPORT_SEQUENCE || $item === self::SUPPORT_SUBSELECT; + return $item === self::SupportSequence || $item === self::SupportSubselect; } } diff --git a/src/Database/Drivers/OdbcDriver.php b/src/Database/Drivers/OdbcDriver.php index c80a80764..050dffe44 100644 --- a/src/Database/Drivers/OdbcDriver.php +++ b/src/Database/Drivers/OdbcDriver.php @@ -15,24 +15,8 @@ /** * Supplemental ODBC database driver. */ -class OdbcDriver implements Nette\Database\Driver +class OdbcDriver extends PdoDriver { - use Nette\SmartObject; - - public function initialize(Nette\Database\Connection $connection, array $options): void - { - } - - - public function convertException(\PDOException $e): Nette\Database\DriverException - { - return Nette\Database\DriverException::from($e); - } - - - /********************* SQL ****************d*g**/ - - public function delimite(string $name): string { return '[' . str_replace(['[', ']'], ['[[', ']]'], $name) . ']'; @@ -110,6 +94,6 @@ public function getColumnTypes(\PDOStatement $statement): array public function isSupported(string $item): bool { - return $item === self::SUPPORT_SUBSELECT; + return $item === self::SupportSubselect; } } diff --git a/src/Database/Drivers/PdoDriver.php b/src/Database/Drivers/PdoDriver.php new file mode 100644 index 000000000..eb0c8e301 --- /dev/null +++ b/src/Database/Drivers/PdoDriver.php @@ -0,0 +1,147 @@ +pdo = new PDO($dsn, $user, $password, $options); + $this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + } catch (PDOException $e) { + throw $this->convertException($e, Nette\Database\ConnectionException::class); + } + } + + + public function getPdo(): ?PDO + { + return $this->pdo; + } + + + public function query(string $queryString, array $params): PdoResultDriver + { + try { + $types = ['boolean' => PDO::PARAM_BOOL, 'integer' => PDO::PARAM_INT, 'resource' => PDO::PARAM_LOB, 'NULL' => PDO::PARAM_NULL]; + + $statement = $this->pdo->prepare($queryString); + foreach ($params as $key => $value) { + $type = gettype($value); + $statement->bindValue(is_int($key) ? $key + 1 : $key, $value, $types[$type] ?? PDO::PARAM_STR); + } + + $statement->setFetchMode(PDO::FETCH_ASSOC); + @$statement->execute(); // @ PHP generates warning when ATTR_ERRMODE = ERRMODE_EXCEPTION bug #73878 + return new PdoResultDriver($statement, $this); + + } catch (PDOException $e) { + $e = $this->convertException($e, Nette\Database\QueryException::class); + $e->setQueryInfo($queryString, $params); + throw $e; + } + } + + + public function beginTransaction(): void + { + try { + $this->pdo->beginTransaction(); + } catch (PDOException $e) { + throw $this->convertException($e); + } + } + + + public function commit(): void + { + try { + $this->pdo->commit(); + } catch (PDOException $e) { + throw $this->convertException($e); + } + } + + + public function rollBack(): void + { + try { + $this->pdo->rollBack(); + } catch (PDOException $e) { + throw $this->convertException($e); + } + } + + + public function getInsertId(?string $sequence = null): string + { + try { + $res = $this->pdo->lastInsertId($sequence); + return $res === false ? '0' : $res; + } catch (PDOException $e) { + throw $this->convertException($e); + } + } + + + public function quote(string $string, int $type = PDO::PARAM_STR): string + { + try { + return $this->pdo->quote($string, $type); + } catch (PDOException $e) { + throw $this->convertException($e); + } + } + + + public function convertException(\PDOException $src, ?string $class = null): DriverException + { + if ($src->errorInfo) { + [$sqlState, $driverCode] = $src->errorInfo; + } elseif (preg_match('#SQLSTATE\[(.*?)\] \[(.*?)\] (.*)#A', $src->getMessage(), $m)) { + [, $sqlState, $driverCode] = $m; + } + + $class = $this->detectExceptionClass($src) ?? $class ?? DriverException::class; + $e = new $class($src->getMessage(), $sqlState ?? $src->getCode(), $src); + if (isset($sqlState)) { + $e->setDriverCode($sqlState, (int) $driverCode); + } + + return $e; + } + + + public function detectExceptionClass(\PDOException $e): ?string + { + return null; + } +} diff --git a/src/Database/Drivers/PdoResultDriver.php b/src/Database/Drivers/PdoResultDriver.php new file mode 100644 index 000000000..27d348340 --- /dev/null +++ b/src/Database/Drivers/PdoResultDriver.php @@ -0,0 +1,74 @@ +result = $result; + $this->driver = $driver; + } + + + public function fetch(): ?array + { + $data = $this->result->fetch(); + if (!$data) { + $this->result->closeCursor(); + return null; + } + + return $data; + } + + + public function getColumnCount(): int + { + return $this->result->columnCount(); + } + + + public function getRowCount(): int + { + return $this->result->rowCount(); + } + + + public function getColumnTypes(): array + { + return $this->driver->getColumnTypes($this->result); + } + + + public function getColumnMeta(int $col): array + { + return $this->result->getColumnMeta($col); + } + + + public function getPdoStatement(): \PDOStatement + { + return $this->result; + } +} diff --git a/src/Database/Drivers/PgSqlDriver.php b/src/Database/Drivers/PgSqlDriver.php index e16871555..79596f066 100644 --- a/src/Database/Drivers/PgSqlDriver.php +++ b/src/Database/Drivers/PgSqlDriver.php @@ -15,40 +15,28 @@ /** * Supplemental PostgreSQL database driver. */ -class PgSqlDriver implements Nette\Database\Driver +class PgSqlDriver extends PdoDriver { - use Nette\SmartObject; - - /** @var Nette\Database\Connection */ - private $connection; - - - public function initialize(Nette\Database\Connection $connection, array $options): void - { - $this->connection = $connection; - } - - - public function convertException(\PDOException $e): Nette\Database\DriverException + public function detectExceptionClass(\PDOException $e): ?string { $code = $e->errorInfo[0] ?? null; - if ($code === '0A000' && strpos($e->getMessage(), 'truncate') !== false) { - return Nette\Database\ForeignKeyConstraintViolationException::from($e); + if ($code === '0A000' && str_contains($e->getMessage(), 'truncate')) { + return Nette\Database\ForeignKeyConstraintViolationException::class; } elseif ($code === '23502') { - return Nette\Database\NotNullConstraintViolationException::from($e); + return Nette\Database\NotNullConstraintViolationException::class; } elseif ($code === '23503') { - return Nette\Database\ForeignKeyConstraintViolationException::from($e); + return Nette\Database\ForeignKeyConstraintViolationException::class; } elseif ($code === '23505') { - return Nette\Database\UniqueConstraintViolationException::from($e); + return Nette\Database\UniqueConstraintViolationException::class; } elseif ($code === '08006') { - return Nette\Database\ConnectionException::from($e); + return Nette\Database\ConnectionException::class; } else { - return Nette\Database\DriverException::from($e); + return null; } } @@ -77,8 +65,8 @@ public function formatDateInterval(\DateInterval $value): string public function formatLike(string $value, int $pos): string { - $bs = substr($this->connection->quote('\\'), 1, -1); // standard_conforming_strings = on/off - $value = substr($this->connection->quote($value), 1, -1); + $bs = substr($this->pdo->quote('\\'), 1, -1); // standard_conforming_strings = on/off + $value = substr($this->pdo->quote($value), 1, -1); $value = strtr($value, ['%' => $bs . '%', '_' => $bs . '_', '\\' => '\\\\']); return ($pos <= 0 ? "'%" : "'") . $value . ($pos >= 0 ? "%'" : "'"); } @@ -105,12 +93,11 @@ public function applyLimit(string &$sql, ?int $limit, ?int $offset): void public function getTables(): array { - $tables = []; - foreach ($this->connection->query(" + return $this->pdo->query(<<<'X' SELECT DISTINCT ON (c.relname) - c.relname::varchar AS name, - c.relkind IN ('v', 'm') AS view, - n.nspname::varchar || '.' || c.relname::varchar AS \"fullName\" + c.relname::varchar, + c.relkind IN ('v', 'm'), + n.nspname::varchar || '.' || c.relname::varchar FROM pg_catalog.pg_class AS c JOIN pg_catalog.pg_namespace AS n ON n.oid = c.relnamespace @@ -119,28 +106,27 @@ public function getTables(): array AND n.nspname = ANY (pg_catalog.current_schemas(FALSE)) ORDER BY c.relname - ") as $row) { - $tables[] = (array) $row; - } - - return $tables; + X)->fetchAll( + \PDO::FETCH_FUNC, + fn($name, $view, $full) => new Nette\Database\Reflection\Table($name, $view, $full), + ); } public function getColumns(string $table): array { $columns = []; - foreach ($this->connection->query(" + foreach ($this->pdo->query(<<connection->quote($this->delimiteFQN($table))}::regclass + AND c.oid = {$this->pdo->quote($this->delimiteFQN($table))}::regclass AND a.attnum > 0 AND NOT a.attisdropped ORDER BY a.attnum - ") as $row) { - $column = (array) $row; - $column['vendor'] = $column; - unset($column['sequence']); - - $columns[] = $column; + X, \PDO::FETCH_ASSOC) as $row + ) { + $row['vendor'] = $row; + unset($row['sequence']); + $columns[] = new Nette\Database\Reflection\Column(...$row); } return $columns; @@ -171,7 +156,7 @@ public function getColumns(string $table): array public function getIndexes(string $table): array { $indexes = []; - foreach ($this->connection->query(" + foreach ($this->pdo->query(<<connection->quote($this->delimiteFQN($table))}::regclass - ") as $row) { + AND c1.oid = {$this->pdo->quote($this->delimiteFQN($table))}::regclass + X) as $row) { $id = $row['name']; $indexes[$id]['name'] = $id; $indexes[$id]['unique'] = $row['unique']; @@ -193,14 +178,15 @@ public function getIndexes(string $table): array $indexes[$id]['columns'][] = $row['column']; } - return array_values($indexes); + return array_map(fn($data) => new Nette\Database\Reflection\Index(...$data), array_values($indexes)); } public function getForeignKeys(string $table): array { - /* Does't work with multicolumn foreign keys */ - return $this->connection->query(" + /* Doesn't work with multi-column foreign keys */ + $keys = []; + foreach ($this->pdo->query(<<connection->quote($this->delimiteFQN($table))}::regclass + AND cl.oid = {$this->pdo->quote($this->delimiteFQN($table))}::regclass AND nf.nspname = ANY (pg_catalog.current_schemas(FALSE)) - ")->fetchAll(); + X) as $row) { + $id = $row['name']; + $keys[$id]['name'] = $id; + $keys[$id]['columns'][] = $row['local']; + $keys[$id]['targetTable'] = $row['table']; + $keys[$id]['targetColumns'][] = $row['foreign']; + } + + return array_map(fn($data) => new Nette\Database\Reflection\ForeignKey(...$data), array_values($keys)); } @@ -235,7 +229,7 @@ public function getColumnTypes(\PDOStatement $statement): array public function isSupported(string $item): bool { - return $item === self::SUPPORT_SEQUENCE || $item === self::SUPPORT_SUBSELECT || $item === self::SUPPORT_SCHEMA; + return $item === self::SupportSequence || $item === self::SupportSubselect || $item === self::SupportSchema; } diff --git a/src/Database/Drivers/SqliteDriver.php b/src/Database/Drivers/SqliteDriver.php index 1c4d0e2e2..7469b3c47 100644 --- a/src/Database/Drivers/SqliteDriver.php +++ b/src/Database/Drivers/SqliteDriver.php @@ -15,52 +15,53 @@ /** * Supplemental SQLite3 database driver. */ -class SqliteDriver implements Nette\Database\Driver +class SqliteDriver extends PdoDriver { - use Nette\SmartObject; + /** Datetime format */ + private string $fmtDateTime; - /** @var Nette\Database\Connection */ - private $connection; - /** @var string Datetime format */ - private $fmtDateTime; - - - public function initialize(Nette\Database\Connection $connection, array $options): void + public function connect( + string $dsn, + ?string $user = null, + #[\SensitiveParameter] + ?string $password = null, + ?array $options = null, + ): void { - $this->connection = $connection; + parent::connect($dsn, $user, $password, $options); $this->fmtDateTime = $options['formatDateTime'] ?? 'U'; } - public function convertException(\PDOException $e): Nette\Database\DriverException + public function detectExceptionClass(\PDOException $e): ?string { $code = $e->errorInfo[1] ?? null; $msg = $e->getMessage(); if ($code !== 19) { - return Nette\Database\DriverException::from($e); + return null; } elseif ( - strpos($msg, 'must be unique') !== false - || strpos($msg, 'is not unique') !== false - || strpos($msg, 'UNIQUE constraint failed') !== false + str_contains($msg, 'must be unique') + || str_contains($msg, 'is not unique') + || str_contains($msg, 'UNIQUE constraint failed') ) { - return Nette\Database\UniqueConstraintViolationException::from($e); + return Nette\Database\UniqueConstraintViolationException::class; } elseif ( - strpos($msg, 'may not be null') !== false - || strpos($msg, 'NOT NULL constraint failed') !== false + str_contains($msg, 'may not be null') + || str_contains($msg, 'NOT NULL constraint failed') ) { - return Nette\Database\NotNullConstraintViolationException::from($e); + return Nette\Database\NotNullConstraintViolationException::class; } elseif ( - strpos($msg, 'foreign key constraint failed') !== false - || strpos($msg, 'FOREIGN KEY constraint failed') !== false + str_contains($msg, 'foreign key constraint failed') + || str_contains($msg, 'FOREIGN KEY constraint failed') ) { - return Nette\Database\ForeignKeyConstraintViolationException::from($e); + return Nette\Database\ForeignKeyConstraintViolationException::class; } else { - return Nette\Database\ConstraintViolationException::from($e); + return Nette\Database\ConstraintViolationException::class; } } @@ -88,7 +89,7 @@ public function formatDateInterval(\DateInterval $value): string public function formatLike(string $value, int $pos): string { - $value = addcslashes(substr($this->connection->quote($value), 1, -1), '%_\\'); + $value = addcslashes(substr($this->pdo->quote($value), 1, -1), '%_\\'); return ($pos <= 0 ? "'%" : "'") . $value . ($pos >= 0 ? "%'" : "'") . " ESCAPE '\\'"; } @@ -110,47 +111,50 @@ public function applyLimit(string &$sql, ?int $limit, ?int $offset): void public function getTables(): array { - $tables = []; - foreach ($this->connection->query(" - SELECT name, type = 'view' as view FROM sqlite_master WHERE type IN ('table', 'view') AND name NOT LIKE 'sqlite_%' + return $this->pdo->query(<<<'X' + SELECT name, type = 'view' + FROM sqlite_master + WHERE type IN ('table', 'view') AND name NOT LIKE 'sqlite_%' UNION ALL - SELECT name, type = 'view' as view FROM sqlite_temp_master WHERE type IN ('table', 'view') AND name NOT LIKE 'sqlite_%' + SELECT name, type = 'view' as view + FROM sqlite_temp_master + WHERE type IN ('table', 'view') AND name NOT LIKE 'sqlite_%' ORDER BY name - ") as $row) { - $tables[] = [ - 'name' => $row->name, - 'view' => (bool) $row->view, - ]; - } - - return $tables; + X)->fetchAll( + \PDO::FETCH_FUNC, + fn($name, $view) => new Nette\Database\Reflection\Table($name, (bool) $view), + ); } public function getColumns(string $table): array { - $meta = $this->connection->query(" - SELECT sql FROM sqlite_master WHERE type = 'table' AND name = {$this->connection->quote($table)} + $meta = $this->pdo->query(<<pdo->quote($table)} UNION ALL - SELECT sql FROM sqlite_temp_master WHERE type = 'table' AND name = {$this->connection->quote($table)} - ")->fetch(); + SELECT sql + FROM sqlite_temp_master + WHERE type = 'table' AND name = {$this->pdo->quote($table)} + X)->fetch(); $columns = []; - foreach ($this->connection->query("PRAGMA table_info({$this->delimite($table)})") as $row) { + foreach ($this->pdo->query("PRAGMA table_info({$this->delimite($table)})", \PDO::FETCH_ASSOC) as $row) { $column = $row['name']; $pattern = "/(\"$column\"|`$column`|\\[$column\\]|$column)\\s+[^,]+\\s+PRIMARY\\s+KEY\\s+AUTOINCREMENT/Ui"; $type = explode('(', $row['type']); - $columns[] = [ - 'name' => $column, - 'table' => $table, - 'nativetype' => strtoupper($type[0]), - 'size' => isset($type[1]) ? (int) $type[1] : null, - 'nullable' => $row['notnull'] === 0, - 'default' => $row['dflt_value'], - 'autoincrement' => $meta && preg_match($pattern, (string) $meta['sql']), - 'primary' => $row['pk'] > 0, - 'vendor' => (array) $row, - ]; + $columns[] = new Nette\Database\Reflection\Column( + name: $column, + table: $table, + nativeType: $type[0], + size: isset($type[1]) ? (int) $type[1] : null, + nullable: !$row['notnull'], + default: $row['dflt_value'], + autoIncrement: $meta && preg_match($pattern, (string) $meta['sql']), + primary: $row['pk'] > 0, + vendor: $row, + ); } return $columns; @@ -160,7 +164,7 @@ public function getColumns(string $table): array public function getIndexes(string $table): array { $indexes = []; - foreach ($this->connection->query("PRAGMA index_list({$this->delimite($table)})") as $row) { + foreach ($this->pdo->query("PRAGMA index_list({$this->delimite($table)})") as $row) { $id = $row['name']; $indexes[$id]['name'] = $id; $indexes[$id]['unique'] = (bool) $row['unique']; @@ -168,8 +172,7 @@ public function getIndexes(string $table): array } foreach ($indexes as $index => $values) { - $res = $this->connection->query("PRAGMA index_info({$this->delimite($index)})"); - while ($row = $res->fetch()) { + foreach ($this->pdo->query("PRAGMA index_info({$this->delimite($index)})") as $row) { $indexes[$index]['columns'][] = $row['name']; } } @@ -178,8 +181,8 @@ public function getIndexes(string $table): array foreach ($indexes as $index => $values) { $column = $indexes[$index]['columns'][0]; foreach ($columns as $info) { - if ($column === $info['name']) { - $indexes[$index]['primary'] = (bool) $info['primary']; + if ($column === $info->name) { + $indexes[$index]['primary'] = $info->primary; break; } } @@ -199,22 +202,25 @@ public function getIndexes(string $table): array } } - return array_values($indexes); + return array_map(fn($data) => new Nette\Database\Reflection\Index(...$data), array_values($indexes)); } public function getForeignKeys(string $table): array { $keys = []; - foreach ($this->connection->query("PRAGMA foreign_key_list({$this->delimite($table)})") as $row) { + foreach ($this->pdo->query("PRAGMA foreign_key_list({$this->delimite($table)})") as $row) { $id = $row['id']; - $keys[$id]['name'] = $id; - $keys[$id]['local'] = $row['from']; - $keys[$id]['table'] = $row['table']; - $keys[$id]['foreign'] = $row['to']; + $keys[$id]['name'] = (string) $id; + $keys[$id]['columns'][] = $row['from']; + $keys[$id]['targetTable'] = $row['table']; + $keys[$id]['targetColumns'][] = $row['to']; + if ($keys[$id]['targetColumns'][0] == null) { + $keys[$id]['targetColumns'] = []; + } } - return array_values($keys); + return array_map(fn($data) => new Nette\Database\Reflection\ForeignKey(...$data), array_values($keys)); } @@ -239,6 +245,6 @@ public function getColumnTypes(\PDOStatement $statement): array public function isSupported(string $item): bool { - return $item === self::SUPPORT_MULTI_INSERT_AS_SELECT || $item === self::SUPPORT_SUBSELECT || $item === self::SUPPORT_MULTI_COLUMN_AS_OR_COND; + return $item === self::SupportMultiInsertAsSelect || $item === self::SupportSubselect || $item === self::SupportMultiColumnAsOrCond; } } diff --git a/src/Database/Drivers/SqlsrvDriver.php b/src/Database/Drivers/SqlsrvDriver.php index 63b5c4bde..19bc68c45 100644 --- a/src/Database/Drivers/SqlsrvDriver.php +++ b/src/Database/Drivers/SqlsrvDriver.php @@ -15,27 +15,21 @@ /** * Supplemental SQL Server 2005 and later database driver. */ -class SqlsrvDriver implements Nette\Database\Driver +class SqlsrvDriver extends PdoDriver { - use Nette\SmartObject; + private string $version; - /** @var Nette\Database\Connection */ - private $connection; - /** @var string */ - private $version; - - - public function initialize(Nette\Database\Connection $connection, array $options): void + public function connect( + string $dsn, + ?string $user = null, + #[\SensitiveParameter] + ?string $password = null, + ?array $options = null, + ): void { - $this->connection = $connection; - $this->version = $connection->getPdo()->getAttribute(\PDO::ATTR_SERVER_VERSION); - } - - - public function convertException(\PDOException $e): Nette\Database\DriverException - { - return Nette\Database\DriverException::from($e); + parent::connect($dsn, $user, $password, $options); + $this->version = $this->pdo->getAttribute(\PDO::ATTR_SERVER_VERSION); } @@ -98,41 +92,36 @@ public function applyLimit(string &$sql, ?int $limit, ?int $offset): void public function getTables(): array { - $tables = []; - foreach ($this->connection->query(" + return $this->pdo->query(<<<'X' SELECT name, CASE type WHEN 'U' THEN 0 WHEN 'V' THEN 1 - END AS [view] + END FROM sys.objects WHERE type IN ('U', 'V') - ") as $row) { - $tables[] = [ - 'name' => $row->name, - 'view' => (bool) $row->view, - ]; - } - - return $tables; + X)->fetchAll( + \PDO::FETCH_FUNC, + fn($name, $view) => new Nette\Database\Reflection\Table($name, (bool) $view), + ); } public function getColumns(string $table): array { $columns = []; - foreach ($this->connection->query(" + foreach ($this->pdo->query(<<connection->quote($table)} - ") as $row) { - $row = (array) $row; + AND o.name = {$this->pdo->quote($table)} + X, \PDO::FETCH_ASSOC) as $row) { $row['vendor'] = $row; $row['nullable'] = (bool) $row['nullable']; - $row['autoincrement'] = (bool) $row['autoincrement']; + $row['autoIncrement'] = (bool) $row['autoIncrement']; $row['primary'] = (bool) $row['primary']; - $columns[] = $row; + $columns[] = new Nette\Database\Reflection\Column(...$row); } return $columns; @@ -163,7 +151,7 @@ public function getColumns(string $table): array public function getIndexes(string $table): array { $indexes = []; - foreach ($this->connection->query(" + foreach ($this->pdo->query(<<connection->quote($table)} + t.name = {$this->pdo->quote($table)} ORDER BY i.index_id, ic.index_column_id - ") as $row) { + X) as $row) { $id = $row['name']; $indexes[$id]['name'] = $id; $indexes[$id]['unique'] = (bool) $row['unique']; @@ -190,7 +178,7 @@ public function getIndexes(string $table): array $indexes[$id]['columns'][] = $row['column']; } - return array_values($indexes); + return array_map(fn($data) => new Nette\Database\Reflection\Index(...$data), array_values($indexes)); } @@ -198,7 +186,7 @@ public function getForeignKeys(string $table): array { // Does't work with multicolumn foreign keys $keys = []; - foreach ($this->connection->query(" + foreach ($this->pdo->query(<<connection->quote($table)} - ") as $row) { - $keys[$row->name] = (array) $row; + tl.name = {$this->pdo->quote($table)} + X, \PDO::FETCH_ASSOC) as $row) { + $id = $row['name']; + $keys[$id]['name'] = $id; + $keys[$id]['columns'][] = $row['local']; + $keys[$id]['targetTable'] = $row['table']; + $keys[$id]['targetColumns'][] = $row['column']; } - return array_values($keys); + return array_map(fn($data) => new Nette\Database\Reflection\ForeignKey(...$data), array_values($keys)); } @@ -243,6 +235,6 @@ public function getColumnTypes(\PDOStatement $statement): array public function isSupported(string $item): bool { - return $item === self::SUPPORT_SUBSELECT; + return $item === self::SupportSubselect; } } diff --git a/src/Database/Explorer.php b/src/Database/Explorer.php index 0f8bdb530..fb3f332b7 100644 --- a/src/Database/Explorer.php +++ b/src/Database/Explorer.php @@ -9,6 +9,7 @@ namespace Nette\Database; +use JetBrains\PhpStorm\Language; use Nette; use Nette\Database\Conventions\StaticConventions; @@ -20,24 +21,20 @@ class Explorer { use Nette\SmartObject; - /** @var Connection */ - private $connection; + private Connection $connection; - /** @var IStructure */ - private $structure; + private IStructure $structure; - /** @var Conventions */ - private $conventions; + private Conventions $conventions; - /** @var Nette\Caching\IStorage */ - private $cacheStorage; + private ?Nette\Caching\Storage $cacheStorage; public function __construct( Connection $connection, Structure $structure, ?Conventions $conventions = null, - ?Nette\Caching\IStorage $cacheStorage = null + ?Nette\Caching\Storage $cacheStorage = null, ) { $this->connection = $connection; $this->structure = $structure; @@ -64,14 +61,9 @@ public function rollBack(): void } - /** - * @return mixed - */ - public function transaction(callable $callback) + public function transaction(callable $callback): mixed { - return $this->connection->transaction(function () use ($callback) { - return $callback($this); - }); + return $this->connection->transaction(fn() => $callback($this)); } @@ -85,7 +77,7 @@ public function getInsertId(?string $sequence = null): string * Generates and executes SQL query. * @param literal-string $sql */ - public function query(string $sql, ...$params): ResultSet + public function query(#[Language('SQL')] string $sql, #[Language('GenericSQL')] ...$params): ResultSet { return $this->connection->query($sql, ...$params); } @@ -94,6 +86,7 @@ public function query(string $sql, ...$params): ResultSet /** @deprecated use query() */ public function queryArgs(string $sql, array $params): ResultSet { + trigger_error(__METHOD__ . '() is deprecated, use query()', E_USER_DEPRECATED); return $this->connection->query($sql, ...$params); } @@ -129,7 +122,7 @@ public function getConventions(): Conventions * Shortcut for query()->fetch() * @param literal-string $sql */ - public function fetch(string $sql, ...$params): ?Row + public function fetch(#[Language('SQL')] string $sql, #[Language('GenericSQL')] ...$params): ?Row { return $this->connection->query($sql, ...$params)->fetch(); } @@ -138,9 +131,8 @@ public function fetch(string $sql, ...$params): ?Row /** * Shortcut for query()->fetchField() * @param literal-string $sql - * @return mixed */ - public function fetchField(string $sql, ...$params) + public function fetchField(#[Language('SQL')] string $sql, #[Language('GenericSQL')] ...$params): mixed { return $this->connection->query($sql, ...$params)->fetchField(); } @@ -150,7 +142,7 @@ public function fetchField(string $sql, ...$params) * Shortcut for query()->fetchFields() * @param literal-string $sql */ - public function fetchFields(string $sql, ...$params): ?array + public function fetchFields(#[Language('SQL')] string $sql, #[Language('GenericSQL')] ...$params): ?array { return $this->connection->query($sql, ...$params)->fetchFields(); } @@ -160,7 +152,7 @@ public function fetchFields(string $sql, ...$params): ?array * Shortcut for query()->fetchPairs() * @param literal-string $sql */ - public function fetchPairs(string $sql, ...$params): array + public function fetchPairs(#[Language('SQL')] string $sql, #[Language('GenericSQL')] ...$params): array { return $this->connection->query($sql, ...$params)->fetchPairs(); } @@ -170,7 +162,7 @@ public function fetchPairs(string $sql, ...$params): array * Shortcut for query()->fetchAll() * @param literal-string $sql */ - public function fetchAll(string $sql, ...$params): array + public function fetchAll(#[Language('SQL')] string $sql, #[Language('GenericSQL')] ...$params): array { return $this->connection->query($sql, ...$params)->fetchAll(); } diff --git a/src/Database/Helpers.php b/src/Database/Helpers.php index 510280cd1..e2dbcedc1 100644 --- a/src/Database/Helpers.php +++ b/src/Database/Helpers.php @@ -21,14 +21,14 @@ class Helpers { use Nette\StaticClass; - /** @var int maximum SQL length */ - public static $maxLength = 100; + /** maximum SQL length */ + public static int $maxLength = 100; - /** @var array */ - public static $typePatterns = [ + public static array $typePatterns = [ '^_' => IStructure::FIELD_TEXT, // PostgreSQL arrays '(TINY|SMALL|SHORT|MEDIUM|BIG|LONG)(INT)?|INT(EGER|\d+| IDENTITY)?|(SMALL|BIG|)SERIAL\d*|COUNTER|YEAR|BYTE|LONGLONG|UNSIGNED BIG INT' => IStructure::FIELD_INTEGER, - '(NEW)?DEC(IMAL)?(\(.*)?|NUMERIC|REAL|DOUBLE( PRECISION)?|FLOAT\d*|(SMALL)?MONEY|CURRENCY|NUMBER' => IStructure::FIELD_FLOAT, + '(NEW)?DEC(IMAL)?(\(.*)?|NUMERIC|(SMALL)?MONEY|CURRENCY|NUMBER' => IStructure::FIELD_FIXED, + 'REAL|DOUBLE( PRECISION)?|FLOAT\d*' => IStructure::FIELD_FLOAT, 'BOOL(EAN)?' => IStructure::FIELD_BOOL, 'TIME' => IStructure::FIELD_TIME, 'DATE' => IStructure::FIELD_DATE, @@ -201,45 +201,10 @@ public static function detectType(string $type): string } - /** @internal */ + /** @deprecated use RowNormalizer::normalizeRow() */ public static function normalizeRow(array $row, ResultSet $resultSet): array { - foreach ($resultSet->getColumnTypes() as $key => $type) { - $value = $row[$key]; - if ($value === null || $value === false || $type === IStructure::FIELD_TEXT) { - // do nothing - } elseif ($type === IStructure::FIELD_INTEGER) { - $row[$key] = is_float($tmp = $value * 1) ? $value : $tmp; - - } elseif ($type === IStructure::FIELD_FLOAT) { - if (is_string($value) && ($pos = strpos($value, '.')) !== false) { - $value = rtrim(rtrim($pos === 0 ? "0$value" : $value, '0'), '.'); - } - - $row[$key] = (float) $value; - - } elseif ($type === IStructure::FIELD_BOOL) { - $row[$key] = ((bool) $value) && $value !== 'f' && $value !== 'F'; - - } elseif ( - $type === IStructure::FIELD_DATETIME - || $type === IStructure::FIELD_DATE - || $type === IStructure::FIELD_TIME - ) { - $row[$key] = new Nette\Utils\DateTime($value); - - } elseif ($type === IStructure::FIELD_TIME_INTERVAL) { - preg_match('#^(-?)(\d+)\D(\d+)\D(\d+)(\.\d+)?$#D', $value, $m); - $row[$key] = new \DateInterval("PT$m[2]H$m[3]M$m[4]S"); - $row[$key]->f = isset($m[5]) ? (float) $m[5] : 0.0; - $row[$key]->invert = (int) (bool) $m[1]; - - } elseif ($type === IStructure::FIELD_UNIX_TIMESTAMP) { - $row[$key] = Nette\Utils\DateTime::from($value); - } - } - - return $row; + return (new RowNormalizer)($row, $resultSet); } @@ -299,9 +264,10 @@ public static function createDebugPanel( bool $explain, string $name, Tracy\Bar $bar, - Tracy\BlueScreen $blueScreen + Tracy\BlueScreen $blueScreen, ): ?ConnectionPanel { + trigger_error(__METHOD__ . '() is deprecated, use Nette\Bridges\DatabaseTracy\ConnectionPanel::initialize()', E_USER_DEPRECATED); return ConnectionPanel::initialize($connection, true, $name, $explain, $bar, $blueScreen); } @@ -313,9 +279,10 @@ public static function initializeTracy( string $name = '', bool $explain = true, ?Tracy\Bar $bar = null, - ?Tracy\BlueScreen $blueScreen = null + ?Tracy\BlueScreen $blueScreen = null, ): ?ConnectionPanel { + trigger_error(__METHOD__ . '() is deprecated, use Nette\Bridges\DatabaseTracy\ConnectionPanel::initialize()', E_USER_DEPRECATED); return ConnectionPanel::initialize($connection, $addBarPanel, $name, $explain, $bar, $blueScreen); } @@ -359,11 +326,11 @@ public static function toPairs(array $rows, $key = null, $value = null): array /** * Finds duplicate columns in select statement */ - public static function findDuplicates(\PDOStatement $statement): string + public static function findDuplicates(ResultDriver $result): string { $cols = []; - for ($i = 0; $i < $statement->columnCount(); $i++) { - $meta = $statement->getColumnMeta($i); + for ($i = 0; $i < $result->getColumnCount(); $i++) { + $meta = $result->getColumnMeta($i); $cols[$meta['name']][] = $meta['table'] ?? ''; } diff --git a/src/Database/IStructure.php b/src/Database/IStructure.php index 8aa1b38e7..deaff9c7b 100644 --- a/src/Database/IStructure.php +++ b/src/Database/IStructure.php @@ -21,6 +21,7 @@ interface IStructure FIELD_BOOL = 'bool', FIELD_INTEGER = 'int', FIELD_FLOAT = 'float', + FIELD_FIXED = 'fixed', FIELD_DATE = 'date', FIELD_TIME = 'time', FIELD_DATETIME = 'datetime', @@ -41,7 +42,7 @@ function getColumns(string $table): array; * Returns table primary key. * @return string|string[]|null */ - function getPrimaryKey(string $table); + function getPrimaryKey(string $table): string|array|null; /** * Returns autoincrement primary key name. diff --git a/src/Database/QueryException.php b/src/Database/QueryException.php new file mode 100644 index 000000000..3ab5ce014 --- /dev/null +++ b/src/Database/QueryException.php @@ -0,0 +1,37 @@ +queryString = $queryString; + $this->params = $params; + } + + + public function getQueryString(): string + { + return $this->queryString; + } + + + public function getParameters(): array + { + return $this->params; + } +} diff --git a/src/Database/Reflection/Column.php b/src/Database/Reflection/Column.php new file mode 100644 index 000000000..c2607ebbf --- /dev/null +++ b/src/Database/Reflection/Column.php @@ -0,0 +1,34 @@ +connection = $connection; @@ -63,25 +49,11 @@ public function __construct( $this->params = $params; $this->normalizer = $normalizer; - try { - if (substr($queryString, 0, 2) === '::') { - $connection->getPdo()->{substr($queryString, 2)}(); - } elseif ($queryString !== null) { - $types = ['boolean' => PDO::PARAM_BOOL, 'integer' => PDO::PARAM_INT, 'resource' => PDO::PARAM_LOB, 'NULL' => PDO::PARAM_NULL]; - $this->pdoStatement = $connection->getPdo()->prepare($queryString); - foreach ($params as $key => $value) { - $type = gettype($value); - $this->pdoStatement->bindValue(is_int($key) ? $key + 1 : $key, $value, $types[$type] ?? PDO::PARAM_STR); - } - - $this->pdoStatement->setFetchMode(PDO::FETCH_ASSOC); - @$this->pdoStatement->execute(); // @ PHP generates warning when ATTR_ERRMODE = ERRMODE_EXCEPTION bug #73878 - } - } catch (\PDOException $e) { - $e = $connection->getDriver()->convertException($e); - $e->queryString = $queryString; - $e->params = $params; - throw $e; + $driver = $connection->getDriver(); + if (str_starts_with($queryString, '::')) { + $driver->{substr($queryString, 2)}(); + } elseif ($queryString !== null) { + $this->result = $driver->query($queryString, $params); } $this->time = microtime(true) - $time; @@ -91,7 +63,7 @@ public function __construct( /** @deprecated */ public function getConnection(): Connection { - return $this->connection; + throw new Nette\DeprecatedException(__METHOD__ . '() is deprecated.'); } @@ -100,7 +72,7 @@ public function getConnection(): Connection */ public function getPdoStatement(): ?\PDOStatement { - return $this->pdoStatement; + return $this->result->getPDOStatement(); } @@ -118,20 +90,20 @@ public function getParameters(): array public function getColumnCount(): ?int { - return $this->pdoStatement ? $this->pdoStatement->columnCount() : null; + return $this->result?->getColumnCount(); } public function getRowCount(): ?int { - return $this->pdoStatement ? $this->pdoStatement->rowCount() : null; + return $this->result?->getRowCount(); } public function getColumnTypes(): array { - if ($this->types === null) { - $this->types = $this->connection->getDriver()->getColumnTypes($this->pdoStatement); + if (!isset($this->types)) { + $this->types = $this->result->getColumnTypes(); } return $this->types; @@ -176,15 +148,13 @@ public function rewind(): void } - #[\ReturnTypeWillChange] - public function current() + public function current(): mixed { return $this->lastRow; } - #[\ReturnTypeWillChange] - public function key() + public function key(): mixed { return $this->lastRowKey; } @@ -211,13 +181,12 @@ public function valid(): bool */ public function fetch(): ?Row { - $data = $this->pdoStatement ? $this->pdoStatement->fetch() : null; - if (!$data) { - $this->pdoStatement->closeCursor(); + $data = $this->result?->fetch(); + if ($data === null) { return null; - } elseif ($this->lastRow === null && count($data) !== $this->pdoStatement->columnCount()) { - $duplicates = Helpers::findDuplicates($this->pdoStatement); + } elseif (!isset($this->lastRow) && count($data) !== $this->result->getColumnCount()) { + $duplicates = Helpers::findDuplicates($this->result); trigger_error("Found duplicate columns in database result set: $duplicates.", E_USER_NOTICE); } @@ -233,18 +202,20 @@ public function fetch(): ?Row } + /** @internal */ + public function fetchArray(): ?array + { + return $this->result?->fetch(); + } + + /** * Fetches single field. - * @return mixed */ - public function fetchField($column = 0) + public function fetchField(): mixed { - if (func_num_args()) { - trigger_error(__METHOD__ . '() argument is deprecated.', E_USER_DEPRECATED); - } - $row = $this->fetch(); - return $row ? $row[$column] : null; + return $row ? $row[0] : null; } @@ -263,7 +234,7 @@ public function fetchFields(): ?array * @param string|int $key column name used for an array key or null for numeric index * @param string|int $value column name used for an array value or null for the whole row */ - public function fetchPairs($key = null, $value = null): array + public function fetchPairs(string|int|null $key = null, string|int|null $value = null): array { return Helpers::toPairs($this->fetchAll(), $key, $value); } @@ -275,7 +246,7 @@ public function fetchPairs($key = null, $value = null): array */ public function fetchAll(): array { - if ($this->rows === null) { + if (!isset($this->rows)) { $this->rows = iterator_to_array($this); } @@ -285,7 +256,6 @@ public function fetchAll(): array /** * Fetches all rows and returns associative tree. - * @param string $path associative descriptor */ public function fetchAssoc(string $path): array { diff --git a/src/Database/Row.php b/src/Database/Row.php index b57b728db..1d7187d8e 100644 --- a/src/Database/Row.php +++ b/src/Database/Row.php @@ -33,10 +33,8 @@ public function __isset($key) /** * Returns a item. * @param string|int $key key or index - * @return mixed */ - #[\ReturnTypeWillChange] - public function offsetGet($key) + public function offsetGet($key): mixed { if (is_int($key)) { $arr = array_slice((array) $this, $key, 1); diff --git a/src/Database/RowNormalizer.php b/src/Database/RowNormalizer.php new file mode 100644 index 000000000..55b1cb1ca --- /dev/null +++ b/src/Database/RowNormalizer.php @@ -0,0 +1,103 @@ +skipped[IStructure::FIELD_FIXED] = true; + return $this; + } + + + public function skipDateTime(): static + { + $this->skipped[IStructure::FIELD_DATETIME] = true; + $this->skipped[IStructure::FIELD_DATE] = true; + $this->skipped[IStructure::FIELD_TIME] = true; + $this->skipped[IStructure::FIELD_UNIX_TIMESTAMP] = true; + return $this; + } + + + public function skipInterval(): static + { + $this->skipped[IStructure::FIELD_TIME_INTERVAL] = true; + return $this; + } + + + public function __invoke(array $row, ResultSet $resultSet): array + { + foreach ($resultSet->getColumnTypes() as $key => $type) { + if (!isset($this->skipped[$type])) { + $row[$key] = $this->normalizeField($row[$key], $type); + } + } + + return $row; + } + + + public function normalizeField(mixed $value, string $type): mixed + { + if ($value === null || $value === false) { + return $value; + } + + switch ($type) { + case IStructure::FIELD_INTEGER: + return is_float($tmp = $value * 1) ? $value : $tmp; + + case IStructure::FIELD_FLOAT: + case IStructure::FIELD_FIXED: + if (is_string($value) && ($pos = strpos($value, '.')) !== false) { + $value = rtrim(rtrim($pos === 0 ? "0$value" : $value, '0'), '.'); + } + + return (float) $value; + + case IStructure::FIELD_BOOL: + return $value && $value !== 'f' && $value !== 'F'; + + case IStructure::FIELD_DATETIME: + case IStructure::FIELD_DATE: + case IStructure::FIELD_TIME: + return $value && !str_starts_with((string) $value, '0000-00') + ? new DateTime($value) + : null; + + case IStructure::FIELD_TIME_INTERVAL: + preg_match('#^(-?)(\d+)\D(\d+)\D(\d+)(\.\d+)?$#D', $value, $m); + $di = new \DateInterval("PT$m[2]H$m[3]M$m[4]S"); + $di->f = isset($m[5]) ? (float) $m[5] : 0.0; + $di->invert = (int) (bool) $m[1]; + return $di; + + case IStructure::FIELD_UNIX_TIMESTAMP: + return new DateTime($value); + + default: + return $value; + } + } +} diff --git a/src/Database/SqlLiteral.php b/src/Database/SqlLiteral.php index 46ec925cd..8e2e010a9 100644 --- a/src/Database/SqlLiteral.php +++ b/src/Database/SqlLiteral.php @@ -19,11 +19,9 @@ class SqlLiteral { use Nette\SmartObject; - /** @var string */ - private $value; + private string $value; - /** @var array */ - private $parameters; + private array $parameters; public function __construct(string $value, array $parameters = []) diff --git a/src/Database/SqlPreprocessor.php b/src/Database/SqlPreprocessor.php index f5cb36227..c6c8af25d 100644 --- a/src/Database/SqlPreprocessor.php +++ b/src/Database/SqlPreprocessor.php @@ -50,26 +50,22 @@ class SqlPreprocessor 'EXPLAIN' => 1, ]; - /** @var Connection */ - private $connection; + private Connection $connection; - /** @var Driver */ - private $driver; + private Driver $driver; - /** @var array of input parameters */ - private $params; + /** input parameters */ + private array $params; - /** @var array of parameters to be processed by PDO */ - private $remaining; + /** parameters to be processed by PDO */ + private array $remaining; - /** @var int */ - private $counter; + private int $counter; - /** @var bool */ - private $useParams; + private bool $useParams; - /** @var string|null values|set|and|order|items */ - private $arrayMode; + /** values|set|and|order|items */ + private ?string $arrayMode; public function __construct(Connection $connection) @@ -103,17 +99,19 @@ public function process(array $params, bool $useParams = false): array $this->arrayMode = null; $res[] = Nette\Utils\Strings::replace( $param, - '~ - \'[^\']*+\' - |"[^"]*+" - |\?[a-z]* - |^\s*+(?:\(?\s*SELECT|INSERT|UPDATE|DELETE|REPLACE|EXPLAIN)\b - |\b(?:SET|WHERE|HAVING|ORDER\ BY|GROUP\ BY|KEY\ UPDATE)(?=\s*$|\s*\?) - |\bIN\s+(?:\?|\(\?\)) - |/\*.*?\*/ - |--[^\n]* - ~Dsix', - \Closure::fromCallable([$this, 'callback']) + <<<'X' + ~ + '[^']*+' + |"[^"]*+" + |\?[a-z]* + |^\s*+(?:\(?\s*SELECT|INSERT|UPDATE|DELETE|REPLACE|EXPLAIN)\b + |\b(?:SET|WHERE|HAVING|ORDER\ BY|GROUP\ BY|KEY\ UPDATE)(?=\s*$|\s*\?) + |\bIN\s+(?:\?|\(\?\)) + |/\*.*?\*/ + |--[^\n]* + ~Dsix + X, + \Closure::fromCallable([$this, 'callback']), ); } else { throw new Nette\InvalidArgumentException('There are more parameters than placeholders.'); @@ -197,13 +195,13 @@ private function formatValue($value, ?string $mode = null): string $this->remaining[] = $value->value; return '?'; - } elseif (is_object($value) && method_exists($value, '__toString')) { + } elseif ($value instanceof \Stringable) { $this->remaining[] = (string) $value; return '?'; } } elseif ($mode === 'name') { if (!is_string($value)) { - $type = gettype($value); + $type = get_debug_type($value); throw new Nette\InvalidArgumentException("Placeholder ?$mode expects string, $type given."); } @@ -225,7 +223,7 @@ private function formatValue($value, ?string $mode = null): string if (!is_array($value[0]) && !$value[0] instanceof Row) { throw new Nette\InvalidArgumentException( 'Automaticaly detected multi-insert, but values aren\'t array. If you need try to change mode like "?[' - . implode('|', self::Modes) . ']". Mode "' . $mode . '" was used.' + . implode('|', self::Modes) . ']". Mode "' . $mode . '" was used.', ); } @@ -242,7 +240,7 @@ private function formatValue($value, ?string $mode = null): string $vx[] = implode(', ', $vx2); } - $select = $this->driver->isSupported(Driver::SUPPORT_MULTI_INSERT_AS_SELECT); + $select = $this->driver->isSupported(Driver::SupportMultiInsertAsSelect); return '(' . implode(', ', $kx) . ($select ? ') SELECT ' : ') VALUES (') . implode($select ? ' UNION ALL SELECT ' : '), (', $vx) . ($select ? '' : ')'); } @@ -258,7 +256,7 @@ private function formatValue($value, ?string $mode = null): string foreach ($value as $k => $v) { if (is_int($k)) { // value, value, ... $vx[] = $this->formatValue($v); - } elseif (substr($k, -1) === '=') { // key+=value, key-=value, ... + } elseif (str_ends_with($k, '=')) { // key+=value, key-=value, ... $k2 = $this->delimite(substr($k, 0, -2)); $vx[] = $k2 . '=' . $k2 . ' ' . substr($k, -2, 1) . ' ' . $this->formatValue($v); } else { // key=value, key=value, ... @@ -288,7 +286,23 @@ private function formatValue($value, ?string $mode = null): string $k = $this->delimite($k); if (is_array($v)) { if ($v) { - $vx[] = $k . ' ' . ($operator ? $operator . ' ' : '') . 'IN (' . $this->formatValue(array_values($v), self::ModeList) . ')'; + switch ($operator) { + case '': + case 'IN': + $vx[] = $k . ' ' . ($operator ? $operator . ' ' : 'IN') . ' (' . $this->formatValue(array_values($v), self::ModeList) . ')'; + break; + + case 'NOT': + $vx[] = $k . ' ' . $operator . ' IN (' . $this->formatValue(array_values($v), self::ModeList) . ')'; + break; + + case 'BETWEEN': + $vx[] = $k . ' ' . $operator . $this->formatValue(array_values($v), self::ModeAnd); + break; + + default: + throw new Nette\InvalidArgumentException("unsupported operator {$operator}"); + } } elseif ($operator === 'NOT') { } else { $vx[] = '1=0'; @@ -317,14 +331,14 @@ private function formatValue($value, ?string $mode = null): string throw new Nette\InvalidArgumentException("Unknown placeholder ?$mode."); } } elseif (in_array($mode, self::Modes, true)) { - $type = gettype($value); + $type = get_debug_type($value); throw new Nette\InvalidArgumentException("Placeholder ?$mode expects array or Traversable object, $type given."); } elseif ($mode && $mode !== self::ModeAuto) { throw new Nette\InvalidArgumentException("Unknown placeholder ?$mode."); } else { - throw new Nette\InvalidArgumentException('Unexpected type of parameter: ' . (is_object($value) ? get_class($value) : gettype($value))); + throw new Nette\InvalidArgumentException('Unexpected type of parameter: ' . get_debug_type($value)); } } diff --git a/src/Database/Structure.php b/src/Database/Structure.php index 395c00f53..c869c2180 100644 --- a/src/Database/Structure.php +++ b/src/Database/Structure.php @@ -19,26 +19,23 @@ class Structure implements IStructure { use Nette\SmartObject; - /** @var Connection */ - protected $connection; + protected Connection $connection; - /** @var Nette\Caching\Cache */ - protected $cache; + protected Nette\Caching\Cache $cache; - /** @var array */ - protected $structure; + protected array $structure; - /** @var bool */ - protected $isRebuilt = false; + protected bool $isRebuilt = false; - public function __construct(Connection $connection, Nette\Caching\IStorage $cacheStorage) + public function __construct(Connection $connection, Nette\Caching\Storage $cacheStorage) { $this->connection = $connection; - $this->cache = new Nette\Caching\Cache($cacheStorage, 'Nette.Database.Structure.' . md5($this->connection->getDsn())); + $this->cache = new Nette\Caching\Cache($cacheStorage, 'Nette.Database.Structure4.' . md5($this->connection->getDsn())); } + /** @return Reflection\Table[] */ public function getTables(): array { $this->needStructure(); @@ -46,6 +43,7 @@ public function getTables(): array } + /** @return Reflection\Column[] */ public function getColumns(string $table): array { $this->needStructure(); @@ -58,7 +56,7 @@ public function getColumns(string $table): array /** * @return string|string[]|null */ - public function getPrimaryKey(string $table) + public function getPrimaryKey(string $table): string|array|null { $this->needStructure(); $table = $this->resolveFQTableName($table); @@ -77,8 +75,8 @@ public function getPrimaryAutoincrementKey(string $table): ?string if (is_array($primaryKey)) { $keys = array_flip($primaryKey); foreach ($this->getColumns($table) as $column) { - if (isset($keys[$column['name']]) && $column['autoincrement']) { - return $column['name']; + if (isset($keys[$column->name]) && $column->autoIncrement) { + return $column->name; } } @@ -87,8 +85,8 @@ public function getPrimaryAutoincrementKey(string $table): ?string // Search for autoincrement key from simple primary key foreach ($this->getColumns($table) as $column) { - if ($column['name'] === $primaryKey) { - return $column['autoincrement'] ? $column['name'] : null; + if ($column->name === $primaryKey) { + return $column->autoIncrement ? $column->name : null; } } @@ -101,7 +99,7 @@ public function getPrimaryKeySequence(string $table): ?string $this->needStructure(); $table = $this->resolveFQTableName($table); - if (!$this->connection->getDriver()->isSupported(Driver::SUPPORT_SEQUENCE)) { + if (!$this->connection->getDriver()->isSupported(Driver::SupportSequence)) { return null; } @@ -112,8 +110,8 @@ public function getPrimaryKeySequence(string $table): ?string // Search for sequence from simple primary key foreach ($this->structure['columns'][$table] as $columnMeta) { - if ($columnMeta['name'] === $autoincrementPrimaryKeyName) { - return $columnMeta['vendor']['sequence'] ?? null; + if ($columnMeta->name === $autoincrementPrimaryKeyName) { + return $columnMeta->vendor['sequence'] ?? null; } } @@ -174,7 +172,7 @@ public function isRebuilt(): bool protected function needStructure(): void { - if ($this->structure !== null) { + if (isset($this->structure)) { return; } @@ -189,27 +187,25 @@ protected function loadStructure(): array $structure = []; $structure['tables'] = $driver->getTables(); - foreach ($structure['tables'] as $tablePair) { - if (isset($tablePair['fullName'])) { - $table = $tablePair['fullName']; - $structure['aliases'][strtolower($tablePair['name'])] = strtolower($table); + foreach ($structure['tables'] as $table) { + if (isset($table->fullName)) { + $tableName = $table->fullName; + $structure['aliases'][strtolower($table->name)] = strtolower($tableName); } else { - $table = $tablePair['name']; + $tableName = $table->name; } - $structure['columns'][strtolower($table)] = $columns = $driver->getColumns($table); + $structure['columns'][strtolower($tableName)] = $columns = $driver->getColumns($tableName); - if (!$tablePair['view']) { - $structure['primary'][strtolower($table)] = $this->analyzePrimaryKey($columns); - $this->analyzeForeignKeys($structure, $table); + if (!$table->view) { + $structure['primary'][strtolower($tableName)] = $this->analyzePrimaryKey($columns); + $this->analyzeForeignKeys($structure, $tableName); } } if (isset($structure['hasMany'])) { - foreach ($structure['hasMany'] as &$table) { - uksort($table, function ($a, $b): int { - return strlen($a) <=> strlen($b); - }); + foreach ($structure['hasMany'] as &$tableName) { + uksort($tableName, fn($a, $b): int => strlen($a) <=> strlen($b)); } } @@ -219,12 +215,13 @@ protected function loadStructure(): array } + /** @param Reflection\Column[] $columns */ protected function analyzePrimaryKey(array $columns) { $primary = []; foreach ($columns as $column) { - if ($column['primary']) { - $primary[] = $column['name']; + if ($column->primary) { + $primary[] = $column->name; } } @@ -244,25 +241,15 @@ protected function analyzeForeignKeys(array &$structure, string $table): void $foreignKeys = $this->connection->getDriver()->getForeignKeys($table); - $fksColumnsCounts = []; - foreach ($foreignKeys as $foreignKey) { - $tmp = &$fksColumnsCounts[$foreignKey['name']]; - $tmp++; - } - - usort($foreignKeys, function ($a, $b) use ($fksColumnsCounts): int { - return $fksColumnsCounts[$b['name']] <=> $fksColumnsCounts[$a['name']]; - }); + usort($foreignKeys, fn($a, $b): int => count($b->columns) <=> count($a->columns)); - foreach ($foreignKeys as $row) { - $structure['belongsTo'][$lowerTable][$row['local']] = $row['table']; - $structure['hasMany'][strtolower($row['table'])][$table][] = $row['local']; + foreach ($foreignKeys as $key) { + $structure['belongsTo'][$lowerTable][$key->columns[0]] = $key->targetTable; + $structure['hasMany'][strtolower($key->targetTable)][$table][] = $key->columns[0]; } if (isset($structure['belongsTo'][$lowerTable])) { - uksort($structure['belongsTo'][$lowerTable], function ($a, $b): int { - return strlen($a) <=> strlen($b); - }); + uksort($structure['belongsTo'][$lowerTable], fn($a, $b): int => strlen($a) <=> strlen($b)); } } diff --git a/src/Database/Table/ActiveRow.php b/src/Database/Table/ActiveRow.php index a44bc36af..7c3ec200a 100644 --- a/src/Database/Table/ActiveRow.php +++ b/src/Database/Table/ActiveRow.php @@ -18,14 +18,11 @@ */ class ActiveRow implements \IteratorAggregate, IRow { - /** @var Selection */ - private $table; + private Selection $table; - /** @var array of row data */ - private $data; + private array $data; - /** @var bool */ - private $dataRefreshed = false; + private bool $dataRefreshed = false; public function __construct(array $data, Selection $table) @@ -55,16 +52,7 @@ public function getTable(): Selection public function __toString() { - try { - return (string) $this->getPrimary(); - } catch (\Throwable $e) { - if (func_num_args() || PHP_VERSION_ID >= 70400) { - throw $e; - } - - trigger_error('Exception in ' . __METHOD__ . "(): {$e->getMessage()} in {$e->getFile()}:{$e->getLine()}", E_USER_ERROR); - return ''; - } + return (string) $this->getPrimary(); } @@ -77,9 +65,9 @@ public function toArray(): array /** * Returns primary key value. - * @return mixed possible int, string, array, object (Nette\Utils\DateTime) + * @return mixed possible int, string, array, object (Nette\Database\DateTime) */ - public function getPrimary(bool $throw = true) + public function getPrimary(bool $throw = true): mixed { $primary = $this->table->getPrimary($throw); if ($primary === null) { @@ -231,10 +219,8 @@ public function offsetSet($column, $value): void /** * Returns value of column. * @param string $column - * @return mixed */ - #[\ReturnTypeWillChange] - public function offsetGet($column) + public function offsetGet($column): mixed { return $this->__get($column); } @@ -270,7 +256,7 @@ public function __set($column, $value) * @return ActiveRow|mixed * @throws Nette\MemberAccessException */ - public function &__get(string $key) + public function &__get(string $key): mixed { if ($this->accessColumn($key)) { return $this->data[$key]; diff --git a/src/Database/Table/GroupedSelection.php b/src/Database/Table/GroupedSelection.php index 85b41b89a..0f621bdcd 100644 --- a/src/Database/Table/GroupedSelection.php +++ b/src/Database/Table/GroupedSelection.php @@ -20,17 +20,17 @@ */ class GroupedSelection extends Selection { - /** @var Selection referenced table */ - protected $refTable; + /** referenced table */ + protected Selection $refTable; - /** @var mixed current assigned referencing array */ - protected $refCacheCurrent; + /** current assigned referencing array */ + protected mixed $refCacheCurrent; - /** @var string grouping column name */ - protected $column; + /** grouping column name */ + protected string $column; - /** @var int primary key */ - protected $active; + /** primary key */ + protected int|string $active; /** @@ -42,7 +42,7 @@ public function __construct( string $tableName, string $column, Selection $refTable, - ?Nette\Caching\IStorage $cacheStorage = null + ?Nette\Caching\Storage $cacheStorage = null, ) { $this->refTable = $refTable; $this->column = $column; @@ -54,19 +54,15 @@ public function __construct( * Sets active group. * @internal * @param int|string $active primary key of grouped rows - * @return static */ - public function setActive($active) + public function setActive(int|string $active): static { $this->active = $active; return $this; } - /** - * @return static - */ - public function select($columns, ...$params) + public function select($columns, ...$params): static { if (!$this->sqlBuilder->getSelect()) { $this->sqlBuilder->addSelect("$this->name.$this->column"); @@ -76,10 +72,7 @@ public function select($columns, ...$params) } - /** - * @return static - */ - public function order(string $columns, ...$params) + public function order(string $columns, ...$params): static { if (!$this->sqlBuilder->getOrder()) { // improve index utilization @@ -93,10 +86,7 @@ public function order(string $columns, ...$params) /********************* aggregations ****************d*g**/ - /** - * @return mixed - */ - public function aggregation(string $function, ?string $groupFunction = null) + public function aggregation(string $function, ?string $groupFunction = null): mixed { $aggregation = &$this->getRefTable($refPath)->aggregation[$refPath . $function . $this->sqlBuilder->getSelectQueryHash($this->getPreviousAccessedColumns())]; @@ -241,7 +231,7 @@ protected function emptyResultSet(bool $saveCache = true, bool $deleteRererenced /********************* manipulation ****************d*g**/ - public function insert(iterable $data) + public function insert(iterable $data): ActiveRow|array|int|bool { 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 9ecacb098..aa3c7b28c 100644 --- a/src/Database/Table/Selection.php +++ b/src/Database/Table/Selection.php @@ -22,65 +22,52 @@ class Selection implements \Iterator, IRowContainer, \ArrayAccess, \Countable { use Nette\SmartObject; - /** @var Explorer */ - protected $explorer; + protected Explorer $explorer; - /** @var Explorer back compatibility */ - protected $context; + /** back compatibility */ + protected Explorer $context; + protected Conventions $conventions; + protected ?Nette\Caching\Cache $cache; + protected SqlBuilder $sqlBuilder; - /** @var Conventions */ - protected $conventions; - - /** @var Nette\Caching\Cache */ - protected $cache; - - /** @var SqlBuilder */ - protected $sqlBuilder; - - /** @var string table name */ - protected $name; + /** table name */ + protected string $name; /** @var string|string[]|null primary key field name */ - protected $primary; - - /** @var string|bool primary column sequence name, false for autodetection */ - protected $primarySequence = false; - - /** @var ActiveRow[] data read from database in [primary key => ActiveRow] format */ - protected $rows; + protected string|array|null $primary; - /** @var ActiveRow[] modifiable data in [primary key => ActiveRow] format */ - protected $data; + /** primary column sequence name, false for autodetection */ + protected string|bool|null $primarySequence = false; - /** @var bool */ - protected $dataRefreshed = false; + /** @var ActiveRow[]|null data read from database in [primary key => ActiveRow] format */ + protected ?array $rows = null; - /** @var mixed cache array of Selection and GroupedSelection prototypes */ - protected $globalRefCache; + /** @var ActiveRow[]|null modifiable data in [primary key => ActiveRow] format */ + protected ?array $data = null; - /** @var mixed */ - protected $refCache; + protected bool $dataRefreshed = false; - /** @var string|null */ - protected $generalCacheKey; + /** cache array of Selection and GroupedSelection prototypes */ + protected mixed $globalRefCache; - /** @var string|null */ - protected $specificCacheKey; + protected mixed $refCache; + protected ?string $generalCacheKey = null; + protected ?string $specificCacheKey = null; - /** @var array of [conditions => [key => ActiveRow]]; used by GroupedSelection */ - protected $aggregation = []; + /** of [conditions => [key => ActiveRow]]; used by GroupedSelection */ + protected array $aggregation = []; - /** @var array|false|null of touched columns */ - protected $accessedColumns; + /** touched columns */ + protected array|false|null $accessedColumns = null; - /** @var array|false|null of earlier touched columns */ - protected $previousAccessedColumns; + /** earlier touched columns */ + protected array|false|null $previousAccessedColumns = null; - /** @var self|null should instance observe accessed columns caching */ - protected $observeCache; + /** should instance observe accessed columns caching */ + protected ?self $observeCache = null; - /** @var array of primary key values */ - protected $keys = []; + /** of primary key values */ + protected array $keys = []; /** @@ -90,7 +77,7 @@ public function __construct( Explorer $explorer, Conventions $conventions, string $tableName, - ?Nette\Caching\IStorage $cacheStorage = null + ?Nette\Caching\Storage $cacheStorage = null, ) { $this->explorer = $this->context = $explorer; $this->conventions = $conventions; @@ -126,7 +113,7 @@ public function getName(): string /** * @return string|string[]|null */ - public function getPrimary(bool $throw = true) + public function getPrimary(bool $throw = true): string|array|null { if ($this->primary === null && $throw) { throw new \LogicException("Table '{$this->name}' does not have a primary key."); @@ -146,10 +133,7 @@ public function getPrimarySequence(): ?string } - /** - * @return static - */ - public function setPrimarySequence(string $sequence) + public function setPrimarySequence(string $sequence): static { $this->primarySequence = $sequence; return $this; @@ -165,9 +149,8 @@ public function getSql(): string /** * Loads cache of previous accessed columns and returns it. * @internal - * @return array|bool */ - public function getPreviousAccessedColumns() + public function getPreviousAccessedColumns(): array|bool { if ($this->cache && $this->previousAccessedColumns === null) { $this->accessedColumns = $this->previousAccessedColumns = $this->cache->load($this->getGeneralCacheKey()); @@ -194,9 +177,8 @@ public function getSqlBuilder(): SqlBuilder /** * Returns row specified by primary key. - * @param mixed $key primary key */ - public function get($key): ?ActiveRow + public function get(mixed $key): ?ActiveRow { $clone = clone $this; return $clone->wherePrimary($key)->fetch(); @@ -217,10 +199,9 @@ public function fetch(): ?ActiveRow /** * Fetches single field. - * @return mixed * @deprecated */ - public function fetchField(?string $column = null) + public function fetchField(?string $column = null): mixed { if ($column) { $this->select($column); @@ -240,7 +221,7 @@ public function fetchField(?string $column = null) * @param string|int $key column name used for an array key or null for numeric index * @param string|int $value column name used for an array value or null for the whole row */ - public function fetchPairs($key = null, $value = null): array + public function fetchPairs(string|int|null $key = null, string|int|null $value = null): array { return Nette\Database\Helpers::toPairs($this->fetchAll(), $key, $value); } @@ -258,7 +239,6 @@ public function fetchAll(): array /** * Fetches all rows and returns associative tree. - * @param string $path associative descriptor */ public function fetchAssoc(string $path): array { @@ -273,9 +253,8 @@ public function fetchAssoc(string $path): array /** * Adds select clause, more calls appends to the end. * @param string|string[] $columns for example "column, MD5(column) AS column_md5" - * @return static */ - public function select($columns, ...$params) + public function select($columns, ...$params): static { $this->emptyResultSet(); $this->sqlBuilder->addSelect($columns, ...$params); @@ -285,10 +264,8 @@ public function select($columns, ...$params) /** * Adds condition for primary key. - * @param mixed $key - * @return static */ - public function wherePrimary($key) + public function wherePrimary(mixed $key): static { if (is_array($this->primary) && Nette\Utils\Arrays::isList($key)) { if (isset($key[0]) && is_array($key[0])) { @@ -311,9 +288,8 @@ public function wherePrimary($key) /** * Adds where condition, more calls appends with AND. * @param string|array $condition possibly containing ? - * @return static */ - public function where($condition, ...$params) + public function where(string|array $condition, ...$params): static { $this->condition($condition, $params); return $this; @@ -324,9 +300,8 @@ public function where($condition, ...$params) * 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) + public function joinWhere(string $tableChain, string $condition, ...$params): static { $this->condition($condition, $params, $tableChain); return $this; @@ -360,10 +335,9 @@ protected function condition($condition, array $params, $tableChain = null): voi * Adds where condition using the OR operator between parameters. * More calls appends with AND. * @param array $parameters ['column1' => 1, 'column2 > ?' => 2, 'full condition'] - * @return static * @throws Nette\InvalidArgumentException */ - public function whereOr(array $parameters) + public function whereOr(array $parameters): static { if (count($parameters) < 2) { return $this->where($parameters); @@ -374,7 +348,7 @@ public function whereOr(array $parameters) foreach ($parameters as $key => $val) { if (is_int($key)) { // whereOr(['full condition']) $columns[] = $val; - } elseif (strpos($key, '?') === false) { // whereOr(['column1' => 1]) + } elseif (!str_contains($key, '?')) { // whereOr(['column1' => 1]) $columns[] = $key . ' ?'; $values[] = $val; } else { // whereOr(['column1 > ?' => 1]) @@ -396,9 +370,8 @@ public function whereOr(array $parameters) /** * Adds order clause, more calls appends to the end. * @param string $columns for example 'column1, column2 DESC' - * @return static */ - public function order(string $columns, ...$params) + public function order(string $columns, ...$params): static { $this->emptyResultSet(); $this->sqlBuilder->addOrder($columns, ...$params); @@ -408,9 +381,8 @@ public function order(string $columns, ...$params) /** * Sets limit clause, more calls rewrite old values. - * @return static */ - public function limit(?int $limit, ?int $offset = null) + public function limit(?int $limit, ?int $offset = null): static { $this->emptyResultSet(); $this->sqlBuilder->setLimit($limit, $offset); @@ -420,9 +392,8 @@ public function limit(?int $limit, ?int $offset = null) /** * Sets offset using page number, more calls rewrite old values. - * @return static */ - public function page(int $page, int $itemsPerPage, &$numOfPages = null) + public function page(int $page, int $itemsPerPage, &$numOfPages = null): static { if (func_num_args() > 2) { $numOfPages = (int) ceil($this->count('*') / $itemsPerPage); @@ -438,9 +409,8 @@ public function page(int $page, int $itemsPerPage, &$numOfPages = null) /** * Sets group clause, more calls rewrite old value. - * @return static */ - public function group(string $columns, ...$params) + public function group(string $columns, ...$params): static { $this->emptyResultSet(); $this->sqlBuilder->setGroup($columns, ...$params); @@ -450,9 +420,8 @@ public function group(string $columns, ...$params) /** * Sets having clause, more calls rewrite old value. - * @return static */ - public function having(string $having, ...$params) + public function having(string $having, ...$params): static { $this->emptyResultSet(); $this->sqlBuilder->setHaving($having, ...$params); @@ -462,9 +431,8 @@ public function having(string $having, ...$params) /** * Aliases table. Example ':book:book_tag.tag', 'tg' - * @return static */ - public function alias(string $tableChain, string $alias) + public function alias(string $tableChain, string $alias): static { $this->sqlBuilder->addAlias($tableChain, $alias); return $this; @@ -477,9 +445,8 @@ public function alias(string $tableChain, string $alias) /** * Executes aggregation function. * @param string $function select call in "FUNCTION(column)" format - * @return mixed */ - public function aggregation(string $function, ?string $groupFunction = null) + public function aggregation(string $function, ?string $groupFunction = null): mixed { $selection = $this->createSelectionInstance(); $selection->getSqlBuilder()->importConditions($this->getSqlBuilder()); @@ -513,9 +480,8 @@ public function count(?string $column = null): int /** * Returns minimum value from a column. - * @return mixed */ - public function min(string $column) + public function min(string $column): mixed { return $this->aggregation("MIN($column)", 'MIN'); } @@ -523,9 +489,8 @@ public function min(string $column) /** * Returns maximum value from a column. - * @return mixed */ - public function max(string $column) + public function max(string $column): mixed { return $this->aggregation("MAX($column)", 'MAX'); } @@ -533,9 +498,8 @@ public function max(string $column) /** * Returns sum of values in a column. - * @return mixed */ - public function sum(string $column) + public function sum(string $column): mixed { return $this->aggregation("SUM($column)", 'SUM'); } @@ -571,11 +535,13 @@ protected function execute(): void $this->rows = []; $usedPrimary = true; - foreach ($result->getPdoStatement() as $key => $row) { + $key = 0; + while ($row = $result->fetchArray()) { $row = $this->createRow($result->normalizeRow($row)); $primary = $row->getSignature(false); $usedPrimary = $usedPrimary && $primary !== ''; $this->rows[$usedPrimary ? $primary : $key] = $row; + $key++; } $this->data = $this->rows; @@ -661,9 +627,8 @@ protected function saveCacheState(): void /** * Returns Selection parent for caching. - * @return static */ - protected function getRefTable(&$refPath) + protected function getRefTable(&$refPath): self { return $this; } @@ -810,7 +775,7 @@ public function getDataRefreshed(): bool * @param array|\Traversable|Selection $data [$column => $value]|\Traversable|Selection for INSERT ... SELECT * @return ActiveRow|int|bool Returns ActiveRow or number of affected rows for Selection or table without primary key */ - public function insert(iterable $data) + public function insert(iterable $data): ActiveRow|array|int|bool { //should be called before query for not to spoil PDO::lastInsertId $primarySequenceName = $this->getPrimarySequence(); @@ -906,7 +871,7 @@ public function update(iterable $data): int return $this->explorer->query( $this->sqlBuilder->buildUpdateQuery(), - ...array_merge([$data], $this->sqlBuilder->getParameters()) + ...array_merge([$data], $this->sqlBuilder->getParameters()), )->getRowCount(); } @@ -928,7 +893,7 @@ 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 */ - public function getReferencedTable(ActiveRow $row, ?string $table, ?string $column = null) + public function getReferencedTable(ActiveRow $row, ?string $table, ?string $column = null): ActiveRow|false|null { if (!$column) { $belongsTo = $this->conventions->getBelongsToReference($this->name, $table); @@ -976,9 +941,13 @@ public function getReferencedTable(ActiveRow $row, ?string $table, ?string $colu * Returns referencing rows. * @param int|string $active primary key */ - public function getReferencingTable(string $table, ?string $column = null, $active = null): ?GroupedSelection + public function getReferencingTable( + string $table, + ?string $column = null, + int|string|null $active = null, + ): ?GroupedSelection { - if (strpos($table, '.') !== false) { + if (str_contains($table, '.')) { [$table, $column] = explode('.', $table); } elseif (!$column) { $hasMany = $this->conventions->getHasManyReference($this->name, $table); @@ -1012,9 +981,7 @@ public function rewind(): void } - /** @return ActiveRow|false */ - #[\ReturnTypeWillChange] - public function current() + public function current(): ActiveRow|false { return ($key = current($this->keys)) !== false ? $this->data[$key] @@ -1022,11 +989,7 @@ public function current() } - /** - * @return string|int row ID - */ - #[\ReturnTypeWillChange] - public function key() + public function key(): string|int { return current($this->keys); } diff --git a/src/Database/Table/SqlBuilder.php b/src/Database/Table/SqlBuilder.php index 06155aaa6..f80ce1b9d 100644 --- a/src/Database/Table/SqlBuilder.php +++ b/src/Database/Table/SqlBuilder.php @@ -25,29 +25,24 @@ class SqlBuilder { use Nette\SmartObject; - /** @var string */ - protected $tableName; + protected string $tableName; - /** @var Conventions */ - protected $conventions; + protected Conventions $conventions; - /** @var string delimited table name */ - protected $delimitedTable; + protected string $delimitedTable; - /** @var array of column to select */ - protected $select = []; + /** column to select */ + protected array $select = []; - /** @var array of where conditions */ - protected $where = []; + protected array $where = []; - /** @var array of array of join conditions */ - protected $joinCondition = []; + protected array $joinCondition = []; - /** @var array of where conditions for caching */ - protected $conditions = []; + /** where conditions for caching */ + protected array $conditions = []; - /** @var array of parameters passed to where conditions */ - protected $parameters = [ + /** parameters passed to where conditions */ + protected array $parameters = [ 'select' => [], 'joinCondition' => [], 'where' => [], @@ -56,41 +51,38 @@ class SqlBuilder 'order' => [], ]; - /** @var array or columns to order by */ - protected $order = []; + /** columns to order by */ + protected array $order = []; - /** @var int number of rows to fetch */ - protected $limit; + /** number of rows to fetch */ + protected ?int $limit = null; - /** @var int first row to fetch */ - protected $offset; + /** first row to fetch */ + protected ?int $offset = null; - /** @var string columns to grouping */ - protected $group = ''; + /** columns to grouping */ + protected string $group = ''; - /** @var string grouping condition */ - protected $having = ''; + /** grouping condition */ + protected string $having = ''; - /** @var array of reserved table names associated with chain */ - protected $reservedTableNames = []; + /** reserved table names associated with chain */ + protected array $reservedTableNames = []; - /** @var array of table aliases */ - protected $aliases = []; + /** table aliases */ + protected array $aliases = []; - /** @var string currently parsing alias for joins */ - protected $currentAlias; + /** currently parsing alias for joins */ + protected string $currentAlias = ''; - /** @var Driver */ - private $driver; + private Driver $driver; - /** @var IStructure */ - private $structure; + private IStructure $structure; - /** @var array */ - private $cacheTableList; + private array $cacheTableList = []; - /** @var array of expanding joins */ - private $expandingJoins = []; + /** expanding joins */ + private array $expandingJoins = []; public function __construct(string $tableName, Explorer $explorer) @@ -160,7 +152,7 @@ public function getSelectQueryHash(?array $columns = null): string $parts[] = $this->select; } elseif ($columns) { $parts[] = [$this->delimitedTable, $columns]; - } elseif ($this->group && !$this->driver->isSupported(Driver::SUPPORT_SELECT_UNGROUPED_COLUMNS)) { + } elseif ($this->group && !$this->driver->isSupported(Driver::SupportSelectUngroupedColumns)) { $parts[] = [$this->group]; } else { $parts[] = "{$this->delimitedTable}.*"; @@ -185,8 +177,8 @@ public function buildSelectQuery(?array $columns = null): string { if (!$this->order && ($this->limit !== null || $this->offset)) { $this->order = array_map( - function ($col) { return "$this->tableName.$col"; }, - (array) $this->conventions->getPrimary($this->tableName) + fn($col) => "$this->tableName.$col", + (array) $this->conventions->getPrimary($this->tableName), ); } @@ -212,7 +204,7 @@ function ($col) { return "$this->tableName.$col"; }, $querySelect = $this->buildSelect($cols); - } elseif ($this->group && !$this->driver->isSupported(Driver::SUPPORT_SELECT_UNGROUPED_COLUMNS)) { + } elseif ($this->group && !$this->driver->isSupported(Driver::SupportSelectUngroupedColumns)) { $querySelect = $this->buildSelect([$this->group]); $this->parseJoins($joins, $querySelect); @@ -242,7 +234,7 @@ public function getParameters(): array $this->parameters['where'], $this->parameters['group'], $this->parameters['having'], - $this->parameters['order'] + $this->parameters['order'], ); } @@ -380,7 +372,7 @@ protected function addCondition($condition, array $params, array &$conditions, a } } - if ($this->driver->isSupported(Driver::SUPPORT_SUBSELECT)) { + if ($this->driver->isSupported(Driver::SupportSubselect)) { $arg = null; $subSelectPlaceholderCount = substr_count($clone->getSql(), '?'); $replace = $match[2][0] . '(' . $clone->getSql() . (!$subSelectPlaceholderCount && count($clone->getSqlBuilder()->getParameters()) === 1 ? ' ?' : '') . ')'; @@ -397,10 +389,10 @@ protected function addCondition($condition, array $params, array &$conditions, a if ($arg !== null) { if (!$arg) { - $hasBrackets = strpos($condition, '(') !== false; + $hasBrackets = str_contains($condition, '('); $hasOperators = preg_match('#AND|OR#', $condition); - $hasNot = strpos($condition, 'NOT') !== false; - $hasPrefixNot = strpos($match[2][0], 'NOT') !== false; + $hasNot = str_contains($condition, 'NOT'); + $hasPrefixNot = str_contains($match[2][0], 'NOT'); if (!$hasBrackets && ($hasOperators || ($hasNot && !$hasPrefixNot))) { throw new Nette\InvalidArgumentException('Possible SQL query corruption. Add parentheses around operators.'); } @@ -590,7 +582,7 @@ protected function parseJoinConditions(&$joins, $joinConditions): array protected function getSortedJoins(string $table, &$leftJoinDependency, &$tableJoins, &$finalJoins): void { if (isset($this->expandingJoins[$table])) { - $path = implode("' => '", array_map(function (string $value): string { return $this->reservedTableNames[$value]; }, array_merge(array_keys($this->expandingJoins), [$table]))); + $path = implode("' => '", array_map(fn(string $value): string => $this->reservedTableNames[$value], array_merge(array_keys($this->expandingJoins), [$table]))); throw new Nette\InvalidArgumentException("Circular reference detected at left join conditions (tables '$path')."); } @@ -663,7 +655,7 @@ public function parseJoinsCb(&$joins, $match): string $parentAlias = preg_replace('#^(.*\.)?(.*)$#', '$2', $this->tableName); // join schema keyMatch and table keyMatch to schema.table keyMatch - if ($this->driver->isSupported(Driver::SUPPORT_SCHEMA) && count($keyMatches) > 1) { + if ($this->driver->isSupported(Driver::SupportSchema) && count($keyMatches) > 1) { $tables = $this->getCachedTableList(); if ( !isset($tables[$keyMatches[0]['key']]) @@ -820,11 +812,9 @@ protected function buildQueryEnd(): string protected function tryDelimite(string $s): string { - return preg_replace_callback('#(?<=[^\w`"\[?:]|^)[a-z_][a-z0-9_]*(?=[^\w`"(\]]|$)#Di', function (array $m): string { - return strtoupper($m[0]) === $m[0] + return preg_replace_callback('#(?<=[^\w`"\[?:]|^)[a-z_][a-z0-9_]*(?=[^\w`"(\]]|$)#Di', fn(array $m): string => strtoupper($m[0]) === $m[0] ? $m[0] - : $this->driver->delimite($m[0]); - }, $s); + : $this->driver->delimite($m[0]), $s); } @@ -832,10 +822,10 @@ protected function addConditionComposition( array $columns, array $parameters, array &$conditions, - array &$conditionsParameters + array &$conditionsParameters, ): bool { - if ($this->driver->isSupported(Driver::SUPPORT_MULTI_COLUMN_AS_OR_COND)) { + if ($this->driver->isSupported(Driver::SupportMultiColumnAsOrCond)) { $conditionFragment = '(' . implode(' = ? AND ', $columns) . ' = ?) OR '; $condition = substr(str_repeat($conditionFragment, count($parameters)), 0, -4); return $this->addCondition($condition, [Nette\Utils\Arrays::flatten($parameters)], $conditions, $conditionsParameters); @@ -852,7 +842,7 @@ private function getConditionHash($condition, array $parameters): string $parameter = $this->getConditionHash($parameter->getSql(), $parameter->getSqlBuilder()->getParameters()); } elseif ($parameter instanceof SqlLiteral) { $parameter = $this->getConditionHash($parameter->__toString(), $parameter->getParameters()); - } elseif (is_object($parameter) && method_exists($parameter, '__toString')) { + } elseif ($parameter instanceof \Stringable) { $parameter = $parameter->__toString(); } elseif (is_array($parameter) || $parameter instanceof \ArrayAccess) { $parameter = $this->getConditionHash($key, $parameter); @@ -866,9 +856,7 @@ private function getConditionHash($condition, array $parameters): string private function getCachedTableList(): array { if (!$this->cacheTableList) { - $this->cacheTableList = array_flip(array_map(function (array $pair): string { - return $pair['fullName'] ?? $pair['name']; - }, $this->structure->getTables())); + $this->cacheTableList = array_flip(array_map(fn($pair): string => $pair->fullName ?? $pair->name, $this->structure->getTables())); } return $this->cacheTableList; diff --git a/src/Database/exceptions.php b/src/Database/exceptions.php index c968ef2b7..d6e1bfd77 100644 --- a/src/Database/exceptions.php +++ b/src/Database/exceptions.php @@ -21,7 +21,7 @@ class ConnectionException extends DriverException /** * Base class for all constraint violation related exceptions. */ -class ConstraintViolationException extends DriverException +class ConstraintViolationException extends QueryException { } diff --git a/tests/Database/Connection.lazy.phpt b/tests/Database/Connection.lazy.phpt index c7f9d7483..9114e8b73 100644 --- a/tests/Database/Connection.lazy.phpt +++ b/tests/Database/Connection.lazy.phpt @@ -44,7 +44,7 @@ test('connect & disconnect', function () { try { $connection = new Nette\Database\Connection($options['dsn'], $options['user'], $options['password']); - } catch (PDOException $e) { + } catch (Nette\Database\ConnectionException $e) { Tester\Environment::skip("Connection to '$options[dsn]' failed. Reason: " . $e->getMessage()); } diff --git a/tests/Database/Connection.query.phpt b/tests/Database/Connection.query.phpt index b7fcfa8d8..d8c530aec 100644 --- a/tests/Database/Connection.query.phpt +++ b/tests/Database/Connection.query.phpt @@ -31,7 +31,7 @@ test('', function () use ($connection) { test('', function () use ($connection) { - $res = $connection->queryArgs('SELECT id FROM author WHERE id = ? OR id = ?', [11, 12]); + $res = @$connection->queryArgs('SELECT id FROM author WHERE id = ? OR id = ?', [11, 12]); // is deprecated Assert::same('SELECT id FROM author WHERE id = ? OR id = ?', $res->getQueryString()); Assert::same([11, 12], $res->getParameters()); }); diff --git a/tests/Database/DateTime.phpt b/tests/Database/DateTime.phpt new file mode 100644 index 000000000..90c7e2c2e --- /dev/null +++ b/tests/Database/DateTime.phpt @@ -0,0 +1,23 @@ +getTimestamp()); + +Assert::same(is_int(2_544_000_000) ? 2_544_000_000 : '2544000000', (new DateTime(2_544_000_000))->getTimestamp()); // 64 bit + +// to string +Assert::same('1978-01-23 11:40:00.000000', (string) new DateTime('1978-01-23 11:40')); + +// JSON +Assert::same('"1978-01-23T11:40:00+01:00"', json_encode(new DateTime('1978-01-23 11:40'))); diff --git a/tests/Database/Explorer.query.phpt b/tests/Database/Explorer.query.phpt index ef984fb8a..dd1e3a7a5 100644 --- a/tests/Database/Explorer.query.phpt +++ b/tests/Database/Explorer.query.phpt @@ -30,7 +30,7 @@ test('', function () use ($explorer) { test('', function () use ($explorer) { - $res = $explorer->queryArgs('SELECT id FROM author WHERE id = ? OR id = ?', [11, 12]); + $res = @$explorer->queryArgs('SELECT id FROM author WHERE id = ? OR id = ?', [11, 12]); // is deprecated Assert::same('SELECT id FROM author WHERE id = ? OR id = ?', $res->getQueryString()); Assert::same([11, 12], $res->getParameters()); }); diff --git a/tests/Database/Explorer/Explorer.backjoin.phpt b/tests/Database/Explorer/Explorer.backjoin.phpt index a56a8d89f..212234a79 100644 --- a/tests/Database/Explorer/Explorer.backjoin.phpt +++ b/tests/Database/Explorer/Explorer.backjoin.phpt @@ -39,15 +39,15 @@ test('', function () use ($explorer) { test('', function () use ($explorer, $driver) { $authorsSelection = $explorer->table('author')->where(':book.translator_id IS NOT NULL')->wherePrimary(12); - if ($driver->isSupported(Driver::SUPPORT_SCHEMA)) { + if ($driver->isSupported(Driver::SupportSchema)) { Assert::same( reformat('SELECT [author].* FROM [author] LEFT JOIN [public].[book] [book] ON [author].[id] = [book].[author_id] WHERE ([book].[translator_id] IS NOT NULL) AND ([author].[id] = ?)'), - $authorsSelection->getSql() + $authorsSelection->getSql(), ); } else { Assert::same( reformat('SELECT [author].* FROM [author] LEFT JOIN [book] ON [author].[id] = [book].[author_id] WHERE ([book].[translator_id] IS NOT NULL) AND ([author].[id] = ?)'), - $authorsSelection->getSql() + $authorsSelection->getSql(), ); } diff --git a/tests/Database/Explorer/Explorer.basic.phpt b/tests/Database/Explorer/Explorer.basic.phpt index ddad637ed..3a56f50b3 100644 --- a/tests/Database/Explorer/Explorer.basic.phpt +++ b/tests/Database/Explorer/Explorer.basic.phpt @@ -78,7 +78,7 @@ test('', function () use ($connection, $structure) { $explorer = new Nette\Database\Explorer( $connection, $structure, - new Nette\Database\Conventions\DiscoveredConventions($structure) + new Nette\Database\Conventions\DiscoveredConventions($structure), ); $book = $explorer->table('book')->get(1); diff --git a/tests/Database/Explorer/Explorer.cache.observer2.phpt b/tests/Database/Explorer/Explorer.cache.observer2.phpt index 42bf9f30e..010a22306 100644 --- a/tests/Database/Explorer/Explorer.cache.observer2.phpt +++ b/tests/Database/Explorer/Explorer.cache.observer2.phpt @@ -17,7 +17,7 @@ Nette\Database\Helpers::loadFromFile($connection, __DIR__ . "/../files/{$driverN class CacheMock extends MemoryStorage { - public $writes = 0; + public int $writes = 0; public function write(string $key, $data, array $dependencies): void diff --git a/tests/Database/Explorer/Explorer.cache.phpt b/tests/Database/Explorer/Explorer.cache.phpt index 12c1a30a9..2e6194ef5 100644 --- a/tests/Database/Explorer/Explorer.cache.phpt +++ b/tests/Database/Explorer/Explorer.cache.phpt @@ -79,7 +79,7 @@ test('Testing GroupedSelection reinvalidation caching', function () use ($explor }); -before(function () use ($cacheMemoryStorage) { +setup(function () use ($cacheMemoryStorage) { $cacheMemoryStorage->clean([Nette\Caching\Cache::ALL => true]); }); diff --git a/tests/Database/Explorer/Explorer.join-condition.phpt b/tests/Database/Explorer/Explorer.join-condition.phpt index c999edce0..a21d6838c 100644 --- a/tests/Database/Explorer/Explorer.join-condition.phpt +++ b/tests/Database/Explorer/Explorer.join-condition.phpt @@ -15,14 +15,14 @@ require __DIR__ . '/../connect.inc.php'; // create $connection Nette\Database\Helpers::loadFromFile($connection, __DIR__ . "/../files/{$driverName}-nette_test1.sql"); $driver = $connection->getDriver(); test('', function () use ($explorer, $driver) { - $schema = $driver->isSupported(Driver::SUPPORT_SCHEMA) + $schema = $driver->isSupported(Driver::SupportSchema) ? '[public].' : ''; $sql = $explorer->table('book')->joinWhere('translator', 'translator.name', 'Geek')->select('book.*')->getSql(); Assert::same(reformat( 'SELECT [book].* FROM [book] ' . - "LEFT JOIN {$schema}[author] [translator] ON [book].[translator_id] = [translator].[id] AND ([translator].[name] = ?)" + "LEFT JOIN {$schema}[author] [translator] ON [book].[translator_id] = [translator].[id] AND ([translator].[name] = ?)", ), $sql); }); @@ -34,7 +34,7 @@ test('', function () use ($explorer, $driver) { ->where('tag.name', 'PHP') ->group('tag.name') ->getSql(); - if ($driver->isSupported(Driver::SUPPORT_SCHEMA)) { + if ($driver->isSupported(Driver::SupportSchema)) { Assert::same( reformat( 'SELECT [tag].[name], COUNT([book].[id]) AS [count_of_next_volume_written_by_younger_author] FROM [tag] ' . @@ -44,9 +44,9 @@ test('', function () use ($explorer, $driver) { 'LEFT JOIN [public].[author] [next_volume_author] ON [book_ref].[author_id] = [next_volume_author].[id] ' . 'LEFT JOIN [public].[author] [author] ON [book].[author_id] = [author].[id] AND ([author].[born] < [next_volume_author].[born]) ' . 'WHERE ([tag].[name] = ?) ' . - 'GROUP BY [tag].[name]' + 'GROUP BY [tag].[name]', ), - $sql + $sql, ); } else { Assert::same( @@ -58,9 +58,9 @@ test('', function () use ($explorer, $driver) { 'LEFT JOIN [author] [next_volume_author] ON [book_ref].[author_id] = [next_volume_author].[id] ' . 'LEFT JOIN [author] ON [book].[author_id] = [author].[id] AND ([author].[born] < [next_volume_author].[born]) ' . 'WHERE ([tag].[name] = ?) ' . - 'GROUP BY [tag].[name]' + 'GROUP BY [tag].[name]', ), - $sql + $sql, ); } }); diff --git a/tests/Database/Explorer/Explorer.join.phpt b/tests/Database/Explorer/Explorer.join.phpt index 62ddc1967..2fb9f32de 100644 --- a/tests/Database/Explorer/Explorer.join.phpt +++ b/tests/Database/Explorer/Explorer.join.phpt @@ -34,15 +34,15 @@ test('', function () use ($explorer) { test('', function () use ($explorer, $driver) { $joinSql = $explorer->table('book_tag')->where('book_id', 1)->select('tag.*')->getSql(); - if ($driver->isSupported(Driver::SUPPORT_SCHEMA)) { + if ($driver->isSupported(Driver::SupportSchema)) { Assert::same( reformat('SELECT [tag].* FROM [book_tag] LEFT JOIN [public].[tag] [tag] ON [book_tag].[tag_id] = [tag].[id] WHERE ([book_id] = ?)'), - $joinSql + $joinSql, ); } else { Assert::same( reformat('SELECT [tag].* FROM [book_tag] LEFT JOIN [tag] ON [book_tag].[tag_id] = [tag].[id] WHERE ([book_id] = ?)'), - $joinSql + $joinSql, ); } }); @@ -51,15 +51,15 @@ test('', function () use ($explorer, $driver) { test('', function () use ($explorer, $driver) { $joinSql = $explorer->table('book_tag')->where('book_id', 1)->select('Tag.id')->getSql(); - if ($driver->isSupported(Driver::SUPPORT_SCHEMA)) { + if ($driver->isSupported(Driver::SupportSchema)) { Assert::same( reformat('SELECT [Tag].[id] FROM [book_tag] LEFT JOIN [public].[tag] [Tag] ON [book_tag].[tag_id] = [Tag].[id] WHERE ([book_id] = ?)'), - $joinSql + $joinSql, ); } else { Assert::same( reformat('SELECT [Tag].[id] FROM [book_tag] LEFT JOIN [tag] [Tag] ON [book_tag].[tag_id] = [Tag].[id] WHERE ([book_id] = ?)'), - $joinSql + $joinSql, ); } }); @@ -88,7 +88,7 @@ test('', function () use ($connection, $structure) { $explorer = new Nette\Database\Explorer( $connection, $structure, - new Nette\Database\Conventions\DiscoveredConventions($structure) + new Nette\Database\Conventions\DiscoveredConventions($structure), ); $books = $explorer->table('book')->select('book.*, author.name, translator.name'); diff --git a/tests/Database/Explorer/Explorer.limit.phpt b/tests/Database/Explorer/Explorer.limit.phpt index a407d5bc0..859a5bd59 100644 --- a/tests/Database/Explorer/Explorer.limit.phpt +++ b/tests/Database/Explorer/Explorer.limit.phpt @@ -16,38 +16,38 @@ Nette\Database\Helpers::loadFromFile($connection, __DIR__ . "/../files/{$driverN Assert::same( reformat('SELECT * FROM [author] ORDER BY [author].[id] LIMIT 2'), - $explorer->table('author')->limit(2)->getSql() + $explorer->table('author')->limit(2)->getSql(), ); Assert::same( reformat('SELECT * FROM [author] ORDER BY [author].[id] LIMIT 2 OFFSET 10'), - $explorer->table('author')->limit(2, 10)->getSql() + $explorer->table('author')->limit(2, 10)->getSql(), ); Assert::same( reformat('SELECT * FROM [author] ORDER BY [name] LIMIT 2'), - $explorer->table('author')->order('name')->limit(2)->getSql() + $explorer->table('author')->order('name')->limit(2)->getSql(), ); Assert::same( reformat('SELECT * FROM [author] ORDER BY [author].[id] LIMIT 10'), - $explorer->table('author')->page(1, 10)->getSql() + $explorer->table('author')->page(1, 10)->getSql(), ); Assert::same( reformat('SELECT * FROM [author] ORDER BY [author].[id] LIMIT 0'), - $explorer->table('author')->page(0, 10, $count)->getSql() + $explorer->table('author')->page(0, 10, $count)->getSql(), ); Assert::same(1, $count); Assert::same( reformat('SELECT * FROM [author] ORDER BY [author].[id] LIMIT 10 OFFSET 10'), - $explorer->table('author')->page(2, 10, $count)->getSql() + $explorer->table('author')->page(2, 10, $count)->getSql(), ); Assert::same(1, $count); Assert::same( reformat('SELECT * FROM [author] ORDER BY [author].[id] LIMIT 2 OFFSET 2'), - $explorer->table('author')->page(2, 2, $count)->getSql() + $explorer->table('author')->page(2, 2, $count)->getSql(), ); Assert::same(2, $count); diff --git a/tests/Database/Explorer/Explorer.limit.sqlsrv.phpt b/tests/Database/Explorer/Explorer.limit.sqlsrv.phpt index 29e47556f..ca894fa82 100644 --- a/tests/Database/Explorer/Explorer.limit.sqlsrv.phpt +++ b/tests/Database/Explorer/Explorer.limit.sqlsrv.phpt @@ -19,28 +19,28 @@ Assert::same( $version2008 ? 'SELECT TOP 2 * FROM [author] ORDER BY [author].[id]' : 'SELECT * FROM [author] ORDER BY [author].[id] OFFSET 0 ROWS FETCH NEXT 2 ROWS ONLY', - $explorer->table('author')->limit(2)->getSql() + $explorer->table('author')->limit(2)->getSql(), ); Assert::same( $version2008 ? 'SELECT TOP 2 * FROM [author] ORDER BY [name]' : 'SELECT * FROM [author] ORDER BY [name] OFFSET 0 ROWS FETCH NEXT 2 ROWS ONLY', - $explorer->table('author')->order('name')->limit(2)->getSql() + $explorer->table('author')->order('name')->limit(2)->getSql(), ); Assert::same( $version2008 ? 'SELECT TOP 10 * FROM [author] ORDER BY [author].[id]' : 'SELECT * FROM [author] ORDER BY [author].[id] OFFSET 0 ROWS FETCH NEXT 10 ROWS ONLY', - $explorer->table('author')->page(1, 10)->getSql() + $explorer->table('author')->page(1, 10)->getSql(), ); Assert::same( $version2008 ? 'SELECT TOP 0 * FROM [author] ORDER BY [author].[id]' : 'SELECT * FROM [author] ORDER BY [author].[id] OFFSET 0 ROWS FETCH NEXT 0 ROWS ONLY', - $explorer->table('author')->page(0, 10)->getSql() + $explorer->table('author')->page(0, 10)->getSql(), ); if ($version2008) { @@ -55,13 +55,13 @@ if ($version2008) { } else { Assert::same( reformat('SELECT * FROM [author] ORDER BY [author].[id] OFFSET 10 ROWS FETCH NEXT 10 ROWS ONLY'), - $explorer->table('author')->page(2, 10, $count)->getSql() + $explorer->table('author')->page(2, 10, $count)->getSql(), ); Assert::same(1, $count); Assert::same( reformat('SELECT * FROM [author] ORDER BY [author].[id] OFFSET 2 ROWS FETCH NEXT 2 ROWS ONLY'), - $explorer->table('author')->page(2, 2, $count)->getSql() + $explorer->table('author')->page(2, 2, $count)->getSql(), ); Assert::same(2, $count); } diff --git a/tests/Database/Explorer/Selection.fetchPairs().phpt b/tests/Database/Explorer/Selection.fetchPairs().phpt index 684c43cac..5eb1a9a50 100644 --- a/tests/Database/Explorer/Selection.fetchPairs().phpt +++ b/tests/Database/Explorer/Selection.fetchPairs().phpt @@ -41,7 +41,7 @@ test('', function () use ($explorer) { $explorer->table('author')->get(12)->update(['born' => new DateTime('2002-02-02')]); $list = $explorer->table('author')->where('born IS NOT NULL')->order('born')->fetchPairs('born', 'name'); Assert::same([ - '2002-02-02 00:00:00' => 'David Grudl', - '2002-02-20 00:00:00' => 'Jakub Vrana', + '2002-02-02 00:00:00.000000' => 'David Grudl', + '2002-02-20 00:00:00.000000' => 'Jakub Vrana', ], $list); }); diff --git a/tests/Database/Explorer/Selection.insert().phpt b/tests/Database/Explorer/Selection.insert().phpt index e3eb97902..ac79ef56b 100644 --- a/tests/Database/Explorer/Selection.insert().phpt +++ b/tests/Database/Explorer/Selection.insert().phpt @@ -22,7 +22,7 @@ $book = $explorer->table('author')->insert([ // id = 14 Assert::equal('eddard stark', $book->name); -Assert::equal(new Nette\Utils\DateTime('2011-11-11'), $book->born); +Assert::equal(new Nette\Database\DateTime('2011-11-11'), $book->born); $books = $explorer->table('book'); @@ -47,7 +47,7 @@ if ($driverName !== 'sqlsrv') { 'name' => 'Jon Snow', 'web' => 'http://example.com', ]); - }, PDOException::class); + }, Nette\Database\DriverException::class); } @@ -79,7 +79,7 @@ if ($driverName !== 'sqlsrv') { $explorer = new Nette\Database\Explorer( $connection, $structure, - new Nette\Database\Conventions\DiscoveredConventions($structure) + new Nette\Database\Conventions\DiscoveredConventions($structure), ); $inserted = $explorer->table('note')->insert([ diff --git a/tests/Database/Explorer/SqlBuilder.addAlias().phpt b/tests/Database/Explorer/SqlBuilder.addAlias().phpt index a5bf3378d..8094bda29 100644 --- a/tests/Database/Explorer/SqlBuilder.addAlias().phpt +++ b/tests/Database/Explorer/SqlBuilder.addAlias().phpt @@ -33,7 +33,7 @@ $driver = $connection->getDriver(); test('test duplicated table names throw exception', function () use ($explorer, $driver) { - $authorTable = ($driver->isSupported(Driver::SUPPORT_SCHEMA) ? 'public.' : '') . 'author'; + $authorTable = ($driver->isSupported(Driver::SupportSchema) ? 'public.' : '') . 'author'; $sqlBuilder = new SqlBuilderMock($authorTable, $explorer); $sqlBuilder->addAlias(':book(translator)', 'book1'); $sqlBuilder->addAlias(':book:book_tag', 'book2'); @@ -76,13 +76,13 @@ test('test same table chain with another alias', function () use ($explorer, $dr Assert::same( 'LEFT JOIN book translated_book ON author.id = translated_book.translator_id ' . 'LEFT JOIN book translated_book2 ON author.id = translated_book2.translator_id', - trim($join) + trim($join), ); }); test('test nested alias', function () use ($explorer, $driver) { - $sqlBuilder = $driver->isSupported(Driver::SUPPORT_SCHEMA) + $sqlBuilder = $driver->isSupported(Driver::SupportSchema) ? new SqlBuilderMock('public.author', $explorer) : new SqlBuilderMock('author', $explorer); $sqlBuilder->addAlias(':book(translator)', 'translated_book'); @@ -91,18 +91,18 @@ test('test nested alias', function () use ($explorer, $driver) { $joins = []; $sqlBuilder->parseJoins($joins, $query); $join = $sqlBuilder->buildQueryJoins($joins); - if ($driver->isSupported(Driver::SUPPORT_SCHEMA)) { + if ($driver->isSupported(Driver::SupportSchema)) { Assert::same( 'LEFT JOIN book translated_book ON author.id = translated_book.translator_id ' . 'LEFT JOIN public.book next ON translated_book.next_volume = next.id', - trim($join) + trim($join), ); } else { Assert::same( 'LEFT JOIN book translated_book ON author.id = translated_book.translator_id ' . 'LEFT JOIN book next ON translated_book.next_volume = next.id', - trim($join) + trim($join), ); } }); diff --git a/tests/Database/Explorer/SqlBuilder.addWhere().phpt b/tests/Database/Explorer/SqlBuilder.addWhere().phpt index b75a42443..66048910d 100644 --- a/tests/Database/Explorer/SqlBuilder.addWhere().phpt +++ b/tests/Database/Explorer/SqlBuilder.addWhere().phpt @@ -80,7 +80,7 @@ test('test more ActiveRow as a parameter', function () use ($explorer) { test('test Selection with parameters as a parameter', function () use ($explorer) { $sqlBuilder = new SqlBuilder('book', $explorer); $sqlBuilder->addWhere('id', $explorer->table('book')->having('COUNT(:book_tag.tag_id) >', 1)); - $schemaSupported = $explorer->getConnection()->getDriver()->isSupported(Driver::SUPPORT_SCHEMA); + $schemaSupported = $explorer->getConnection()->getDriver()->isSupported(Driver::SupportSchema); Assert::equal(reformat([ 'mysql' => 'SELECT * FROM `book` WHERE (`id` IN (?))', 'SELECT * FROM [book] WHERE ([id] IN (SELECT [id] FROM [book] LEFT JOIN ' . ($schemaSupported ? '[public].[book_tag] ' : '') . '[book_tag] ON [book].[id] = [book_tag].[book_id] HAVING COUNT([book_tag].[tag_id]) > ?))', @@ -203,7 +203,7 @@ test('tests operator suffix', function () use ($explorer) { test('', function () use ($explorer) { $books = $explorer->table('book')->where( 'id', - $explorer->table('book_tag')->select('book_id')->where('tag_id', 21) + $explorer->table('book_tag')->select('book_id')->where('tag_id', 21), ); Assert::same(3, $books->count()); }); @@ -212,7 +212,7 @@ test('', function () use ($explorer) { Assert::exception(function () use ($explorer) { $explorer->table('book')->where( 'id', - $explorer->table('book_tag')->where('tag_id', 21) + $explorer->table('book_tag')->where('tag_id', 21), ); }, Nette\InvalidArgumentException::class, 'Selection argument must have defined a select column.'); @@ -272,7 +272,7 @@ test('', function () use ($driverName, $explorer, $connection, $structure) { $e = Assert::exception(function () use ($dao) { $books = $dao->table('book')->where( 'id', - $dao->table('book_tag')->where('tag_id', 21) + $dao->table('book_tag')->where('tag_id', 21), ); $books->fetch(); }, Nette\InvalidArgumentException::class, 'Selection argument must have defined a select column.'); diff --git a/tests/Database/Explorer/SqlBuilder.parseJoinConditions().phpt b/tests/Database/Explorer/SqlBuilder.parseJoinConditions().phpt index 7f16686b2..19c73e6cc 100644 --- a/tests/Database/Explorer/SqlBuilder.parseJoinConditions().phpt +++ b/tests/Database/Explorer/SqlBuilder.parseJoinConditions().phpt @@ -74,17 +74,17 @@ test('', function () use ($explorer, $driver) { $leftJoinConditions = $sqlBuilder->parseJoinConditions($joins, $sqlBuilder->buildJoinConditions()); $join = $sqlBuilder->buildQueryJoins($joins, $leftJoinConditions); - if ($driver->isSupported(Driver::SUPPORT_SCHEMA)) { + if ($driver->isSupported(Driver::SupportSchema)) { Assert::same( 'LEFT JOIN book ON author.id = book.translator_id AND (book.id > ?) ' . 'LEFT JOIN public.book_tag_alt book_tag_alt ON book.id = book_tag_alt.book_id AND (book_tag_alt.state = ?)', - trim($join) + trim($join), ); } else { Assert::same( 'LEFT JOIN book ON author.id = book.translator_id AND (book.id > ?) ' . 'LEFT JOIN book_tag_alt ON book.id = book_tag_alt.book_id AND (book_tag_alt.state = ?)', - trim($join) + trim($join), ); } diff --git a/tests/Database/Explorer/SqlBuilder.parseJoins().phpt b/tests/Database/Explorer/SqlBuilder.parseJoins().phpt index edf39933a..2127e1cfa 100644 --- a/tests/Database/Explorer/SqlBuilder.parseJoins().phpt +++ b/tests/Database/Explorer/SqlBuilder.parseJoins().phpt @@ -43,13 +43,13 @@ $join = $sqlBuilder->buildQueryJoins($joins); Assert::same('WHERE priorit.id IS NULL', $query); $tables = $connection->getDriver()->getTables(); -if (!in_array($tables[0]['name'], ['npriorities', 'ntopics', 'nusers', 'nusers_ntopics', 'nusers_ntopics_alt'], true)) { - if ($driver->isSupported(Driver::SUPPORT_SCHEMA)) { +if (!in_array($tables[0]->name, ['npriorities', 'ntopics', 'nusers', 'nusers_ntopics', 'nusers_ntopics_alt'], true)) { + if ($driver->isSupported(Driver::SupportSchema)) { Assert::same( 'LEFT JOIN public.nUsers_nTopics nusers_ntopics ON nUsers.nUserId = nusers_ntopics.nUserId ' . 'LEFT JOIN public.nTopics topic ON nusers_ntopics.nTopicId = topic.nTopicId ' . 'LEFT JOIN public.nPriorities priorit ON topic.nPriorityId = priorit.nPriorityId', - trim($join) + trim($join), ); } else { @@ -57,7 +57,7 @@ if (!in_array($tables[0]['name'], ['npriorities', 'ntopics', 'nusers', 'nusers_n 'LEFT JOIN nUsers_nTopics nusers_ntopics ON nUsers.nUserId = nusers_ntopics.nUserId ' . 'LEFT JOIN nTopics topic ON nusers_ntopics.nTopicId = topic.nTopicId ' . 'LEFT JOIN nPriorities priorit ON topic.nPriorityId = priorit.nPriorityId', - trim($join) + trim($join), ); } @@ -66,7 +66,7 @@ if (!in_array($tables[0]['name'], ['npriorities', 'ntopics', 'nusers', 'nusers_n 'LEFT JOIN nusers_ntopics ON nUsers.nUserId = nusers_ntopics.nUserId ' . 'LEFT JOIN ntopics topic ON nusers_ntopics.nTopicId = topic.nTopicId ' . 'LEFT JOIN npriorities priorit ON topic.nPriorityId = priorit.nPriorityId', - trim($join) + trim($join), ); } @@ -83,11 +83,11 @@ $join = $sqlBuilder->buildQueryJoins($joins); Assert::same('WHERE book.next_volume IS NULL', $query); Assert::same( 'LEFT JOIN book ON author.id = book.translator_id', - trim($join) + trim($join), ); -$sqlBuilder = $driver->isSupported(Driver::SUPPORT_SCHEMA) +$sqlBuilder = $driver->isSupported(Driver::SupportSchema) ? new SqlBuilderMock('public.book', $explorer) : new SqlBuilderMock('book', $explorer); @@ -97,16 +97,16 @@ $sqlBuilder->parseJoins($joins, $query); $join = $sqlBuilder->buildQueryJoins($joins); Assert::same('WHERE book_ref.translator_id IS NULL AND book_ref_ref.translator_id IS NULL', $query); -if ($driver->isSupported(Driver::SUPPORT_SCHEMA)) { +if ($driver->isSupported(Driver::SupportSchema)) { Assert::same( 'LEFT JOIN public.book book_ref ON book.id = book_ref.next_volume ' . 'LEFT JOIN public.book book_ref_ref ON book_ref.id = book_ref_ref.next_volume', - trim($join) + trim($join), ); } else { Assert::same( 'LEFT JOIN book book_ref ON book.id = book_ref.next_volume ' . 'LEFT JOIN book book_ref_ref ON book_ref.id = book_ref_ref.next_volume', - trim($join) + trim($join), ); } diff --git a/tests/Database/Explorer/bugs/bug216.phpt b/tests/Database/Explorer/bugs/bug216.phpt index fe775ef36..03ae60b2a 100644 --- a/tests/Database/Explorer/bugs/bug216.phpt +++ b/tests/Database/Explorer/bugs/bug216.phpt @@ -16,11 +16,11 @@ $options = Tester\Environment::loadData() + ['user' => null, 'password' => null] try { $connection = new Nette\Database\Connection($options['dsn'], $options['user'], $options['password']); -} catch (PDOException $e) { +} catch (Nette\Database\ConnectionException $e) { Tester\Environment::skip("Connection to '$options[dsn]' failed. Reason: " . $e->getMessage()); } -if (strpos($options['dsn'], 'sqlite::memory:') === false) { +if (!str_contains($options['dsn'], 'sqlite::memory:')) { Tester\Environment::lock($options['dsn'], getTempDir()); } @@ -43,4 +43,4 @@ $book = $explorer->table('author')->insert([ Assert::type(Nette\Database\Table\ActiveRow::class, $book); Assert::equal('eddard stark', $book->name); -Assert::equal(new Nette\Utils\DateTime('2011-11-11'), $book->born); +Assert::equal(new Nette\Database\DateTime('2011-11-11'), $book->born); diff --git a/tests/Database/Explorer/bugs/bug49.phpt b/tests/Database/Explorer/bugs/bug49.phpt index 428af14a5..2c2706529 100644 --- a/tests/Database/Explorer/bugs/bug49.phpt +++ b/tests/Database/Explorer/bugs/bug49.phpt @@ -17,5 +17,5 @@ $explorer->query('CREATE TABLE `TABLE 30` (id int)'); Assert::same( reformat('SELECT * FROM `TABLE 30`'), - $explorer->table('TABLE 30')->getSql() + $explorer->table('TABLE 30')->getSql(), ); diff --git a/tests/Database/Explorer/bugs/view.bug.phpt b/tests/Database/Explorer/bugs/view.bug.phpt index 89907b349..0ce78920a 100644 --- a/tests/Database/Explorer/bugs/view.bug.phpt +++ b/tests/Database/Explorer/bugs/view.bug.phpt @@ -22,9 +22,7 @@ test('', function () use ($explorer) { test('', function () use ($connection) { $driver = $connection->getDriver(); $columns = $driver->getColumns('books_view'); - $columnsNames = array_map(function ($item) { - return $item['name']; - }, $columns); + $columnsNames = array_map(fn($item) => $item->name, $columns); Assert::same(['id', 'author_id', 'translator_id', 'title', 'next_volume'], $columnsNames); }); diff --git a/tests/Database/Helpers.dumpSql.phpt b/tests/Database/Helpers.dumpSql.phpt index 631f9caa7..c5b410613 100644 --- a/tests/Database/Helpers.dumpSql.phpt +++ b/tests/Database/Helpers.dumpSql.phpt @@ -17,35 +17,35 @@ Nette\Database\Helpers::loadFromFile($connection, __DIR__ . "/files/{$driverName test('int check', function () use ($connection) { Assert::same( "
SELECT id \nFROM author \nWHERE id = 10 OR id = 11
\n", - Nette\Database\Helpers::dumpSql('SELECT id FROM author WHERE id = ? OR id = ?', [10, 11], $connection) + Nette\Database\Helpers::dumpSql('SELECT id FROM author WHERE id = ? OR id = ?', [10, 11], $connection), ); }); test('bool check', function () use ($connection) { Assert::same( "
SELECT id \nFROM author \nWHERE deleted = 0
\n", - Nette\Database\Helpers::dumpSql('SELECT id FROM author WHERE deleted = ?', [false], $connection) + Nette\Database\Helpers::dumpSql('SELECT id FROM author WHERE deleted = ?', [false], $connection), ); }); test('string check', function () use ($connection) { Assert::same( "
SELECT id \nFROM author \nWHERE name = 'Alexej Chruščev'
\n", - Nette\Database\Helpers::dumpSql('SELECT id FROM author WHERE name = ?', ['Alexej Chruščev'], $connection) + Nette\Database\Helpers::dumpSql('SELECT id FROM author WHERE name = ?', ['Alexej Chruščev'], $connection), ); }); test('string check with \'', function () use ($connection) { Assert::same( "
SELECT id \nFROM author \nWHERE name = 'Alexej Ch\\'ruščev'
\n", - Nette\Database\Helpers::dumpSql('SELECT id FROM author WHERE name = ?', ["Alexej Ch'ruščev"], $connection) + Nette\Database\Helpers::dumpSql('SELECT id FROM author WHERE name = ?', ["Alexej Ch'ruščev"], $connection), ); }); test('string check without connection', function () { Assert::same( "
SELECT id \nFROM author \nWHERE name = 'Alexej Ch'ruščev'
\n", - Nette\Database\Helpers::dumpSql('SELECT id FROM author WHERE name = ?', ["Alexej Ch'ruščev"]) + Nette\Database\Helpers::dumpSql('SELECT id FROM author WHERE name = ?', ["Alexej Ch'ruščev"]), ); }); @@ -58,6 +58,6 @@ test('string check with \'', function () use ($connection) { Nette\Database\Helpers::$maxLength = 10; Assert::same( "
SELECT id \nFROM author \nWHERE name = 'Alexej Ch…'
\n", - Nette\Database\Helpers::dumpSql('SELECT id FROM author WHERE name = ?', ["Alexej Ch'ruščev"], $connection) + Nette\Database\Helpers::dumpSql('SELECT id FROM author WHERE name = ?', ["Alexej Ch'ruščev"], $connection), ); }); diff --git a/tests/Database/Reflection.phpt b/tests/Database/Reflection.phpt index e5d5073f1..40ed2a61a 100644 --- a/tests/Database/Reflection.phpt +++ b/tests/Database/Reflection.phpt @@ -8,6 +8,9 @@ declare(strict_types=1); use Nette\Database\Driver; +use Nette\Database\Reflection\Column; +use Nette\Database\Reflection\Index; +use Nette\Database\Reflection\Table; use Tester\Assert; require __DIR__ . '/connect.inc.php'; // create $connection @@ -17,74 +20,73 @@ Nette\Database\Helpers::loadFromFile($connection, __DIR__ . "/files/{$driverName $driver = $connection->getDriver(); $tables = $driver->getTables(); -$tables = array_filter($tables, function ($t) { return in_array($t['name'], ['author', 'book', 'book_tag', 'tag'], true); }); -usort($tables, function ($a, $b) { return strcmp($a['name'], $b['name']); }); +$tables = array_filter($tables, fn($t) => in_array($t->name, ['author', 'book', 'book_tag', 'tag'], true)); +usort($tables, fn($a, $b) => strcmp($a->name, $b->name)); -if ($driver->isSupported(Driver::SUPPORT_SCHEMA)) { - Assert::same( +if ($driver->isSupported(Driver::SupportSchema)) { + Assert::equal( [ - ['name' => 'author', 'view' => false, 'fullName' => 'public.author'], - ['name' => 'book', 'view' => false, 'fullName' => 'public.book'], - ['name' => 'book_tag', 'view' => false, 'fullName' => 'public.book_tag'], - ['name' => 'tag', 'view' => false, 'fullName' => 'public.tag'], + new Table(name: 'author', view: false, fullName: 'public.author'), + new Table(name: 'book', view: false, fullName: 'public.book'), + new Table(name: 'book_tag', view: false, fullName: 'public.book_tag'), + new Table(name: 'tag', view: false, fullName: 'public.tag'), ], - $tables + $tables, ); } else { - Assert::same([ - ['name' => 'author', 'view' => false], - ['name' => 'book', 'view' => false], - ['name' => 'book_tag', 'view' => false], - ['name' => 'tag', 'view' => false], + Assert::equal([ + new Table(name: 'author', view: false), + new Table(name: 'book', view: false), + new Table(name: 'book_tag', view: false), + new Table(name: 'tag', view: false), ], $tables); } $columns = $driver->getColumns('author'); array_walk($columns, function (&$item) { - Assert::type('array', $item['vendor']); - unset($item['vendor']); + $item->vendor = []; }); $expectedColumns = [ [ 'name' => 'id', 'table' => 'author', - 'nativetype' => 'INT', + 'nativeType' => 'int', 'size' => 11, 'nullable' => false, 'default' => null, - 'autoincrement' => true, + 'autoIncrement' => true, 'primary' => true, ], [ 'name' => 'name', 'table' => 'author', - 'nativetype' => 'VARCHAR', + 'nativeType' => 'varchar', 'size' => 30, 'nullable' => false, 'default' => null, - 'autoincrement' => false, + 'autoIncrement' => false, 'primary' => false, ], [ 'name' => 'web', 'table' => 'author', - 'nativetype' => 'VARCHAR', + 'nativeType' => 'varchar', 'size' => 100, 'nullable' => false, 'default' => null, - 'autoincrement' => false, + 'autoIncrement' => false, 'primary' => false, ], [ 'name' => 'born', 'table' => 'author', - 'nativetype' => 'DATE', + 'nativeType' => 'date', 'size' => null, 'nullable' => true, 'default' => null, - 'autoincrement' => false, + 'autoIncrement' => false, 'primary' => false, ], ]; @@ -97,17 +99,18 @@ switch ($driverName) { } break; case 'pgsql': - $expectedColumns[0]['nativetype'] = 'INT4'; + $expectedColumns[0]['nativeType'] = 'int4'; $expectedColumns[0]['default'] = "nextval('author_id_seq'::regclass)"; $expectedColumns[0]['size'] = null; break; case 'sqlite': - $expectedColumns[0]['nativetype'] = 'INTEGER'; + $expectedColumns[0]['nativeType'] = 'INTEGER'; $expectedColumns[0]['size'] = null; - $expectedColumns[1]['nativetype'] = 'TEXT'; + $expectedColumns[1]['nativeType'] = 'TEXT'; $expectedColumns[1]['size'] = null; - $expectedColumns[2]['nativetype'] = 'TEXT'; + $expectedColumns[2]['nativeType'] = 'TEXT'; $expectedColumns[2]['size'] = null; + $expectedColumns[3]['nativeType'] = 'DATE'; break; case 'sqlsrv': $expectedColumns[0]['size'] = null; @@ -118,69 +121,70 @@ switch ($driverName) { Assert::fail("Unsupported driver $driverName"); } -Assert::same($expectedColumns, $columns); +$expectedColumns = array_map(fn($data) => new Column(...$data), $expectedColumns); +Assert::equal($expectedColumns, $columns); $indexes = $driver->getIndexes('book_tag'); switch ($driverName) { case 'pgsql': - Assert::same([ - [ - 'name' => 'book_tag_pkey', - 'unique' => true, - 'primary' => true, - 'columns' => [ + Assert::equal([ + new Index( + name: 'book_tag_pkey', + unique: true, + primary: true, + columns: [ 'book_id', 'tag_id', ], - ], + ), ], $indexes); break; case 'sqlite': - Assert::same([ - [ - 'name' => 'sqlite_autoindex_book_tag_1', - 'unique' => true, - 'primary' => true, - 'columns' => [ + Assert::equal([ + new Index( + name: 'sqlite_autoindex_book_tag_1', + unique: true, + primary: true, + columns: [ 'book_id', 'tag_id', ], - ], + ), ], $indexes); break; case 'sqlsrv': - Assert::same([ - [ - 'name' => 'PK_book_tag', - 'unique' => true, - 'primary' => true, - 'columns' => [ + Assert::equal([ + new Index( + name: 'PK_book_tag', + unique: true, + primary: true, + columns: [ 'book_id', 'tag_id', ], - ], + ), ], $indexes); break; case 'mysql': - Assert::same([ - [ - 'name' => 'PRIMARY', - 'unique' => true, - 'primary' => true, - 'columns' => [ + Assert::equal([ + new Index( + name: 'PRIMARY', + unique: true, + primary: true, + columns: [ 'book_id', 'tag_id', ], - ], - [ - 'name' => 'book_tag_tag', - 'unique' => false, - 'primary' => false, - 'columns' => [ + ), + new Index( + name: 'book_tag_tag', + unique: false, + primary: false, + columns: [ 'tag_id', ], - ], + ), ], $indexes); break; default: diff --git a/tests/Database/Reflection.postgre.10.phpt b/tests/Database/Reflection.postgre.10.phpt index 38f58327a..b11263765 100644 --- a/tests/Database/Reflection.postgre.10.phpt +++ b/tests/Database/Reflection.postgre.10.phpt @@ -7,6 +7,9 @@ declare(strict_types=1); +use Nette\Database\Reflection\Column; +use Nette\Database\Reflection\Index; +use Nette\Database\Reflection\Table; use Tester\Assert; use Tester\Environment; @@ -21,13 +24,11 @@ if (version_compare($ver, '10') < 0) { function shortInfo(array $columns): array { - return array_map(function (array $col): array { - return [ - 'name' => $col['name'], - 'autoincrement' => $col['autoincrement'], - 'sequence' => $col['vendor']['sequence'], - ]; - }, $columns); + return array_map(fn(Column $col): array => [ + 'name' => $col->name, + 'autoincrement' => $col->autoIncrement, + 'sequence' => $col->vendor['sequence'], + ], $columns); } @@ -116,14 +117,14 @@ test('Materialized view columns', function () use ($connection) { $connection->query('SET search_path TO reflection_10'); - Assert::same([ - ['name' => 'source', 'view' => false, 'fullName' => 'reflection_10.source'], - ['name' => 'source_mt', 'view' => true, 'fullName' => 'reflection_10.source_mt'], + Assert::equal([ + new Table(name: 'source', view: false, fullName: 'reflection_10.source'), + new Table(name: 'source_mt', view: true, fullName: 'reflection_10.source_mt'), ], $driver->getTables()); Assert::same( ['name', 'id'], - array_column($driver->getColumns('source_mt'), 'name') + array_column($driver->getColumns('source_mt'), 'name'), ); }); @@ -145,18 +146,18 @@ test('Partitioned table', function () use ($connection) { $connection->query('SET search_path TO reflection_10'); - Assert::same([ - ['name' => 'part_1', 'view' => false, 'fullName' => 'reflection_10.part_1'], - ['name' => 'parted', 'view' => false, 'fullName' => 'reflection_10.parted'], + Assert::equal([ + new Table(name: 'part_1', view: false, fullName: 'reflection_10.part_1'), + new Table(name: 'parted', view: false, fullName: 'reflection_10.parted'), ], $driver->getTables()); Assert::same(['id', 'value'], array_column($driver->getColumns('parted'), 'name')); Assert::same(['id', 'value'], array_column($driver->getColumns('part_1'), 'name')); - Assert::same([[ - 'name' => 'parted_pkey', - 'unique' => true, - 'primary' => true, - 'columns' => ['id'], - ]], $driver->getIndexes('parted')); + Assert::equal([new Index( + name: 'parted_pkey', + unique: true, + primary: true, + columns: ['id'], + )], $driver->getIndexes('parted')); }); diff --git a/tests/Database/Reflection.postgre.phpt b/tests/Database/Reflection.postgre.phpt index c5460661d..187e916ad 100644 --- a/tests/Database/Reflection.postgre.phpt +++ b/tests/Database/Reflection.postgre.phpt @@ -7,6 +7,7 @@ declare(strict_types=1); +use Nette\Database\Reflection\ForeignKey; use Tester\Assert; require __DIR__ . '/connect.inc.php'; // create $connection @@ -61,12 +62,12 @@ test('Tables in schema', function () use ($connection) { Assert::same(['one_id'], names($driver->getColumns('one.master'))); Assert::same(['one_master_pkey'], names($driver->getIndexes('one.master'))); $foreign = $driver->getForeignKeys('one.slave'); - Assert::same([ - 'name' => 'one_slave_fk', - 'local' => 'one_id', - 'table' => 'one.master', - 'foreign' => 'one_id', - ], (array) $foreign[0]); + Assert::equal(new ForeignKey( + name: 'one_slave_fk', + columns: ['one_id'], + targetTable: 'one.master', + targetColumns: ['one_id'], + ), $foreign[0]); // Limit foreign keys for current schemas only diff --git a/tests/Database/ResultSet.customNormalizer.phpt b/tests/Database/ResultSet.customNormalizer.phpt index 25cf4a3b6..7c85d01fa 100644 --- a/tests/Database/ResultSet.customNormalizer.phpt +++ b/tests/Database/ResultSet.customNormalizer.phpt @@ -24,7 +24,21 @@ test('disabled normalization', function () use ($connection) { 'id' => $asInt ? 11 : '11', 'name' => 'Jakub Vrana', 'web' => 'http://www.vrana.cz/', - 'born' => $driverName === 'sqlite' ? ($asInt ? 1642892400 : '1642892400') : '2022-01-23', + 'born' => $driverName === 'sqlite' ? ($asInt ? 1_642_892_400 : '1642892400') : '2022-01-23', + ], (array) $res->fetch()); +}); + + +test('configured RowNormalizer', function () use ($connection) { + $driverName = $GLOBALS['driverName']; + + $connection->setRowNormalizer((new Nette\Database\RowNormalizer)->skipDateTime()); + $res = $connection->query('SELECT * FROM author'); + Assert::same([ + 'id' => 11, + 'name' => 'Jakub Vrana', + 'web' => 'http://www.vrana.cz/', + 'born' => $driverName === 'sqlite' ? (PHP_VERSION_ID >= 80100 ? 1_642_892_400 : '1642892400') : '2022-01-23', ], (array) $res->fetch()); }); diff --git a/tests/Database/ResultSet.fetchAssoc().phpt b/tests/Database/ResultSet.fetchAssoc().phpt index 92bd13fec..cd8f0fc5b 100644 --- a/tests/Database/ResultSet.fetchAssoc().phpt +++ b/tests/Database/ResultSet.fetchAssoc().phpt @@ -51,7 +51,7 @@ test('', function () use ($connection) { $pairs = $connection->query('UPDATE author SET born = ? WHERE id = 12', new DateTime('2002-02-02')); $pairs = $connection->query('SELECT * FROM author WHERE born IS NOT NULL ORDER BY born')->fetchAssoc('born=name'); Assert::same([ - '2002-02-02 00:00:00' => 'David Grudl', - '2002-02-20 00:00:00' => 'Jakub Vrana', + '2002-02-02 00:00:00.000000' => 'David Grudl', + '2002-02-20 00:00:00.000000' => 'Jakub Vrana', ], $pairs); }); diff --git a/tests/Database/ResultSet.fetchPairs().phpt b/tests/Database/ResultSet.fetchPairs().phpt index 58d6696c1..48acb6af9 100644 --- a/tests/Database/ResultSet.fetchPairs().phpt +++ b/tests/Database/ResultSet.fetchPairs().phpt @@ -70,8 +70,8 @@ test('', function () use ($connection) { $pairs = $connection->query('UPDATE author SET born = ? WHERE id = 12', new DateTime('2002-02-02')); $pairs = $connection->query('SELECT * FROM author WHERE born IS NOT NULL ORDER BY born')->fetchPairs('born', 'name'); Assert::same([ - '2002-02-02 00:00:00' => 'David Grudl', - '2002-02-20 00:00:00' => 'Jakub Vrana', + '2002-02-02 00:00:00.000000' => 'David Grudl', + '2002-02-20 00:00:00.000000' => 'Jakub Vrana', ], $pairs); }); @@ -125,8 +125,8 @@ $pairs = $connection->query('UPDATE author SET born = ? WHERE id = 11', new Date $pairs = $connection->query('UPDATE author SET born = ? WHERE id = 12', new DateTime('2002-02-02')); $pairs = $connection->query('SELECT * FROM author WHERE born IS NOT NULL ORDER BY born')->fetchPairs('born', 'name'); Assert::same([ - '2002-02-02 00:00:00' => 'David Grudl', - '2002-02-20 00:00:00' => 'Jakub Vrana', + '2002-02-02 00:00:00.000000' => 'David Grudl', + '2002-02-20 00:00:00.000000' => 'Jakub Vrana', ], $pairs); diff --git a/tests/Database/ResultSet.normalizeRow.mysql.phpt b/tests/Database/ResultSet.normalizeRow.mysql.phpt index 35a02a313..c69f5f415 100644 --- a/tests/Database/ResultSet.normalizeRow.mysql.phpt +++ b/tests/Database/ResultSet.normalizeRow.mysql.phpt @@ -7,7 +7,7 @@ declare(strict_types=1); -use Nette\Utils\DateTime; +use Nette\Database\DateTime; use Tester\Assert; require __DIR__ . '/connect.inc.php'; // create $connection @@ -62,10 +62,10 @@ Assert::equal([ 'decimal2' => 0.5, 'float' => 0.5, 'double' => 0.5, - 'date' => new DateTime('0000-00-00 00:00:00'), + 'date' => null, 'time' => new DateInterval('P0D'), - 'datetime' => new DateTime('0000-00-00 00:00:00'), - 'timestamp' => new DateTime('0000-00-00 00:00:00'), + 'datetime' => null, + 'timestamp' => null, 'year' => 2000, 'char' => '', 'varchar' => '', diff --git a/tests/Database/ResultSet.normalizeRow.postgre.phpt b/tests/Database/ResultSet.normalizeRow.postgre.phpt index 2266c2469..30a58776e 100644 --- a/tests/Database/ResultSet.normalizeRow.postgre.phpt +++ b/tests/Database/ResultSet.normalizeRow.postgre.phpt @@ -7,7 +7,7 @@ declare(strict_types=1); -use Nette\Utils\DateTime; +use Nette\Database\DateTime; use Tester\Assert; require __DIR__ . '/connect.inc.php'; // create $connection diff --git a/tests/Database/ResultSet.normalizeRow.sqlite.phpt b/tests/Database/ResultSet.normalizeRow.sqlite.phpt index 58a897812..916ef9f87 100644 --- a/tests/Database/ResultSet.normalizeRow.sqlite.phpt +++ b/tests/Database/ResultSet.normalizeRow.sqlite.phpt @@ -7,7 +7,7 @@ declare(strict_types=1); -use Nette\Utils\DateTime; +use Nette\Database\DateTime; use Tester\Assert; require __DIR__ . '/connect.inc.php'; // create $connection diff --git a/tests/Database/ResultSet.normalizeRow.sqlsrv.phpt b/tests/Database/ResultSet.normalizeRow.sqlsrv.phpt index e93ef75e1..19ed59e46 100644 --- a/tests/Database/ResultSet.normalizeRow.sqlsrv.phpt +++ b/tests/Database/ResultSet.normalizeRow.sqlsrv.phpt @@ -7,7 +7,7 @@ declare(strict_types=1); -use Nette\Utils\DateTime; +use Nette\Database\DateTime; use Tester\Assert; require __DIR__ . '/connect.inc.php'; // create $connection @@ -131,13 +131,13 @@ function isTimestamp($str) $row = (array) $connection->query('SELECT [datetimeoffset], CAST([sql_variant] AS int) AS [sql_variant], [timestamp] FROM types2 WHERE id = 1')->fetch(); -Assert::type('DateTime', $row['datetimeoffset']); +Assert::type(DateTime::class, $row['datetimeoffset']); Assert::same($row['datetimeoffset']->format('Y-m-d H:i:s P'), '2012-10-13 10:10:10 +02:00'); Assert::same($row['sql_variant'], 123456); Assert::true(isTimestamp($row['timestamp'])); $row = (array) $connection->query('SELECT [datetimeoffset], CAST([sql_variant] AS varchar) AS [sql_variant], [timestamp] FROM types2 WHERE id = 2')->fetch(); -Assert::type('DateTime', $row['datetimeoffset']); +Assert::type(DateTime::class, $row['datetimeoffset']); Assert::same($row['datetimeoffset']->format('Y-m-d H:i:s P'), '0001-01-01 00:00:00 +00:00'); Assert::same($row['sql_variant'], 'abcd'); Assert::true(isTimestamp($row['timestamp'])); diff --git a/tests/Database/ResultSet.parameters.mysql.phpt b/tests/Database/ResultSet.parameters.mysql.phpt index 99498ae7a..9f0bfd464 100644 --- a/tests/Database/ResultSet.parameters.mysql.phpt +++ b/tests/Database/ResultSet.parameters.mysql.phpt @@ -15,5 +15,5 @@ $res = $connection->fetch('SELECT ? AS c1, ? AS c2, ? AS c3, ? as c4', fopen(__F Assert::same( ['c1' => file_get_contents(__FILE__), 'c2' => 1, 'c3' => null, 'c4' => 123], - (array) $res + (array) $res, ); diff --git a/tests/Database/ResultSet.parameters.postgre.phpt b/tests/Database/ResultSet.parameters.postgre.phpt index 327207c48..03b8a5958 100644 --- a/tests/Database/ResultSet.parameters.postgre.phpt +++ b/tests/Database/ResultSet.parameters.postgre.phpt @@ -15,5 +15,5 @@ $res = $connection->fetch('SELECT ?::bool AS c1, ? AS c2, ?::int AS c3', true, n Assert::same( ['c1' => true, 'c2' => null, 'c3' => 123], - (array) $res + (array) $res, ); diff --git a/tests/Database/SqlPreprocessor.phpt b/tests/Database/SqlPreprocessor.phpt index d85beebc1..b43e3a3c6 100644 --- a/tests/Database/SqlPreprocessor.phpt +++ b/tests/Database/SqlPreprocessor.phpt @@ -104,6 +104,11 @@ test('IN', function () use ($preprocessor) { Assert::same([10, 11], $params); }); +test('BETWEEN', function () use ($preprocessor) { + [$sql, $params] = $preprocessor->process(['SELECT id FROM author WHERE id BETWEEN (?, ?)', [10, 11]]); + Assert::same('SELECT id FROM author WHERE id BETWEEN (?, ?)', $sql); + Assert::same([10, 11], $params); +}); test('?name', function () use ($preprocessor) { [$sql, $params] = $preprocessor->process(['SELECT id FROM author WHERE ?name = ? OR ?name = ?', 'id', 12, 'table.number', 23]); diff --git a/tests/Database/Structure.phpt b/tests/Database/Structure.phpt index 852dfb5db..54307ae76 100644 --- a/tests/Database/Structure.phpt +++ b/tests/Database/Structure.phpt @@ -7,6 +7,9 @@ declare(strict_types=1); use Mockery\MockInterface; +use Nette\Database\Reflection\Column; +use Nette\Database\Reflection\ForeignKey; +use Nette\Database\Reflection\Table; use Nette\Database\Structure; use Tester\Assert; use Tester\TestCase; @@ -18,7 +21,7 @@ class StructureMock extends Structure { protected function needStructure(): void { - if (!$this->structure) { + if (!isset($this->structure)) { $this->structure = $this->loadStructure(); } } @@ -30,17 +33,13 @@ class StructureMock extends Structure */ class StructureTestCase extends TestCase { - /** @var MockInterface */ - private $connection; + private MockInterface $connection; - /** @var MockInterface */ - private $driver; + private MockInterface $driver; - /** @var MockInterface */ - private $storage; + private MockInterface $storage; - /** @var Structure */ - private $structure; + private Structure $structure; protected function setUp() @@ -53,42 +52,42 @@ class StructureTestCase extends TestCase $this->connection->shouldReceive('getDsn')->once()->andReturn(''); $this->connection->shouldReceive('getDriver')->once()->andReturn($this->driver); $this->driver->shouldReceive('getTables')->once()->andReturn([ - ['name' => 'authors', 'view' => false], - ['name' => 'Books', 'view' => false], - ['name' => 'tags', 'view' => false], - ['name' => 'books_x_tags', 'view' => false], - ['name' => 'books_view', 'view' => true], + new Table(name: 'authors', view: false), + new Table(name: 'Books', view: false), + new Table(name: 'tags', view: false), + new Table(name: 'books_x_tags', view: false), + new Table(name: 'books_view', view: true), ]); $this->driver->shouldReceive('getColumns')->with('authors')->once()->andReturn([ - ['name' => 'id', 'primary' => true, 'autoincrement' => true, 'vendor' => ['sequence' => '"public"."authors_id_seq"']], - ['name' => 'name', 'primary' => false, 'autoincrement' => false, 'vendor' => []], + new Column(name: 'id', primary: true, autoIncrement: true, vendor: ['sequence' => '"public"."authors_id_seq"']), + new Column(name: 'name', primary: false, autoIncrement: false, vendor: []), ]); $this->driver->shouldReceive('getColumns')->with('Books')->once()->andReturn([ - ['name' => 'id', 'primary' => true, 'autoincrement' => true, 'vendor' => ['sequence' => '"public"."Books_id_seq"']], - ['name' => 'title', 'primary' => false, 'autoincrement' => false, 'vendor' => []], + new Column(name: 'id', primary: true, autoIncrement: true, vendor: ['sequence' => '"public"."Books_id_seq"']), + new Column(name: 'title', primary: false, autoIncrement: false, vendor: []), ]); $this->driver->shouldReceive('getColumns')->with('tags')->once()->andReturn([ - ['name' => 'id', 'primary' => true, 'autoincrement' => false, 'vendor' => []], - ['name' => 'name', 'primary' => false, 'autoincrement' => false, 'vendor' => []], + new Column(name: 'id', primary: true, autoIncrement: false, vendor: []), + new Column(name: 'name', primary: false, autoIncrement: false, vendor: []), ]); $this->driver->shouldReceive('getColumns')->with('books_x_tags')->once()->andReturn([ - ['name' => 'book_id', 'primary' => true, 'autoincrement' => false, 'vendor' => []], - ['name' => 'tag_id', 'primary' => true, 'autoincrement' => false, 'vendor' => []], + new Column(name: 'book_id', primary: true, autoIncrement: false, vendor: []), + new Column(name: 'tag_id', primary: true, autoIncrement: false, vendor: []), ]); $this->driver->shouldReceive('getColumns')->with('books_view')->once()->andReturn([ - ['name' => 'id', 'primary' => false, 'autoincrement' => false, 'vendor' => []], - ['name' => 'title', 'primary' => false, 'autoincrement' => false, 'vendor' => []], + new Column(name: 'id', primary: false, autoIncrement: false, vendor: []), + new Column(name: 'title', primary: false, autoIncrement: false, vendor: []), ]); $this->connection->shouldReceive('getDriver')->times(4)->andReturn($this->driver); $this->driver->shouldReceive('getForeignKeys')->with('authors')->once()->andReturn([]); $this->driver->shouldReceive('getForeignKeys')->with('Books')->once()->andReturn([ - ['local' => 'author_id', 'table' => 'authors', 'foreign' => 'id', 'name' => 'authors_fk1'], - ['local' => 'translator_id', 'table' => 'authors', 'foreign' => 'id', 'name' => 'authors_fk2'], + new ForeignKey(columns: ['author_id'], targetTable: 'authors', targetColumns: ['id'], name: 'authors_fk1'), + new ForeignKey(columns: ['translator_id'], targetTable: 'authors', targetColumns: ['id'], name: 'authors_fk2'), ]); $this->driver->shouldReceive('getForeignKeys')->with('tags')->once()->andReturn([]); $this->driver->shouldReceive('getForeignKeys')->with('books_x_tags')->once()->andReturn([ - ['local' => 'book_id', 'table' => 'Books', 'foreign' => 'id', 'name' => 'books_x_tags_fk1'], - ['local' => 'tag_id', 'table' => 'tags', 'foreign' => 'id', 'name' => 'books_x_tags_fk2'], + new ForeignKey(columns: ['book_id'], targetTable: 'Books', targetColumns: ['id'], name: 'books_x_tags_fk1'), + new ForeignKey(columns: ['tag_id'], targetTable: 'tags', targetColumns: ['id'], name: 'books_x_tags_fk2'), ]); $this->structure = new StructureMock($this->connection, $this->storage); @@ -97,12 +96,12 @@ class StructureTestCase extends TestCase public function testGetTables() { - Assert::same([ - ['name' => 'authors', 'view' => false], - ['name' => 'Books', 'view' => false], - ['name' => 'tags', 'view' => false], - ['name' => 'books_x_tags', 'view' => false], - ['name' => 'books_view', 'view' => true], + Assert::equal([ + new Table(name: 'authors', view: false), + new Table(name: 'Books', view: false), + new Table(name: 'tags', view: false), + new Table(name: 'books_x_tags', view: false), + new Table(name: 'books_view', view: true), ], $this->structure->getTables()); } @@ -110,12 +109,12 @@ class StructureTestCase extends TestCase public function testGetColumns() { $columns = [ - ['name' => 'id', 'primary' => true, 'autoincrement' => false, 'vendor' => []], - ['name' => 'name', 'primary' => false, 'autoincrement' => false, 'vendor' => []], + new Column(name: 'id', primary: true, autoIncrement: false, vendor: []), + new Column(name: 'name', primary: false, autoIncrement: false, vendor: []), ]; - Assert::same($columns, $this->structure->getColumns('tags')); - Assert::same($columns, $this->structure->getColumns('Tags')); + Assert::equal($columns, $this->structure->getColumns('tags')); + Assert::equal($columns, $this->structure->getColumns('Tags')); $structure = $this->structure; Assert::exception(function () use ($structure) { @@ -155,7 +154,7 @@ class StructureTestCase extends TestCase Assert::same( ['author_id', 'translator_id'], - $this->structure->getHasManyReference('authors', 'books') + $this->structure->getHasManyReference('authors', 'books'), ); } @@ -176,7 +175,7 @@ class StructureTestCase extends TestCase Assert::same( ['Books', 'book_id'], - $this->structure->getBelongsToReference('books_x_tags', 'book_id') + $this->structure->getBelongsToReference('books_x_tags', 'book_id'), ); Assert::null($this->structure->getBelongsToReference('books_x_tags', 'non_exist')); diff --git a/tests/Database/Structure.schemas.phpt b/tests/Database/Structure.schemas.phpt index e840c1747..077a18875 100644 --- a/tests/Database/Structure.schemas.phpt +++ b/tests/Database/Structure.schemas.phpt @@ -7,6 +7,9 @@ declare(strict_types=1); use Mockery\MockInterface; +use Nette\Database\Reflection\Column; +use Nette\Database\Reflection\ForeignKey; +use Nette\Database\Reflection\Table; use Nette\Database\Structure; use Tester\Assert; use Tester\TestCase; @@ -18,7 +21,7 @@ class StructureMock extends Structure { protected function needStructure(): void { - if (!$this->structure) { + if (!isset($this->structure)) { $this->structure = $this->loadStructure(); } } @@ -30,17 +33,13 @@ class StructureMock extends Structure */ class StructureSchemasTestCase extends TestCase { - /** @var MockInterface */ - private $connection; + private MockInterface $connection; - /** @var MockInterface */ - private $driver; + private MockInterface $driver; - /** @var MockInterface */ - private $storage; + private MockInterface $storage; - /** @var Structure */ - private $structure; + private Structure $structure; protected function setUp() @@ -53,23 +52,23 @@ class StructureSchemasTestCase extends TestCase $this->connection->shouldReceive('getDsn')->once()->andReturn(''); $this->connection->shouldReceive('getDriver')->once()->andReturn($this->driver); $this->driver->shouldReceive('getTables')->once()->andReturn([ - ['name' => 'authors', 'view' => false, 'fullName' => 'authors.authors'], - ['name' => 'books', 'view' => false, 'fullName' => 'books.books'], + new Table(name: 'authors', view: false, fullName: 'authors.authors'), + new Table(name: 'books', view: false, fullName: 'books.books'), ]); $this->driver->shouldReceive('getColumns')->with('authors.authors')->once()->andReturn([ - ['name' => 'id', 'primary' => true, 'vendor' => ['sequence' => '"authors"."authors_id_seq"']], - ['name' => 'name', 'primary' => false, 'vendor' => []], + new Column(name: 'id', primary: true, vendor: ['sequence' => '"authors"."authors_id_seq"']), + new Column(name: 'name', primary: false, vendor: []), ]); $this->driver->shouldReceive('getColumns')->with('books.books')->once()->andReturn([ - ['name' => 'id', 'primary' => true, 'vendor' => ['sequence' => '"books"."books_id_seq"']], - ['name' => 'title', 'primary' => false, 'vendor' => []], + new Column(name: 'id', primary: true, vendor: ['sequence' => '"books"."books_id_seq"']), + new Column(name: 'title', primary: false, vendor: []), ]); $this->connection->shouldReceive('getDriver')->times(2)->andReturn($this->driver); $this->driver->shouldReceive('getForeignKeys')->with('authors.authors')->once()->andReturn([]); $this->driver->shouldReceive('getForeignKeys')->with('books.books')->once()->andReturn([ - ['local' => 'author_id', 'table' => 'authors.authors', 'foreign' => 'id', 'name' => 'authors_authors_fk1'], - ['local' => 'translator_id', 'table' => 'authors.authors', 'foreign' => 'id', 'name' => 'authors_authors_fk2'], + new ForeignKey(columns: ['author_id'], targetTable: 'authors.authors', targetColumns: ['id'], name: 'authors_authors_fk1'), + new ForeignKey(columns: ['translator_id'], targetTable: 'authors.authors', targetColumns: ['id'], name: 'authors_authors_fk2'), ]); $this->structure = new StructureMock($this->connection, $this->storage); diff --git a/tests/Database/connect.inc.php b/tests/Database/connect.inc.php index eaa773522..6defd138f 100644 --- a/tests/Database/connect.inc.php +++ b/tests/Database/connect.inc.php @@ -13,11 +13,11 @@ try { $connection = new Nette\Database\Connection($options['dsn'], $options['user'], $options['password']); -} catch (PDOException $e) { +} catch (Nette\Database\ConnectionException $e) { Tester\Environment::skip("Connection to '$options[dsn]' failed. Reason: " . $e->getMessage()); } -if (strpos($options['dsn'], 'sqlite::memory:') === false) { +if (!str_contains($options['dsn'], 'sqlite::memory:')) { Tester\Environment::lock($options['dsn'], getTempDir()); } diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 0a821deca..537cfda16 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -13,7 +13,7 @@ // configure environment Tester\Environment::setup(); -date_default_timezone_set('Europe/Prague'); +Tester\Environment::setupFunctions(); function getTempDir(): string @@ -22,20 +22,3 @@ function getTempDir(): string @mkdir($dir); return $dir; } - - -function before(?Closure $function = null) -{ - static $val; - if (!func_num_args()) { - return $val ? $val() : null; - } - $val = $function; -} - - -function test(string $title, Closure $function): void -{ - before(); - $function(); -} diff --git a/tests/php-win.ini b/tests/php-win.ini deleted file mode 100644 index 7d024ac2e..000000000 --- a/tests/php-win.ini +++ /dev/null @@ -1,9 +0,0 @@ -[PHP] -extension_dir = "./ext" -extension=php_pdo_mysql.dll -extension=php_pdo_sqlite.dll -extension=php_pdo_pgsql.dll -extension=php_pdo_sqlsrv_ts.dll - -[Zend] -;zend_extension="./ext/php_xdebug-2.0.5-5.3-vc6.dll" diff --git a/tests/php.ini b/tests/php.ini new file mode 100644 index 000000000..0fcf36853 --- /dev/null +++ b/tests/php.ini @@ -0,0 +1,5 @@ +[PHP] +extension=pdo_mysql +extension=pdo_sqlite +extension=pdo_pgsql +extension=pdo_sqlsrv_ts