1
0
mirror of https://github.com/dg/dibi.git synced 2025-08-31 01:39:46 +02:00

Compare commits

...

20 Commits

Author SHA1 Message Date
David Grudl
97053089e0 Released version 5.0.2 2024-09-03 03:18:11 +02:00
Lukáš Kotržena
2c7b35c29d Result::normalize() Fixed normalization of "-." numbers 2024-09-03 03:16:52 +02:00
Marek Bartoš
c04d2197e3 Translator: fixed numeric column formatting 2024-09-03 03:10:05 +02:00
Matěj Koubík
d342d8d78f DibiExtension3: fixed schema 2024-09-03 03:10:05 +02:00
David Grudl
7d8c39f42a cs 2024-09-03 03:00:38 +02:00
David Grudl
29b58d64dd PdoDriver: applied #332 #287 2024-09-03 03:00:38 +02:00
David Grudl
0a32bb5bdf support for PHP 8.4 2024-09-03 03:00:38 +02:00
David Grudl
d707b4ba0e PascalCase constants 2024-09-03 03:00:38 +02:00
David Grudl
490cf143ba GitHub actions fixed
- https://learn.microsoft.com/en-us/answers/questions/1853144/error-failed-to-initialize-container-mcr-microsoft
- https://learn.microsoft.com/en-us/sql/tools/sqlcmd/sqlcmd-utility?view=sql-server-ver16&tabs=go%2Cwindows&pivots=cs1-bash
2024-09-03 03:00:38 +02:00
stanley89
12ffa0ffd1 Tracy\Panel: fixed type error 2024-09-02 23:53:15 +02:00
David Grudl
23f65ef837 Revert "SqliteDriver: disables exceptions (is enabled since PHP 8.3)"
This reverts commit bb1f7d4b93.
2024-09-02 23:53:15 +02:00
David Grudl
86a71dde28 Released version 5.0.1 2023-11-25 14:08:47 +01:00
David Grudl
bb1f7d4b93 SqliteDriver: disables exceptions (is enabled since PHP 8.3) 2023-11-25 14:08:47 +01:00
David Grudl
680026747e added DibiExtension3 2023-11-25 14:08:47 +01:00
Jan Rössler
7ca47508cb PostgreReflector: detect IDENTITY columns as autoincrement 2023-11-05 20:38:26 +01:00
Jan Rössler
beba7b3592 PostgreReflector: fix autoincrement column detection 2023-11-05 20:38:26 +01:00
David Grudl
6cc7ce8e44 used PhpStorm Language attribute 2023-11-05 20:38:25 +01:00
Petr Hubík
92b8e6077e fix: PDO::errorInfo() can return NULL as a code, but Exception does not accept NULL code 2023-09-29 15:55:05 +02:00
David Grudl
e45638eab4 cs 2023-09-29 15:55:05 +02:00
Marek Bartoš
8564217bc1 Fluent: execute() has conditional return type 2023-09-05 12:40:51 +02:00
32 changed files with 396 additions and 211 deletions

View File

@@ -7,7 +7,7 @@ jobs:
name: Nette Code Checker
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with:
php-version: 8.0
@@ -21,7 +21,7 @@ jobs:
name: Nette Coding Standard
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with:
php-version: 8.0

View File

@@ -10,7 +10,7 @@ jobs:
name: PHPStan
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with:
php-version: 8.0

View File

@@ -3,7 +3,7 @@ name: Tests
on: [push, pull_request]
env:
php-extensions: mbstring, intl, mysqli, pgsql, sqlsrv-5.10.0beta2, pdo_sqlsrv-5.10.0beta2
php-extensions: mbstring, intl, mysqli, pgsql, sqlsrv-5.12.0, pdo_sqlsrv-5.12.0
php-tools: "composer:v2, pecl"
jobs:
@@ -11,7 +11,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
php: ['8.0', '8.1', '8.2', '8.3']
php: ['8.0', '8.1', '8.2', '8.3', '8.4']
fail-fast: false
@@ -43,7 +43,6 @@ jobs:
--health-retries=5
-e MYSQL_ROOT_PASSWORD=root
-e MYSQL_DATABASE=dibi_test
--entrypoint sh mysql:8 -c "exec docker-entrypoint.sh mysqld --default-authentication-plugin=mysql_native_password"
postgres96:
image: postgres:9.6
@@ -83,13 +82,13 @@ jobs:
- 1433:1433
options: >-
--name=mssql
--health-cmd "/opt/mssql-tools/bin/sqlcmd -S localhost -U SA -P 'YourStrong!Passw0rd' -Q 'SELECT 1'"
--health-cmd "/opt/mssql-tools18/bin/sqlcmd -S localhost -U SA -P 'YourStrong!Passw0rd' -Q 'SELECT 1' -N -C"
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
@@ -101,7 +100,7 @@ jobs:
run: cp ./tests/databases.github.ini ./tests/databases.ini
- name: Create MS SQL Database
run: docker exec -i mssql /opt/mssql-tools/bin/sqlcmd -S localhost -U SA -P 'YourStrong!Passw0rd' -Q 'CREATE DATABASE dibi_test'
run: docker exec -i mssql /opt/mssql-tools18/bin/sqlcmd -S localhost -U SA -P 'YourStrong!Passw0rd' -Q 'CREATE DATABASE dibi_test' -N -C
- run: composer install --no-progress --prefer-dist
- run: vendor/bin/tester -p phpdbg tests -s -C --coverage ./coverage.xml --coverage-src ./src

View File

@@ -11,13 +11,14 @@
}
],
"require": {
"php": "8.0 - 8.3"
"php": "8.0 - 8.4"
},
"require-dev": {
"tracy/tracy": "^2.9",
"nette/tester": "^2.5",
"nette/di": "^3.1",
"phpstan/phpstan": "^1.0"
"phpstan/phpstan": "^1.0",
"jetbrains/phpstorm-attributes": "^1.0"
},
"replace": {
"dg/dibi": "*"
@@ -25,6 +26,7 @@
"autoload": {
"classmap": ["src/"]
},
"minimum-stability": "dev",
"scripts": {
"phpstan": "phpstan analyse",
"tester": "tester tests -s"

View File

@@ -27,9 +27,9 @@ $dibi = new Dibi\Connection([
// using manual hints
$res = $dibi->query('SELECT * FROM [customers]');
$res->setType('customer_id', Type::INTEGER)
->setType('added', Type::DATETIME)
->setFormat(Type::DATETIME, 'Y-m-d H:i:s');
$res->setType('customer_id', Type::Integer)
->setType('added', Type::DateTime)
->setFormat(Type::DateTime, 'Y-m-d H:i:s');
Tracy\Dumper::dump($res->fetch());

View File

@@ -34,7 +34,7 @@ Install Dibi via Composer:
composer require dibi/dibi
```
The Dibi 5.0 requires PHP version 8.0 and supports PHP up to 8.3.
The Dibi 5.0 requires PHP version 8.0 and supports PHP up to 8.4.
Usage
@@ -608,7 +608,7 @@ $database->query("UPDATE [:blog:items] SET [text]='Hello World'");
Dibi automatically detects the types of query columns and converts fields them to native PHP types. We can also specify the type manually. You can find the possible types in the `Dibi\Type` class.
```php
$result->setType('id', Dibi\Type::INTEGER); // id will be integer
$result->setType('id', Dibi\Type::Integer); // id will be integer
$row = $result->fetch();
is_int($row->id) // true
@@ -639,7 +639,7 @@ In the configuration file, we will register the DI extensions and add the `dibi`
```neon
extensions:
dibi: Dibi\Bridges\Nette\DibiExtension22
dibi: Dibi\Bridges\Nette\DibiExtension3
dibi:
host: localhost

View File

@@ -0,0 +1,96 @@
<?php
/**
* This file is part of the Dibi, smart database abstraction layer (https://dibiphp.com)
* Copyright (c) 2005 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Dibi\Bridges\Nette;
use Dibi;
use Nette;
use Nette\Schema\Expect;
use Tracy;
/**
* Dibi extension for Nette Framework 3. Creates 'connection' & 'panel' services.
*/
class DibiExtension3 extends Nette\DI\CompilerExtension
{
private ?bool $debugMode;
private ?bool $cliMode;
public function __construct(?bool $debugMode = null, ?bool $cliMode = null)
{
$this->debugMode = $debugMode;
$this->cliMode = $cliMode;
}
public function getConfigSchema(): Nette\Schema\Schema
{
return Expect::structure([
'autowired' => Expect::bool(true),
'flags' => Expect::anyOf(Expect::arrayOf('string'), Expect::type('dynamic')),
'profiler' => Expect::bool(),
'explain' => Expect::bool(true),
'filter' => Expect::bool(true),
'driver' => Expect::string()->dynamic(),
'name' => Expect::string()->dynamic(),
'lazy' => Expect::bool(false)->dynamic(),
'onConnect' => Expect::array()->dynamic(),
'substitutes' => Expect::arrayOf('string')->dynamic(),
'result' => Expect::structure([
'normalize' => Expect::bool(true),
'formatDateTime' => Expect::string(),
'formatTimeInterval' => Expect::string(),
'formatJson' => Expect::string(),
])->castTo('array'),
])->otherItems(Expect::type('mixed'))
->castTo('array');
}
public function loadConfiguration()
{
$container = $this->getContainerBuilder();
$config = $this->getConfig();
$this->debugMode ??= $container->parameters['debugMode'];
$this->cliMode ??= $container->parameters['consoleMode'];
$useProfiler = $config['profiler'] ?? (class_exists(Tracy\Debugger::class) && $this->debugMode && !$this->cliMode);
unset($config['profiler']);
if (is_array($config['flags'])) {
$flags = 0;
foreach ((array) $config['flags'] as $flag) {
$flags |= constant($flag);
}
$config['flags'] = $flags;
}
$connection = $container->addDefinition($this->prefix('connection'))
->setCreator(Dibi\Connection::class, [$config])
->setAutowired($config['autowired']);
if (class_exists(Tracy\Debugger::class)) {
$connection->addSetup(
[new Nette\DI\Definitions\Statement('Tracy\Debugger::getBlueScreen'), 'addPanel'],
[[Dibi\Bridges\Tracy\Panel::class, 'renderException']],
);
}
if ($useProfiler) {
$panel = $container->addDefinition($this->prefix('panel'))
->setCreator(Dibi\Bridges\Tracy\Panel::class, [
$config['explain'],
$config['filter'] ? Dibi\Event::QUERY : Dibi\Event::ALL,
]);
$connection->addSetup([$panel, 'register'], [$connection]);
}
}
}

View File

@@ -1,8 +1,8 @@
# This will create service named 'dibi.connection'.
# Requires Nette Framework 2.2 or later
# Requires Nette Framework 3 or later
extensions:
dibi: Dibi\Bridges\Nette\DibiExtension22
dibi: Dibi\Bridges\Nette\DibiExtension3
dibi:
host: localhost

View File

@@ -62,7 +62,7 @@ class Panel implements Tracy\IBarPanel
if ($e instanceof Dibi\Exception && $e->getSql()) {
return [
'tab' => 'SQL',
'panel' => Helpers::dump($e->getSql(), true),
'panel' => Helpers::dump($e->getSql(), return: true),
];
}
@@ -118,7 +118,7 @@ class Panel implements Tracy\IBarPanel
? $this->explain
: ($connection->getConfig('driver') === 'oracle' ? 'EXPLAIN PLAN FOR' : 'EXPLAIN');
try {
$explain = @Helpers::dump($connection->nativeQuery("$cmd $event->sql"), true);
$explain = @Helpers::dump($connection->nativeQuery("$cmd $event->sql"), return: true);
} catch (Dibi\Exception $e) {
}
@@ -132,7 +132,7 @@ class Panel implements Tracy\IBarPanel
$s .= "<br /><a href='#tracy-debug-DibiProfiler-row-$counter' class='tracy-toggle tracy-collapsed' rel='#tracy-debug-DibiProfiler-row-$counter'>explain</a>";
}
$s .= '</td><td class="tracy-DibiProfiler-sql">' . Helpers::dump(strlen($event->sql) > self::$maxLength ? substr($event->sql, 0, self::$maxLength) . '...' : $event->sql, true);
$s .= '</td><td class="tracy-DibiProfiler-sql">' . Helpers::dump(strlen($event->sql) > self::$maxLength ? substr($event->sql, 0, self::$maxLength) . '...' : $event->sql, return: true);
if ($explain) {
$s .= "<div id='tracy-debug-DibiProfiler-row-$counter' class='tracy-collapsed'>{$explain}</div>";
}
@@ -151,8 +151,8 @@ class Panel implements Tracy\IBarPanel
#tracy-debug .tracy-DibiProfiler-source { color: #999 !important }
#tracy-debug tracy-DibiProfiler tr table { margin: 8px 0; max-height: 150px; overflow:auto } </style>
<h1>Queries:' . "\u{a0}" . count($this->events)
. ($totalTime === null ? '' : ", time:\u{a0}" . number_format($totalTime * 1000, 1, '.', "\u{202f}") . "\u{202f}ms") . ', '
. htmlspecialchars($this->getConnectionName($singleConnection)) . '</h1>
. ($totalTime === null ? '' : ", time:\u{a0}" . number_format($totalTime * 1000, 1, '.', "\u{202f}") . "\u{202f}ms")
. ($singleConnection === null ? '' : ', ' . htmlspecialchars($this->getConnectionName($singleConnection))) . '</h1>
<div class="tracy-inner tracy-DibiProfiler">
<table class="tracy-sortable">
<tr><th>Time&nbsp;ms</th><th>SQL Statement</th><th>Rows</th>' . (!$singleConnection ? '<th>Connection</th>' : '') . '</tr>

View File

@@ -9,6 +9,7 @@ declare(strict_types=1);
namespace Dibi;
use JetBrains\PhpStorm\Language;
use Traversable;
@@ -73,10 +74,10 @@ class Connection implements IConnection
$this->config = $config;
$this->formats = [
Type::DATE => $this->config['result']['formatDate'],
Type::DATETIME => $this->config['result']['formatDateTime'],
Type::Date => $this->config['result']['formatDate'],
Type::DateTime => $this->config['result']['formatDateTime'],
Type::JSON => $this->config['result']['formatJson'] ?? 'array',
Type::TIME_INTERVAL => $this->config['result']['formatTimeInterval'] ?? null,
Type::TimeInterval => $this->config['result']['formatTimeInterval'] ?? null,
];
// profiler
@@ -209,7 +210,7 @@ class Connection implements IConnection
* Generates (translates) and executes SQL query.
* @throws Exception
*/
final public function query(mixed ...$args): Result
final public function query(#[Language('GenericSQL')] mixed ...$args): Result
{
return $this->nativeQuery($this->translate(...$args));
}
@@ -219,7 +220,7 @@ class Connection implements IConnection
* Generates SQL query.
* @throws Exception
*/
final public function translate(mixed ...$args): string
final public function translate(#[Language('GenericSQL')] mixed ...$args): string
{
if (!$this->driver) {
$this->connect();
@@ -232,7 +233,7 @@ class Connection implements IConnection
/**
* Generates and prints SQL query.
*/
final public function test(mixed ...$args): bool
final public function test(#[Language('GenericSQL')] mixed ...$args): bool
{
try {
Helpers::dump($this->translate(...$args));
@@ -254,7 +255,7 @@ class Connection implements IConnection
* Generates (translates) and returns SQL query as DataSource.
* @throws Exception
*/
final public function dataSource(mixed ...$args): DataSource
final public function dataSource(#[Language('GenericSQL')] mixed ...$args): DataSource
{
return new DataSource($this->translate(...$args), $this);
}
@@ -264,7 +265,7 @@ class Connection implements IConnection
* Executes the SQL query.
* @throws Exception
*/
final public function nativeQuery(string $sql): Result
final public function nativeQuery(#[Language('SQL')] string $sql): Result
{
if (!$this->driver) {
$this->connect();
@@ -593,7 +594,7 @@ class Connection implements IConnection
* Executes SQL query and fetch result - shortcut for query() & fetch().
* @throws Exception
*/
public function fetch(mixed ...$args): ?Row
public function fetch(#[Language('GenericSQL')] mixed ...$args): ?Row
{
return $this->query($args)->fetch();
}
@@ -604,7 +605,7 @@ class Connection implements IConnection
* @return Row[]|array[]
* @throws Exception
*/
public function fetchAll(mixed ...$args): array
public function fetchAll(#[Language('GenericSQL')] mixed ...$args): array
{
return $this->query($args)->fetchAll();
}
@@ -614,7 +615,7 @@ class Connection implements IConnection
* Executes SQL query and fetch first column - shortcut for query() & fetchSingle().
* @throws Exception
*/
public function fetchSingle(mixed ...$args): mixed
public function fetchSingle(#[Language('GenericSQL')] mixed ...$args): mixed
{
return $this->query($args)->fetchSingle();
}
@@ -624,7 +625,7 @@ class Connection implements IConnection
* Executes SQL query and fetch pairs - shortcut for query() & fetchPairs().
* @throws Exception
*/
public function fetchPairs(mixed ...$args): array
public function fetchPairs(#[Language('GenericSQL')] mixed ...$args): array
{
return $this->query($args)->fetchPairs();
}

View File

@@ -26,7 +26,10 @@ use Dibi\Helpers;
*/
class FirebirdDriver implements Dibi\Driver
{
public const ERROR_EXCEPTION_THROWN = -836;
public const ErrorExceptionThrown = -836;
/** @deprecated use FirebirdDriver::ErrorExceptionThrown */
public const ERROR_EXCEPTION_THROWN = self::ErrorExceptionThrown;
/** @var resource */
private $connection;

View File

@@ -32,9 +32,18 @@ use Dibi;
*/
class MySqliDriver implements Dibi\Driver
{
public const ERROR_ACCESS_DENIED = 1045;
public const ERROR_DUPLICATE_ENTRY = 1062;
public const ERROR_DATA_TRUNCATED = 1265;
public const ErrorAccessDenied = 1045;
public const ErrorDuplicateEntry = 1062;
public const ErrorDataTruncated = 1265;
/** @deprecated use MySqliDriver::ErrorAccessDenied */
public const ERROR_ACCESS_DENIED = self::ErrorAccessDenied;
/** @deprecated use MySqliDriver::ErrorDuplicateEntry */
public const ERROR_DUPLICATE_ENTRY = self::ErrorDuplicateEntry;
/** @deprecated use MySqliDriver::ErrorDataTruncated */
public const ERROR_DATA_TRUNCATED = self::ErrorDataTruncated;
private \mysqli $connection;
private bool $buffered = false;

View File

@@ -103,7 +103,7 @@ class MySqliResult implements Dibi\ResultDriver
'table' => $row['orgtable'],
'fullname' => $row['table'] ? $row['table'] . '.' . $row['name'] : $row['name'],
'nativetype' => $types[$row['type']] ?? $row['type'],
'type' => $row['type'] === MYSQLI_TYPE_TIME ? Dibi\Type::TIME_INTERVAL : null,
'type' => $row['type'] === MYSQLI_TYPE_TIME ? Dibi\Type::TimeInterval : null,
'vendor' => $row,
];
}

View File

@@ -119,7 +119,7 @@ class OdbcDriver implements Dibi\Driver
*/
public function begin(?string $savepoint = null): void
{
if (!odbc_autocommit($this->connection, false)) {
if (!odbc_autocommit($this->connection)) {
throw new Dibi\DriverException(odbc_errormsg($this->connection) . ' ' . odbc_error($this->connection));
}
}

View File

@@ -80,7 +80,7 @@ class OracleResult implements Dibi\ResultDriver
'name' => oci_field_name($this->resultSet, $i),
'table' => null,
'fullname' => oci_field_name($this->resultSet, $i),
'type' => $type === 'LONG' ? Dibi\Type::TEXT : null,
'type' => $type === 'LONG' ? Dibi\Type::Text : null,
'nativetype' => $type === 'NUMBER' && oci_field_scale($this->resultSet, $i) === 0 ? 'INTEGER' : $type,
];
}

View File

@@ -93,23 +93,15 @@ class PdoDriver implements Dibi\Driver
$this->affectedRows = null;
[$sqlState, $code, $message] = $this->connection->errorInfo();
$code ??= 0;
$message = "SQLSTATE[$sqlState]: $message";
switch ($this->driverName) {
case 'mysql':
throw MySqliDriver::createException($message, $code, $sql);
case 'oci':
throw OracleDriver::createException($message, $code, $sql);
case 'pgsql':
throw PostgreDriver::createException($message, $sqlState, $sql);
case 'sqlite':
throw SqliteDriver::createException($message, $code, $sql);
default:
throw new Dibi\DriverException($message, $code, $sql);
}
throw match ($this->driverName) {
'mysql' => MySqliDriver::createException($message, $code, $sql),
'oci' => OracleDriver::createException($message, $code, $sql),
'pgsql' => PostgreDriver::createException($message, $sqlState, $sql),
'sqlite' => SqliteDriver::createException($message, $code, $sql),
default => new Dibi\DriverException($message, $code, $sql),
};
}
@@ -139,7 +131,7 @@ class PdoDriver implements Dibi\Driver
{
if (!$this->connection->beginTransaction()) {
$err = $this->connection->errorInfo();
throw new Dibi\DriverException("SQLSTATE[$err[0]]: $err[2]", $err[1]);
throw new Dibi\DriverException("SQLSTATE[$err[0]]: $err[2]", $err[1] ?? 0);
}
}
@@ -152,7 +144,7 @@ class PdoDriver implements Dibi\Driver
{
if (!$this->connection->commit()) {
$err = $this->connection->errorInfo();
throw new Dibi\DriverException("SQLSTATE[$err[0]]: $err[2]", $err[1]);
throw new Dibi\DriverException("SQLSTATE[$err[0]]: $err[2]", $err[1] ?? 0);
}
}
@@ -165,7 +157,7 @@ class PdoDriver implements Dibi\Driver
{
if (!$this->connection->rollBack()) {
$err = $this->connection->errorInfo();
throw new Dibi\DriverException("SQLSTATE[$err[0]]: $err[2]", $err[1]);
throw new Dibi\DriverException("SQLSTATE[$err[0]]: $err[2]", $err[1] ?? 0);
}
}
@@ -184,27 +176,14 @@ class PdoDriver implements Dibi\Driver
*/
public function getReflector(): Dibi\Reflector
{
switch ($this->driverName) {
case 'mysql':
return new MySqlReflector($this);
case 'oci':
return new OracleReflector($this);
case 'pgsql':
return new PostgreReflector($this, $this->connection->getAttribute(PDO::ATTR_SERVER_VERSION));
case 'sqlite':
return new SqliteReflector($this);
case 'mssql':
case 'dblib':
case 'sqlsrv':
return new SqlsrvReflector($this);
default:
throw new Dibi\NotSupportedException;
}
return match ($this->driverName) {
'mysql' => new MySqlReflector($this),
'oci' => new OracleReflector($this),
'pgsql' => new PostgreReflector($this, $this->connection->getAttribute(PDO::ATTR_SERVER_VERSION)),
'sqlite' => new SqliteReflector($this),
'mssql', 'dblib', 'sqlsrv' => new SqlsrvReflector($this),
default => throw new Dibi\NotSupportedException,
};
}
@@ -225,44 +204,34 @@ class PdoDriver implements Dibi\Driver
*/
public function escapeText(string $value): string
{
return $this->driverName === 'odbc'
? "'" . str_replace("'", "''", $value) . "'"
: $this->connection->quote($value, PDO::PARAM_STR);
return match ($this->driverName) {
'odbc' => "'" . str_replace("'", "''", $value) . "'",
'sqlsrv' => "N'" . str_replace("'", "''", $value) . "'",
default => $this->connection->quote($value, PDO::PARAM_STR),
};
}
public function escapeBinary(string $value): string
{
return $this->driverName === 'odbc'
? "'" . str_replace("'", "''", $value) . "'"
: $this->connection->quote($value, PDO::PARAM_LOB);
return match ($this->driverName) {
'odbc' => "'" . str_replace("'", "''", $value) . "'",
'sqlsrv' => '0x' . bin2hex($value),
default => $this->connection->quote($value, PDO::PARAM_LOB),
};
}
public function escapeIdentifier(string $value): string
{
switch ($this->driverName) {
case 'mysql':
return '`' . str_replace('`', '``', $value) . '`';
case 'oci':
case 'pgsql':
return '"' . str_replace('"', '""', $value) . '"';
case 'sqlite':
return '[' . strtr($value, '[]', ' ') . ']';
case 'odbc':
case 'mssql':
return '[' . str_replace(['[', ']'], ['[[', ']]'], $value) . ']';
case 'dblib':
case 'sqlsrv':
return '[' . str_replace(']', ']]', $value) . ']';
default:
return $value;
}
return match ($this->driverName) {
'mysql' => '`' . str_replace('`', '``', $value) . '`',
'oci', 'pgsql' => '"' . str_replace('"', '""', $value) . '"',
'sqlite' => '[' . strtr($value, '[]', ' ') . ']',
'odbc', 'mssql' => '[' . str_replace(['[', ']'], ['[[', ']]'], $value) . ']',
'dblib', 'sqlsrv' => '[' . str_replace(']', ']]', $value) . ']',
default => $value,
};
}
@@ -284,16 +253,11 @@ class PdoDriver implements Dibi\Driver
public function escapeDateTime(\DateTimeInterface $value): string
{
switch ($this->driverName) {
case 'odbc':
return $value->format('#m/d/Y H:i:s.u#');
case 'mssql':
case 'dblib':
case 'sqlsrv':
return 'CONVERT(DATETIME2(7), ' . $value->format("'Y-m-d H:i:s.u'") . ')';
default:
return $value->format("'Y-m-d H:i:s.u'");
}
return match ($this->driverName) {
'odbc' => $value->format('#m/d/Y H:i:s.u#'),
'mssql', 'dblib', 'sqlsrv' => 'CONVERT(DATETIME2(7), ' . $value->format("'Y-m-d H:i:s.u'") . ')',
default => $value->format("'Y-m-d H:i:s.u'"),
};
}

View File

@@ -90,7 +90,7 @@ class PdoResult implements Dibi\ResultDriver
'name' => $row['name'],
'table' => $row['table'],
'nativetype' => $row['native_type'],
'type' => $row['native_type'] === 'TIME' && $this->driverName === 'mysql' ? Dibi\Type::TIME_INTERVAL : null,
'type' => $row['native_type'] === 'TIME' && $this->driverName === 'mysql' ? Dibi\Type::TimeInterval : null,
'fullname' => $row['table'] ? $row['table'] . '.' . $row['name'] : $row['name'],
'vendor' => $row,
];

View File

@@ -72,13 +72,6 @@ class PostgreReflector implements Dibi\Reflector
public function getColumns(string $table): array
{
$_table = $this->driver->escapeText($this->driver->escapeIdentifier($table));
$res = $this->driver->query("
SELECT indkey
FROM pg_class
LEFT JOIN pg_index on pg_class.oid = pg_index.indrelid AND pg_index.indisprimary
WHERE pg_class.oid = $_table::regclass
");
$primary = (int) $res->fetch(true)['indkey'];
$res = $this->driver->query("
SELECT *
@@ -98,7 +91,8 @@ class PostgreReflector implements Dibi\Reflector
a.atttypmod-4 AS character_maximum_length,
NOT a.attnotnull AS is_nullable,
a.attnum AS ordinal_position,
pg_get_expr(adef.adbin, adef.adrelid) AS column_default
pg_get_expr(adef.adbin, adef.adrelid) AS column_default,
CASE WHEN a.attidentity IN ('a', 'd') THEN 'YES' ELSE 'NO' END AS is_identity
FROM
pg_attribute a
JOIN pg_type ON a.atttypid = pg_type.oid
@@ -123,7 +117,7 @@ class PostgreReflector implements Dibi\Reflector
'size' => $size > 0 ? $size : null,
'nullable' => $row['is_nullable'] === 'YES' || $row['is_nullable'] === 't' || $row['is_nullable'] === true,
'default' => $row['column_default'],
'autoincrement' => (int) $row['ordinal_position'] === $primary && str_starts_with($row['column_default'] ?? '', 'nextval'),
'autoincrement' => $row['is_identity'] === 'YES' || str_starts_with($row['column_default'] ?? '', 'nextval('),
'vendor' => $row,
];
}

View File

@@ -45,7 +45,13 @@ namespace Dibi;
*/
class Fluent implements IDataSource
{
public const REMOVE = false;
public const
AffectedRows = 'a',
Identifier = 'n',
Remove = false;
/** @deprecated use Fluent::Remove */
public const REMOVE = self::Remove;
public static array $masks = [
'SELECT' => ['SELECT', 'DISTINCT', 'FROM', 'WHERE', 'GROUP BY',
@@ -140,7 +146,7 @@ class Fluent implements IDataSource
$this->cursor = &$this->clauses[$clause];
// TODO: really delete?
if ($args === [self::REMOVE]) {
if ($args === [self::Remove]) {
$this->cursor = null;
return $this;
}
@@ -156,7 +162,7 @@ class Fluent implements IDataSource
}
} else {
// append to currect flow
if ($args === [self::REMOVE]) {
if ($args === [self::Remove]) {
return $this;
}
@@ -279,19 +285,17 @@ class Fluent implements IDataSource
/**
* Generates and executes SQL query.
* Returns result set or number of affected rows
* @return ($return is self::Identifier|self::AffectedRows ? int : Result)
* @throws Exception
*/
public function execute(?string $return = null): Result|int|null
{
$res = $this->query($this->_export());
switch ($return) {
case \dibi::IDENTIFIER:
return $this->connection->getInsertId();
case \dibi::AFFECTED_ROWS:
return $this->connection->getAffectedRows();
default:
return $res;
}
return match ($return) {
self::Identifier => $this->connection->getInsertId(),
self::AffectedRows => $this->connection->getAffectedRows(),
default => $res,
};
}

View File

@@ -46,7 +46,7 @@ abstract class HashMapBase
*/
final class HashMap extends HashMapBase
{
public function __set(string $nm, $val)
public function __set(string $nm, mixed $val): void
{
if ($nm === '') {
$nm = "\xFF";
@@ -56,7 +56,7 @@ final class HashMap extends HashMapBase
}
public function __get(string $nm)
public function __get(string $nm): mixed
{
if ($nm === '') {
$nm = "\xFF";

View File

@@ -159,12 +159,12 @@ class Helpers
public static function escape(Driver $driver, $value, string $type): string
{
$types = [
Type::TEXT => 'text',
Type::BINARY => 'binary',
Type::BOOL => 'bool',
Type::DATE => 'date',
Type::DATETIME => 'datetime',
\dibi::IDENTIFIER => 'identifier',
Type::Text => 'text',
Type::Binary => 'binary',
Type::Bool => 'bool',
Type::Date => 'date',
Type::DateTime => 'datetime',
Fluent::Identifier => 'identifier',
];
if (isset($types[$type])) {
return $driver->{'escape' . $types[$type]}($value);
@@ -181,16 +181,16 @@ class Helpers
public static function detectType(string $type): ?string
{
$patterns = [
'^_' => Type::TEXT, // PostgreSQL arrays
'RANGE$' => Type::TEXT, // PostgreSQL range types
'BYTEA|BLOB|BIN' => Type::BINARY,
'TEXT|CHAR|POINT|INTERVAL|STRING' => Type::TEXT,
'YEAR|BYTE|COUNTER|SERIAL|INT|LONG|SHORT|^TINY$' => Type::INTEGER,
'CURRENCY|REAL|MONEY|FLOAT|DOUBLE|DECIMAL|NUMERIC|NUMBER' => Type::FLOAT,
'^TIME$' => Type::TIME,
'TIME' => Type::DATETIME, // DATETIME, TIMESTAMP
'DATE' => Type::DATE,
'BOOL' => Type::BOOL,
'^_' => Type::Text, // PostgreSQL arrays
'RANGE$' => Type::Text, // PostgreSQL range types
'BYTEA|BLOB|BIN' => Type::Binary,
'TEXT|CHAR|POINT|INTERVAL|STRING' => Type::Text,
'YEAR|BYTE|COUNTER|SERIAL|INT|LONG|SHORT|^TINY$' => Type::Integer,
'CURRENCY|REAL|MONEY|FLOAT|DOUBLE|DECIMAL|NUMERIC|NUMBER' => Type::Float,
'^TIME$' => Type::Time,
'TIME' => Type::DateTime, // DATETIME, TIMESTAMP
'DATE' => Type::Date,
'BOOL' => Type::Bool,
'JSON' => Type::JSON,
];

View File

@@ -457,16 +457,22 @@ class Result implements IDataSource
if ($type === null || $format === 'native') {
$row[$key] = $value;
} elseif ($type === Type::TEXT) {
} elseif ($type === Type::Text) {
$row[$key] = (string) $value;
} elseif ($type === Type::INTEGER) {
} elseif ($type === Type::Integer) {
$row[$key] = is_float($tmp = $value * 1)
? (is_string($value) ? $value : (int) $value)
: $tmp;
} elseif ($type === Type::FLOAT) {
$value = ltrim((string) $value, '0');
} elseif ($type === Type::Float) {
if (!is_string($value)) {
$row[$key] = (float) $value;
continue;
}
$negative = ($value[0] ?? null) === '-';
$value = ltrim($value, '0-');
$p = strpos($value, '.');
$e = strpos($value, 'e');
if ($p !== false && $e === false) {
@@ -479,27 +485,31 @@ class Result implements IDataSource
$value = '0' . $value;
}
if ($negative) {
$value = '-' . $value;
}
$row[$key] = $value === str_replace(',', '.', (string) ($float = (float) $value))
? $float
: $value;
} elseif ($type === Type::BOOL) {
} elseif ($type === Type::Bool) {
$row[$key] = ((bool) $value) && $value !== 'f' && $value !== 'F';
} elseif ($type === Type::DATETIME || $type === Type::DATE || $type === Type::TIME) {
} elseif ($type === Type::DateTime || $type === Type::Date || $type === Type::Time) {
if ($value && !str_starts_with((string) $value, '0000-00')) { // '', null, false, '0000-00-00', ...
$value = new DateTime($value);
$row[$key] = $format ? $value->format($format) : $value;
} else {
$row[$key] = null;
}
} elseif ($type === Type::TIME_INTERVAL) {
} elseif ($type === Type::TimeInterval) {
preg_match('#^(-?)(\d+)\D(\d+)\D(\d+)\z#', $value, $m);
$value = new \DateInterval("PT$m[2]H$m[3]M$m[4]S");
$value->invert = (int) (bool) $m[1];
$row[$key] = $format ? $value->format($format) : $value;
} elseif ($type === Type::BINARY) {
} elseif ($type === Type::Binary) {
$row[$key] = is_string($value)
? $this->getResultDriver()->unescapeBinary($value)
: $value;

View File

@@ -48,10 +48,11 @@ class Row implements \ArrayAccess, \IteratorAggregate, \Countable
}
public function __get(string $key)
public function __get(string $key): mixed
{
$hint = Helpers::getSuggestion(array_keys((array) $this), $key);
trigger_error("Attempt to read missing column '$key'" . ($hint ? ", did you mean '$hint'?" : '.'), E_USER_NOTICE);
return null;
}

View File

@@ -219,7 +219,7 @@ final class Translator
case 'a': // key=val, key=val, ...
foreach ($value as $k => $v) {
$pair = explode('%', $k, 2); // split into identifier & modifier
$pair = explode('%', (string) $k, 2); // split into identifier & modifier
$vx[] = $this->identifiers->{$pair[0]} . '='
. $this->formatValue($v, $pair[1] ?? (is_array($v) ? 'ex!' : null));
}

View File

@@ -16,16 +16,43 @@ namespace Dibi;
class Type
{
public const
TEXT = 's', // as 'string'
BINARY = 'bin',
Text = 's', // as 'string'
Binary = 'bin',
JSON = 'json',
BOOL = 'b',
INTEGER = 'i',
FLOAT = 'f',
DATE = 'd',
DATETIME = 'dt',
TIME = 't',
TIME_INTERVAL = 'ti';
Bool = 'b',
Integer = 'i',
Float = 'f',
Date = 'd',
DateTime = 'dt',
Time = 't',
TimeInterval = 'ti';
/** @deprecated use Type::Text */
public const TEXT = self::Text;
/** @deprecated use Type::Binary */
public const BINARY = self::Binary;
/** @deprecated use Type::Bool */
public const BOOL = self::Bool;
/** @deprecated use Type::Integer */
public const INTEGER = self::Integer;
/** @deprecated use Type::Float */
public const FLOAT = self::Float;
/** @deprecated use Type::Date */
public const DATE = self::Date;
/** @deprecated use Type::DateTime */
public const DATETIME = self::DateTime;
/** @deprecated use Type::Time */
public const TIME = self::Time;
/** @deprecated use Type::TimeInterval */
public const TIME_INTERVAL = self::TimeInterval;
final public function __construct()

View File

@@ -37,12 +37,16 @@ declare(strict_types=1);
*/
class dibi
{
public const
AFFECTED_ROWS = 'a',
IDENTIFIER = 'n';
public const Version = '5.0.2';
/** version */
public const VERSION = '5.0.0';
/** @deprecated use dibi::Version */
public const VERSION = self::Version;
/** @deprecated use Dibi\Fluent::AffectedRows */
public const AFFECTED_ROWS = Dibi\Fluent::AffectedRows;
/** @deprecated use Dibi\Fluent::Identifier */
public const IDENTIFIER = Dibi\Fluent::Identifier;
/** sorting order */
public const

View File

@@ -74,7 +74,7 @@ Assert::same(
(string) $fluent,
);
$fluent->orderBy(Dibi\Fluent::REMOVE);
$fluent->orderBy(Dibi\Fluent::Remove);
Assert::same(
reformat('SELECT * , [a] , [b] AS [bAlias] , [c], [d], [e] , [d] FROM [anotherTable] AS [anotherAlias] INNER JOIN [table3] ON table.col = table3.col WHERE col > 10 OR col < 5 AND active = 1 AND [col] IN (1, 2, 3)'),

View File

@@ -27,8 +27,8 @@ class MockResult extends Dibi\Result
test('', function () {
$result = new MockResult;
$result->setType('col', Type::TEXT);
$result->setFormat(Type::TEXT, 'native');
$result->setType('col', Type::Text);
$result->setFormat(Type::Text, 'native');
Assert::same(['col' => null], $result->test(['col' => null]));
Assert::same(['col' => true], $result->test(['col' => true]));
@@ -38,7 +38,7 @@ test('', function () {
test('', function () {
$result = new MockResult;
$result->setType('col', Type::BOOL);
$result->setType('col', Type::Bool);
Assert::same(['col' => null], $result->test(['col' => null]));
Assert::same(['col' => true], $result->test(['col' => true]));
@@ -60,7 +60,7 @@ test('', function () {
test('', function () {
$result = new MockResult;
$result->setType('col', Type::TEXT);
$result->setType('col', Type::Text);
Assert::same(['col' => null], $result->test(['col' => null]));
Assert::same(['col' => '1'], $result->test(['col' => true]));
@@ -76,7 +76,7 @@ test('', function () {
test('', function () {
$result = new MockResult;
$result->setType('col', Type::FLOAT);
$result->setType('col', Type::Float);
Assert::same(['col' => null], $result->test(['col' => null]));
Assert::same(['col' => 1.0], $result->test(['col' => true]));
@@ -117,6 +117,37 @@ test('', function () {
Assert::same(['col' => '1.1e+10'], $result->test(['col' => '001.1e+10']));
Assert::notSame(['col' => '1.1e+1'], $result->test(['col' => '1.1e+10']));
// negative
Assert::same(['col' => -0.0], $result->test(['col' => '-']));
Assert::same(['col' => -0.0], $result->test(['col' => '-0']));
Assert::same(['col' => -1.0], $result->test(['col' => '-1']));
Assert::same(['col' => -0.0], $result->test(['col' => '-.0']));
Assert::same(['col' => -0.1], $result->test(['col' => '-.1']));
Assert::same(['col' => -0.0], $result->test(['col' => '-0.0']));
Assert::same(['col' => -0.1], $result->test(['col' => '-0.1']));
Assert::same(['col' => -0.0], $result->test(['col' => '-0.000']));
Assert::same(['col' => -0.1], $result->test(['col' => '-0.100']));
Assert::same(['col' => -1.0], $result->test(['col' => '-1.0']));
Assert::same(['col' => -1.1], $result->test(['col' => '-1.1']));
Assert::same(['col' => -1.0], $result->test(['col' => '-1.000']));
Assert::same(['col' => -1.1], $result->test(['col' => '-1.100']));
Assert::same(['col' => -1.0], $result->test(['col' => '-001.000']));
Assert::same(['col' => -1.1], $result->test(['col' => '-001.100']));
Assert::same(['col' => -10.0], $result->test(['col' => '-10']));
Assert::same(['col' => -11.0], $result->test(['col' => '-11']));
Assert::same(['col' => -10.0], $result->test(['col' => '-0010']));
Assert::same(['col' => -11.0], $result->test(['col' => '-0011']));
Assert::same(['col' => '-0.00000000000000000001'], $result->test(['col' => '-0.00000000000000000001']));
Assert::same(['col' => '-12345678901234567890'], $result->test(['col' => '-12345678901234567890']));
Assert::same(['col' => '-12345678901234567890'], $result->test(['col' => '-012345678901234567890']));
Assert::same(['col' => '-12345678901234567890'], $result->test(['col' => '-12345678901234567890.000']));
Assert::same(['col' => '-12345678901234567890.1'], $result->test(['col' => '-012345678901234567890.100']));
Assert::same(['col' => '-1.1e+10'], $result->test(['col' => '-1.1e+10']));
Assert::same(['col' => '-1.1e-10'], $result->test(['col' => '-1.1e-10']));
Assert::same(['col' => '-1.1e+10'], $result->test(['col' => '-001.1e+10']));
Assert::notSame(['col' => '-1.1e+1'], $result->test(['col' => '-1.1e+10']));
setlocale(LC_ALL, 'de_DE@euro', 'de_DE', 'deu_deu');
Assert::same(['col' => 0.0], $result->test(['col' => '']));
Assert::same(['col' => 0.0], $result->test(['col' => '0']));
@@ -147,13 +178,45 @@ test('', function () {
Assert::same(['col' => 0.0], $result->test(['col' => 0.0]));
Assert::same(['col' => 1.0], $result->test(['col' => 1]));
Assert::same(['col' => 1.0], $result->test(['col' => 1.0]));
// Same but negative
Assert::same(['col' => -0.0], $result->test(['col' => '-']));
Assert::same(['col' => -0.0], $result->test(['col' => '-0']));
Assert::same(['col' => -1.0], $result->test(['col' => '-1']));
Assert::same(['col' => -0.0], $result->test(['col' => '-.0']));
Assert::same(['col' => -0.1], $result->test(['col' => '-.1']));
Assert::same(['col' => -0.0], $result->test(['col' => '-0.0']));
Assert::same(['col' => -0.1], $result->test(['col' => '-0.1']));
Assert::same(['col' => -0.0], $result->test(['col' => '-0.000']));
Assert::same(['col' => -0.1], $result->test(['col' => '-0.100']));
Assert::same(['col' => -1.0], $result->test(['col' => '-1.0']));
Assert::same(['col' => -1.1], $result->test(['col' => '-1.1']));
Assert::same(['col' => -1.0], $result->test(['col' => '-1.000']));
Assert::same(['col' => -1.1], $result->test(['col' => '-1.100']));
Assert::same(['col' => -1.0], $result->test(['col' => '-001.000']));
Assert::same(['col' => -1.1], $result->test(['col' => '-001.100']));
Assert::same(['col' => -10.0], $result->test(['col' => '-10']));
Assert::same(['col' => -11.0], $result->test(['col' => '-11']));
Assert::same(['col' => -10.0], $result->test(['col' => '-0010']));
Assert::same(['col' => -11.0], $result->test(['col' => '-0011']));
Assert::same(['col' => '-0.00000000000000000001'], $result->test(['col' => '-0.00000000000000000001']));
Assert::same(['col' => '-12345678901234567890'], $result->test(['col' => '-12345678901234567890']));
Assert::same(['col' => '-12345678901234567890'], $result->test(['col' => '-012345678901234567890']));
Assert::same(['col' => '-12345678901234567890'], $result->test(['col' => '-12345678901234567890.000']));
Assert::same(['col' => '-12345678901234567890.1'], $result->test(['col' => '-012345678901234567890.100']));
Assert::same(['col' => -0.0], $result->test(['col' => -0]));
Assert::same(['col' => -0.0], $result->test(['col' => -0.0]));
Assert::same(['col' => -1.0], $result->test(['col' => -1]));
Assert::same(['col' => -1.0], $result->test(['col' => -1.0]));
setlocale(LC_NUMERIC, 'C');
});
test('', function () {
$result = new MockResult;
$result->setType('col', Type::INTEGER);
$result->setType('col', Type::Integer);
Assert::same(['col' => null], $result->test(['col' => null]));
Assert::same(['col' => 1], $result->test(['col' => true]));
@@ -187,7 +250,7 @@ test('', function () {
test('', function () {
$result = new MockResult;
$result->setType('col', Type::DATETIME);
$result->setType('col', Type::DateTime);
Assert::same(['col' => null], $result->test(['col' => null]));
Assert::exception(
@@ -206,8 +269,8 @@ test('', function () {
test('', function () {
$result = new MockResult;
$result->setType('col', Type::DATETIME);
$result->setFormat(Type::DATETIME, 'Y-m-d H:i:s');
$result->setType('col', Type::DateTime);
$result->setFormat(Type::DateTime, 'Y-m-d H:i:s');
Assert::same(['col' => null], $result->test(['col' => null]));
Assert::exception(
@@ -226,7 +289,7 @@ test('', function () {
test('', function () {
$result = new MockResult;
$result->setType('col', Type::DATE);
$result->setType('col', Type::Date);
Assert::same(['col' => null], $result->test(['col' => null]));
Assert::exception(
@@ -243,7 +306,7 @@ test('', function () {
test('', function () {
$result = new MockResult;
$result->setType('col', Type::TIME);
$result->setType('col', Type::Time);
Assert::same(['col' => null], $result->test(['col' => null]));
Assert::exception(

View File

@@ -12,7 +12,7 @@ $conn->loadFile(__DIR__ . "/data/$config[system].sql");
$res = $conn->query('SELECT * FROM [customers]');
// auto-converts this column to integer
$res->setType('customer_id', Dibi\Type::DATETIME);
$res->setType('customer_id', Dibi\Type::DateTime);
Assert::equal(new Dibi\Row([
'customer_id' => new Dibi\DateTime('1970-01-01 01:00:01'),

View File

@@ -24,13 +24,17 @@ Assert::true(isset($row['title']));
// missing
Assert::error(function () use ($row) {
$x = $row->missing;
}, E_USER_NOTICE, "Attempt to read missing column 'missing'.");
Assert::error(
fn() => $x = $row->missing,
E_USER_NOTICE,
"Attempt to read missing column 'missing'.",
);
Assert::error(function () use ($row) {
$x = $row['missing'];
}, E_USER_NOTICE, "Attempt to read missing column 'missing'.");
Assert::error(
fn() => $x = $row['missing'],
E_USER_NOTICE,
"Attempt to read missing column 'missing'.",
);
Assert::false(isset($row->missing));
Assert::false(isset($row['missing']));
@@ -41,13 +45,17 @@ Assert::same(123, $row['missing'] ?? 123);
// suggestions
Assert::error(function () use ($row) {
$x = $row->tilte;
}, E_USER_NOTICE, "Attempt to read missing column 'tilte', did you mean 'title'?");
Assert::error(
fn() => $x = $row->tilte,
E_USER_NOTICE,
"Attempt to read missing column 'tilte', did you mean 'title'?",
);
Assert::error(function () use ($row) {
$x = $row['tilte'];
}, E_USER_NOTICE, "Attempt to read missing column 'tilte', did you mean 'title'?");
Assert::error(
fn() => $x = $row['tilte'],
E_USER_NOTICE,
"Attempt to read missing column 'tilte', did you mean 'title'?",
);
// to array

View File

@@ -33,7 +33,7 @@ enum PureEnum
Assert::equal('1', $translator->formatValue(EnumInt::One, null));
Assert::equal(match ($config['driver']) {
Assert::equal(match ($config['system']) {
'sqlsrv' => "N'one'",
default => "'one'",
}, $translator->formatValue(EnumString::One, null));

View File

@@ -20,7 +20,7 @@ date_default_timezone_set('Europe/Prague');
try {
$config = Tester\Environment::loadData();
} catch (Throwable $e) {
$config = parse_ini_file(__DIR__ . '/../databases.ini', true);
$config = parse_ini_file(__DIR__ . '/../databases.ini', process_sections: true);
$config = reset($config);
}