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
-==============
+[](https://doc.nette.org/database)
[](https://packagist.org/packages/nette/database)
-[](https://github.com/nette/database/actions)
+[](https://github.com/nette/database/actions)
[](https://github.com/nette/database/releases)
[](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