diff --git a/composer.json b/composer.json index 40f6ead4e..3c0eb016f 100644 --- a/composer.json +++ b/composer.json @@ -38,7 +38,7 @@ }, "extra": { "branch-alias": { - "dev-master": "3.2-dev" + "dev-master": "4.0-dev" } } } diff --git a/readme.md b/readme.md index c89cb1e87..144c6bbee 100644 --- a/readme.md +++ b/readme.md @@ -1,25 +1,26 @@ -Nette Database -============== +[![Nette Database](https://github.com/nette/database/assets/194960/97d8f31b-096c-466c-a76f-f5b9e511ea8d)](https://doc.nette.org/database) [![Downloads this Month](https://img.shields.io/packagist/dm/nette/database.svg)](https://packagist.org/packages/nette/database) -[![Tests](https://github.com/nette/database/actions/workflows/tests.yml/badge.svg?branch=v3.2)](https://github.com/nette/database/actions) +[![Tests](https://github.com/nette/database/actions/workflows/tests.yml/badge.svg?branch=master)](https://github.com/nette/database/actions) [![Latest Stable Version](https://poser.pugx.org/nette/database/v/stable)](https://github.com/nette/database/releases) [![License](https://img.shields.io/badge/license-New%20BSD-blue.svg)](https://github.com/nette/database/blob/master/license.md) +  Introduction ------------ Nette provides a powerful layer for accessing your database easily. -- composes SQL queries with ease -- easily fetches data -- uses efficient queries and does not transmit unnecessary data +✅ composes SQL queries with ease
+✅ significantly simplifies retrieving data without writing SQL queries
+✅ uses efficient queries and does not transmit unnecessary data -The [Nette Database Core](https://doc.nette.org/database-core) is a wrapper around the PDO and provides core functionality. +The [Nette Database Core](https://doc.nette.org/en/database/core) is a wrapper around the PDO and provides core functionality. -The [Nette Database Explorer](https://doc.nette.org/database-explorer) layer helps you to fetch database data more easily and in a more optimized way. +The [Nette Database Explorer](https://doc.nette.org/en/database/explorer) layer helps you to fetch database data more easily and in a more optimized way. +  [Support Me](https://github.com/sponsors/dg) -------------------------------------------- @@ -30,6 +31,7 @@ Do you like Nette Database? Are you looking forward to the new features? Thank you! +  Installation ------------ @@ -42,12 +44,14 @@ composer require nette/database It requires PHP version 8.1 and supports PHP up to 8.3. +  Usage ----- This is just a piece of documentation. [Please see our website](https://doc.nette.org/database). +  Database Core ------------- @@ -71,6 +75,8 @@ $database->query('UPDATE users SET ? WHERE id=?', $data, $id); $database->query('SELECT * FROM categories WHERE id=?', 123)->dump(); ``` +  + Database Explorer ----------------- diff --git a/src/Bridges/DatabaseDI/DatabaseExtension.php b/src/Bridges/DatabaseDI/DatabaseExtension.php index 703c18a55..314614cd7 100644 --- a/src/Bridges/DatabaseDI/DatabaseExtension.php +++ b/src/Bridges/DatabaseDI/DatabaseExtension.php @@ -34,7 +34,10 @@ public function getConfigSchema(): Nette\Schema\Schema Expect::structure([ 'dsn' => Expect::string()->required()->dynamic(), 'user' => Expect::string()->nullable()->dynamic(), - 'password' => Expect::string()->nullable()->dynamic(), + 'password' => Expect::anyOf( + Expect::string()->nullable()->dynamic(), + Expect::type(Nette\Database\CredentialProvider::class)->nullable(), + ), 'options' => Expect::array(), 'debugger' => Expect::bool(), 'explain' => Expect::bool(true), diff --git a/src/Database/Connection.php b/src/Database/Connection.php index 3edc9f879..10041ae95 100644 --- a/src/Database/Connection.php +++ b/src/Database/Connection.php @@ -11,8 +11,6 @@ use JetBrains\PhpStorm\Language; use Nette\Utils\Arrays; -use PDO; -use PDOException; /** @@ -25,12 +23,11 @@ class Connection /** @var array Occurs after query is executed */ public array $onQuery = []; - private Driver $driver; + private ?Driver $driver = null; private SqlPreprocessor $preprocessor; - private ?PDO $pdo = null; /** @var callable(array, ResultSet): array */ - private $rowNormalizer = [Helpers::class, 'normalizeRow']; + private $rowNormalizer; private ?string $sql = null; private int $transactionDepth = 0; @@ -40,12 +37,11 @@ public function __construct( #[\SensitiveParameter] private readonly ?string $user = null, #[\SensitiveParameter] - private readonly ?string $password = null, + private readonly string|CredentialProvider|null $password = null, private readonly array $options = [], ) { - if (!empty($options['newDateTime'])) { - $this->rowNormalizer = fn($row, $resultSet) => Helpers::normalizeRow($row, $resultSet, DateTime::class); - } + $this->rowNormalizer = new RowNormalizer; + if (empty($options['lazy'])) { $this->connect(); } @@ -54,23 +50,25 @@ public function __construct( public function connect(): void { - if ($this->pdo) { + if ($this->driver) { return; } - try { - $this->pdo = new PDO($this->dsn, $this->user, $this->password, $this->options); - $this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); - } catch (PDOException $e) { - throw ConnectionException::from($e); - } - + $dsn = explode(':', $this->dsn)[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'."); + } + + $password = $this->password instanceof CredentialProvider + ? $this->password->getPassword($this) + : $this->password; + $this->driver = new $class; + $this->driver->connect($this->dsn, $this->user, $password, $this->options); $this->preprocessor = new SqlPreprocessor($this); - $this->driver->initialize($this, $this->options); Arrays::invoke($this->onConnect, $this); } @@ -84,7 +82,7 @@ public function reconnect(): void public function disconnect(): void { - $this->pdo = null; + $this->driver = null; } @@ -94,10 +92,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(); } @@ -111,6 +110,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; } @@ -131,22 +131,15 @@ public function setRowNormalizer(?callable $normalizer): static 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); } @@ -216,7 +209,7 @@ public function query(#[Language('SQL')] string $sql, #[Language('GenericSQL')] [$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; } @@ -229,6 +222,7 @@ public function query(#[Language('SQL')] string $sql, #[Language('GenericSQL')] /** @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); } diff --git a/src/Database/CredentialProvider.php b/src/Database/CredentialProvider.php new file mode 100644 index 000000000..99f05f78c --- /dev/null +++ b/src/Database/CredentialProvider.php @@ -0,0 +1,22 @@ + */ function getForeignKeys(string $table): array; - /** - * Returns associative array of detected types (IStructure::FIELD_*) in result set. - * @return array - */ - function getColumnTypes(\PDOStatement $statement): array; - /** * Cheks if driver supports specific property - * @param string $item self::SUPPORT_* property + * @param self::Support* $item */ function isSupported(string $item): bool; } diff --git a/src/Database/DriverException.php b/src/Database/DriverException.php index aad02cd6e..af9cee5b8 100644 --- a/src/Database/DriverException.php +++ b/src/Database/DriverException.php @@ -13,51 +13,39 @@ /** * Base class for all errors in the driver or SQL server. */ -class DriverException extends \PDOException +class DriverException extends \Exception { - public ?string $queryString = null; - public ?array $params = null; + private int|string|null $driverCode = null; + private string|null $sqlState = null; - public static function from(\PDOException $src): static + 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; } - public function getDriverCode(): int|string|null + /** @internal */ + public function setDriverCode(string $state, int|string $code): void { - return $this->errorInfo[1] ?? null; + $this->sqlState = $state; + $this->driverCode = $code; } - public function getSqlState(): ?string - { - return $this->errorInfo[0] ?? null; - } - - - 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 105dda7ad..d56392fe2 100644 --- a/src/Database/Drivers/MsSqlDriver.php +++ b/src/Database/Drivers/MsSqlDriver.php @@ -15,26 +15,8 @@ /** * Supplemental MS SQL database driver. */ -class MsSqlDriver implements Nette\Database\Driver +class MsSqlDriver extends PdoDriver { - private Nette\Database\Connection $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 @@ -84,7 +66,7 @@ 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) { + foreach ($this->pdo->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', @@ -112,14 +94,15 @@ public function getColumns(string $table): array FROM INFORMATION_SCHEMA.COLUMNS WHERE - TABLE_SCHEMA = {$this->connection->quote($table_schema)} - AND TABLE_NAME = {$this->connection->quote($table_name)} + TABLE_SCHEMA = {$this->pdo->quote($table_schema)} + AND TABLE_NAME = {$this->pdo->quote($table_name)} X; - foreach ($this->connection->query($query) as $row) { + foreach ($this->pdo->query($query, \PDO::FETCH_ASSOC) as $row) { $columns[] = [ 'name' => $row['COLUMN_NAME'], 'table' => $table, + 'type' => Nette\Database\Helpers::detectType($row['DATA_TYPE']), 'nativetype' => strtoupper($row['DATA_TYPE']), 'size' => $row['CHARACTER_MAXIMUM_LENGTH'] ?? ($row['NUMERIC_PRECISION'] ?? null), 'unsigned' => false, @@ -127,7 +110,7 @@ public function getColumns(string $table): array 'default' => $row['COLUMN_DEFAULT'], 'autoincrement' => $row['DOMAIN_NAME'] === 'COUNTER', 'primary' => $row['COLUMN_NAME'] === 'ID', - 'vendor' => (array) $row, + 'vendor' => $row, ]; } @@ -153,12 +136,12 @@ public function getIndexes(string $table): array INNER JOIN sys.columns col ON ic.object_id = col.object_id and ic.column_id = col.column_id INNER JOIN sys.tables t ON ind.object_id = t.object_id WHERE - t.name = {$this->connection->quote($table_name)} + t.name = {$this->pdo->quote($table_name)} ORDER BY 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'; @@ -196,14 +179,15 @@ public function getForeignKeys(string $table): array INNER JOIN sys.columns col2 ON col2.column_id = referenced_column_id AND col2.object_id = tab2.object_id WHERE - tab1.name = {$this->connection->quote($table_name)} + tab1.name = {$this->pdo->quote($table_name)} X; - foreach ($this->connection->query($query) as $id => $row) { - $keys[$id]['name'] = $row['fk_name']; - $keys[$id]['local'] = $row['column']; + foreach ($this->pdo->query($query) as $row) { + $id = $row['fk_name']; + $keys[$id]['name'] = $id; + $keys[$id]['local'][] = $row['column']; $keys[$id]['table'] = $table_schema . '.' . $row['referenced_table']; - $keys[$id]['foreign'] = $row['referenced_column']; + $keys[$id]['foreign'][] = $row['referenced_column']; } return array_values($keys); @@ -218,6 +202,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 0217f535d..1c6f4e137 100644 --- a/src/Database/Drivers/MySqlDriver.php +++ b/src/Database/Drivers/MySqlDriver.php @@ -10,19 +10,28 @@ namespace Nette\Database\Drivers; use Nette; +use Nette\Database\Type; /** * Supplemental MySQL database driver. */ -class MySqlDriver implements Nette\Database\Driver +class MySqlDriver extends PdoDriver { - public const - ERROR_ACCESS_DENIED = 1045, - ERROR_DUPLICATE_ENTRY = 1062, - ERROR_DATA_TRUNCATED = 1265; + public const ErrorAccessDenied = 1045; + public const ErrorDuplicateEntry = 1062; + public const ErrorDataTruncated = 1265; + + /** @deprecated use MySqlDriver::ErrorAccessDenied */ + public const ERROR_ACCESS_DENIED = self::ErrorAccessDenied; + + /** @deprecated use MySqlDriver::ErrorDuplicateEntry */ + public const ERROR_DUPLICATE_ENTRY = self::ErrorDuplicateEntry; + + /** @deprecated use MySqlDriver::ErrorDataTruncated */ + public const ERROR_DATA_TRUNCATED = self::ErrorDataTruncated; + - private Nette\Database\Connection $connection; private bool $supportBooleans; @@ -32,40 +41,46 @@ class MySqlDriver implements Nette\Database\Driver * - sqlmode => see http://dev.mysql.com/doc/refman/5.0/en/server-sql-mode.html * - supportBooleans => converts INT(1) to boolean */ - 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'])); } $this->supportBooleans = (bool) ($options['supportBooleans'] ?? false); } - 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], strict: true)) { - return Nette\Database\ForeignKeyConstraintViolationException::from($e); + return Nette\Database\ForeignKeyConstraintViolationException::class; } elseif (in_array($code, [1062, 1557, 1569, 1586], strict: 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], strict: true)) { - return Nette\Database\NotNullConstraintViolationException::from($e); + return Nette\Database\NotNullConstraintViolationException::class; } else { - return Nette\Database\DriverException::from($e); + return null; } } @@ -95,7 +110,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 ? "%'" : "'"); } @@ -119,7 +134,7 @@ public function applyLimit(string &$sql, ?int $limit, ?int $offset): void public function getTables(): array { $tables = []; - foreach ($this->connection->query('SHOW FULL TABLES') as $row) { + foreach ($this->pdo->query('SHOW FULL TABLES') as $row) { $tables[] = [ 'name' => $row[0], 'view' => ($row[1] ?? null) === 'VIEW', @@ -133,14 +148,21 @@ public function getTables(): array 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']); + foreach ($this->pdo->query('SHOW FULL COLUMNS FROM ' . $this->delimite($table), \PDO::FETCH_ASSOC) as $row) { + $row = array_change_key_case($row, CASE_LOWER); + $pair = explode('(', $row['type']); + $type = match (true) { + $pair[0] === 'decimal' && str_ends_with($pair[1], ',0)') => Type::Integer, + $row['type'] === 'tinyint(1)' && $this->supportBooleans => Type::Bool, + $row['type'] === 'time' => Type::TimeInterval, + default => Nette\Database\Helpers::detectType($pair[0]), + }; $columns[] = [ 'name' => $row['field'], 'table' => $table, - 'nativetype' => strtoupper($type[0]), - 'size' => isset($type[1]) ? (int) $type[1] : null, + 'type' => $type, + 'nativetype' => strtoupper($pair[0]), + 'size' => isset($pair[1]) ? (int) $pair[1] : null, 'nullable' => $row['null'] === 'YES', 'default' => $row['default'], 'autoincrement' => $row['extra'] === 'auto_increment', @@ -156,7 +178,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']; @@ -171,17 +193,18 @@ public function getIndexes(string $table): array public function getForeignKeys(string $table): array { $keys = []; - foreach ($this->connection->query(<<pdo->query(<<connection->quote($table)} - X) as $id => $row) { - $keys[$id]['name'] = $row['CONSTRAINT_NAME']; - $keys[$id]['local'] = $row['COLUMN_NAME']; + AND TABLE_NAME = {$this->pdo->quote($table)} + X) as $row) { + $id = $row['CONSTRAINT_NAME']; + $keys[$id]['name'] = $id; + $keys[$id]['local'][] = $row['COLUMN_NAME']; $keys[$id]['table'] = $row['REFERENCED_TABLE_NAME']; - $keys[$id]['foreign'] = $row['REFERENCED_COLUMN_NAME']; + $keys[$id]['foreign'][] = $row['REFERENCED_COLUMN_NAME']; } return array_values($keys); @@ -196,10 +219,10 @@ public function getColumnTypes(\PDOStatement $statement): array $meta = $statement->getColumnMeta($col); if (isset($meta['native_type'])) { $types[$meta['name']] = match (true) { - $meta['native_type'] === 'NEWDECIMAL' && $meta['precision'] === 0 => Nette\Database\IStructure::FIELD_INTEGER, - $meta['native_type'] === 'TINY' && $meta['len'] === 1 && $this->supportBooleans => Nette\Database\IStructure::FIELD_BOOL, - $meta['native_type'] === 'TIME' => Nette\Database\IStructure::FIELD_TIME_INTERVAL, - default => Nette\Database\Helpers::detectType($meta['native_type']), + $meta['native_type'] === 'NEWDECIMAL' && $meta['precision'] === 0 => Type::Integer, + $meta['native_type'] === 'TINY' && $meta['len'] === 1 && $this->supportBooleans => Type::Bool, + $meta['native_type'] === 'TIME' => Type::TimeInterval, + default => Nette\Database\RowNormalizer::detectType($meta['native_type']), }; } } @@ -214,6 +237,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 ba26f9c3d..2ee3df3a4 100644 --- a/src/Database/Drivers/OciDriver.php +++ b/src/Database/Drivers/OciDriver.php @@ -15,33 +15,38 @@ /** * Supplemental Oracle database driver. */ -class OciDriver implements Nette\Database\Driver +class OciDriver extends PdoDriver { - private Nette\Database\Connection $connection; private string $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], strict: true)) { - return Nette\Database\UniqueConstraintViolationException::from($e); + return Nette\Database\UniqueConstraintViolationException::class; } elseif (in_array($code, [1400], strict: true)) { - return Nette\Database\NotNullConstraintViolationException::from($e); + return Nette\Database\NotNullConstraintViolationException::class; } elseif (in_array($code, [2266, 2291, 2292], strict: true)) { - return Nette\Database\ForeignKeyConstraintViolationException::from($e); + return Nette\Database\ForeignKeyConstraintViolationException::class; } else { - return Nette\Database\DriverException::from($e); + return null; } } @@ -97,7 +102,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], @@ -136,6 +141,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 f9c02a1ff..050dffe44 100644 --- a/src/Database/Drivers/OdbcDriver.php +++ b/src/Database/Drivers/OdbcDriver.php @@ -15,22 +15,8 @@ /** * Supplemental ODBC database driver. */ -class OdbcDriver implements Nette\Database\Driver +class OdbcDriver extends PdoDriver { - 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) . ']'; @@ -108,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..986c71645 --- /dev/null +++ b/src/Database/Drivers/PdoDriver.php @@ -0,0 +1,145 @@ +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..28386a4c9 --- /dev/null +++ b/src/Database/Drivers/PdoResultDriver.php @@ -0,0 +1,72 @@ +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 d4e6e6899..d30e9808f 100644 --- a/src/Database/Drivers/PgSqlDriver.php +++ b/src/Database/Drivers/PgSqlDriver.php @@ -15,37 +15,28 @@ /** * Supplemental PostgreSQL database driver. */ -class PgSqlDriver implements Nette\Database\Driver +class PgSqlDriver extends PdoDriver { - private Nette\Database\Connection $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' && str_contains($e->getMessage(), 'truncate')) { - return Nette\Database\ForeignKeyConstraintViolationException::from($e); + 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; } } @@ -74,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 ? "%'" : "'"); } @@ -102,8 +93,7 @@ public function applyLimit(string &$sql, ?int $limit, ?int $offset): void public function getTables(): array { - $tables = []; - foreach ($this->connection->query(<<<'X' + return $this->pdo->query(<<<'X' SELECT DISTINCT ON (c.relname) c.relname::varchar AS name, c.relkind IN ('v', 'm') AS view, @@ -116,21 +106,18 @@ public function getTables(): array AND n.nspname = ANY (pg_catalog.current_schemas(FALSE)) ORDER BY c.relname - X) as $row) { - $tables[] = (array) $row; - } - - return $tables; + X)->fetchAll(\PDO::FETCH_ASSOC); } public function getColumns(string $table): array { $columns = []; - foreach ($this->connection->query(<<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 - X) as $row) { - $column = (array) $row; + X, \PDO::FETCH_ASSOC) as $column) { + $column['type'] = Nette\Database\Helpers::detectType($column['type']); $column['vendor'] = $column; unset($column['sequence']); @@ -168,7 +155,7 @@ public function getColumns(string $table): array public function getIndexes(string $table): array { $indexes = []; - foreach ($this->connection->query(<<pdo->query(<<connection->quote($this->delimiteFQN($table))}::regclass + AND c1.oid = {$this->pdo->quote($this->delimiteFQN($table))}::regclass X) as $row) { $id = $row['name']; $indexes[$id]['name'] = $id; @@ -197,7 +184,8 @@ public function getIndexes(string $table): array public function getForeignKeys(string $table): array { /* Doesn't work with multi-column foreign keys */ - return $this->connection->query(<<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)) - X)->fetchAll(); + X) as $row) { + $id = $row['name']; + $keys[$id]['name'] = $id; + $keys[$id]['local'][] = $row['local']; + $keys[$id]['table'] = $row['table']; + $keys[$id]['foreign'][] = $row['foreign']; + } + + return array_values($keys); } @@ -229,7 +225,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 03f902520..3755921c8 100644 --- a/src/Database/Drivers/SqliteDriver.php +++ b/src/Database/Drivers/SqliteDriver.php @@ -15,47 +15,52 @@ /** * Supplemental SQLite3 database driver. */ -class SqliteDriver implements Nette\Database\Driver +class SqliteDriver extends PdoDriver { - private Nette\Database\Connection $connection; private string $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 ( 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 ( 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 ( 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; } } @@ -83,7 +88,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 '\\'"; } @@ -106,7 +111,7 @@ public function applyLimit(string &$sql, ?int $limit, ?int $offset): void public function getTables(): array { $tables = []; - foreach ($this->connection->query(<<<'X' + foreach ($this->pdo->query(<<<'X' SELECT name, type = 'view' as view FROM sqlite_master WHERE type IN ('table', 'view') AND name NOT LIKE 'sqlite_%' @@ -117,8 +122,8 @@ public function getTables(): array ORDER BY name X) as $row) { $tables[] = [ - 'name' => $row->name, - 'view' => (bool) $row->view, + 'name' => $row['name'], + 'view' => (bool) $row['view'], ]; } @@ -128,31 +133,36 @@ public function getTables(): array public function getColumns(string $table): array { - $meta = $this->connection->query(<<pdo->query(<<connection->quote($table)} + WHERE type = 'table' AND name = {$this->pdo->quote($table)} UNION ALL SELECT sql FROM sqlite_temp_master - WHERE type = 'table' AND name = {$this->connection->quote($table)} + 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']); + $pair = explode('(', $row['type']); + $type = match (true) { + $this->fmtDateTime === 'U' && in_array($pair[0], ['DATE', 'DATETIME'], strict: true) => Nette\Database\Type::UnixTimestamp, + default => Nette\Database\Helpers::detectType($pair[0]), + }; $columns[] = [ 'name' => $column, 'table' => $table, - 'nativetype' => strtoupper($type[0]), - 'size' => isset($type[1]) ? (int) $type[1] : null, - 'nullable' => $row['notnull'] === 0, + 'type' => $type, + 'nativetype' => strtoupper($pair[0]), + 'size' => isset($pair[1]) ? (int) $pair[1] : null, + 'nullable' => !$row['notnull'], 'default' => $row['dflt_value'], 'autoincrement' => $meta && preg_match($pattern, (string) $meta['sql']), 'primary' => $row['pk'] > 0, - 'vendor' => (array) $row, + 'vendor' => $row, ]; } @@ -163,7 +173,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']; @@ -171,8 +181,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']; } } @@ -209,12 +218,15 @@ public function getIndexes(string $table): array 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]['local'][] = $row['from']; $keys[$id]['table'] = $row['table']; - $keys[$id]['foreign'] = $row['to']; + $keys[$id]['foreign'][] = $row['to']; + if ($keys[$id]['foreign'][0] == null) { + $keys[$id]['foreign'] = []; + } } return array_values($keys); @@ -229,10 +241,10 @@ public function getColumnTypes(\PDOStatement $statement): array $meta = $statement->getColumnMeta($col); if (isset($meta['sqlite:decl_type'])) { $types[$meta['name']] = $this->fmtDateTime === 'U' && in_array($meta['sqlite:decl_type'], ['DATE', 'DATETIME'], strict: true) - ? Nette\Database\IStructure::FIELD_UNIX_TIMESTAMP - : Nette\Database\Helpers::detectType($meta['sqlite:decl_type']); + ? Nette\Database\Type::UnixTimestamp + : Nette\Database\RowNormalizer::detectType($meta['sqlite:decl_type']); } elseif (isset($meta['native_type'])) { - $types[$meta['name']] = Nette\Database\Helpers::detectType($meta['native_type']); + $types[$meta['name']] = Nette\Database\RowNormalizer::detectType($meta['native_type']); } } @@ -242,6 +254,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 61e0bd3cf..e892bc47f 100644 --- a/src/Database/Drivers/SqlsrvDriver.php +++ b/src/Database/Drivers/SqlsrvDriver.php @@ -15,22 +15,21 @@ /** * Supplemental SQL Server 2005 and later database driver. */ -class SqlsrvDriver implements Nette\Database\Driver +class SqlsrvDriver extends PdoDriver { - private Nette\Database\Connection $connection; private string $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); } @@ -94,7 +93,7 @@ public function applyLimit(string &$sql, ?int $limit, ?int $offset): void public function getTables(): array { $tables = []; - foreach ($this->connection->query(<<<'X' + foreach ($this->pdo->query(<<<'X' SELECT name, CASE type @@ -107,8 +106,8 @@ public function getTables(): array type IN ('U', 'V') X) as $row) { $tables[] = [ - 'name' => $row->name, - 'view' => (bool) $row->view, + 'name' => $row['name'], + 'view' => (bool) $row['view'], ]; } @@ -119,10 +118,11 @@ public function getTables(): array public function getColumns(string $table): array { $columns = []; - foreach ($this->connection->query(<<pdo->query(<<connection->quote($table)} - X) as $row) { - $row = (array) $row; + AND o.name = {$this->pdo->quote($table)} + X, \PDO::FETCH_ASSOC) as $row) { + $row['type'] = Nette\Database\Helpers::detectType($row['type']); $row['vendor'] = $row; $row['nullable'] = (bool) $row['nullable']; $row['autoincrement'] = (bool) $row['autoincrement']; @@ -158,7 +158,7 @@ public function getColumns(string $table): array public function getIndexes(string $table): array { $indexes = []; - foreach ($this->connection->query(<<pdo->query(<<connection->quote($table)} + t.name = {$this->pdo->quote($table)} ORDER BY i.index_id, ic.index_column_id @@ -193,7 +193,7 @@ public function getForeignKeys(string $table): array { // Does't work with multicolumn foreign keys $keys = []; - foreach ($this->connection->query(<<pdo->query(<<connection->quote($table)} - X) 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]['local'][] = $row['local']; + $keys[$id]['table'] = $row['table']; + $keys[$id]['foreign'][] = $row['column']; } return array_values($keys); @@ -226,9 +230,9 @@ public function getColumnTypes(\PDOStatement $statement): array isset($meta['sqlsrv:decl_type']) && $meta['sqlsrv:decl_type'] !== 'timestamp' ) { // timestamp does not mean time in sqlsrv - $types[$meta['name']] = Nette\Database\Helpers::detectType($meta['sqlsrv:decl_type']); + $types[$meta['name']] = Nette\Database\RowNormalizer::detectType($meta['sqlsrv:decl_type']); } elseif (isset($meta['native_type'])) { - $types[$meta['name']] = Nette\Database\Helpers::detectType($meta['native_type']); + $types[$meta['name']] = Nette\Database\RowNormalizer::detectType($meta['native_type']); } } @@ -238,6 +242,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 06481f8c3..05151efa4 100644 --- a/src/Database/Explorer.php +++ b/src/Database/Explorer.php @@ -75,6 +75,7 @@ public function query(#[Language('SQL')] string $sql, #[Language('GenericSQL')] /** @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); } diff --git a/src/Database/Helpers.php b/src/Database/Helpers.php index 696948e47..2e2b3d089 100644 --- a/src/Database/Helpers.php +++ b/src/Database/Helpers.php @@ -23,18 +23,7 @@ class Helpers /** maximum SQL length */ public static int $maxLength = 100; - - 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|(SMALL)?MONEY|CURRENCY|NUMBER' => IStructure::FIELD_DECIMAL, - 'REAL|DOUBLE( PRECISION)?|FLOAT\d*' => IStructure::FIELD_FLOAT, - 'BOOL(EAN)?' => IStructure::FIELD_BOOL, - 'TIME' => IStructure::FIELD_TIME, - 'DATE' => IStructure::FIELD_DATE, - '(SMALL)?DATETIME(OFFSET)?\d*|TIME(STAMP.*)?' => IStructure::FIELD_DATETIME, - 'BYTEA|(TINY|MEDIUM|LONG|)BLOB|(LONG )?(VAR)?BINARY|IMAGE' => IStructure::FIELD_BINARY, - ]; + public static array $typePatterns = []; /** @@ -165,6 +154,7 @@ public static function dumpSql(string $sql, ?array $params = null, ?Connection $ /** * Common column type detection. + * @return array */ public static function detectTypes(\PDOStatement $statement): array { @@ -173,7 +163,7 @@ public static function detectTypes(\PDOStatement $statement): array for ($col = 0; $col < $count; $col++) { $meta = $statement->getColumnMeta($col); if (isset($meta['native_type'])) { - $types[$meta['name']] = self::detectType($meta['native_type']); + $types[$meta['name']] = RowNormalizer::detectType($meta['native_type']); } } @@ -181,70 +171,17 @@ public static function detectTypes(\PDOStatement $statement): array } - /** - * Heuristic column type detection. - * @internal - */ + /** @deprecated use RowNormalizer::detectType() */ public static function detectType(string $type): string { - static $cache; - if (!isset($cache[$type])) { - $cache[$type] = 'string'; - foreach (self::$typePatterns as $s => $val) { - if (preg_match("#^($s)$#i", $type)) { - return $cache[$type] = $val; - } - } - } - - return $cache[$type]; + return RowNormalizer::detectType($type); } - /** @internal */ - public static function normalizeRow( - array $row, - ResultSet $resultSet, - $dateTimeClass = Nette\Utils\DateTime::class, - ): array + /** @deprecated use RowNormalizer */ + 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 || $type === IStructure::FIELD_DECIMAL) { - 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] = $value && $value !== 'f' && $value !== 'F'; - - } elseif ($type === IStructure::FIELD_DATETIME || $type === IStructure::FIELD_DATE) { - $row[$key] = str_starts_with($value, '0000-00') - ? null - : new $dateTimeClass($value); - - } elseif ($type === IStructure::FIELD_TIME) { - $row[$key] = (new $dateTimeClass($value))->setDate(1, 1, 1); - - } 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] = (new $dateTimeClass)->setTimestamp($value); - } - } - - return $row; + return (new RowNormalizer)($row, $resultSet); } @@ -307,6 +244,7 @@ public static function createDebugPanel( 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); } @@ -321,6 +259,7 @@ public static function initializeTracy( ?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); } @@ -364,11 +303,11 @@ public static function toPairs(array $rows, string|int|null $key = null, string| /** * 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 8c09e7497..295b4ef2d 100644 --- a/src/Database/IStructure.php +++ b/src/Database/IStructure.php @@ -15,6 +15,7 @@ */ interface IStructure { + /** @deprecated use Type::* */ public const FIELD_TEXT = 'string', FIELD_BINARY = 'bin', 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.php b/src/Database/Reflection.php index 609a426d4..8d02a6552 100644 --- a/src/Database/Reflection.php +++ b/src/Database/Reflection.php @@ -15,14 +15,13 @@ final class Reflection { /** @var array */ - public readonly array $tables; - private ?string $schema; + public array $tables; + private ?string $schema = null; public function __construct( private readonly Driver $driver, ) { - $this->schema = $this->driver->isSupported(Driver::SUPPORT_SCHEMA) ? 'public' : null; unset($this->tables); } @@ -37,7 +36,17 @@ public function getTables(): array public function getTable(string $name): Table { $name = $this->getFullName($name); - return $this->tables[$name] ?? throw new \InvalidArgumentException("Table '$name' not found."); + if (isset($this->tables[$name])) { + return $this->tables[$name]; + } + + try { + $this->driver->getColumns($name); + return $this->tables[$name] = new Table($this, $name); + } catch (DriverException) { + } + + throw new \InvalidArgumentException("Table '$name' not found."); } diff --git a/src/Database/Reflection/Column.php b/src/Database/Reflection/Column.php index 48eaa87c7..7381e2820 100644 --- a/src/Database/Reflection/Column.php +++ b/src/Database/Reflection/Column.php @@ -19,7 +19,7 @@ final class Column public function __construct( public readonly string $name, public readonly ?Table $table = null, - public readonly string $nativeType = '', + public readonly string $type = '', public readonly ?int $size = null, public readonly bool $nullable = false, public readonly mixed $default = null, diff --git a/src/Database/Reflection/Table.php b/src/Database/Reflection/Table.php index d4beb4cc3..22df3fb2d 100644 --- a/src/Database/Reflection/Table.php +++ b/src/Database/Reflection/Table.php @@ -49,7 +49,7 @@ private function initColumns(): void { $res = []; foreach ($this->reflection->getDriver()->getColumns($this->name) as $row) { - $res[$row['name']] = new Column($row['name'], $this, $row['nativetype'], $row['size'], $row['nullable'], $row['default'], $row['autoincrement'], $row['primary'], $row['vendor']); + $res[$row['name']] = new Column($row['name'], $this, $row['type'], $row['size'], $row['nullable'], $row['default'], $row['autoincrement'], $row['primary'], $row['vendor']); } $this->columns = $res; } diff --git a/src/Database/ResultDriver.php b/src/Database/ResultDriver.php new file mode 100644 index 000000000..8b623ec31 --- /dev/null +++ b/src/Database/ResultDriver.php @@ -0,0 +1,43 @@ + + */ + function getColumnTypes(): array; + + /** + * Returns associative array of original table names. + */ + function getColumnMeta(int $col): array; +} diff --git a/src/Database/ResultSet.php b/src/Database/ResultSet.php index 1e5c9592f..fb4111666 100644 --- a/src/Database/ResultSet.php +++ b/src/Database/ResultSet.php @@ -10,7 +10,6 @@ namespace Nette\Database; use Nette; -use PDO; /** @@ -18,7 +17,7 @@ */ class ResultSet implements \Iterator, IRowContainer { - private ?\PDOStatement $pdoStatement = null; + private ?ResultDriver $result = null; /** @var callable(array, ResultSet): array */ private $normalizer; @@ -39,26 +38,12 @@ public function __construct( ) { $time = microtime(true); $this->normalizer = $normalizer; - $types = ['boolean' => PDO::PARAM_BOOL, 'integer' => PDO::PARAM_INT, 'resource' => PDO::PARAM_LOB, 'NULL' => PDO::PARAM_NULL]; - - try { - if (str_starts_with($queryString, '::')) { - $connection->getPdo()->{substr($queryString, 2)}(); - } else { - $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)}(); + } else { + $this->result = $driver->query($queryString, $params); } $this->time = microtime(true) - $time; @@ -68,7 +53,7 @@ public function __construct( /** @deprecated */ public function getConnection(): Connection { - return $this->connection; + throw new Nette\DeprecatedException(__METHOD__ . '() is deprecated.'); } @@ -77,7 +62,7 @@ public function getConnection(): Connection */ public function getPdoStatement(): ?\PDOStatement { - return $this->pdoStatement; + return $this->result->getPDOStatement(); } @@ -95,19 +80,19 @@ 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 { - $this->types ??= $this->connection->getDriver()->getColumnTypes($this->pdoStatement); + $this->types ??= $this->result->getColumnTypes(); return $this->types; } @@ -183,13 +168,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 ($this->lastRow === null && count($data) !== $this->result->getColumnCount()) { + $duplicates = Helpers::findDuplicates($this->result); trigger_error("Found duplicate columns in database result set: $duplicates."); } @@ -205,6 +189,13 @@ public function fetch(): ?Row } + /** @internal */ + public function fetchArray(): ?array + { + return $this->result?->fetch(); + } + + /** * Fetches single field. */ diff --git a/src/Database/RowNormalizer.php b/src/Database/RowNormalizer.php new file mode 100644 index 000000000..3cbeb4beb --- /dev/null +++ b/src/Database/RowNormalizer.php @@ -0,0 +1,135 @@ + Type::Text, // PostgreSQL arrays + '(TINY|SMALL|SHORT|MEDIUM|BIG|LONG)(INT)?|INT(EGER|\d+| IDENTITY| UNSIGNED)?|(SMALL|BIG|)SERIAL\d*|COUNTER|YEAR|BYTE|LONGLONG|UNSIGNED BIG INT' => Type::Integer, + '(NEW)?DEC(IMAL)?(\(.*)?|NUMERIC|(SMALL)?MONEY|CURRENCY|NUMBER' => Type::Decimal, + 'REAL|DOUBLE( PRECISION)?|FLOAT\d*' => Type::Float, + 'BOOL(EAN)?' => Type::Bool, + 'TIME' => Type::Time, + 'DATE' => Type::Date, + '(SMALL)?DATETIME(OFFSET)?\d*|TIME(STAMP.*)?' => Type::DateTime, + 'BYTEA|(TINY|MEDIUM|LONG|)BLOB|(LONG )?(VAR)?BINARY|IMAGE' => Type::Binary, + ]; + + + private $skipped = []; + + + /** + * Heuristic column type detection. + * @return Type::* + * @internal + */ + public static function detectType(string $type): string + { + static $cache; + if (!isset($cache[$type])) { + $cache[$type] = 'string'; + foreach (self::TypePatterns as $s => $val) { + if (preg_match("#^($s)$#i", $type)) { + return $cache[$type] = $val; + } + } + } + + return $cache[$type]; + } + + + public function skipNumeric(): static + { + $this->skipped[Type::Decimal] = true; + return $this; + } + + + public function skipDateTime(): static + { + $this->skipped[Type::DateTime] = true; + $this->skipped[Type::Date] = true; + $this->skipped[Type::Time] = true; + $this->skipped[Type::UnixTimestamp] = true; + return $this; + } + + + public function skipInterval(): static + { + $this->skipped[Type::TimeInterval] = 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; + } + + + private function normalizeField(mixed $value, string $type): mixed + { + if ($value === null || $value === false) { + return $value; + } + + switch ($type) { + case Type::Integer: + return is_float($tmp = $value * 1) ? $value : $tmp; + + case Type::Float: + case Type::Decimal: + if (is_string($value) && ($pos = strpos($value, '.')) !== false) { + $value = rtrim(rtrim($pos === 0 ? "0$value" : $value, '0'), '.'); + } + + return (float) $value; + + case Type::Bool: + return $value && $value !== 'f' && $value !== 'F'; + + case Type::DateTime: + case Type::Date: + return str_starts_with($value, '0000-00') + ? null + : new DateTime($value); + + case Type::Time: + return (new DateTime($value))->setDate(1, 1, 1); + + case Type::TimeInterval: + 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 Type::UnixTimestamp: + return (new DateTime)->setTimestamp($value); + + default: + return $value; + } + } +} diff --git a/src/Database/SqlPreprocessor.php b/src/Database/SqlPreprocessor.php index e5292aff2..4a7e394db 100644 --- a/src/Database/SqlPreprocessor.php +++ b/src/Database/SqlPreprocessor.php @@ -231,7 +231,7 @@ private function formatValue(mixed $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 ? '' : ')'); } diff --git a/src/Database/Structure.php b/src/Database/Structure.php index 438079caa..e89c74f2e 100644 --- a/src/Database/Structure.php +++ b/src/Database/Structure.php @@ -94,7 +94,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; } @@ -235,17 +235,11 @@ 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, fn($a, $b): int => $fksColumnsCounts[$b['name']] <=> $fksColumnsCounts[$a['name']]); + usort($foreignKeys, fn($a, $b): int => count($b['local']) <=> count($a['local'])); foreach ($foreignKeys as $row) { - $structure['belongsTo'][$lowerTable][$row['local']] = $row['table']; - $structure['hasMany'][strtolower($row['table'])][$table][] = $row['local']; + $structure['belongsTo'][$lowerTable][$row['local'][0]] = $row['table']; + $structure['hasMany'][strtolower($row['table'])][$table][] = $row['local'][0]; } if (isset($structure['belongsTo'][$lowerTable])) { diff --git a/src/Database/Table/ActiveRow.php b/src/Database/Table/ActiveRow.php index 82b3d0ee0..5c671d2d9 100644 --- a/src/Database/Table/ActiveRow.php +++ b/src/Database/Table/ActiveRow.php @@ -61,7 +61,7 @@ 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): mixed { diff --git a/src/Database/Table/Selection.php b/src/Database/Table/Selection.php index b773bf718..a9527eb0d 100644 --- a/src/Database/Table/Selection.php +++ b/src/Database/Table/Selection.php @@ -17,6 +17,9 @@ /** * Filtered table representation. * Selection is based on the great library NotORM http://www.notorm.com written by Jakub Vrana. + * @template T of ActiveRow + * @implements \Iterator + * @implements \ArrayAccess */ class Selection implements \Iterator, IRowContainer, \ArrayAccess, \Countable { @@ -169,6 +172,7 @@ public function getSqlBuilder(): SqlBuilder /** * Returns row specified by primary key. + * @return T */ public function get(mixed $key): ?ActiveRow { @@ -179,6 +183,7 @@ public function get(mixed $key): ?ActiveRow /** * Fetches single row object. + * @return T */ public function fetch(): ?ActiveRow { @@ -219,7 +224,7 @@ public function fetchPairs(string|int|null $key = null, string|int|null $value = /** * Fetches all rows. - * @return ActiveRow[] + * @return T[] */ public function fetchAll(): array { @@ -525,11 +530,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; @@ -761,6 +768,7 @@ public function getDataRefreshed(): bool /** * Inserts row in a table. Returns ActiveRow or number of affected rows for Selection or table without primary key. * @param iterable|Selection $data [$column => $value]|\Traversable|Selection for INSERT ... SELECT + * @return T|array|int|bool */ public function insert(iterable $data): ActiveRow|array|int|bool { @@ -964,6 +972,7 @@ public function rewind(): void } + /** @return T|false */ public function current(): ActiveRow|false { return ($key = current($this->keys)) !== false @@ -1010,6 +1019,7 @@ public function offsetSet($key, $value): void /** * Returns specified row. * @param string $key + * @return ?T */ public function offsetGet($key): ?ActiveRow { diff --git a/src/Database/Table/SqlBuilder.php b/src/Database/Table/SqlBuilder.php index b020ab443..12629c959 100644 --- a/src/Database/Table/SqlBuilder.php +++ b/src/Database/Table/SqlBuilder.php @@ -119,7 +119,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}.*"; @@ -171,7 +171,7 @@ public function buildSelectQuery(?array $columns = null): string $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); @@ -343,7 +343,7 @@ protected function addCondition( } } - 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 ? ' ?' : '') . ')'; @@ -634,7 +634,7 @@ public function parseJoinsCb(array &$joins, array $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']]) @@ -808,7 +808,7 @@ protected function addConditionComposition( 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); diff --git a/src/Database/Type.php b/src/Database/Type.php new file mode 100644 index 000000000..bdfb3657a --- /dev/null +++ b/src/Database/Type.php @@ -0,0 +1,31 @@ +load(Tester\FileMock::create(' + database: + dsn: "sqlite::memory:" + user: name + password: @passwords + debugger: no + options: + lazy: yes + + services: + cache: Nette\Caching\Storages\DevNullStorage + passwords: \CredentialProviderImpl + ', 'neon')); + + $compiler = new DI\Compiler; + $compiler->addExtension('database', new DatabaseExtension(false)); + eval($compiler->addConfig($config)->setClassName('Container1')->compile()); + + $container = new Container1; + $container->initialize(); + + $connection = $container->getService('database.default'); + Assert::type(Nette\Database\Connection::class, $connection); + + Assert::exception( + fn() => $connection->getPdo(), + RuntimeException::class, + 'password requested from provider', + ); +}); diff --git a/tests/Database/Connection.credentialprovider.phpt b/tests/Database/Connection.credentialprovider.phpt new file mode 100644 index 000000000..834809f59 --- /dev/null +++ b/tests/Database/Connection.credentialprovider.phpt @@ -0,0 +1,71 @@ + new Nette\Database\Connection('mysql://127.0.0.1', 'user', new class implements Nette\Database\CredentialProvider { + public function getPassword(): string { + throw new RuntimeException('Tried to obtain password'); + } + }), + RuntimeException::class, + 'Tried to obtain password', + ); +}); + +test('password obtained from provider at connect time for lazy connection', function () { + $connection = new Nette\Database\Connection('mysql://127.0.0.1', 'user', new class implements Nette\Database\CredentialProvider { + public function getPassword(): string { + throw new RuntimeException('Tried to obtain password'); + } + }, ['lazy' => true]); + $explorer = new Nette\Database\Explorer($connection, new Structure($connection, new DevNullStorage)); + Assert::exception( + fn() => $explorer->query('SELECT ?', 10), + RuntimeException::class, + 'Tried to obtain password', + ); +}); + +test('password obtained from provider on each reconnect', function () { + $connection = new Nette\Database\Connection('mysql://127.0.0.1', 'user', new class implements Nette\Database\CredentialProvider { + private int $counter = 0; + + + public function getPassword(): string { + if ($this->counter++ === 0) { + return 'password'; + } else { + throw new RuntimeException('Provider called second time'); + } + } + }, ['lazy' => true]); + + $explorer = new Nette\Database\Explorer($connection, new Structure($connection, new DevNullStorage)); + Assert::exception( + fn() => $explorer->query('SELECT ?', 10), + Nette\Database\DriverRuntimeException::class, + '%a%', + ); + + $connection->disconnect(); + + Assert::exception( + fn() => $explorer->query('SELECT ?', 10), + RuntimeException::class, + 'Provider called second time', + ); +}); diff --git a/tests/Database/Connection.lazy.phpt b/tests/Database/Connection.lazy.phpt index 4f10da139..f16cb580d 100644 --- a/tests/Database/Connection.lazy.phpt +++ b/tests/Database/Connection.lazy.phpt @@ -50,7 +50,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/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 9b070e76a..212234a79 100644 --- a/tests/Database/Explorer/Explorer.backjoin.phpt +++ b/tests/Database/Explorer/Explorer.backjoin.phpt @@ -39,7 +39,7 @@ 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(), diff --git a/tests/Database/Explorer/Explorer.join-condition.phpt b/tests/Database/Explorer/Explorer.join-condition.phpt index 4c0b152dd..a21d6838c 100644 --- a/tests/Database/Explorer/Explorer.join-condition.phpt +++ b/tests/Database/Explorer/Explorer.join-condition.phpt @@ -15,7 +15,7 @@ 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(); @@ -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] ' . diff --git a/tests/Database/Explorer/Explorer.join.phpt b/tests/Database/Explorer/Explorer.join.phpt index 68f06e41d..2fb9f32de 100644 --- a/tests/Database/Explorer/Explorer.join.phpt +++ b/tests/Database/Explorer/Explorer.join.phpt @@ -34,7 +34,7 @@ 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, @@ -51,7 +51,7 @@ 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, diff --git a/tests/Database/Explorer/Selection.insert().phpt b/tests/Database/Explorer/Selection.insert().phpt index de83ee383..ce585514b 100644 --- a/tests/Database/Explorer/Selection.insert().phpt +++ b/tests/Database/Explorer/Selection.insert().phpt @@ -47,7 +47,7 @@ if ($driverName !== 'sqlsrv') { 'name' => 'Jon Snow', 'web' => 'http://example.com', ]), - PDOException::class, + Nette\Database\DriverException::class, ); } diff --git a/tests/Database/Explorer/SqlBuilder.addAlias().phpt b/tests/Database/Explorer/SqlBuilder.addAlias().phpt index 5ed19e0f9..8a7f54932 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'); @@ -88,7 +88,7 @@ test('test same table chain with another alias', function () use ($explorer, $dr 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'); @@ -97,7 +97,7 @@ 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', diff --git a/tests/Database/Explorer/SqlBuilder.addWhere().phpt b/tests/Database/Explorer/SqlBuilder.addWhere().phpt index 44851fbfb..8dd508cdf 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]) > ?))', diff --git a/tests/Database/Explorer/SqlBuilder.parseJoinConditions().phpt b/tests/Database/Explorer/SqlBuilder.parseJoinConditions().phpt index e41f75e17..9e509fd54 100644 --- a/tests/Database/Explorer/SqlBuilder.parseJoinConditions().phpt +++ b/tests/Database/Explorer/SqlBuilder.parseJoinConditions().phpt @@ -80,7 +80,7 @@ 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 = ?)', diff --git a/tests/Database/Explorer/SqlBuilder.parseJoins().phpt b/tests/Database/Explorer/SqlBuilder.parseJoins().phpt index 8a75cb465..d437e231b 100644 --- a/tests/Database/Explorer/SqlBuilder.parseJoins().phpt +++ b/tests/Database/Explorer/SqlBuilder.parseJoins().phpt @@ -44,7 +44,7 @@ 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 ($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 ' . @@ -87,7 +87,7 @@ Assert::same( ); -$sqlBuilder = $driver->isSupported(Driver::SUPPORT_SCHEMA) +$sqlBuilder = $driver->isSupported(Driver::SupportSchema) ? new SqlBuilderMock('public.book', $explorer) : new SqlBuilderMock('book', $explorer); @@ -97,7 +97,7 @@ $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', diff --git a/tests/Database/Reflection.columns.mysql.phpt b/tests/Database/Reflection.columns.mysql.phpt new file mode 100644 index 000000000..f96a4890f --- /dev/null +++ b/tests/Database/Reflection.columns.mysql.phpt @@ -0,0 +1,346 @@ +getPdo()->getAttribute(PDO::ATTR_SERVER_VERSION), '8.0', '>='); +$reflection = $connection->getReflection(); +$columns = $reflection->getTable('types')->columns; + +$expectedColumns = [ + 'unsigned_int' => [ + 'name' => 'unsigned_int', + 'table' => 'types', + 'type' => 'int', + 'size' => $version80 ? null : 11, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'int' => [ + 'name' => 'int', + 'table' => 'types', + 'type' => 'int', + 'size' => $version80 ? null : 11, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'smallint' => [ + 'name' => 'smallint', + 'table' => 'types', + 'type' => 'int', + 'size' => $version80 ? null : 6, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'tinyint' => [ + 'name' => 'tinyint', + 'table' => 'types', + 'type' => 'int', + 'size' => $version80 ? null : 4, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'mediumint' => [ + 'name' => 'mediumint', + 'table' => 'types', + 'type' => 'int', + 'size' => $version80 ? null : 9, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'bigint' => [ + 'name' => 'bigint', + 'table' => 'types', + 'type' => 'int', + 'size' => $version80 ? null : 20, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'bool' => [ + 'name' => 'bool', + 'table' => 'types', + 'type' => 'bool', + 'size' => 1, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'bit' => [ + 'name' => 'bit', + 'table' => 'types', + 'type' => 'string', + 'size' => 1, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'decimal' => [ + 'name' => 'decimal', + 'table' => 'types', + 'type' => 'int', + 'size' => 10, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'decimal2' => [ + 'name' => 'decimal2', + 'table' => 'types', + 'type' => 'decimal', + 'size' => 10, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'float' => [ + 'name' => 'float', + 'table' => 'types', + 'type' => 'float', + 'size' => null, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'double' => [ + 'name' => 'double', + 'table' => 'types', + 'type' => 'float', + 'size' => null, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'date' => [ + 'name' => 'date', + 'table' => 'types', + 'type' => 'date', + 'size' => null, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'time' => [ + 'name' => 'time', + 'table' => 'types', + 'type' => 'timeint', + 'size' => null, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'datetime' => [ + 'name' => 'datetime', + 'table' => 'types', + 'type' => 'datetime', + 'size' => null, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'timestamp' => [ + 'name' => 'timestamp', + 'table' => 'types', + 'type' => 'datetime', + 'size' => null, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'year' => [ + 'name' => 'year', + 'table' => 'types', + 'type' => 'int', + 'size' => $version80 ? null : 4, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'char' => [ + 'name' => 'char', + 'table' => 'types', + 'type' => 'string', + 'size' => 1, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'varchar' => [ + 'name' => 'varchar', + 'table' => 'types', + 'type' => 'string', + 'size' => 30, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'binary' => [ + 'name' => 'binary', + 'table' => 'types', + 'type' => 'bin', + 'size' => 1, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'varbinary' => [ + 'name' => 'varbinary', + 'table' => 'types', + 'type' => 'bin', + 'size' => 30, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'blob' => [ + 'name' => 'blob', + 'table' => 'types', + 'type' => 'bin', + 'size' => null, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'tinyblob' => [ + 'name' => 'tinyblob', + 'table' => 'types', + 'type' => 'bin', + 'size' => null, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'mediumblob' => [ + 'name' => 'mediumblob', + 'table' => 'types', + 'type' => 'bin', + 'size' => null, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'longblob' => [ + 'name' => 'longblob', + 'table' => 'types', + 'type' => 'bin', + 'size' => null, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'text' => [ + 'name' => 'text', + 'table' => 'types', + 'type' => 'string', + 'size' => null, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'tinytext' => [ + 'name' => 'tinytext', + 'table' => 'types', + 'type' => 'string', + 'size' => null, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'mediumtext' => [ + 'name' => 'mediumtext', + 'table' => 'types', + 'type' => 'string', + 'size' => null, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'longtext' => [ + 'name' => 'longtext', + 'table' => 'types', + 'type' => 'string', + 'size' => null, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'enum' => [ + 'name' => 'enum', + 'table' => 'types', + 'type' => 'string', + 'size' => 0, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'set' => [ + 'name' => 'set', + 'table' => 'types', + 'type' => 'string', + 'size' => 0, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], +]; + +Assert::same( + $expectedColumns, + array_map(fn($c) => [ + 'name' => $c->name, + 'table' => $c->table->name, + 'type' => $c->type, + 'size' => $c->size, + 'nullable' => $c->nullable, + 'default' => $c->default, + 'autoIncrement' => $c->autoIncrement, + 'primary' => $c->primary, + ], $columns), +); diff --git a/tests/Database/Reflection.columns.postgre.phpt b/tests/Database/Reflection.columns.postgre.phpt new file mode 100644 index 000000000..954604859 --- /dev/null +++ b/tests/Database/Reflection.columns.postgre.phpt @@ -0,0 +1,355 @@ +getReflection(); +$columns = $reflection->getTable('types')->columns; + +$expectedColumns = [ + 'smallint' => [ + 'name' => 'smallint', + 'table' => 'types', + 'type' => 'int', + 'size' => null, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'integer' => [ + 'name' => 'integer', + 'table' => 'types', + 'type' => 'int', + 'size' => null, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'bigint' => [ + 'name' => 'bigint', + 'table' => 'types', + 'type' => 'int', + 'size' => null, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'numeric' => [ + 'name' => 'numeric', + 'table' => 'types', + 'type' => 'decimal', + 'size' => null, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'real' => [ + 'name' => 'real', + 'table' => 'types', + 'type' => 'float', + 'size' => null, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'double' => [ + 'name' => 'double', + 'table' => 'types', + 'type' => 'float', + 'size' => null, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'money' => [ + 'name' => 'money', + 'table' => 'types', + 'type' => 'decimal', + 'size' => null, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'bool' => [ + 'name' => 'bool', + 'table' => 'types', + 'type' => 'bool', + 'size' => null, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'date' => [ + 'name' => 'date', + 'table' => 'types', + 'type' => 'date', + 'size' => null, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'time' => [ + 'name' => 'time', + 'table' => 'types', + 'type' => 'time', + 'size' => null, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'timestamp' => [ + 'name' => 'timestamp', + 'table' => 'types', + 'type' => 'datetime', + 'size' => null, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'timestampZone' => [ + 'name' => 'timestampZone', + 'table' => 'types', + 'type' => 'datetime', + 'size' => null, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'interval' => [ + 'name' => 'interval', + 'table' => 'types', + 'type' => 'string', + 'size' => null, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'character' => [ + 'name' => 'character', + 'table' => 'types', + 'type' => 'string', + 'size' => 30, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'character_varying' => [ + 'name' => 'character_varying', + 'table' => 'types', + 'type' => 'string', + 'size' => 30, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'text' => [ + 'name' => 'text', + 'table' => 'types', + 'type' => 'string', + 'size' => null, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'tsquery' => [ + 'name' => 'tsquery', + 'table' => 'types', + 'type' => 'string', + 'size' => null, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'tsvector' => [ + 'name' => 'tsvector', + 'table' => 'types', + 'type' => 'string', + 'size' => null, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'uuid' => [ + 'name' => 'uuid', + 'table' => 'types', + 'type' => 'string', + 'size' => null, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'xml' => [ + 'name' => 'xml', + 'table' => 'types', + 'type' => 'string', + 'size' => null, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'cidr' => [ + 'name' => 'cidr', + 'table' => 'types', + 'type' => 'string', + 'size' => null, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'inet' => [ + 'name' => 'inet', + 'table' => 'types', + 'type' => 'string', + 'size' => null, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'macaddr' => [ + 'name' => 'macaddr', + 'table' => 'types', + 'type' => 'string', + 'size' => null, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'bit' => [ + 'name' => 'bit', + 'table' => 'types', + 'type' => 'string', + 'size' => -3, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'bit_varying' => [ + 'name' => 'bit_varying', + 'table' => 'types', + 'type' => 'string', + 'size' => null, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'bytea' => [ + 'name' => 'bytea', + 'table' => 'types', + 'type' => 'bin', + 'size' => null, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'box' => [ + 'name' => 'box', + 'table' => 'types', + 'type' => 'string', + 'size' => null, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'circle' => [ + 'name' => 'circle', + 'table' => 'types', + 'type' => 'string', + 'size' => null, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'lseg' => [ + 'name' => 'lseg', + 'table' => 'types', + 'type' => 'string', + 'size' => null, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'path' => [ + 'name' => 'path', + 'table' => 'types', + 'type' => 'string', + 'size' => null, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'point' => [ + 'name' => 'point', + 'table' => 'types', + 'type' => 'string', + 'size' => null, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'polygon' => [ + 'name' => 'polygon', + 'table' => 'types', + 'type' => 'string', + 'size' => null, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], +]; + +Assert::same( + $expectedColumns, + array_map(fn($c) => [ + 'name' => $c->name, + 'table' => $c->table->name, + 'type' => $c->type, + 'size' => $c->size, + 'nullable' => $c->nullable, + 'default' => $c->default, + 'autoIncrement' => $c->autoIncrement, + 'primary' => $c->primary, + ], $columns), +); diff --git a/tests/Database/Reflection.columns.sqlite.phpt b/tests/Database/Reflection.columns.sqlite.phpt new file mode 100644 index 000000000..30c031743 --- /dev/null +++ b/tests/Database/Reflection.columns.sqlite.phpt @@ -0,0 +1,305 @@ +getReflection(); +$columns = $reflection->getTable('types')->columns; + +$expectedColumns = [ + 'int' => [ + 'name' => 'int', + 'table' => 'types', + 'type' => 'int', + 'size' => null, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'integer' => [ + 'name' => 'integer', + 'table' => 'types', + 'type' => 'int', + 'size' => null, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'tinyint' => [ + 'name' => 'tinyint', + 'table' => 'types', + 'type' => 'int', + 'size' => null, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'smallint' => [ + 'name' => 'smallint', + 'table' => 'types', + 'type' => 'int', + 'size' => null, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'mediumint' => [ + 'name' => 'mediumint', + 'table' => 'types', + 'type' => 'int', + 'size' => null, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'bigint' => [ + 'name' => 'bigint', + 'table' => 'types', + 'type' => 'int', + 'size' => null, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'unsigned_big_int' => [ + 'name' => 'unsigned_big_int', + 'table' => 'types', + 'type' => 'int', + 'size' => null, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'int2' => [ + 'name' => 'int2', + 'table' => 'types', + 'type' => 'int', + 'size' => null, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'int8' => [ + 'name' => 'int8', + 'table' => 'types', + 'type' => 'int', + 'size' => null, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'character_20' => [ + 'name' => 'character_20', + 'table' => 'types', + 'type' => 'string', + 'size' => 20, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'varchar_255' => [ + 'name' => 'varchar_255', + 'table' => 'types', + 'type' => 'string', + 'size' => 255, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'varying_character_255' => [ + 'name' => 'varying_character_255', + 'table' => 'types', + 'type' => 'string', + 'size' => 255, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'nchar_55' => [ + 'name' => 'nchar_55', + 'table' => 'types', + 'type' => 'string', + 'size' => 55, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'native_character_70' => [ + 'name' => 'native_character_70', + 'table' => 'types', + 'type' => 'string', + 'size' => 70, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'nvarchar_100' => [ + 'name' => 'nvarchar_100', + 'table' => 'types', + 'type' => 'string', + 'size' => 100, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'text' => [ + 'name' => 'text', + 'table' => 'types', + 'type' => 'string', + 'size' => null, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'clob' => [ + 'name' => 'clob', + 'table' => 'types', + 'type' => 'string', + 'size' => null, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'blob' => [ + 'name' => 'blob', + 'table' => 'types', + 'type' => 'bin', + 'size' => null, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'real' => [ + 'name' => 'real', + 'table' => 'types', + 'type' => 'float', + 'size' => null, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'double' => [ + 'name' => 'double', + 'table' => 'types', + 'type' => 'float', + 'size' => null, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'double precision' => [ + 'name' => 'double precision', + 'table' => 'types', + 'type' => 'float', + 'size' => null, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'float' => [ + 'name' => 'float', + 'table' => 'types', + 'type' => 'float', + 'size' => null, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'numeric' => [ + 'name' => 'numeric', + 'table' => 'types', + 'type' => 'decimal', + 'size' => null, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'decimal_10_5' => [ + 'name' => 'decimal_10_5', + 'table' => 'types', + 'type' => 'decimal', + 'size' => 10, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'boolean' => [ + 'name' => 'boolean', + 'table' => 'types', + 'type' => 'bool', + 'size' => null, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'date' => [ + 'name' => 'date', + 'table' => 'types', + 'type' => 'timestamp', + 'size' => null, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'datetime' => [ + 'name' => 'datetime', + 'table' => 'types', + 'type' => 'timestamp', + 'size' => null, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], +]; + +Assert::same( + $expectedColumns, + array_map(fn($c) => [ + 'name' => $c->name, + 'table' => $c->table->name, + 'type' => $c->type, + 'size' => $c->size, + 'nullable' => $c->nullable, + 'default' => $c->default, + 'autoIncrement' => $c->autoIncrement, + 'primary' => $c->primary, + ], $columns), +); diff --git a/tests/Database/Reflection.columns.sqlsrv.phpt b/tests/Database/Reflection.columns.sqlsrv.phpt new file mode 100644 index 000000000..95b2ac2fb --- /dev/null +++ b/tests/Database/Reflection.columns.sqlsrv.phpt @@ -0,0 +1,335 @@ +getReflection(); +$columns = $reflection->getTable('types')->columns; + +$expectedColumns = [ + 'bigint' => [ + 'name' => 'bigint', + 'table' => 'types', + 'type' => 'int', + 'size' => null, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'binary_3' => [ + 'name' => 'binary_3', + 'table' => 'types', + 'type' => 'bin', + 'size' => null, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'bit' => [ + 'name' => 'bit', + 'table' => 'types', + 'type' => 'string', + 'size' => null, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'char_5' => [ + 'name' => 'char_5', + 'table' => 'types', + 'type' => 'string', + 'size' => null, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'date' => [ + 'name' => 'date', + 'table' => 'types', + 'type' => 'date', + 'size' => null, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'datetime' => [ + 'name' => 'datetime', + 'table' => 'types', + 'type' => 'datetime', + 'size' => null, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'datetime2' => [ + 'name' => 'datetime2', + 'table' => 'types', + 'type' => 'datetime', + 'size' => null, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'decimal' => [ + 'name' => 'decimal', + 'table' => 'types', + 'type' => 'decimal', + 'size' => null, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'float' => [ + 'name' => 'float', + 'table' => 'types', + 'type' => 'float', + 'size' => null, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'geography' => [ + 'name' => 'geography', + 'table' => 'types', + 'type' => 'string', + 'size' => null, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'geometry' => [ + 'name' => 'geometry', + 'table' => 'types', + 'type' => 'string', + 'size' => null, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'hierarchyid' => [ + 'name' => 'hierarchyid', + 'table' => 'types', + 'type' => 'string', + 'size' => null, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'int' => [ + 'name' => 'int', + 'table' => 'types', + 'type' => 'int', + 'size' => null, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'money' => [ + 'name' => 'money', + 'table' => 'types', + 'type' => 'decimal', + 'size' => null, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'nchar' => [ + 'name' => 'nchar', + 'table' => 'types', + 'type' => 'string', + 'size' => null, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'ntext' => [ + 'name' => 'ntext', + 'table' => 'types', + 'type' => 'string', + 'size' => null, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'numeric_10_0' => [ + 'name' => 'numeric_10_0', + 'table' => 'types', + 'type' => 'decimal', + 'size' => null, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'numeric_10_2' => [ + 'name' => 'numeric_10_2', + 'table' => 'types', + 'type' => 'decimal', + 'size' => null, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'nvarchar' => [ + 'name' => 'nvarchar', + 'table' => 'types', + 'type' => 'string', + 'size' => null, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'real' => [ + 'name' => 'real', + 'table' => 'types', + 'type' => 'float', + 'size' => null, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'smalldatetime' => [ + 'name' => 'smalldatetime', + 'table' => 'types', + 'type' => 'datetime', + 'size' => null, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'smallint' => [ + 'name' => 'smallint', + 'table' => 'types', + 'type' => 'int', + 'size' => null, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'smallmoney' => [ + 'name' => 'smallmoney', + 'table' => 'types', + 'type' => 'decimal', + 'size' => null, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'text' => [ + 'name' => 'text', + 'table' => 'types', + 'type' => 'string', + 'size' => null, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'time' => [ + 'name' => 'time', + 'table' => 'types', + 'type' => 'time', + 'size' => null, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'tinyint' => [ + 'name' => 'tinyint', + 'table' => 'types', + 'type' => 'int', + 'size' => null, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'uniqueidentifier' => [ + 'name' => 'uniqueidentifier', + 'table' => 'types', + 'type' => 'string', + 'size' => null, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'varbinary' => [ + 'name' => 'varbinary', + 'table' => 'types', + 'type' => 'bin', + 'size' => null, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'varchar' => [ + 'name' => 'varchar', + 'table' => 'types', + 'type' => 'string', + 'size' => null, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], + 'xml' => [ + 'name' => 'xml', + 'table' => 'types', + 'type' => 'string', + 'size' => null, + 'nullable' => true, + 'default' => null, + 'autoIncrement' => false, + 'primary' => false, + ], +]; + +Assert::same( + $expectedColumns, + array_map(fn($c) => [ + 'name' => $c->name, + 'table' => $c->table->name, + 'type' => $c->type, + 'size' => $c->size, + 'nullable' => $c->nullable, + 'default' => $c->default, + 'autoIncrement' => $c->autoIncrement, + 'primary' => $c->primary, + ], $columns), +); diff --git a/tests/Database/Reflection.driver.phpt b/tests/Database/Reflection.driver.phpt index 2a85dde3a..b54e252d0 100644 --- a/tests/Database/Reflection.driver.phpt +++ b/tests/Database/Reflection.driver.phpt @@ -8,6 +8,7 @@ declare(strict_types=1); use Nette\Database\Driver; +use Nette\Database\Type; use Tester\Assert; require __DIR__ . '/connect.inc.php'; // create $connection @@ -20,7 +21,7 @@ $tables = $driver->getTables(); $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)) { +if ($driver->isSupported(Driver::SupportSchema)) { Assert::same( [ ['name' => 'author', 'view' => false, 'fullName' => 'public.author'], @@ -50,6 +51,7 @@ $expectedColumns = [ [ 'name' => 'id', 'table' => 'author', + 'type' => Type::Integer, 'nativetype' => 'INT', 'size' => 11, 'nullable' => false, @@ -60,6 +62,7 @@ $expectedColumns = [ [ 'name' => 'name', 'table' => 'author', + 'type' => Type::Text, 'nativetype' => 'VARCHAR', 'size' => 30, 'nullable' => false, @@ -70,6 +73,7 @@ $expectedColumns = [ [ 'name' => 'web', 'table' => 'author', + 'type' => Type::Text, 'nativetype' => 'VARCHAR', 'size' => 100, 'nullable' => false, @@ -80,6 +84,7 @@ $expectedColumns = [ [ 'name' => 'born', 'table' => 'author', + 'type' => Type::Date, 'nativetype' => 'DATE', 'size' => null, 'nullable' => true, @@ -108,6 +113,7 @@ switch ($driverName) { $expectedColumns[1]['size'] = null; $expectedColumns[2]['nativetype'] = 'TEXT'; $expectedColumns[2]['size'] = null; + $expectedColumns[3]['type'] = Type::UnixTimestamp; break; case 'sqlsrv': $expectedColumns[0]['size'] = null; diff --git a/tests/Database/Reflection.phpt b/tests/Database/Reflection.phpt index 5f953cabd..8d3f51bd6 100644 --- a/tests/Database/Reflection.phpt +++ b/tests/Database/Reflection.phpt @@ -8,6 +8,7 @@ declare(strict_types=1); use Nette\Database\Driver; +use Nette\Database\Type; use Tester\Assert; require __DIR__ . '/connect.inc.php'; // create $connection @@ -16,7 +17,7 @@ Nette\Database\Helpers::loadFromFile($connection, __DIR__ . "/files/{$driverName $reflection = $connection->getReflection(); -$schemaSupported = $connection->getDriver()->isSupported(Driver::SUPPORT_SCHEMA); +$schemaSupported = $connection->getDriver()->isSupported(Driver::SupportSchema); // table names $tableNames = array_keys($reflection->tables); @@ -72,7 +73,7 @@ $expectedColumns = [ 'id' => [ 'name' => 'id', 'table' => 'author', - 'nativeType' => 'INT', + 'type' => Type::Integer, 'size' => 11, 'nullable' => false, 'default' => null, @@ -82,7 +83,7 @@ $expectedColumns = [ 'name' => [ 'name' => 'name', 'table' => 'author', - 'nativeType' => 'VARCHAR', + 'type' => Type::Text, 'size' => 30, 'nullable' => false, 'default' => null, @@ -92,7 +93,7 @@ $expectedColumns = [ 'web' => [ 'name' => 'web', 'table' => 'author', - 'nativeType' => 'VARCHAR', + 'type' => Type::Text, 'size' => 100, 'nullable' => false, 'default' => null, @@ -102,7 +103,7 @@ $expectedColumns = [ 'born' => [ 'name' => 'born', 'table' => 'author', - 'nativeType' => 'DATE', + 'type' => Type::Date, 'size' => null, 'nullable' => true, 'default' => null, @@ -119,17 +120,14 @@ switch ($driverName) { } break; case 'pgsql': - $expectedColumns['id']['nativeType'] = 'INT4'; $expectedColumns['id']['default'] = "nextval('author_id_seq'::regclass)"; $expectedColumns['id']['size'] = null; break; case 'sqlite': - $expectedColumns['id']['nativeType'] = 'INTEGER'; $expectedColumns['id']['size'] = null; - $expectedColumns['name']['nativeType'] = 'TEXT'; $expectedColumns['name']['size'] = null; - $expectedColumns['web']['nativeType'] = 'TEXT'; $expectedColumns['web']['size'] = null; + $expectedColumns['born']['type'] = Type::UnixTimestamp; break; case 'sqlsrv': $expectedColumns['id']['size'] = null; @@ -146,7 +144,7 @@ Assert::same( array_map(fn($c) => [ 'name' => $c->name, 'table' => $c->table->name, - 'nativeType' => $c->nativeType, + 'type' => $c->type, 'size' => $c->size, 'nullable' => $c->nullable, 'default' => $c->default, diff --git a/tests/Database/Reflection.postgre.phpt b/tests/Database/Reflection.postgre.phpt index c5460661d..ab58272e5 100644 --- a/tests/Database/Reflection.postgre.phpt +++ b/tests/Database/Reflection.postgre.phpt @@ -63,9 +63,9 @@ test('Tables in schema', function () use ($connection) { $foreign = $driver->getForeignKeys('one.slave'); Assert::same([ 'name' => 'one_slave_fk', - 'local' => 'one_id', + 'local' => ['one_id'], 'table' => 'one.master', - 'foreign' => 'one_id', + 'foreign' => ['one_id'], ], (array) $foreign[0]); diff --git a/tests/Database/ResultSet.customNormalizer.phpt b/tests/Database/ResultSet.customNormalizer.phpt index d139e022e..7c85d01fa 100644 --- a/tests/Database/ResultSet.customNormalizer.phpt +++ b/tests/Database/ResultSet.customNormalizer.phpt @@ -29,6 +29,20 @@ test('disabled normalization', function () use ($connection) { }); +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()); +}); + + test('custom normalization', function () use ($connection) { $driverName = $GLOBALS['driverName']; diff --git a/tests/Database/Structure.phpt b/tests/Database/Structure.phpt index 434908402..d7a730b6a 100644 --- a/tests/Database/Structure.phpt +++ b/tests/Database/Structure.phpt @@ -74,13 +74,13 @@ class StructureTestCase extends TestCase $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'], + ['local' => ['author_id'], 'table' => 'authors', 'foreign' => ['id'], 'name' => 'authors_fk1'], + ['local' => ['translator_id'], 'table' => 'authors', 'foreign' => ['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'], + ['local' => ['book_id'], 'table' => 'Books', 'foreign' => ['id'], 'name' => 'books_x_tags_fk1'], + ['local' => ['tag_id'], 'table' => 'tags', 'foreign' => ['id'], 'name' => 'books_x_tags_fk2'], ]); $this->structure = new StructureMock($this->connection, $this->storage); diff --git a/tests/Database/Structure.schemas.phpt b/tests/Database/Structure.schemas.phpt index 0a145a56c..a28f10311 100644 --- a/tests/Database/Structure.schemas.phpt +++ b/tests/Database/Structure.schemas.phpt @@ -60,8 +60,8 @@ class StructureSchemasTestCase extends TestCase $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'], + ['local' => ['author_id'], 'table' => 'authors.authors', 'foreign' => ['id'], 'name' => 'authors_authors_fk1'], + ['local' => ['translator_id'], 'table' => 'authors.authors', 'foreign' => ['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 df87efa35..0565dda12 100644 --- a/tests/Database/connect.inc.php +++ b/tests/Database/connect.inc.php @@ -17,7 +17,7 @@ try { $connection = new Nette\Database\Connection($options['dsn'], $options['user'], $options['password'], $options['options']); -} catch (PDOException $e) { +} catch (Nette\Database\ConnectionException $e) { Tester\Environment::skip("Connection to '$options[dsn]' failed. Reason: " . $e->getMessage()); } diff --git a/tests/databases.github.ini b/tests/databases.github.ini index 912a6abb8..1fa02f5d5 100644 --- a/tests/databases.github.ini +++ b/tests/databases.github.ini @@ -3,33 +3,27 @@ dsn = "mysql:host=127.0.0.1;port=3306;dbname=nette_test" user = root password = root options[supportBooleans] = yes -options[newDateTime] = yes [mysql 8.0] dsn = "mysql:host=127.0.0.1;port=3307;dbname=nette_test" user = root password = root options[supportBooleans] = yes -options[newDateTime] = yes [postgresql 9.6] dsn = "pgsql:host=127.0.0.1;port=5432;dbname=nette_test" user = postgres password = postgres -options[newDateTime] = yes [postgresql 13] dsn = "pgsql:host=127.0.0.1;port=5433;dbname=nette_test" user = postgres password = postgres -options[newDateTime] = yes [sqlsrv] dsn = "sqlsrv:Server=localhost,1433;Database=nette_test" user = SA password = "YourStrong!Passw0rd" -options[newDateTime] = yes [sqlite] dsn = "sqlite::memory:" -options[newDateTime] = yes diff --git a/tests/databases.sqlite.ini b/tests/databases.sqlite.ini index 2e076f7e8..2d89c58ce 100644 --- a/tests/databases.sqlite.ini +++ b/tests/databases.sqlite.ini @@ -1,3 +1,2 @@ [sqlite] dsn = "sqlite::memory:" -options[newDateTime] = yes