1
0
mirror of https://github.com/dg/dibi.git synced 2025-09-04 19:55:26 +02:00

Compare commits

..

1 Commits
v5.0.1 ... v4.1

Author SHA1 Message Date
David Grudl
49f2bf867f SqlsrvDriver: workaround for "Driver's SQLSetConnectAttr failed on ODBC <=13" bug 2021-03-10 16:42:45 +01:00
111 changed files with 1815 additions and 2117 deletions

4
.gitattributes vendored
View File

@@ -1,8 +1,8 @@
.gitattributes export-ignore
.gitignore export-ignore
.github export-ignore
appveyor.yml export-ignore
ncs.* export-ignore
.travis.yml export-ignore
ecs.php export-ignore
phpstan.neon export-ignore
tests/ export-ignore

View File

@@ -1,31 +0,0 @@
name: Coding Style
on: [push, pull_request]
jobs:
nette_cc:
name: Nette Code Checker
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: shivammathur/setup-php@v2
with:
php-version: 8.0
coverage: none
- run: composer create-project nette/code-checker temp/code-checker ^3 --no-progress
- run: php temp/code-checker/code-checker --strict-types --no-progress --ignore "tests/*/fixtures"
nette_cs:
name: Nette Coding Standard
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: shivammathur/setup-php@v2
with:
php-version: 8.0
coverage: none
- run: composer create-project nette/coding-standard temp/coding-standard ^3 --no-progress
- run: php temp/coding-standard/ecs check

View File

@@ -1,21 +0,0 @@
name: Static Analysis (only informative)
on:
push:
branches:
- master
jobs:
phpstan:
name: PHPStan
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: shivammathur/setup-php@v2
with:
php-version: 8.0
coverage: none
- run: composer install --no-progress --prefer-dist
- run: composer phpstan -- --no-progress
continue-on-error: true # is only informative

View File

@@ -1,121 +0,0 @@
name: Tests
on: [push, pull_request]
env:
php-extensions: mbstring, intl, mysqli, pgsql, sqlsrv-5.10.0beta2, pdo_sqlsrv-5.10.0beta2
php-tools: "composer:v2, pecl"
jobs:
tests:
runs-on: ubuntu-latest
strategy:
matrix:
php: ['8.0', '8.1', '8.2', '8.3']
fail-fast: false
name: PHP ${{ matrix.php }} tests
services:
mysql57:
image: mysql:5.7
env:
MYSQL_DATABASE: dibi_test
MYSQL_ROOT_PASSWORD: root
ports:
- 3306:3306
options: >-
--health-cmd "mysqladmin ping -ppass"
--health-interval 10s
--health-start-period 10s
--health-timeout 5s
--health-retries 10
mysql80:
image: mysql:8.0
ports:
- 3307:3306
options: >-
--health-cmd="mysqladmin ping -ppass"
--health-interval=10s
--health-timeout=5s
--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
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: dibi_test
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
postgres13:
image: postgres:13
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: dibi_test
ports:
- 5433:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
mssql:
image: mcr.microsoft.com/mssql/server:latest
env:
ACCEPT_EULA: Y
SA_PASSWORD: YourStrong!Passw0rd
MSSQL_PID: Developer
ports:
- 1433:1433
options: >-
--name=mssql
--health-cmd "/opt/mssql-tools/bin/sqlcmd -S localhost -U SA -P 'YourStrong!Passw0rd' -Q 'SELECT 1'"
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v3
- uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: ${{ env.php-extensions }}
tools: ${{ env.php-tools }}
coverage: none
- name: Create databases.ini
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: composer install --no-progress --prefer-dist
- run: vendor/bin/tester -p phpdbg tests -s -C --coverage ./coverage.xml --coverage-src ./src
- if: failure()
uses: actions/upload-artifact@v3
with:
name: output
path: tests/**/output
- name: Save Code Coverage
if: ${{ matrix.php == '8.0' }}
env:
COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
wget https://github.com/php-coveralls/php-coveralls/releases/download/v2.4.3/php-coveralls.phar
php php-coveralls.phar --verbose --config tests/.coveralls.yml

77
.travis.yml Normal file
View File

@@ -0,0 +1,77 @@
language: php
php:
- 7.1
- 7.2
- 7.3
- 7.4
- 8.0snapshot
services:
- mysql
- postgresql
before_install:
# turn off XDebug
- phpenv config-rm xdebug.ini || return 0
# Create databases.ini
- cp ./tests/databases.travis.ini ./tests/databases.ini
# Create Postgre database
- psql -c 'CREATE DATABASE dibi_test' -U postgres
install:
- travis_retry composer install --no-progress --prefer-dist
script:
- vendor/bin/tester tests -s
after_failure:
# Print *.actual content
- for i in $(find tests -name \*.actual); do echo "--- $i"; cat $i; echo; echo; done
jobs:
include:
- name: Nette Code Checker
install:
- travis_retry composer create-project nette/code-checker temp/code-checker ^3 --no-progress
script:
- php temp/code-checker/code-checker --strict-types
- name: Nette Coding Standard
php: 7.4
install:
- travis_retry composer create-project nette/coding-standard temp/coding-standard ^3 --no-progress
script:
- php temp/coding-standard/ecs check src tests
- stage: Static Analysis (informative)
php: 7.4
script:
- composer run-script phpstan
- stage: Code Coverage
php: 7.4
script:
- vendor/bin/tester -p phpdbg tests -s --coverage ./coverage.xml --coverage-src ./src
after_script:
- wget https://github.com/satooshi/php-coveralls/releases/download/v1.0.1/coveralls.phar
- php coveralls.phar --verbose --config tests/.coveralls.yml
allow_failures:
- stage: Static Analysis (informative)
- stage: Code Coverage
dist: xenial
cache:
directories:
- $HOME/.composer/cache
notifications:
email: false

View File

@@ -1,34 +1,31 @@
build: off
cache:
- c:\php -> appveyor.yml
- c:\php7 -> appveyor.yml
- '%LOCALAPPDATA%\Composer\files -> appveyor.yml'
clone_folder: c:\projects\dibi
environment:
MYSQL_PWD: Password12!
services:
- mssql2012sp1
# - mssql2014
- mysql
init:
- SET PATH=c:\php;c:\Program Files\MySQL\MySQL Server 5.7\bin;%PATH%
- SET PATH=c:\php7;%PATH%
- SET ANSICON=121x90 (121x90)
install:
# Install PHP 8.0
- IF EXIST c:\php (SET PHP=0) ELSE (SET PHP=1)
- IF %PHP%==1 mkdir c:\php
- IF %PHP%==1 cd c:\php
- IF %PHP%==1 curl https://windows.php.net/downloads/releases/archives/php-8.0.1-Win32-vs16-x64.zip --output php.zip
# Install PHP 7.1
- IF EXIST c:\php7 (SET PHP=0) ELSE (SET PHP=1)
- IF %PHP%==1 mkdir c:\php7
- IF %PHP%==1 cd c:\php7
- IF %PHP%==1 curl https://windows.php.net/downloads/releases/archives/php-7.1.5-Win32-VC14-x64.zip --output php.zip
- IF %PHP%==1 7z x php.zip >nul
- IF %PHP%==1 echo extension_dir=ext >> php.ini
- IF %PHP%==1 echo extension=php_openssl.dll >> php.ini
- IF %PHP%==1 curl https://github.com/microsoft/msphpsql/releases/download/v5.9.0/Windows-8.0.zip -L --output sqlsrv.zip
- IF %PHP%==1 7z x sqlsrv.zip >nul
- IF %PHP%==1 copy Windows-8.0\x64\php_sqlsrv_80_ts.dll ext\php_sqlsrv_ts.dll
- IF %PHP%==1 appveyor DownloadFile https://files.nette.org/misc/php-sqlsrv.zip
- IF %PHP%==1 7z x php-sqlsrv.zip >nul
- IF %PHP%==1 copy SQLSRV\php_sqlsrv_71_ts.dll ext\php_sqlsrv_71_ts.dll
- IF %PHP%==1 del /Q *.zip
# Install Microsoft Access Database Engine x64
@@ -43,12 +40,8 @@ install:
# Create databases.ini
- copy tests\databases.appveyor.ini tests\databases.ini
before_test:
# Create MySQL database
- mysql --user=root -e "CREATE DATABASE dibi_test"
test_script:
- vendor\bin\tester tests -s -p c:\php\php -c tests\php-win.ini
- vendor\bin\tester tests -s -p c:\php7\php -c tests\php-win.ini
on_failure:
# Print *.actual content

View File

@@ -11,14 +11,13 @@
}
],
"require": {
"php": "8.0 - 8.3"
"php": ">=7.1"
},
"require-dev": {
"tracy/tracy": "^2.9",
"nette/tester": "^2.5",
"nette/di": "^3.1",
"phpstan/phpstan": "^1.0",
"jetbrains/phpstorm-attributes": "^1.0"
"tracy/tracy": "~2.2",
"nette/tester": "~2.0",
"nette/di": "^3.0",
"phpstan/phpstan": "^0.12"
},
"replace": {
"dg/dibi": "*"
@@ -32,7 +31,7 @@
},
"extra": {
"branch-alias": {
"dev-master": "5.0-dev"
"dev-master": "4.1-dev"
}
}
}

21
ecs.php Normal file
View File

@@ -0,0 +1,21 @@
<?php
/**
* Rules for Nette Coding Standard
* https://github.com/nette/coding-standard
*/
declare(strict_types=1);
return function (Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator $containerConfigurator): void {
$containerConfigurator->import(PRESET_DIR . '/php71.php');
$parameters = $containerConfigurator->parameters();
$parameters->set('skip', [
// issue #260
PhpCsFixer\Fixer\Operator\TernaryToNullCoalescingFixer::class => ['src/Dibi/HashMap.php'],
SlevomatCodingStandard\Sniffs\ControlStructures\RequireNullCoalesceOperatorSniff::class => ['src/Dibi/HashMap.php'],
]);
};

View File

@@ -31,7 +31,7 @@ $name = $cond1 ? 'K%' : null;
$dibi->test('
SELECT *
FROM customers
%if', isset($name), 'WHERE name LIKE ?', $name, '%end',
%if', isset($name), 'WHERE name LIKE ?', $name, '%end'
);
// -> SELECT * FROM customers WHERE name LIKE 'K%'
@@ -54,7 +54,7 @@ $dibi->test('
WHERE
%if', isset($name), 'name LIKE ?', $name, '
%if', $cond2, 'AND admin=1 %end
%else 1 LIMIT 10 %end',
%else 1 LIMIT 10 %end'
);
// -> SELECT * FROM customers WHERE LIMIT 10

View File

@@ -28,7 +28,7 @@ $dibi->test('
SELECT COUNT(*) as [count]
FROM [comments]
WHERE [ip] LIKE ?', $ipMask, '
AND [date] > ', new Dibi\DateTime($timestamp),
AND [date] > ', new Dibi\DateTime($timestamp)
);
// -> SELECT COUNT(*) as [count] FROM [comments] WHERE [ip] LIKE '192.168.%' AND [date] > 876693600
@@ -69,7 +69,7 @@ $array = [1, 2, 3];
$dibi->test('
SELECT *
FROM people
WHERE id IN (?)', $array,
WHERE id IN (?)', $array
);
// -> SELECT * FROM people WHERE id IN ( 1, 2, 3 )

View File

@@ -29,6 +29,6 @@ $dibi->test('
'id' => 123,
'date' => new DateTime('12.3.2007'),
'stamp' => new DateTime('23.1.2007 10:23'),
],
]
);
// -> INSERT INTO [mytable] ([id], [date], [stamp]) VALUES (123, '2007-03-12', '2007-01-23 10-23-00')

View File

@@ -54,6 +54,6 @@ define('SUBST_ACTIVE', 7);
$dibi->test("
UPDATE :account:user
SET name='John Doe', status=:active:
WHERE id=", 7,
WHERE id=", 7
);
// -> UPDATE eshop_user SET name='John Doe', status=7 WHERE id= 7

View File

@@ -2,7 +2,7 @@
=========================================================
[![Downloads this Month](https://img.shields.io/packagist/dm/dibi/dibi.svg)](https://packagist.org/packages/dibi/dibi)
[![Tests](https://github.com/dg/dibi/workflows/Tests/badge.svg?branch=master)](https://github.com/dg/dibi/actions)
[![Build Status](https://travis-ci.org/dg/dibi.svg?branch=master)](https://travis-ci.org/dg/dibi)
[![Build Status Windows](https://ci.appveyor.com/api/projects/status/github/dg/dibi?branch=master&svg=true)](https://ci.appveyor.com/project/dg/dibi/branch/master)
[![Latest Stable Version](https://poser.pugx.org/dibi/dibi/v/stable)](https://github.com/dg/dibi/releases)
[![License](https://img.shields.io/badge/license-New%20BSD-blue.svg)](https://github.com/dg/dibi/blob/master/license.md)
@@ -15,14 +15,12 @@ Database access functions in PHP are not standardised. This library
hides the differences between them, and above all, it gives you a very handy interface.
Support Me
----------
Support Project
---------------
Do you like Dibi? Are you looking forward to the new features?
[![Buy me a coffee](https://files.nette.org/icons/donation-3.svg)](https://github.com/sponsors/dg)
Thank you!
[![Donate](https://files.nette.org/icons/donation-1.svg?)](https://nette.org/make-donation?to=dibi)
Installation
@@ -34,7 +32,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 4.1 requires PHP version 7.1 and supports PHP up to 8.0.
Usage
@@ -341,7 +339,7 @@ $database->query('INSERT INTO users', [
There are three methods for dealing with transactions:
```php
$database->begin();
$database->beginTransaction();
$database->commit();
@@ -639,7 +637,7 @@ In the configuration file, we will register the DI extensions and add the `dibi`
```neon
extensions:
dibi: Dibi\Bridges\Nette\DibiExtension3
dibi: Dibi\Bridges\Nette\DibiExtension22
dibi:
host: localhost

View File

@@ -19,14 +19,13 @@ use Tracy;
*/
class DibiExtension22 extends Nette\DI\CompilerExtension
{
private ?bool $debugMode;
private ?bool $cliMode;
/** @var bool|null */
private $debugMode;
public function __construct(?bool $debugMode = null, ?bool $cliMode = null)
public function __construct(bool $debugMode = null)
{
$this->debugMode = $debugMode;
$this->cliMode = $cliMode;
}
@@ -39,11 +38,7 @@ class DibiExtension22 extends Nette\DI\CompilerExtension
$this->debugMode = $container->parameters['debugMode'];
}
if ($this->cliMode === null) {
$this->cliMode = $container->parameters['consoleMode'];
}
$useProfiler = $config['profiler'] ?? (class_exists(Tracy\Debugger::class) && $this->debugMode && !$this->cliMode);
$useProfiler = $config['profiler'] ?? (class_exists(Tracy\Debugger::class) && $this->debugMode);
unset($config['profiler']);
@@ -52,7 +47,6 @@ class DibiExtension22 extends Nette\DI\CompilerExtension
foreach ((array) $config['flags'] as $flag) {
$flags |= constant($flag);
}
$config['flags'] = $flags;
}
@@ -63,10 +57,9 @@ class DibiExtension22 extends Nette\DI\CompilerExtension
if (class_exists(Tracy\Debugger::class)) {
$connection->addSetup(
[new Nette\DI\Statement('Tracy\Debugger::getBlueScreen'), 'addPanel'],
[[Dibi\Bridges\Tracy\Panel::class, 'renderException']],
[[Dibi\Bridges\Tracy\Panel::class, 'renderException']]
);
}
if ($useProfiler) {
$panel = $container->addDefinition($this->prefix('panel'))
->setFactory(Dibi\Bridges\Tracy\Panel::class, [

View File

@@ -1,96 +0,0 @@
<?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(),
]),
])->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 3 or later
# Requires Nette Framework 2.2 or later
extensions:
dibi: Dibi\Bridges\Nette\DibiExtension3
dibi: Dibi\Bridges\Nette\DibiExtension22
dibi:
host: localhost

View File

@@ -20,13 +20,22 @@ use Tracy;
*/
class Panel implements Tracy\IBarPanel
{
public static int $maxLength = 1000;
public bool|string $explain;
public int $filter;
private array $events = [];
use Dibi\Strict;
/** @var int maximum SQL length */
public static $maxLength = 1000;
/** @var bool|string explain queries? */
public $explain;
/** @var int */
public $filter;
/** @var array */
private $events = [];
public function __construct(bool $explain = true, ?int $filter = null)
public function __construct($explain = true, int $filter = null)
{
$this->filter = $filter ?: Event::QUERY;
$this->explain = $explain;
@@ -49,7 +58,6 @@ class Panel implements Tracy\IBarPanel
if (($event->type & $this->filter) === 0) {
return;
}
$this->events[] = $event;
}
@@ -62,7 +70,7 @@ class Panel implements Tracy\IBarPanel
if ($e instanceof Dibi\Exception && $e->getSql()) {
return [
'tab' => 'SQL',
'panel' => Helpers::dump($e->getSql(), return: true),
'panel' => Helpers::dump($e->getSql(), true),
];
}
@@ -80,10 +88,9 @@ class Panel implements Tracy\IBarPanel
foreach ($this->events as $event) {
$totalTime += $event->time;
}
return '<span title="dibi"><svg viewBox="0 0 2048 2048" style="vertical-align: bottom; width:1.23em; height:1.55em"><path fill="' . ($count ? '#b079d6' : '#aaa') . '" d="M1024 896q237 0 443-43t325-127v170q0 69-103 128t-280 93.5-385 34.5-385-34.5-280-93.5-103-128v-170q119 84 325 127t443 43zm0 768q237 0 443-43t325-127v170q0 69-103 128t-280 93.5-385 34.5-385-34.5-280-93.5-103-128v-170q119 84 325 127t443 43zm0-384q237 0 443-43t325-127v170q0 69-103 128t-280 93.5-385 34.5-385-34.5-280-93.5-103-128v-170q119 84 325 127t443 43zm0-1152q208 0 385 34.5t280 93.5 103 128v128q0 69-103 128t-280 93.5-385 34.5-385-34.5-280-93.5-103-128v-128q0-69 103-128t280-93.5 385-34.5z"/></svg><span class="tracy-label">'
. $count . "\u{a0}queries"
. ($totalTime ? ' / ' . number_format($totalTime * 1000, 1, '.', "\u{202f}") . "\u{202f}ms" : '')
. $count . ' queries'
. ($totalTime ? ' / ' . number_format($totalTime * 1000, 1, '.', '') . 'ms' : '')
. '</span></span>';
}
@@ -118,25 +125,23 @@ class Panel implements Tracy\IBarPanel
? $this->explain
: ($connection->getConfig('driver') === 'oracle' ? 'EXPLAIN PLAN FOR' : 'EXPLAIN');
try {
$explain = @Helpers::dump($connection->nativeQuery("$cmd $event->sql"), return: true);
$explain = @Helpers::dump($connection->nativeQuery("$cmd $event->sql"), true);
} catch (Dibi\Exception $e) {
}
[$connection->onEvent, \dibi::$numOfQueries, \dibi::$totalTime] = $backup;
}
$s .= '<tr><td data-order="' . $event->time . '">' . number_format($event->time * 1000, 3, '.', "\u{202f}");
$s .= '<tr><td data-order="' . $event->time . '">' . number_format($event->time * 1000, 3, '.', '');
if ($explain) {
static $counter;
$counter++;
$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, return: true);
$s .= '</td><td class="tracy-DibiProfiler-sql">' . Helpers::dump(strlen($event->sql) > self::$maxLength ? substr($event->sql, 0, self::$maxLength) . '...' : $event->sql, true);
if ($explain) {
$s .= "<div id='tracy-debug-DibiProfiler-row-$counter' class='tracy-collapsed'>{$explain}</div>";
}
if ($event->source) {
$s .= Tracy\Helpers::editorLink($event->source[0], $event->source[1]); //->class('tracy-DibiProfiler-source');
}
@@ -150,8 +155,8 @@ class Panel implements Tracy\IBarPanel
return '<style> #tracy-debug td.tracy-DibiProfiler-sql { background: white !important }
#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") . ', '
<h1>Queries: ' . count($this->events)
. ($totalTime === null ? '' : ', time: ' . number_format($totalTime * 1000, 1, '.', '') . 'ms') . ', '
. htmlspecialchars($this->getConnectionName($singleConnection)) . '</h1>
<div class="tracy-inner tracy-DibiProfiler">
<table class="tracy-sortable">
@@ -165,8 +170,8 @@ class Panel implements Tracy\IBarPanel
private function getConnectionName(Dibi\Connection $connection): string
{
$driver = $connection->getConfig('driver');
return get_debug_type($driver)
return (is_object($driver) ? get_class($driver) : $driver)
. ($connection->getConfig('name') ? '/' . $connection->getConfig('name') : '')
. ($connection->getConfig('host') ? "\u{202f}@\u{202f}" . $connection->getConfig('host') : '');
. ($connection->getConfig('host') ? '@' . $connection->getConfig('host') : '');
}
}

View File

@@ -9,7 +9,6 @@ declare(strict_types=1);
namespace Dibi;
use JetBrains\PhpStorm\Language;
use Traversable;
@@ -21,39 +20,34 @@ use Traversable;
*/
class Connection implements IConnection
{
/** function (Event $event); Occurs after query is executed */
public ?array $onEvent = [];
private array $config;
use Strict;
/** @var string[] resultset formats */
private array $formats;
private ?Driver $driver = null;
private ?Translator $translator = null;
/** @var array of function (Event $event); Occurs after query is executed */
public $onEvent = [];
/** @var array<string, callable(object): Expression | null> */
private array $translators = [];
private bool $sortTranslators = false;
private HashMap $substitutes;
private int $transactionDepth = 0;
/** @var array Current connection configuration */
private $config;
/** @var Driver|null */
private $driver;
/** @var Translator|null */
private $translator;
/** @var HashMap Substitutes for identifiers */
private $substitutes;
/**
* Connection options: (see driver-specific options too)
* - lazy (bool) => if true, connection will be established only when required
* - result (array) => result set options
* - normalize => normalizes result fields (default: true)
* - formatDateTime => date-time format
* empty for decoding as Dibi\DateTime (default)
* "..." formatted according to given format, see https://www.php.net/manual/en/datetime.format.php
* "native" for leaving value as is
* - formatTimeInterval => time-interval format
* empty for decoding as DateInterval (default)
* "..." formatted according to given format, see https://www.php.net/manual/en/dateinterval.format.php
* "native" for leaving value as is
* - formatJson => json format
* "array" for decoding json as an array (default)
* "object" for decoding json as \stdClass
* "native" for leaving value as is
* - formatDateTime => date-time format (if empty, DateTime objects will be returned)
* - formatJson => json format (
* "string" for leaving value as is,
* "object" for decoding json as \stdClass,
* "array" for decoding json as an array - default
* )
* - profiler (array)
* - run (bool) => enable profiler?
* - file => file to log
@@ -62,24 +56,18 @@ class Connection implements IConnection
* - onConnect (array) => list of SQL queries to execute (by Connection::query()) after connection is established
* @throws Exception
*/
public function __construct(array $config, ?string $name = null)
public function __construct(array $config, string $name = null)
{
Helpers::alias($config, 'username', 'user');
Helpers::alias($config, 'password', 'pass');
Helpers::alias($config, 'host', 'hostname');
Helpers::alias($config, 'result|formatDate', 'resultDate');
Helpers::alias($config, 'result|formatDateTime', 'resultDateTime');
$config['driver'] ??= 'mysqli';
$config['driver'] = $config['driver'] ?? 'mysqli';
$config['name'] = $name;
$config['result']['formatJson'] = $config['result']['formatJson'] ?? 'array';
$this->config = $config;
$this->formats = [
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,
];
// profiler
if (isset($config['profiler']['file']) && (!isset($config['profiler']['run']) || $config['profiler']['run'])) {
$filter = $config['profiler']['filter'] ?? Event::QUERY;
@@ -87,7 +75,7 @@ class Connection implements IConnection
$this->onEvent[] = [new Loggers\FileLogger($config['profiler']['file'], $filter, $errorsOnly), 'logEvent'];
}
$this->substitutes = new HashMap(fn(string $expr) => ":$expr:");
$this->substitutes = new HashMap(function (string $expr) { return ":$expr:"; });
if (!empty($config['substitutes'])) {
foreach ($config['substitutes'] as $key => $value) {
$this->substitutes->$key = $value;
@@ -144,17 +132,16 @@ class Connection implements IConnection
if ($event) {
$this->onEvent($event->done());
}
if (isset($this->config['onConnect'])) {
foreach ($this->config['onConnect'] as $sql) {
$this->query($sql);
}
}
} catch (DriverException $e) {
if ($event) {
$this->onEvent($event->done($e));
}
throw $e;
}
}
@@ -184,8 +171,9 @@ class Connection implements IConnection
/**
* Returns configuration variable. If no $key is passed, returns the entire array.
* @see self::__construct
* @return mixed
*/
final public function getConfig(?string $key = null, $default = null): mixed
final public function getConfig(string $key = null, $default = null)
{
return $key === null
? $this->config
@@ -201,51 +189,48 @@ class Connection implements IConnection
if (!$this->driver) {
$this->connect();
}
return $this->driver;
}
/**
* Generates (translates) and executes SQL query.
* @param mixed ...$args
* @throws Exception
*/
final public function query(#[Language('GenericSQL')] mixed ...$args): Result
final public function query(...$args): Result
{
return $this->nativeQuery($this->translate(...$args));
return $this->nativeQuery($this->translateArgs($args));
}
/**
* Generates SQL query.
* @param mixed ...$args
* @throws Exception
*/
final public function translate(#[Language('GenericSQL')] mixed ...$args): string
final public function translate(...$args): string
{
if (!$this->driver) {
$this->connect();
}
return (clone $this->translator)->translate($args);
return $this->translateArgs($args);
}
/**
* Generates and prints SQL query.
* @param mixed ...$args
*/
final public function test(#[Language('GenericSQL')] mixed ...$args): bool
final public function test(...$args): bool
{
try {
Helpers::dump($this->translate(...$args));
Helpers::dump($this->translateArgs($args));
return true;
} catch (Exception $e) {
if ($e->getSql()) {
Helpers::dump($e->getSql());
} else {
echo $e::class . ': ' . $e->getMessage() . (PHP_SAPI === 'cli' ? "\n" : '<br>');
echo get_class($e) . ': ' . $e->getMessage() . (PHP_SAPI === 'cli' ? "\n" : '<br>');
}
return false;
}
}
@@ -253,11 +238,24 @@ class Connection implements IConnection
/**
* Generates (translates) and returns SQL query as DataSource.
* @param mixed ...$args
* @throws Exception
*/
final public function dataSource(#[Language('GenericSQL')] mixed ...$args): DataSource
final public function dataSource(...$args): DataSource
{
return new DataSource($this->translate(...$args), $this);
return new DataSource($this->translateArgs($args), $this);
}
/**
* Generates SQL query.
*/
protected function translateArgs(array $args): string
{
if (!$this->driver) {
$this->connect();
}
return (clone $this->translator)->translate($args);
}
@@ -265,7 +263,7 @@ class Connection implements IConnection
* Executes the SQL query.
* @throws Exception
*/
final public function nativeQuery(#[Language('SQL')] string $sql): Result
final public function nativeQuery(string $sql): Result
{
if (!$this->driver) {
$this->connect();
@@ -280,7 +278,6 @@ class Connection implements IConnection
if ($event) {
$this->onEvent($event->done($e));
}
throw $e;
}
@@ -288,7 +285,6 @@ class Connection implements IConnection
if ($event) {
$this->onEvent($event->done($res));
}
return $res;
}
@@ -302,12 +298,10 @@ class Connection implements IConnection
if (!$this->driver) {
$this->connect();
}
$rows = $this->driver->getAffectedRows();
if ($rows === null || $rows < 0) {
throw new Exception('Cannot retrieve number of affected rows.');
}
return $rows;
}
@@ -316,17 +310,15 @@ class Connection implements IConnection
* Retrieves the ID generated for an AUTO_INCREMENT column by the previous INSERT query.
* @throws Exception
*/
public function getInsertId(?string $sequence = null): int
public function getInsertId(string $sequence = null): int
{
if (!$this->driver) {
$this->connect();
}
$id = $this->driver->getInsertId($sequence);
if ($id === null) {
throw new Exception('Cannot retrieve last generated ID.');
}
return $id;
}
@@ -334,27 +326,22 @@ class Connection implements IConnection
/**
* Begins a transaction (if supported).
*/
public function begin(?string $savepoint = null): void
public function begin(string $savepoint = null): void
{
if ($this->transactionDepth !== 0) {
throw new \LogicException(__METHOD__ . '() call is forbidden inside a transaction() callback');
}
if (!$this->driver) {
$this->connect();
}
$event = $this->onEvent ? new Event($this, Event::BEGIN, $savepoint) : null;
try {
$this->driver->begin($savepoint);
if ($event) {
$this->onEvent($event->done());
}
} catch (DriverException $e) {
if ($event) {
$this->onEvent($event->done($e));
}
throw $e;
}
}
@@ -363,27 +350,22 @@ class Connection implements IConnection
/**
* Commits statements in a transaction.
*/
public function commit(?string $savepoint = null): void
public function commit(string $savepoint = null): void
{
if ($this->transactionDepth !== 0) {
throw new \LogicException(__METHOD__ . '() call is forbidden inside a transaction() callback');
}
if (!$this->driver) {
$this->connect();
}
$event = $this->onEvent ? new Event($this, Event::COMMIT, $savepoint) : null;
try {
$this->driver->commit($savepoint);
if ($event) {
$this->onEvent($event->done());
}
} catch (DriverException $e) {
if ($event) {
$this->onEvent($event->done($e));
}
throw $e;
}
}
@@ -392,66 +374,36 @@ class Connection implements IConnection
/**
* Rollback changes in a transaction.
*/
public function rollback(?string $savepoint = null): void
public function rollback(string $savepoint = null): void
{
if ($this->transactionDepth !== 0) {
throw new \LogicException(__METHOD__ . '() call is forbidden inside a transaction() callback');
}
if (!$this->driver) {
$this->connect();
}
$event = $this->onEvent ? new Event($this, Event::ROLLBACK, $savepoint) : null;
try {
$this->driver->rollback($savepoint);
if ($event) {
$this->onEvent($event->done());
}
} catch (DriverException $e) {
if ($event) {
$this->onEvent($event->done($e));
}
throw $e;
}
}
public function transaction(callable $callback): mixed
{
if ($this->transactionDepth === 0) {
$this->begin();
}
$this->transactionDepth++;
try {
$res = $callback($this);
} catch (\Throwable $e) {
$this->transactionDepth--;
if ($this->transactionDepth === 0) {
$this->rollback();
}
throw $e;
}
$this->transactionDepth--;
if ($this->transactionDepth === 0) {
$this->commit();
}
return $res;
}
/**
* Result set factory.
*/
public function createResultSet(ResultDriver $resultDriver): Result
{
return (new Result($resultDriver, $this->config['result']['normalize'] ?? true))
->setFormats($this->formats);
$res = new Result($resultDriver);
return $res->setFormat(Type::DATE, $this->config['result']['formatDate'])
->setFormat(Type::DATETIME, $this->config['result']['formatDateTime'])
->setFormat(Type::JSON, $this->config['result']['formatJson']);
}
@@ -484,7 +436,6 @@ class Connection implements IConnection
if ($args instanceof Traversable) {
$args = iterator_to_array($args);
}
return $this->command()->insert()
->into('%n', $table, '(%n)', array_keys($args))->values('%l', $args);
}
@@ -513,77 +464,9 @@ class Connection implements IConnection
*/
public function substitute(string $value): string
{
return str_contains($value, ':')
? preg_replace_callback('#:([^:\s]*):#', fn(array $m) => $this->substitutes->{$m[1]}, $value)
: $value;
}
/********************* value objects translation ****************d*g**/
/**
* @param callable(object): Expression $translator
*/
public function setObjectTranslator(callable $translator): void
{
if (!$translator instanceof \Closure) {
$translator = \Closure::fromCallable($translator);
}
$param = (new \ReflectionFunction($translator))->getParameters()[0] ?? null;
$type = $param?->getType();
$types = match (true) {
$type instanceof \ReflectionNamedType => [$type],
$type instanceof \ReflectionUnionType => $type->getTypes(),
default => throw new Exception('Object translator must have exactly one parameter with class typehint.'),
};
foreach ($types as $type) {
if ($type->isBuiltin() || $type->allowsNull()) {
throw new Exception("Object translator must have exactly one parameter with non-nullable class typehint, got '$type'.");
}
$this->translators[$type->getName()] = $translator;
}
$this->sortTranslators = true;
}
public function translateObject(object $object): ?Expression
{
if ($this->sortTranslators) {
$this->translators = array_filter($this->translators);
uksort($this->translators, fn($a, $b) => is_subclass_of($a, $b) ? -1 : 1);
$this->sortTranslators = false;
}
if (!array_key_exists($object::class, $this->translators)) {
$translator = null;
foreach ($this->translators as $class => $t) {
if ($object instanceof $class) {
$translator = $t;
break;
}
}
$this->translators[$object::class] = $translator;
}
$translator = $this->translators[$object::class];
if ($translator === null) {
return null;
}
$result = $translator($object);
if (!$result instanceof Expression) {
throw new Exception(sprintf(
"Object translator for class '%s' returned '%s' but %s expected.",
$object::class,
get_debug_type($result),
Expression::class,
));
}
return $result;
return strpos($value, ':') === false
? $value
: preg_replace_callback('#:([^:\s]*):#', function (array $m) { return $this->substitutes->{$m[1]}; }, $value);
}
@@ -592,9 +475,10 @@ class Connection implements IConnection
/**
* Executes SQL query and fetch result - shortcut for query() & fetch().
* @param mixed ...$args
* @throws Exception
*/
public function fetch(#[Language('GenericSQL')] mixed ...$args): ?Row
public function fetch(...$args): ?Row
{
return $this->query($args)->fetch();
}
@@ -602,10 +486,11 @@ class Connection implements IConnection
/**
* Executes SQL query and fetch results - shortcut for query() & fetchAll().
* @param mixed ...$args
* @return Row[]|array[]
* @throws Exception
*/
public function fetchAll(#[Language('GenericSQL')] mixed ...$args): array
public function fetchAll(...$args): array
{
return $this->query($args)->fetchAll();
}
@@ -613,9 +498,11 @@ class Connection implements IConnection
/**
* Executes SQL query and fetch first column - shortcut for query() & fetchSingle().
* @param mixed ...$args
* @return mixed
* @throws Exception
*/
public function fetchSingle(#[Language('GenericSQL')] mixed ...$args): mixed
public function fetchSingle(...$args)
{
return $this->query($args)->fetchSingle();
}
@@ -623,9 +510,10 @@ class Connection implements IConnection
/**
* Executes SQL query and fetch pairs - shortcut for query() & fetchPairs().
* @param mixed ...$args
* @throws Exception
*/
public function fetchPairs(#[Language('GenericSQL')] mixed ...$args): array
public function fetchPairs(...$args): array
{
return $this->query($args)->fetchPairs();
}
@@ -651,7 +539,7 @@ class Connection implements IConnection
* @param callable $onProgress function (int $count, ?float $percent): void
* @return int count of sql commands
*/
public function loadFile(string $file, ?callable $onProgress = null): int
public function loadFile(string $file, callable $onProgress = null): int
{
return Helpers::loadFromFile($this, $file, $onProgress);
}
@@ -665,7 +553,6 @@ class Connection implements IConnection
if (!$this->driver) {
$this->connect();
}
return new Reflection\Database($this->driver->getReflector(), $this->config['database'] ?? null);
}

View File

@@ -15,16 +15,37 @@ namespace Dibi;
*/
class DataSource implements IDataSource
{
private Connection $connection;
private string $sql;
private ?Result $result = null;
private ?int $count = null;
private ?int $totalCount = null;
private array $cols = [];
private array $sorting = [];
private array $conds = [];
private ?int $offset = null;
private ?int $limit = null;
use Strict;
/** @var Connection */
private $connection;
/** @var string */
private $sql;
/** @var Result|null */
private $result;
/** @var int|null */
private $count;
/** @var int|null */
private $totalCount;
/** @var array */
private $cols = [];
/** @var array */
private $sorting = [];
/** @var array */
private $conds = [];
/** @var int|null */
private $offset;
/** @var int|null */
private $limit;
/**
@@ -44,14 +65,13 @@ class DataSource implements IDataSource
* @param string|array $col column name or array of column names
* @param string $as column alias
*/
public function select(string|array $col, ?string $as = null): static
public function select($col, string $as = null): self
{
if (is_array($col)) {
$this->cols = $col;
} else {
$this->cols[$col] = $as;
}
$this->result = null;
return $this;
}
@@ -60,7 +80,7 @@ class DataSource implements IDataSource
/**
* Adds conditions to query.
*/
public function where($cond): static
public function where($cond): self
{
$this->conds[] = is_array($cond)
? $cond // TODO: not consistent with select and orderBy
@@ -74,14 +94,13 @@ class DataSource implements IDataSource
* Selects columns to order by.
* @param string|array $row column name or array of column names
*/
public function orderBy(string|array $row, string $direction = 'ASC'): static
public function orderBy($row, string $direction = 'ASC'): self
{
if (is_array($row)) {
$this->sorting = $row;
} else {
$this->sorting[$row] = $direction;
}
$this->result = null;
return $this;
}
@@ -90,7 +109,7 @@ class DataSource implements IDataSource
/**
* Limits number of rows.
*/
public function applyLimit(int $limit, ?int $offset = null): static
public function applyLimit(int $limit, int $offset = null): self
{
$this->limit = $limit;
$this->offset = $offset;
@@ -116,7 +135,6 @@ class DataSource implements IDataSource
if ($this->result === null) {
$this->result = $this->connection->nativeQuery($this->__toString());
}
return $this->result;
}
@@ -140,7 +158,7 @@ class DataSource implements IDataSource
* Like fetch(), but returns only first field.
* @return mixed value on success, null if no next record
*/
public function fetchSingle(): mixed
public function fetchSingle()
{
return $this->getResult()->fetchSingle();
}
@@ -167,7 +185,7 @@ class DataSource implements IDataSource
/**
* Fetches all records from table like $key => $value pairs.
*/
public function fetchPairs(?string $key = null, ?string $value = null): array
public function fetchPairs(string $key = null, string $value = null): array
{
return $this->getResult()->fetchPairs($key, $value);
}
@@ -208,19 +226,24 @@ class DataSource implements IDataSource
*/
public function __toString(): string
{
return $this->connection->translate(
"\nSELECT %n",
(empty($this->cols) ? '*' : $this->cols),
"\nFROM %SQL",
$this->sql,
"\n%ex",
$this->conds ? ['WHERE %and', $this->conds] : null,
"\n%ex",
$this->sorting ? ['ORDER BY %by', $this->sorting] : null,
"\n%ofs %lmt",
$this->offset,
$this->limit,
);
try {
return $this->connection->translate(
"\nSELECT %n",
(empty($this->cols) ? '*' : $this->cols),
"\nFROM %SQL",
$this->sql,
"\n%ex",
$this->conds ? ['WHERE %and', $this->conds] : null,
"\n%ex",
$this->sorting ? ['ORDER BY %by', $this->sorting] : null,
"\n%ofs %lmt",
$this->offset,
$this->limit
);
} catch (\Throwable $e) {
trigger_error($e->getMessage(), E_USER_ERROR);
return '';
}
}
@@ -235,11 +258,10 @@ class DataSource implements IDataSource
if ($this->count === null) {
$this->count = $this->conds || $this->offset || $this->limit
? Helpers::intVal($this->connection->nativeQuery(
'SELECT COUNT(*) FROM (' . $this->__toString() . ') t',
'SELECT COUNT(*) FROM (' . $this->__toString() . ') t'
)->fetchSingle())
: $this->getTotalCount();
}
return $this->count;
}
@@ -251,10 +273,9 @@ class DataSource implements IDataSource
{
if ($this->totalCount === null) {
$this->totalCount = Helpers::intVal($this->connection->nativeQuery(
'SELECT COUNT(*) FROM ' . $this->sql,
'SELECT COUNT(*) FROM ' . $this->sql
)->fetchSingle());
}
return $this->totalCount;
}
}

View File

@@ -15,7 +15,12 @@ namespace Dibi;
*/
class DateTime extends \DateTimeImmutable
{
public function __construct(string|int $time = 'now', ?\DateTimeZone $timezone = null)
use Strict;
/**
* @param string|int $time
*/
public function __construct($time = 'now', \DateTimeZone $timezone = null)
{
$timezone = $timezone ?: new \DateTimeZone(date_default_timezone_get());
if (is_numeric($time)) {

View File

@@ -17,6 +17,8 @@ use Dibi;
*/
class DummyDriver implements Dibi\Driver, Dibi\ResultDriver, Dibi\Reflector
{
use Dibi\Strict;
public function disconnect(): void
{
}
@@ -40,22 +42,22 @@ class DummyDriver implements Dibi\Driver, Dibi\ResultDriver, Dibi\Reflector
}
public function begin(?string $savepoint = null): void
public function begin(string $savepoint = null): void
{
}
public function commit(?string $savepoint = null): void
public function commit(string $savepoint = null): void
{
}
public function rollback(?string $savepoint = null): void
public function rollback(string $savepoint = null): void
{
}
public function getResource(): mixed
public function getResource()
{
return null;
}
@@ -169,9 +171,8 @@ class DummyDriver implements Dibi\Driver, Dibi\ResultDriver, Dibi\Reflector
}
public function getResultResource(): mixed
public function getResultResource()
{
return null;
}

View File

@@ -26,14 +26,18 @@ use Dibi\Helpers;
*/
class FirebirdDriver implements Dibi\Driver
{
use Dibi\Strict;
public const ERROR_EXCEPTION_THROWN = -836;
/** @var resource */
private $connection;
/** @var ?resource */
/** @var resource|null */
private $transaction;
private bool $inTransaction = false;
/** @var bool */
private $inTransaction = false;
/** @throws Dibi\NotSupportedException */
@@ -97,10 +101,10 @@ class FirebirdDriver implements Dibi\Driver
} else {
throw new Dibi\DriverException(ibase_errmsg(), ibase_errcode(), $sql);
}
} elseif (is_resource($res)) {
return $this->createResultDriver($res);
}
return null;
}
@@ -127,12 +131,11 @@ class FirebirdDriver implements Dibi\Driver
* Begins a transaction (if supported).
* @throws Dibi\DriverException
*/
public function begin(?string $savepoint = null): void
public function begin(string $savepoint = null): void
{
if ($savepoint !== null) {
throw new Dibi\NotSupportedException('Savepoints are not supported in Firebird/Interbase.');
}
$this->transaction = ibase_trans($this->getResource());
$this->inTransaction = true;
}
@@ -142,7 +145,7 @@ class FirebirdDriver implements Dibi\Driver
* Commits statements in a transaction.
* @throws Dibi\DriverException
*/
public function commit(?string $savepoint = null): void
public function commit(string $savepoint = null): void
{
if ($savepoint !== null) {
throw new Dibi\NotSupportedException('Savepoints are not supported in Firebird/Interbase.');
@@ -160,7 +163,7 @@ class FirebirdDriver implements Dibi\Driver
* Rollback changes in a transaction.
* @throws Dibi\DriverException
*/
public function rollback(?string $savepoint = null): void
public function rollback(string $savepoint = null): void
{
if ($savepoint !== null) {
throw new Dibi\NotSupportedException('Savepoints are not supported in Firebird/Interbase.');
@@ -187,7 +190,7 @@ class FirebirdDriver implements Dibi\Driver
* Returns the connection resource.
* @return resource|null
*/
public function getResource(): mixed
public function getResource()
{
return is_resource($this->connection) ? $this->connection : null;
}

View File

@@ -17,7 +17,10 @@ use Dibi;
*/
class FirebirdReflector implements Dibi\Reflector
{
private Dibi\Driver $driver;
use Dibi\Strict;
/** @var Dibi\Driver */
private $driver;
public function __construct(Dibi\Driver $driver)
@@ -44,7 +47,6 @@ class FirebirdReflector implements Dibi\Reflector
'view' => $row[1] === 'TRUE',
];
}
return $tables;
}
@@ -97,7 +99,6 @@ class FirebirdReflector implements Dibi\Reflector
'autoincrement' => false,
];
}
return $columns;
}
@@ -130,7 +131,6 @@ class FirebirdReflector implements Dibi\Reflector
$indexes[$key]['table'] = $table;
$indexes[$key]['columns'][$row['FIELD_POSITION']] = $row['FIELD_NAME'];
}
return $indexes;
}
@@ -159,7 +159,6 @@ class FirebirdReflector implements Dibi\Reflector
'table' => $table,
];
}
return $keys;
}
@@ -180,7 +179,6 @@ class FirebirdReflector implements Dibi\Reflector
while ($row = $res->fetch(false)) {
$indices[] = $row[0];
}
return $indices;
}
@@ -203,7 +201,6 @@ class FirebirdReflector implements Dibi\Reflector
while ($row = $res->fetch(false)) {
$constraints[] = $row[0];
}
return $constraints;
}
@@ -212,7 +209,7 @@ class FirebirdReflector implements Dibi\Reflector
* Returns metadata for all triggers in a table or database.
* (Only if user has permissions on ALTER TABLE, INSERT/UPDATE/DELETE record in table)
*/
public function getTriggersMeta(?string $table = null): array
public function getTriggersMeta(string $table = null): array
{
$res = $this->driver->query(
"
@@ -239,7 +236,7 @@ class FirebirdReflector implements Dibi\Reflector
END AS TRIGGER_ENABLED
FROM RDB\$TRIGGERS
WHERE RDB\$SYSTEM_FLAG = 0"
. ($table === null ? ';' : " AND RDB\$RELATION_NAME = UPPER('$table');"),
. ($table === null ? ';' : " AND RDB\$RELATION_NAME = UPPER('$table');")
);
$triggers = [];
while ($row = $res->fetch(true)) {
@@ -251,7 +248,6 @@ class FirebirdReflector implements Dibi\Reflector
'enabled' => trim($row['TRIGGER_ENABLED']) === 'TRUE',
];
}
return $triggers;
}
@@ -260,7 +256,7 @@ class FirebirdReflector implements Dibi\Reflector
* Returns list of triggers for given table.
* (Only if user has permissions on ALTER TABLE, INSERT/UPDATE/DELETE record in table)
*/
public function getTriggers(?string $table = null): array
public function getTriggers(string $table = null): array
{
$q = 'SELECT TRIM(RDB$TRIGGER_NAME)
FROM RDB$TRIGGERS
@@ -274,7 +270,6 @@ class FirebirdReflector implements Dibi\Reflector
while ($row = $res->fetch(false)) {
$triggers[] = $row[0];
}
return $triggers;
}
@@ -326,7 +321,6 @@ class FirebirdReflector implements Dibi\Reflector
$procedures[$key]['params'][$io][$num]['type'] = trim($row['FIELD_TYPE']);
$procedures[$key]['params'][$io][$num]['size'] = $row['FIELD_LENGTH'];
}
return $procedures;
}
@@ -344,7 +338,6 @@ class FirebirdReflector implements Dibi\Reflector
while ($row = $res->fetch(false)) {
$procedures[] = $row[0];
}
return $procedures;
}
@@ -363,7 +356,6 @@ class FirebirdReflector implements Dibi\Reflector
while ($row = $res->fetch(false)) {
$generators[] = $row[0];
}
return $generators;
}
@@ -382,7 +374,6 @@ class FirebirdReflector implements Dibi\Reflector
while ($row = $res->fetch(false)) {
$functions[] = $row[0];
}
return $functions;
}
}

View File

@@ -18,9 +18,14 @@ use Dibi\Helpers;
*/
class FirebirdResult implements Dibi\ResultDriver
{
use Dibi\Strict;
/** @var resource */
private $resultSet;
/** @var bool */
private $autoFree = true;
/**
* @param resource $resultSet
@@ -31,6 +36,17 @@ class FirebirdResult implements Dibi\ResultDriver
}
/**
* Automatically frees the resources allocated for this result set.
*/
public function __destruct()
{
if ($this->autoFree && $this->getResultResource()) {
$this->free();
}
}
/**
* Returns the number of rows in a result set.
*/
@@ -87,8 +103,9 @@ class FirebirdResult implements Dibi\ResultDriver
* Returns the result set resource.
* @return resource|null
*/
public function getResultResource(): mixed
public function getResultResource()
{
$this->autoFree = false;
return is_resource($this->resultSet) ? $this->resultSet : null;
}
@@ -109,7 +126,6 @@ class FirebirdResult implements Dibi\ResultDriver
'nativetype' => $row['type'],
];
}
return $columns;
}

View File

@@ -18,7 +18,10 @@ use Dibi;
*/
class MySqlReflector implements Dibi\Reflector
{
private Dibi\Driver $driver;
use Dibi\Strict;
/** @var Dibi\Driver */
private $driver;
public function __construct(Dibi\Driver $driver)
@@ -40,7 +43,6 @@ class MySqlReflector implements Dibi\Reflector
'view' => isset($row[1]) && $row[1] === 'VIEW',
];
}
return $tables;
}
@@ -65,7 +67,6 @@ class MySqlReflector implements Dibi\Reflector
'vendor' => $row,
];
}
return $columns;
}
@@ -83,7 +84,6 @@ class MySqlReflector implements Dibi\Reflector
$indexes[$row['Key_name']]['primary'] = $row['Key_name'] === 'PRIMARY';
$indexes[$row['Key_name']]['columns'][$row['Seq_in_index'] - 1] = $row['Column_name'];
}
return array_values($indexes);
}
@@ -123,7 +123,6 @@ class MySqlReflector implements Dibi\Reflector
$foreignKeys[$keyName]['onDelete'] = $row['DELETE_RULE'];
$foreignKeys[$keyName]['onUpdate'] = $row['UPDATE_RULE'];
}
return array_values($foreignKeys);
}
}

View File

@@ -32,12 +32,19 @@ use Dibi;
*/
class MySqliDriver implements Dibi\Driver
{
use Dibi\Strict;
public const ERROR_ACCESS_DENIED = 1045;
public const ERROR_DUPLICATE_ENTRY = 1062;
public const ERROR_DATA_TRUNCATED = 1265;
private \mysqli $connection;
private bool $buffered = false;
/** @var \mysqli */
private $connection;
/** @var bool Is buffered (seekable and countable)? */
private $buffered;
/** @throws Dibi\NotSupportedException */
@@ -65,7 +72,7 @@ class MySqliDriver implements Dibi\Driver
$host = ini_get('mysqli.default_host');
if ($host) {
$config['host'] = $host;
$config['port'] = (int) ini_get('mysqli.default_port');
$config['port'] = ini_get('mysqli.default_port');
} else {
$config['host'] = null;
$config['port'] = null;
@@ -81,7 +88,6 @@ class MySqliDriver implements Dibi\Driver
$this->connection->options($key, $value);
}
}
@$this->connection->real_connect( // intentionally @
(empty($config['persistent']) ? '' : 'p:') . $config['host'],
$config['username'],
@@ -89,7 +95,7 @@ class MySqliDriver implements Dibi\Driver
$config['database'] ?? '',
$config['port'] ?? 0,
$config['socket'],
$config['flags'] ?? 0,
$config['flags'] ?? 0
);
if ($this->connection->connect_errno) {
@@ -147,12 +153,14 @@ class MySqliDriver implements Dibi\Driver
} elseif ($res instanceof \mysqli_result) {
return $this->createResultDriver($res);
}
return null;
}
public static function createException(string $message, int|string $code, string $sql): Dibi\DriverException
/**
* @param int|string $code
*/
public static function createException(string $message, $code, string $sql): Dibi\DriverException
{
if (in_array($code, [1216, 1217, 1451, 1452, 1701], true)) {
return new Dibi\ForeignKeyConstraintViolationException($message, $code, $sql);
@@ -183,7 +191,6 @@ class MySqliDriver implements Dibi\Driver
foreach ($matches as $m) {
$res[$m[1]] = (int) $m[2];
}
return $res;
}
@@ -212,7 +219,7 @@ class MySqliDriver implements Dibi\Driver
* Begins a transaction (if supported).
* @throws Dibi\DriverException
*/
public function begin(?string $savepoint = null): void
public function begin(string $savepoint = null): void
{
$this->query($savepoint ? "SAVEPOINT $savepoint" : 'START TRANSACTION');
}
@@ -222,7 +229,7 @@ class MySqliDriver implements Dibi\Driver
* Commits statements in a transaction.
* @throws Dibi\DriverException
*/
public function commit(?string $savepoint = null): void
public function commit(string $savepoint = null): void
{
$this->query($savepoint ? "RELEASE SAVEPOINT $savepoint" : 'COMMIT');
}
@@ -232,7 +239,7 @@ class MySqliDriver implements Dibi\Driver
* Rollback changes in a transaction.
* @throws Dibi\DriverException
*/
public function rollback(?string $savepoint = null): void
public function rollback(string $savepoint = null): void
{
$this->query($savepoint ? "ROLLBACK TO SAVEPOINT $savepoint" : 'ROLLBACK');
}
@@ -243,11 +250,7 @@ class MySqliDriver implements Dibi\Driver
*/
public function getResource(): ?\mysqli
{
try {
return @$this->connection->thread_id ? $this->connection : null;
} catch (\Throwable $e) {
return null;
}
return @$this->connection->thread_id ? $this->connection : null;
}
@@ -316,7 +319,6 @@ class MySqliDriver implements Dibi\Driver
if ($value->y || $value->m || $value->d) {
throw new Dibi\NotSupportedException('Only time interval is supported.');
}
return $value->format("'%r%H:%I:%S.%f'");
}

View File

@@ -17,8 +17,16 @@ use Dibi;
*/
class MySqliResult implements Dibi\ResultDriver
{
private \mysqli_result $resultSet;
private bool $buffered;
use Dibi\Strict;
/** @var \mysqli_result */
private $resultSet;
/** @var bool */
private $autoFree = true;
/** @var bool Is buffered (seekable and countable)? */
private $buffered;
public function __construct(\mysqli_result $resultSet, bool $buffered)
@@ -28,6 +36,17 @@ class MySqliResult implements Dibi\ResultDriver
}
/**
* Automatically frees the resources allocated for this result set.
*/
public function __destruct()
{
if ($this->autoFree && $this->getResultResource()) {
@$this->free();
}
}
/**
* Returns the number of rows in a result set.
*/
@@ -36,7 +55,6 @@ class MySqliResult implements Dibi\ResultDriver
if (!$this->buffered) {
throw new Dibi\NotSupportedException('Row count is not available for unbuffered queries.');
}
return $this->resultSet->num_rows;
}
@@ -62,7 +80,6 @@ class MySqliResult implements Dibi\ResultDriver
if (!$this->buffered) {
throw new Dibi\NotSupportedException('Cannot seek an unbuffered result set.');
}
return $this->resultSet->data_seek($row);
}
@@ -90,7 +107,6 @@ class MySqliResult implements Dibi\ResultDriver
$types[$value] = substr($key, 12);
}
}
$types[MYSQLI_TYPE_TINY] = $types[MYSQLI_TYPE_SHORT] = $types[MYSQLI_TYPE_LONG] = 'INT';
}
@@ -107,7 +123,6 @@ class MySqliResult implements Dibi\ResultDriver
'vendor' => $row,
];
}
return $columns;
}
@@ -117,6 +132,7 @@ class MySqliResult implements Dibi\ResultDriver
*/
public function getResultResource(): \mysqli_result
{
$this->autoFree = false;
return $this->resultSet;
}

View File

@@ -17,7 +17,10 @@ use Dibi;
*/
class NoDataResult implements Dibi\ResultDriver
{
private int $rows;
use Dibi\Strict;
/** @var int */
private $rows;
public function __construct(int $rows)
@@ -58,7 +61,7 @@ class NoDataResult implements Dibi\ResultDriver
}
public function getResultResource(): mixed
public function getResultResource()
{
return null;
}

View File

@@ -25,10 +25,16 @@ use Dibi;
*/
class OdbcDriver implements Dibi\Driver
{
use Dibi\Strict;
/** @var resource */
private $connection;
private ?int $affectedRows;
private bool $microseconds = true;
/** @var int|null Affected rows */
private $affectedRows;
/** @var bool */
private $microseconds = true;
/** @throws Dibi\NotSupportedException */
@@ -90,7 +96,6 @@ class OdbcDriver implements Dibi\Driver
? $this->createResultDriver($res)
: null;
}
return null;
}
@@ -117,9 +122,9 @@ class OdbcDriver implements Dibi\Driver
* Begins a transaction (if supported).
* @throws Dibi\DriverException
*/
public function begin(?string $savepoint = null): void
public function begin(string $savepoint = null): void
{
if (!odbc_autocommit($this->connection)) {
if (!odbc_autocommit($this->connection, PHP_VERSION_ID < 80000 ? 0 : false)) {
throw new Dibi\DriverException(odbc_errormsg($this->connection) . ' ' . odbc_error($this->connection));
}
}
@@ -129,13 +134,12 @@ class OdbcDriver implements Dibi\Driver
* Commits statements in a transaction.
* @throws Dibi\DriverException
*/
public function commit(?string $savepoint = null): void
public function commit(string $savepoint = null): void
{
if (!odbc_commit($this->connection)) {
throw new Dibi\DriverException(odbc_errormsg($this->connection) . ' ' . odbc_error($this->connection));
}
odbc_autocommit($this->connection, true);
odbc_autocommit($this->connection, PHP_VERSION_ID < 80000 ? 1 : true);
}
@@ -143,13 +147,12 @@ class OdbcDriver implements Dibi\Driver
* Rollback changes in a transaction.
* @throws Dibi\DriverException
*/
public function rollback(?string $savepoint = null): void
public function rollback(string $savepoint = null): void
{
if (!odbc_rollback($this->connection)) {
throw new Dibi\DriverException(odbc_errormsg($this->connection) . ' ' . odbc_error($this->connection));
}
odbc_autocommit($this->connection, true);
odbc_autocommit($this->connection, PHP_VERSION_ID < 80000 ? 1 : true);
}
@@ -166,7 +169,7 @@ class OdbcDriver implements Dibi\Driver
* Returns the connection resource.
* @return resource|null
*/
public function getResource(): mixed
public function getResource()
{
return is_resource($this->connection) ? $this->connection : null;
}

View File

@@ -17,7 +17,10 @@ use Dibi;
*/
class OdbcReflector implements Dibi\Reflector
{
private Dibi\Driver $driver;
use Dibi\Strict;
/** @var Dibi\Driver */
private $driver;
public function __construct(Dibi\Driver $driver)
@@ -41,7 +44,6 @@ class OdbcReflector implements Dibi\Reflector
];
}
}
odbc_free_result($res);
return $tables;
}
@@ -66,7 +68,6 @@ class OdbcReflector implements Dibi\Reflector
];
}
}
odbc_free_result($res);
return $columns;
}

View File

@@ -17,9 +17,16 @@ use Dibi;
*/
class OdbcResult implements Dibi\ResultDriver
{
use Dibi\Strict;
/** @var resource */
private $resultSet;
private int $row = 0;
/** @var bool */
private $autoFree = true;
/** @var int Cursor */
private $row = 0;
/**
@@ -31,6 +38,17 @@ class OdbcResult implements Dibi\ResultDriver
}
/**
* Automatically frees the resources allocated for this result set.
*/
public function __destruct()
{
if ($this->autoFree && $this->getResultResource()) {
$this->free();
}
}
/**
* Returns the number of rows in a result set.
*/
@@ -54,13 +72,11 @@ class OdbcResult implements Dibi\ResultDriver
if (!odbc_fetch_row($set, ++$this->row)) {
return null;
}
$count = odbc_num_fields($set);
$cols = [];
for ($i = 1; $i <= $count; $i++) {
$cols[] = odbc_result($set, $i);
}
return $cols;
}
}
@@ -100,7 +116,6 @@ class OdbcResult implements Dibi\ResultDriver
'nativetype' => odbc_field_type($this->resultSet, $i),
];
}
return $columns;
}
@@ -109,8 +124,9 @@ class OdbcResult implements Dibi\ResultDriver
* Returns the result set resource.
* @return resource|null
*/
public function getResultResource(): mixed
public function getResultResource()
{
$this->autoFree = false;
return is_resource($this->resultSet) ? $this->resultSet : null;
}

View File

@@ -27,11 +27,19 @@ use Dibi;
*/
class OracleDriver implements Dibi\Driver
{
use Dibi\Strict;
/** @var resource */
private $connection;
private bool $autocommit = true;
private bool $nativeDate;
private ?int $affectedRows;
/** @var bool */
private $autocommit = true;
/** @var bool use native datetime format */
private $nativeDate;
/** @var int|null Number of affected rows */
private $affectedRows;
/** @throws Dibi\NotSupportedException */
@@ -96,7 +104,6 @@ class OracleDriver implements Dibi\Driver
$err = oci_error($this->connection);
throw new Dibi\DriverException($err['message'], $err['code'], $sql);
}
return null;
}
@@ -140,7 +147,7 @@ class OracleDriver implements Dibi\Driver
/**
* Begins a transaction (if supported).
*/
public function begin(?string $savepoint = null): void
public function begin(string $savepoint = null): void
{
$this->autocommit = false;
}
@@ -150,13 +157,12 @@ class OracleDriver implements Dibi\Driver
* Commits statements in a transaction.
* @throws Dibi\DriverException
*/
public function commit(?string $savepoint = null): void
public function commit(string $savepoint = null): void
{
if (!oci_commit($this->connection)) {
$err = oci_error($this->connection);
throw new Dibi\DriverException($err['message'], $err['code']);
}
$this->autocommit = true;
}
@@ -165,13 +171,12 @@ class OracleDriver implements Dibi\Driver
* Rollback changes in a transaction.
* @throws Dibi\DriverException
*/
public function rollback(?string $savepoint = null): void
public function rollback(string $savepoint = null): void
{
if (!oci_rollback($this->connection)) {
$err = oci_error($this->connection);
throw new Dibi\DriverException($err['message'], $err['code']);
}
$this->autocommit = true;
}
@@ -180,7 +185,7 @@ class OracleDriver implements Dibi\Driver
* Returns the connection resource.
* @return resource|null
*/
public function getResource(): mixed
public function getResource()
{
return is_resource($this->connection) ? $this->connection : null;
}

View File

@@ -17,7 +17,10 @@ use Dibi;
*/
class OracleReflector implements Dibi\Reflector
{
private Dibi\Driver $driver;
use Dibi\Strict;
/** @var Dibi\Driver */
private $driver;
public function __construct(Dibi\Driver $driver)
@@ -41,7 +44,6 @@ class OracleReflector implements Dibi\Reflector
];
}
}
return $tables;
}
@@ -64,7 +66,6 @@ class OracleReflector implements Dibi\Reflector
'vendor' => $row,
];
}
return $columns;
}

View File

@@ -17,9 +17,14 @@ use Dibi;
*/
class OracleResult implements Dibi\ResultDriver
{
use Dibi\Strict;
/** @var resource */
private $resultSet;
/** @var bool */
private $autoFree = true;
/**
* @param resource $resultSet
@@ -30,6 +35,17 @@ class OracleResult implements Dibi\ResultDriver
}
/**
* Automatically frees the resources allocated for this result set.
*/
public function __destruct()
{
if ($this->autoFree && $this->getResultResource()) {
$this->free();
}
}
/**
* Returns the number of rows in a result set.
*/
@@ -84,7 +100,6 @@ class OracleResult implements Dibi\ResultDriver
'nativetype' => $type === 'NUMBER' && oci_field_scale($this->resultSet, $i) === 0 ? 'INTEGER' : $type,
];
}
return $columns;
}
@@ -93,8 +108,9 @@ class OracleResult implements Dibi\ResultDriver
* Returns the result set resource.
* @return resource|null
*/
public function getResultResource(): mixed
public function getResultResource()
{
$this->autoFree = false;
return is_resource($this->resultSet) ? $this->resultSet : null;
}

View File

@@ -27,10 +27,19 @@ use PDO;
*/
class PdoDriver implements Dibi\Driver
{
private ?PDO $connection;
private ?int $affectedRows;
private string $driverName;
private string $serverVersion = '';
use Dibi\Strict;
/** @var PDO|null Connection resource */
private $connection;
/** @var int|null Affected rows */
private $affectedRows;
/** @var string */
private $driverName;
/** @var string */
private $serverVersion = '';
/** @throws Dibi\NotSupportedException */
@@ -51,6 +60,7 @@ class PdoDriver implements Dibi\Driver
if ($this->connection->getAttribute(PDO::ATTR_ERRMODE) !== PDO::ERRMODE_SILENT) {
throw new Dibi\DriverException('PDO connection in exception or warning error mode is not supported.');
}
} else {
try {
$this->connection = new PDO($config['dsn'], $config['username'], $config['password'], $config['options']);
@@ -59,7 +69,6 @@ class PdoDriver implements Dibi\Driver
if ($e->getMessage() === 'could not find driver') {
throw new Dibi\NotSupportedException('PHP extension for PDO is not loaded.');
}
throw new Dibi\DriverException($e->getMessage(), $e->getCode());
}
}
@@ -93,15 +102,23 @@ class PdoDriver implements Dibi\Driver
$this->affectedRows = null;
[$sqlState, $code, $message] = $this->connection->errorInfo();
$code ??= 0;
$message = "SQLSTATE[$sqlState]: $message";
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),
};
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);
}
}
@@ -127,11 +144,11 @@ class PdoDriver implements Dibi\Driver
* Begins a transaction (if supported).
* @throws Dibi\DriverException
*/
public function begin(?string $savepoint = null): void
public function begin(string $savepoint = null): void
{
if (!$this->connection->beginTransaction()) {
$err = $this->connection->errorInfo();
throw new Dibi\DriverException("SQLSTATE[$err[0]]: $err[2]", $err[1] ?? 0);
throw new Dibi\DriverException("SQLSTATE[$err[0]]: $err[2]", $err[1]);
}
}
@@ -140,11 +157,11 @@ class PdoDriver implements Dibi\Driver
* Commits statements in a transaction.
* @throws Dibi\DriverException
*/
public function commit(?string $savepoint = null): void
public function commit(string $savepoint = null): void
{
if (!$this->connection->commit()) {
$err = $this->connection->errorInfo();
throw new Dibi\DriverException("SQLSTATE[$err[0]]: $err[2]", $err[1] ?? 0);
throw new Dibi\DriverException("SQLSTATE[$err[0]]: $err[2]", $err[1]);
}
}
@@ -153,11 +170,11 @@ class PdoDriver implements Dibi\Driver
* Rollback changes in a transaction.
* @throws Dibi\DriverException
*/
public function rollback(?string $savepoint = null): void
public function rollback(string $savepoint = null): void
{
if (!$this->connection->rollBack()) {
$err = $this->connection->errorInfo();
throw new Dibi\DriverException("SQLSTATE[$err[0]]: $err[2]", $err[1] ?? 0);
throw new Dibi\DriverException("SQLSTATE[$err[0]]: $err[2]", $err[1]);
}
}
@@ -349,18 +366,15 @@ class PdoDriver implements Dibi\Driver
$sql .= ' LIMIT ' . ($limit ?? '18446744073709551615')
. ($offset ? ' OFFSET ' . $offset : '');
}
break;
case 'pgsql':
if ($limit !== null) {
$sql .= ' LIMIT ' . $limit;
}
if ($offset) {
$sql .= ' OFFSET ' . $offset;
}
break;
case 'sqlite':
@@ -368,7 +382,6 @@ class PdoDriver implements Dibi\Driver
$sql .= ' LIMIT ' . ($limit ?? '-1')
. ($offset ? ' OFFSET ' . $offset : '');
}
break;
case 'oci':
@@ -381,7 +394,6 @@ class PdoDriver implements Dibi\Driver
} elseif ($limit !== null) {
$sql = 'SELECT * FROM (' . $sql . ') WHERE ROWNUM <= ' . $limit;
}
break;
case 'mssql':
@@ -394,10 +406,10 @@ class PdoDriver implements Dibi\Driver
} elseif ($offset) {
$sql = sprintf('%s OFFSET %d ROWS', rtrim($sql), $offset);
}
break;
}
// break omitted
case 'odbc':
if ($offset) {
throw new Dibi\NotSupportedException('Offset is not supported by this database.');
@@ -407,6 +419,7 @@ class PdoDriver implements Dibi\Driver
break;
}
// break omitted
default:
throw new Dibi\NotSupportedException('PDO or driver does not support applying limit or offset.');
}

View File

@@ -19,8 +19,13 @@ use PDO;
*/
class PdoResult implements Dibi\ResultDriver
{
private ?\PDOStatement $resultSet;
private string $driverName;
use Dibi\Strict;
/** @var \PDOStatement|null */
private $resultSet;
/** @var string */
private $driverName;
public function __construct(\PDOStatement $resultSet, string $driverName)
@@ -80,7 +85,6 @@ class PdoResult implements Dibi\ResultDriver
if ($row === false) {
throw new Dibi\NotSupportedException('Driver does not support meta data.');
}
$row += [
'table' => null,
'native_type' => 'VAR_STRING',
@@ -95,7 +99,6 @@ class PdoResult implements Dibi\ResultDriver
'vendor' => $row,
];
}
return $columns;
}

View File

@@ -11,7 +11,6 @@ namespace Dibi\Drivers;
use Dibi;
use Dibi\Helpers;
use PgSql;
/**
@@ -28,9 +27,13 @@ use PgSql;
*/
class PostgreDriver implements Dibi\Driver
{
/** @var resource|PgSql\Connection */
use Dibi\Strict;
/** @var resource */
private $connection;
private ?int $affectedRows;
/** @var int|null Affected rows */
private $affectedRows;
/** @throws Dibi\NotSupportedException */
@@ -60,7 +63,6 @@ class PostgreDriver implements Dibi\Driver
}
}
}
$connectType = $config['connect_type'] ?? PGSQL_CONNECT_FORCE_NEW;
set_error_handler(function (int $severity, string $message) use (&$error) {
@@ -72,7 +74,7 @@ class PostgreDriver implements Dibi\Driver
restore_error_handler();
}
if (!is_resource($this->connection) && !$this->connection instanceof PgSql\Connection) {
if (!is_resource($this->connection)) {
throw new Dibi\DriverException($error ?: 'Connecting error.');
}
@@ -118,25 +120,24 @@ class PostgreDriver implements Dibi\Driver
if ($res === false) {
throw static::createException(pg_last_error($this->connection), null, $sql);
} elseif (is_resource($res) || $res instanceof PgSql\Result) {
} elseif (is_resource($res)) {
$this->affectedRows = Helpers::false2Null(pg_affected_rows($res));
if (pg_num_fields($res)) {
return $this->createResultDriver($res);
}
}
return null;
}
public static function createException(string $message, $code = null, ?string $sql = null): Dibi\DriverException
public static function createException(string $message, $code = null, string $sql = null): Dibi\DriverException
{
if ($code === null && preg_match('#^ERROR:\s+(\S+):\s*#', $message, $m)) {
$code = $m[1];
$message = substr($message, strlen($m[0]));
}
if ($code === '0A000' && str_contains($message, 'truncate')) {
if ($code === '0A000' && strpos($message, 'truncate') !== false) {
return new Dibi\ForeignKeyConstraintViolationException($message, $code, $sql);
} elseif ($code === '23502') {
@@ -185,9 +186,9 @@ class PostgreDriver implements Dibi\Driver
* Begins a transaction (if supported).
* @throws Dibi\DriverException
*/
public function begin(?string $savepoint = null): void
public function begin(string $savepoint = null): void
{
$this->query($savepoint ? "SAVEPOINT {$this->escapeIdentifier($savepoint)}" : 'START TRANSACTION');
$this->query($savepoint ? "SAVEPOINT $savepoint" : 'START TRANSACTION');
}
@@ -195,9 +196,9 @@ class PostgreDriver implements Dibi\Driver
* Commits statements in a transaction.
* @throws Dibi\DriverException
*/
public function commit(?string $savepoint = null): void
public function commit(string $savepoint = null): void
{
$this->query($savepoint ? "RELEASE SAVEPOINT {$this->escapeIdentifier($savepoint)}" : 'COMMIT');
$this->query($savepoint ? "RELEASE SAVEPOINT $savepoint" : 'COMMIT');
}
@@ -205,9 +206,9 @@ class PostgreDriver implements Dibi\Driver
* Rollback changes in a transaction.
* @throws Dibi\DriverException
*/
public function rollback(?string $savepoint = null): void
public function rollback(string $savepoint = null): void
{
$this->query($savepoint ? "ROLLBACK TO SAVEPOINT {$this->escapeIdentifier($savepoint)}" : 'ROLLBACK');
$this->query($savepoint ? "ROLLBACK TO SAVEPOINT $savepoint" : 'ROLLBACK');
}
@@ -224,11 +225,9 @@ class PostgreDriver implements Dibi\Driver
* Returns the connection resource.
* @return resource|null
*/
public function getResource(): mixed
public function getResource()
{
return is_resource($this->connection) || $this->connection instanceof PgSql\Connection
? $this->connection
: null;
return is_resource($this->connection) ? $this->connection : null;
}
@@ -259,20 +258,18 @@ class PostgreDriver implements Dibi\Driver
*/
public function escapeText(string $value): string
{
if (!$this->getResource()) {
if (!is_resource($this->connection)) {
throw new Dibi\Exception('Lost connection to server.');
}
return "'" . pg_escape_string($this->connection, $value) . "'";
}
public function escapeBinary(string $value): string
{
if (!$this->getResource()) {
if (!is_resource($this->connection)) {
throw new Dibi\Exception('Lost connection to server.');
}
return "'" . pg_escape_bytea($this->connection, $value) . "'";
}
@@ -328,11 +325,9 @@ class PostgreDriver implements Dibi\Driver
if ($limit < 0 || $offset < 0) {
throw new Dibi\NotSupportedException('Negative offset or limit.');
}
if ($limit !== null) {
$sql .= ' LIMIT ' . $limit;
}
if ($offset) {
$sql .= ' OFFSET ' . $offset;
}

View File

@@ -17,12 +17,20 @@ use Dibi;
*/
class PostgreReflector implements Dibi\Reflector
{
private Dibi\Driver $driver;
private string $version;
use Dibi\Strict;
/** @var Dibi\Driver */
private $driver;
/** @var string */
private $version;
public function __construct(Dibi\Driver $driver, string $version)
{
if ($version < 7.4) {
throw new Dibi\DriverException('Reflection requires PostgreSQL 7.4 and newer.');
}
$this->driver = $driver;
$this->version = $version;
}
@@ -61,7 +69,6 @@ class PostgreReflector implements Dibi\Reflector
while ($row = $res->fetch(true)) {
$tables[] = $row;
}
return $tables;
}
@@ -72,6 +79,13 @@ 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 *
@@ -91,8 +105,7 @@ 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,
CASE WHEN a.attidentity IN ('a', 'd') THEN 'YES' ELSE 'NO' END AS is_identity
adef.adsrc AS column_default
FROM
pg_attribute a
JOIN pg_type ON a.atttypid = pg_type.oid
@@ -117,11 +130,10 @@ 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' => $row['is_identity'] === 'YES' || str_starts_with($row['column_default'] ?? '', 'nextval('),
'autoincrement' => (int) $row['ordinal_position'] === $primary && substr($row['column_default'] ?? '', 0, 7) === 'nextval',
'vendor' => $row,
];
}
return $columns;
}
@@ -171,7 +183,6 @@ class PostgreReflector implements Dibi\Reflector
}
}
}
return array_values($indexes);
}

View File

@@ -11,7 +11,6 @@ namespace Dibi\Drivers;
use Dibi;
use Dibi\Helpers;
use PgSql;
/**
@@ -19,12 +18,17 @@ use PgSql;
*/
class PostgreResult implements Dibi\ResultDriver
{
/** @var resource|PgSql\Result */
use Dibi\Strict;
/** @var resource */
private $resultSet;
/** @var bool */
private $autoFree = true;
/**
* @param resource|PgSql\Result $resultSet
* @param resource $resultSet
*/
public function __construct($resultSet)
{
@@ -32,6 +36,17 @@ class PostgreResult implements Dibi\ResultDriver
}
/**
* Automatically frees the resources allocated for this result set.
*/
public function __destruct()
{
if ($this->autoFree && $this->getResultResource()) {
$this->free();
}
}
/**
* Returns the number of rows in a result set.
*/
@@ -87,20 +102,18 @@ class PostgreResult implements Dibi\ResultDriver
: $row['name'];
$columns[] = $row;
}
return $columns;
}
/**
* Returns the result set resource.
* @return resource|PgSql\Result|null
* @return resource|null
*/
public function getResultResource(): mixed
public function getResultResource()
{
return is_resource($this->resultSet) || $this->resultSet instanceof PgSql\Result
? $this->resultSet
: null;
$this->autoFree = false;
return is_resource($this->resultSet) ? $this->resultSet : null;
}

View File

@@ -25,9 +25,16 @@ use SQLite3;
*/
class SqliteDriver implements Dibi\Driver
{
private SQLite3 $connection;
private string $fmtDate;
private string $fmtDateTime;
use Dibi\Strict;
/** @var SQLite3 */
private $connection;
/** @var string Date format */
private $fmtDate;
/** @var string Datetime format */
private $fmtDateTime;
/** @throws Dibi\NotSupportedException */
@@ -50,14 +57,13 @@ class SqliteDriver implements Dibi\Driver
} else {
try {
$this->connection = new SQLite3($config['database']);
} catch (\Throwable $e) {
} catch (\Exception $e) {
throw new Dibi\DriverException($e->getMessage(), $e->getCode());
}
}
// enable foreign keys support (defaultly disabled; if disabled then foreign key constraints are not enforced)
$version = SQLite3::version();
$this->connection->enableExceptions(false);
if ($version['versionNumber'] >= '3006019') {
$this->query('PRAGMA foreign_keys = ON');
}
@@ -86,7 +92,6 @@ class SqliteDriver implements Dibi\Driver
} elseif ($res instanceof \SQLite3Result && $res->numColumns()) {
return $this->createResultDriver($res);
}
return null;
}
@@ -96,19 +101,19 @@ class SqliteDriver implements Dibi\Driver
if ($code !== 19) {
return new Dibi\DriverException($message, $code, $sql);
} elseif (str_contains($message, 'must be unique')
|| str_contains($message, 'is not unique')
|| str_contains($message, 'UNIQUE constraint failed')
} elseif (strpos($message, 'must be unique') !== false
|| strpos($message, 'is not unique') !== false
|| strpos($message, 'UNIQUE constraint failed') !== false
) {
return new Dibi\UniqueConstraintViolationException($message, $code, $sql);
} elseif (str_contains($message, 'may not be null')
|| str_contains($message, 'NOT NULL constraint failed')
} elseif (strpos($message, 'may not be null') !== false
|| strpos($message, 'NOT NULL constraint failed') !== false
) {
return new Dibi\NotNullConstraintViolationException($message, $code, $sql);
} elseif (str_contains($message, 'foreign key constraint failed')
|| str_contains($message, 'FOREIGN KEY constraint failed')
} elseif (strpos($message, 'foreign key constraint failed') !== false
|| strpos($message, 'FOREIGN KEY constraint failed') !== false
) {
return new Dibi\ForeignKeyConstraintViolationException($message, $code, $sql);
@@ -140,7 +145,7 @@ class SqliteDriver implements Dibi\Driver
* Begins a transaction (if supported).
* @throws Dibi\DriverException
*/
public function begin(?string $savepoint = null): void
public function begin(string $savepoint = null): void
{
$this->query($savepoint ? "SAVEPOINT $savepoint" : 'BEGIN');
}
@@ -150,7 +155,7 @@ class SqliteDriver implements Dibi\Driver
* Commits statements in a transaction.
* @throws Dibi\DriverException
*/
public function commit(?string $savepoint = null): void
public function commit(string $savepoint = null): void
{
$this->query($savepoint ? "RELEASE SAVEPOINT $savepoint" : 'COMMIT');
}
@@ -160,7 +165,7 @@ class SqliteDriver implements Dibi\Driver
* Rollback changes in a transaction.
* @throws Dibi\DriverException
*/
public function rollback(?string $savepoint = null): void
public function rollback(string $savepoint = null): void
{
$this->query($savepoint ? "ROLLBACK TO SAVEPOINT $savepoint" : 'ROLLBACK');
}
@@ -285,9 +290,8 @@ class SqliteDriver implements Dibi\Driver
string $name,
callable $rowCallback,
callable $agrCallback,
int $numArgs = -1,
): void
{
int $numArgs = -1
): void {
$this->connection->createAggregate($name, $rowCallback, $agrCallback, $numArgs);
}
}

View File

@@ -17,7 +17,10 @@ use Dibi;
*/
class SqliteReflector implements Dibi\Reflector
{
private Dibi\Driver $driver;
use Dibi\Strict;
/** @var Dibi\Driver */
private $driver;
public function __construct(Dibi\Driver $driver)
@@ -41,7 +44,6 @@ class SqliteReflector implements Dibi\Reflector
while ($row = $res->fetch(true)) {
$tables[] = $row;
}
return $tables;
}
@@ -68,7 +70,6 @@ class SqliteReflector implements Dibi\Reflector
'vendor' => $row,
];
}
return $columns;
}
@@ -102,10 +103,8 @@ class SqliteReflector implements Dibi\Reflector
break;
}
}
$indexes[$index]['primary'] = (bool) $primary;
}
if (!$indexes) { // @see http://www.sqlite.org/lang_createtable.html#rowid
foreach ($columns as $column) {
if ($column['vendor']['pk']) {
@@ -143,7 +142,6 @@ class SqliteReflector implements Dibi\Reflector
$keys[$row['id']]['foreign'] = null;
}
}
return array_values($keys);
}
}

View File

@@ -18,7 +18,13 @@ use Dibi\Helpers;
*/
class SqliteResult implements Dibi\ResultDriver
{
private \SQLite3Result $resultSet;
use Dibi\Strict;
/** @var \SQLite3Result */
private $resultSet;
/** @var bool */
private $autoFree = true;
public function __construct(\SQLite3Result $resultSet)
@@ -27,6 +33,17 @@ class SqliteResult implements Dibi\ResultDriver
}
/**
* Automatically frees the resources allocated for this result set.
*/
public function __destruct()
{
if ($this->autoFree && $this->getResultResource()) {
@$this->free();
}
}
/**
* Returns the number of rows in a result set.
* @throws Dibi\NotSupportedException
@@ -73,7 +90,7 @@ class SqliteResult implements Dibi\ResultDriver
{
$count = $this->resultSet->numColumns();
$columns = [];
$types = [SQLITE3_INTEGER => 'int', SQLITE3_FLOAT => 'float', SQLITE3_TEXT => 'text', SQLITE3_BLOB => 'blob', SQLITE3_NULL => 'null'];
static $types = [SQLITE3_INTEGER => 'int', SQLITE3_FLOAT => 'float', SQLITE3_TEXT => 'text', SQLITE3_BLOB => 'blob', SQLITE3_NULL => 'null'];
for ($i = 0; $i < $count; $i++) {
$columns[] = [
'name' => $this->resultSet->columnName($i),
@@ -82,7 +99,6 @@ class SqliteResult implements Dibi\ResultDriver
'nativetype' => $types[$this->resultSet->columnType($i)] ?? null, // buggy in PHP 7.4.4 & 7.3.16, bug 79414
];
}
return $columns;
}
@@ -92,6 +108,7 @@ class SqliteResult implements Dibi\ResultDriver
*/
public function getResultResource(): \SQLite3Result
{
$this->autoFree = false;
return $this->resultSet;
}

View File

@@ -27,10 +27,16 @@ use Dibi\Helpers;
*/
class SqlsrvDriver implements Dibi\Driver
{
use Dibi\Strict;
/** @var resource */
private $connection;
private ?int $affectedRows;
private string $version = '';
/** @var int|null Affected rows */
private $affectedRows;
/** @var string */
private $version = '';
/** @throws Dibi\NotSupportedException */
@@ -47,28 +53,25 @@ class SqlsrvDriver implements Dibi\Driver
if (isset($config['resource'])) {
$this->connection = $config['resource'];
if (!is_resource($this->connection)) {
throw new \InvalidArgumentException("Configuration option 'resource' is not resource.");
}
} else {
$options = $config['options'];
// Default values
$options['CharacterSet'] ??= 'UTF-8';
$options['CharacterSet'] = $options['CharacterSet'] ?? 'UTF-8';
$options['PWD'] = (string) $options['PWD'];
$options['UID'] = (string) $options['UID'];
$options['Database'] = (string) $options['Database'];
sqlsrv_configure('WarningsReturnAsErrors', 0);
$this->connection = sqlsrv_connect($config['host'], $options);
if (!is_resource($this->connection)) {
$info = sqlsrv_errors(SQLSRV_ERR_ERRORS);
throw new Dibi\DriverException($info[0]['message'], $info[0]['code']);
}
sqlsrv_configure('WarningsReturnAsErrors', 1);
}
if (!is_resource($this->connection)) {
$info = sqlsrv_errors(SQLSRV_ERR_ERRORS);
throw new Dibi\DriverException($info[0]['message'], $info[0]['code']);
}
$this->version = sqlsrv_server_info($this->connection)['SQLServerVersion'];
}
@@ -101,7 +104,6 @@ class SqlsrvDriver implements Dibi\Driver
? $this->createResultDriver($res)
: null;
}
return null;
}
@@ -125,7 +127,6 @@ class SqlsrvDriver implements Dibi\Driver
$row = sqlsrv_fetch_array($res, SQLSRV_FETCH_NUMERIC);
return Dibi\Helpers::intVal($row[0]);
}
return null;
}
@@ -134,7 +135,7 @@ class SqlsrvDriver implements Dibi\Driver
* Begins a transaction (if supported).
* @throws Dibi\DriverException
*/
public function begin(?string $savepoint = null): void
public function begin(string $savepoint = null): void
{
sqlsrv_begin_transaction($this->connection);
}
@@ -144,7 +145,7 @@ class SqlsrvDriver implements Dibi\Driver
* Commits statements in a transaction.
* @throws Dibi\DriverException
*/
public function commit(?string $savepoint = null): void
public function commit(string $savepoint = null): void
{
sqlsrv_commit($this->connection);
}
@@ -154,7 +155,7 @@ class SqlsrvDriver implements Dibi\Driver
* Rollback changes in a transaction.
* @throws Dibi\DriverException
*/
public function rollback(?string $savepoint = null): void
public function rollback(string $savepoint = null): void
{
sqlsrv_rollback($this->connection);
}
@@ -164,7 +165,7 @@ class SqlsrvDriver implements Dibi\Driver
* Returns the connection resource.
* @return resource|null
*/
public function getResource(): mixed
public function getResource()
{
return is_resource($this->connection) ? $this->connection : null;
}
@@ -263,6 +264,7 @@ class SqlsrvDriver implements Dibi\Driver
} elseif ($limit !== null) {
$sql = sprintf('SELECT TOP (%d) * FROM (%s) t', $limit, $sql);
}
} elseif ($limit !== null) {
// requires ORDER BY, see https://technet.microsoft.com/en-us/library/gg699618(v=sql.110).aspx
$sql = sprintf('%s OFFSET %d ROWS FETCH NEXT %d ROWS ONLY', rtrim($sql), $offset, $limit);

View File

@@ -17,7 +17,10 @@ use Dibi;
*/
class SqlsrvReflector implements Dibi\Reflector
{
private Dibi\Driver $driver;
use Dibi\Strict;
/** @var Dibi\Driver */
private $driver;
public function __construct(Dibi\Driver $driver)
@@ -39,7 +42,6 @@ class SqlsrvReflector implements Dibi\Reflector
'view' => isset($row[1]) && $row[1] === 'VIEW',
];
}
return $tables;
}
@@ -89,7 +91,6 @@ class SqlsrvReflector implements Dibi\Reflector
'vendor' => $row,
];
}
return $columns;
}
@@ -113,7 +114,6 @@ class SqlsrvReflector implements Dibi\Reflector
$indexes[$row['name']]['primary'] = $row['is_primary_key'] === 1;
$indexes[$row['name']]['columns'] = $keyUsages[$row['name']] ?? [];
}
return array_values($indexes);
}

View File

@@ -17,9 +17,14 @@ use Dibi;
*/
class SqlsrvResult implements Dibi\ResultDriver
{
use Dibi\Strict;
/** @var resource */
private $resultSet;
/** @var bool */
private $autoFree = true;
/**
* @param resource $resultSet
@@ -30,6 +35,17 @@ class SqlsrvResult implements Dibi\ResultDriver
}
/**
* Automatically frees the resources allocated for this result set.
*/
public function __destruct()
{
if ($this->autoFree && $this->getResultResource()) {
$this->free();
}
}
/**
* Returns the number of rows in a result set.
*/
@@ -80,7 +96,6 @@ class SqlsrvResult implements Dibi\ResultDriver
'nativetype' => $fieldMetadata['Type'],
];
}
return $columns;
}
@@ -89,8 +104,9 @@ class SqlsrvResult implements Dibi\ResultDriver
* Returns the result set resource.
* @return resource|null
*/
public function getResultResource(): mixed
public function getResultResource()
{
$this->autoFree = false;
return is_resource($this->resultSet) ? $this->resultSet : null;
}

View File

@@ -15,6 +15,8 @@ namespace Dibi;
*/
class Event
{
use Strict;
/** event type */
public const
CONNECT = 1,
@@ -29,16 +31,29 @@ class Event
TRANSACTION = 448, // BEGIN | COMMIT | ROLLBACK
ALL = 1023;
public Connection $connection;
public int $type;
public string $sql;
public Result|DriverException|null $result;
public float $time;
public ?int $count = null;
public ?array $source = null;
/** @var Connection */
public $connection;
/** @var int */
public $type;
/** @var string */
public $sql;
/** @var Result|DriverException|null */
public $result;
/** @var float */
public $time;
/** @var int|null */
public $count;
/** @var array|null */
public $source;
public function __construct(Connection $connection, int $type, ?string $sql = null)
public function __construct(Connection $connection, int $type, string $sql = null)
{
$this->connection = $connection;
$this->type = $type;
@@ -46,7 +61,7 @@ class Event
$this->time = -microtime(true);
if ($type === self::QUERY && preg_match('#\(?\s*(SELECT|UPDATE|INSERT|DELETE)#iA', $this->sql, $matches)) {
$types = [
static $types = [
'SELECT' => self::SELECT, 'UPDATE' => self::UPDATE,
'INSERT' => self::INSERT, 'DELETE' => self::DELETE,
];
@@ -55,11 +70,7 @@ class Event
$dibiDir = dirname((new \ReflectionClass('dibi'))->getFileName()) . DIRECTORY_SEPARATOR;
foreach (debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS) as $row) {
if (
isset($row['file'])
&& preg_match('~\.(php.?|phtml)$~', $row['file'])
&& !str_starts_with($row['file'], $dibiDir)
) {
if (isset($row['file']) && is_file($row['file']) && strpos($row['file'], $dibiDir) !== 0) {
$this->source = [$row['file'], (int) $row['line']];
break;
}
@@ -71,7 +82,10 @@ class Event
}
public function done(Result|DriverException|null $result = null): static
/**
* @param Result|DriverException|null $result
*/
public function done($result = null): self
{
$this->result = $result;
try {

View File

@@ -15,7 +15,10 @@ namespace Dibi;
*/
class Expression
{
private array $values;
use Strict;
/** @var array */
private $values;
public function __construct(...$values)

View File

@@ -27,8 +27,6 @@ namespace Dibi;
* @method Fluent innerJoin(...$table)
* @method Fluent rightJoin(...$table)
* @method Fluent outerJoin(...$table)
* @method Fluent union(Fluent $fluent)
* @method Fluent unionAll(Fluent $fluent)
* @method Fluent as(...$field)
* @method Fluent on(...$cond)
* @method Fluent and(...$cond)
@@ -45,9 +43,12 @@ namespace Dibi;
*/
class Fluent implements IDataSource
{
use Strict;
public const REMOVE = false;
public static array $masks = [
/** @var array */
public static $masks = [
'SELECT' => ['SELECT', 'DISTINCT', 'FROM', 'WHERE', 'GROUP BY',
'HAVING', 'ORDER BY', 'LIMIT', 'OFFSET', ],
'UPDATE' => ['UPDATE', 'SET', 'WHERE', 'ORDER BY', 'LIMIT'],
@@ -55,8 +56,8 @@ class Fluent implements IDataSource
'DELETE' => ['DELETE', 'FROM', 'USING', 'WHERE', 'ORDER BY', 'LIMIT'],
];
/** default modifiers for arrays */
public static array $modifiers = [
/** @var array default modifiers for arrays */
public static $modifiers = [
'SELECT' => '%n',
'FROM' => '%n',
'IN' => '%in',
@@ -68,8 +69,8 @@ class Fluent implements IDataSource
'GROUP BY' => '%by',
];
/** clauses separators */
public static array $separators = [
/** @var array clauses separators */
public static $separators = [
'SELECT' => ',',
'FROM' => ',',
'WHERE' => 'AND',
@@ -83,30 +84,41 @@ class Fluent implements IDataSource
'INTO' => false,
];
/** clauses */
public static array $clauseSwitches = [
/** @var array clauses */
public static $clauseSwitches = [
'JOIN' => 'FROM',
'INNER JOIN' => 'FROM',
'LEFT JOIN' => 'FROM',
'RIGHT JOIN' => 'FROM',
];
private Connection $connection;
private array $setups = [];
private ?string $command = null;
private array $clauses = [];
private array $flags = [];
/** @var Connection */
private $connection;
/** @var array */
private $setups = [];
/** @var string|null */
private $command;
/** @var array */
private $clauses = [];
/** @var array */
private $flags = [];
/** @var array|null */
private $cursor;
/** normalized clauses */
private static HashMap $normalizer;
/** @var HashMap normalized clauses */
private static $normalizer;
public function __construct(Connection $connection)
{
$this->connection = $connection;
if (!isset(self::$normalizer)) {
if (self::$normalizer === null) {
self::$normalizer = new HashMap([self::class, '_formatClause']);
}
}
@@ -115,7 +127,7 @@ class Fluent implements IDataSource
/**
* Appends new argument to the clause.
*/
public function __call(string $clause, array $args): static
public function __call(string $clause, array $args): self
{
$clause = self::$normalizer->$clause;
@@ -124,7 +136,6 @@ class Fluent implements IDataSource
if (isset(self::$masks[$clause])) {
$this->clauses = array_fill_keys(self::$masks[$clause], null);
}
$this->cursor = &$this->clauses[$clause];
$this->cursor = [];
$this->command = $clause;
@@ -154,6 +165,7 @@ class Fluent implements IDataSource
$this->cursor[] = $sep;
}
}
} else {
// append to currect flow
if ($args === [self::REMOVE]) {
@@ -191,7 +203,6 @@ class Fluent implements IDataSource
if ($arg instanceof self) {
$arg = new Literal("($arg)");
}
$this->cursor[] = $arg;
}
@@ -202,7 +213,7 @@ class Fluent implements IDataSource
/**
* Switch to a clause.
*/
public function clause(string $clause): static
public function clause(string $clause): self
{
$this->cursor = &$this->clauses[self::$normalizer->$clause];
if ($this->cursor === null) {
@@ -216,7 +227,7 @@ class Fluent implements IDataSource
/**
* Removes a clause.
*/
public function removeClause(string $clause): static
public function removeClause(string $clause): self
{
$this->clauses[self::$normalizer->$clause] = null;
return $this;
@@ -226,7 +237,7 @@ class Fluent implements IDataSource
/**
* Change a SQL flag.
*/
public function setFlag(string $flag, bool $value = true): static
public function setFlag(string $flag, bool $value = true): self
{
$flag = strtoupper($flag);
if ($value) {
@@ -234,7 +245,6 @@ class Fluent implements IDataSource
} else {
unset($this->flags[$flag]);
}
return $this;
}
@@ -266,7 +276,7 @@ class Fluent implements IDataSource
/**
* Adds Result setup.
*/
public function setupResult(string $method): static
public function setupResult(string $method): self
{
$this->setups[] = func_get_args();
return $this;
@@ -278,25 +288,28 @@ class Fluent implements IDataSource
/**
* Generates and executes SQL query.
* Returns result set or number of affected rows
* @return ($return is \dibi::IDENTIFIER|\dibi::AFFECTED_ROWS ? int : Result)
* @return Result|int|null result set or number of affected rows
* @throws Exception
*/
public function execute(?string $return = null): Result|int|null
public function execute(string $return = null)
{
$res = $this->query($this->_export());
return match ($return) {
\dibi::IDENTIFIER => $this->connection->getInsertId(),
\dibi::AFFECTED_ROWS => $this->connection->getAffectedRows(),
default => $res,
};
switch ($return) {
case \dibi::IDENTIFIER:
return $this->connection->getInsertId();
case \dibi::AFFECTED_ROWS:
return $this->connection->getAffectedRows();
default:
return $res;
}
}
/**
* Generates, executes SQL query and fetches the single row.
* @return Row|array|null
*/
public function fetch(): Row|array|null
public function fetch()
{
return $this->command === 'SELECT' && !$this->clauses['LIMIT']
? $this->query($this->_export(null, ['%lmt', 1]))->fetch()
@@ -306,9 +319,9 @@ class Fluent implements IDataSource
/**
* Like fetch(), but returns only first field.
* Returns value on success, null if no next record
* @return mixed value on success, null if no next record
*/
public function fetchSingle(): mixed
public function fetchSingle()
{
return $this->command === 'SELECT' && !$this->clauses['LIMIT']
? $this->query($this->_export(null, ['%lmt', 1]))->fetchSingle()
@@ -319,7 +332,7 @@ class Fluent implements IDataSource
/**
* Fetches all records from table.
*/
public function fetchAll(?int $offset = null, ?int $limit = null): array
public function fetchAll(int $offset = null, int $limit = null): array
{
return $this->query($this->_export(null, ['%ofs %lmt', $offset, $limit]))->fetchAll();
}
@@ -338,7 +351,7 @@ class Fluent implements IDataSource
/**
* Fetches all records from table like $key => $value pairs.
*/
public function fetchPairs(?string $key = null, ?string $value = null): array
public function fetchPairs(string $key = null, string $value = null): array
{
return $this->query($this->_export())->fetchPairs($key, $value);
}
@@ -347,7 +360,7 @@ class Fluent implements IDataSource
/**
* Required by the IteratorAggregate interface.
*/
public function getIterator(?int $offset = null, ?int $limit = null): ResultIterator
public function getIterator(int $offset = null, int $limit = null): ResultIterator
{
return $this->query($this->_export(null, ['%ofs %lmt', $offset, $limit]))->getIterator();
}
@@ -356,7 +369,7 @@ class Fluent implements IDataSource
/**
* Generates and prints SQL query or it's part.
*/
public function test(?string $clause = null): bool
public function test(string $clause = null): bool
{
return $this->connection->test($this->_export($clause));
}
@@ -377,7 +390,6 @@ class Fluent implements IDataSource
$method = array_shift($setup);
$res->$method(...$setup);
}
return $res;
}
@@ -396,14 +408,19 @@ class Fluent implements IDataSource
*/
final public function __toString(): string
{
return $this->connection->translate($this->_export());
try {
return $this->connection->translate($this->_export());
} catch (\Throwable $e) {
trigger_error($e->getMessage(), E_USER_ERROR);
return '';
}
}
/**
* Generates parameters for Translator.
*/
protected function _export(?string $clause = null, array $args = []): array
protected function _export(string $clause = null, array $args = []): array
{
if ($clause === null) {
$data = $this->clauses;
@@ -411,6 +428,7 @@ class Fluent implements IDataSource
$args = array_merge(['%lmt %ofs', $data['LIMIT'][0] ?? null, $data['OFFSET'][0] ?? null], $args);
unset($data['LIMIT'], $data['OFFSET']);
}
} else {
$clause = self::$normalizer->$clause;
if (array_key_exists($clause, $this->clauses)) {
@@ -426,7 +444,6 @@ class Fluent implements IDataSource
if ($clause === $this->command && $this->flags) {
$args[] = implode(' ', array_keys($this->flags));
}
foreach ($statement as $arg) {
$args[] = $arg;
}
@@ -447,7 +464,6 @@ class Fluent implements IDataSource
$s .= 'By';
trigger_error("Did you mean '$s'?", E_USER_NOTICE);
}
return strtoupper(preg_replace('#[a-z](?=[A-Z])#', '$0 ', $s));
}
@@ -459,7 +475,6 @@ class Fluent implements IDataSource
$this->clauses[$clause] = &$val;
unset($val);
}
$this->cursor = &$foo;
}
}

View File

@@ -14,7 +14,6 @@ namespace Dibi;
* Lazy cached storage.
* @internal
*/
#[\AllowDynamicProperties]
abstract class HashMapBase
{
/** @var callable */
@@ -46,21 +45,20 @@ abstract class HashMapBase
*/
final class HashMap extends HashMapBase
{
public function __set(string $nm, mixed $val): void
public function __set(string $nm, $val)
{
if ($nm === '') {
$nm = "\xFF";
}
$this->$nm = $val;
}
public function __get(string $nm): mixed
public function __get(string $nm)
{
if ($nm === '') {
$nm = "\xFF";
return isset($this->$nm) && true ? $this->$nm : $this->$nm = $this->getCallback()('');
return isset($this->$nm) ? $this->$nm : $this->$nm = $this->getCallback()('');
} else {
return $this->$nm = $this->getCallback()($nm);
}

View File

@@ -12,17 +12,21 @@ namespace Dibi;
class Helpers
{
private static HashMap $types;
use Strict;
/** @var HashMap */
private static $types;
/**
* Prints out a syntax highlighted version of the SQL command or Result.
* @param string|Result $sql
*/
public static function dump(string|Result|null $sql = null, bool $return = false): ?string
public static function dump($sql = null, bool $return = false): ?string
{
ob_start();
if ($sql instanceof Result && PHP_SAPI === 'cli') {
$hasColors = (str_starts_with((string) getenv('TERM'), 'xterm'));
$hasColors = (substr((string) getenv('TERM'), 0, 5) === 'xterm');
$maxLen = 0;
foreach ($sql as $i => $row) {
if ($i === 0) {
@@ -37,7 +41,6 @@ class Helpers
$spaces = $maxLen - mb_strlen($col) + 2;
echo "$col" . str_repeat(' ', $spaces) . "$val\n";
}
echo "\n";
}
@@ -50,7 +53,6 @@ class Helpers
foreach ($row as $col => $foo) {
echo "\t\t<th>" . htmlspecialchars((string) $col) . "</th>\n";
}
echo "\t</tr>\n</thead>\n<tbody>\n";
}
@@ -58,7 +60,6 @@ class Helpers
foreach ($row as $col) {
echo "\t\t<td>", htmlspecialchars((string) $col), "</td>\n";
}
echo "\t</tr>\n";
}
@@ -71,8 +72,8 @@ class Helpers
$sql = \dibi::$sql;
}
$keywords1 = 'SELECT|(?:ON\s+DUPLICATE\s+KEY)?UPDATE|INSERT(?:\s+INTO)?|REPLACE(?:\s+INTO)?|DELETE|CALL|UNION|FROM|WHERE|HAVING|GROUP\s+BY|ORDER\s+BY|LIMIT|OFFSET|FETCH\s+NEXT|SET|VALUES|LEFT\s+JOIN|INNER\s+JOIN|TRUNCATE|START\s+TRANSACTION|BEGIN|COMMIT|ROLLBACK(?:\s+TO\s+SAVEPOINT)?|(?:RELEASE\s+)?SAVEPOINT';
$keywords2 = 'ALL|DISTINCT|DISTINCTROW|IGNORE|AS|USING|ON|AND|OR|IN|IS|NOT|NULL|LIKE|RLIKE|REGEXP|TRUE|FALSE';
static $keywords1 = 'SELECT|(?:ON\s+DUPLICATE\s+KEY)?UPDATE|INSERT(?:\s+INTO)?|REPLACE(?:\s+INTO)?|DELETE|CALL|UNION|FROM|WHERE|HAVING|GROUP\s+BY|ORDER\s+BY|LIMIT|OFFSET|FETCH\s+NEXT|SET|VALUES|LEFT\s+JOIN|INNER\s+JOIN|TRUNCATE|START\s+TRANSACTION|BEGIN|COMMIT|ROLLBACK(?:\s+TO\s+SAVEPOINT)?|(?:RELEASE\s+)?SAVEPOINT';
static $keywords2 = 'ALL|DISTINCT|DISTINCTROW|IGNORE|AS|USING|ON|AND|OR|IN|IS|NOT|NULL|LIKE|RLIKE|REGEXP|TRUE|FALSE';
// insert new lines
$sql = " $sql ";
@@ -87,7 +88,7 @@ class Helpers
// syntax highlight
$highlighter = "#(/\\*.+?\\*/)|(\\*\\*.+?\\*\\*)|(?<=[\\s,(])($keywords1)(?=[\\s,)])|(?<=[\\s,(=])($keywords2)(?=[\\s,)=])#is";
if (PHP_SAPI === 'cli') {
if (str_starts_with((string) getenv('TERM'), 'xterm')) {
if (substr((string) getenv('TERM'), 0, 5) === 'xterm') {
$sql = preg_replace_callback($highlighter, function (array $m) {
if (!empty($m[1])) { // comment
return "\033[1;30m" . $m[1] . "\033[0m";
@@ -103,7 +104,6 @@ class Helpers
}
}, $sql);
}
echo trim($sql) . "\n\n";
} else {
@@ -150,7 +150,6 @@ class Helpers
$best = $item;
}
}
return $best;
}
@@ -158,7 +157,7 @@ class Helpers
/** @internal */
public static function escape(Driver $driver, $value, string $type): string
{
$types = [
static $types = [
Type::TEXT => 'text',
Type::BINARY => 'binary',
Type::BOOL => 'bool',
@@ -180,9 +179,8 @@ class Helpers
*/
public static function detectType(string $type): ?string
{
$patterns = [
static $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,
@@ -199,7 +197,6 @@ class Helpers
return $val;
}
}
return null;
}
@@ -207,10 +204,9 @@ class Helpers
/** @internal */
public static function getTypeCache(): HashMap
{
if (!isset(self::$types)) {
if (self::$types === null) {
self::$types = new HashMap([self::class, 'detectType']);
}
return self::$types;
}
@@ -234,9 +230,9 @@ class Helpers
/**
* Import SQL dump from file.
* Returns count of sql commands
* @return int count of sql commands
*/
public static function loadFromFile(Connection $connection, string $file, ?callable $onProgress = null): int
public static function loadFromFile(Connection $connection, string $file, callable $onProgress = null): int
{
@set_time_limit(0); // intentionally @
@@ -255,7 +251,7 @@ class Helpers
if (strtoupper(substr($s, 0, 10)) === 'DELIMITER ') {
$delimiter = trim(substr($s, 10));
} elseif (str_ends_with($ts = rtrim($s), $delimiter)) {
} elseif (substr($ts = rtrim($s), -strlen($delimiter)) === $delimiter) {
$sql .= substr($ts, 0, -strlen($delimiter));
$driver->query($sql);
$sql = '';
@@ -263,6 +259,7 @@ class Helpers
if ($onProgress) {
$onProgress($count, isset($stat['size']) ? $size * 100 / $stat['size'] : null);
}
} else {
$sql .= $s;
}
@@ -275,21 +272,20 @@ class Helpers
$onProgress($count, isset($stat['size']) ? 100 : null);
}
}
fclose($handle);
return $count;
}
/** @internal */
public static function false2Null(mixed $val): mixed
public static function false2Null($val)
{
return $val === false ? null : $val;
}
/** @internal */
public static function intVal(mixed $value): int
public static function intVal($value): int
{
if (is_int($value)) {
return $value;
@@ -297,7 +293,6 @@ class Helpers
if (is_float($value * 1)) {
throw new Exception("Number $value is greater than integer.");
}
return (int) $value;
} else {
throw new Exception("Expected number, '$value' given.");

View File

@@ -15,7 +15,10 @@ namespace Dibi;
*/
class Literal
{
private string $value;
use Strict;
/** @var string */
private $value;
public function __construct($value)

View File

@@ -17,13 +17,19 @@ use Dibi;
*/
class FileLogger
{
/** Name of the file where SQL errors should be logged */
public string $file;
public int $filter;
private bool $errorsOnly;
use Dibi\Strict;
/** @var string Name of the file where SQL errors should be logged */
public $file;
/** @var int */
public $filter;
/** @var bool */
private $errorsOnly;
public function __construct(string $file, ?int $filter = null, bool $errorsOnly = false)
public function __construct(string $file, int $filter = null, bool $errorsOnly = false)
{
$this->file = $file;
$this->filter = $filter ?: Dibi\Event::QUERY;
@@ -48,11 +54,10 @@ class FileLogger
if ($code = $event->result->getCode()) {
$message = "[$code] $message";
}
$this->writeToFile(
$event,
"ERROR: $message"
. "\n-- SQL: " . $event->sql,
. "\n-- SQL: " . $event->sql
);
} else {
$this->writeToFile(
@@ -60,7 +65,7 @@ class FileLogger
'OK: ' . $event->sql
. ($event->count ? ";\n-- rows: " . $event->count : '')
. "\n-- takes: " . sprintf('%0.3f ms', $event->time * 1000)
. "\n-- source: " . implode(':', $event->source),
. "\n-- source: " . implode(':', $event->source)
);
}
}
@@ -70,7 +75,7 @@ class FileLogger
{
$driver = $event->connection->getConfig('driver');
$message .=
"\n-- driver: " . get_debug_type($driver) . '/' . $event->connection->getConfig('name')
"\n-- driver: " . (is_object($driver) ? get_class($driver) : $driver) . '/' . $event->connection->getConfig('name')
. "\n-- " . date('Y-m-d H:i:s')
. "\n\n";
file_put_contents($this->file, $message, FILE_APPEND | LOCK_EX);

View File

@@ -27,14 +27,16 @@ use Dibi;
*/
class Column
{
/** when created by Result */
private ?Dibi\Reflector $reflector;
use Dibi\Strict;
/** @var Dibi\Reflector|null when created by Result */
private $reflector;
/** @var array (name, nativetype, [table], [fullname], [size], [nullable], [default], [autoincrement], [vendor]) */
private array $info;
private $info;
public function __construct(?Dibi\Reflector $reflector, array $info)
public function __construct(Dibi\Reflector $reflector = null, array $info)
{
$this->reflector = $reflector;
$this->info = $info;
@@ -64,7 +66,6 @@ class Column
if (empty($this->info['table']) || !$this->reflector) {
throw new Dibi\Exception('Table is unknown or not available.');
}
return new Table($this->reflector, ['name' => $this->info['table']]);
}
@@ -107,13 +108,15 @@ class Column
}
public function getDefault(): mixed
/** @return mixed */
public function getDefault()
{
return $this->info['default'] ?? null;
}
public function getVendorInfo(string $key): mixed
/** @return mixed */
public function getVendorInfo(string $key)
{
return $this->info['vendor'][$key] ?? null;
}

View File

@@ -21,14 +21,19 @@ use Dibi;
*/
class Database
{
private Dibi\Reflector $reflector;
private ?string $name;
use Dibi\Strict;
/** @var Table[] */
private array $tables;
/** @var Dibi\Reflector */
private $reflector;
/** @var string|null */
private $name;
/** @var Table[]|null */
private $tables;
public function __construct(Dibi\Reflector $reflector, ?string $name = null)
public function __construct(Dibi\Reflector $reflector, string $name = null)
{
$this->reflector = $reflector;
$this->name = $name;
@@ -57,7 +62,6 @@ class Database
foreach ($this->tables as $table) {
$res[] = $table->getName();
}
return $res;
}
@@ -84,7 +88,7 @@ class Database
protected function init(): void
{
if (!isset($this->tables)) {
if ($this->tables === null) {
$this->tables = [];
foreach ($this->reflector->getTables() as $info) {
$this->tables[strtolower($info['name'])] = new Table($this->reflector, $info);

View File

@@ -9,6 +9,7 @@ declare(strict_types=1);
namespace Dibi\Reflection;
use Dibi;
/**
@@ -19,10 +20,13 @@ namespace Dibi\Reflection;
*/
class ForeignKey
{
private string $name;
use Dibi\Strict;
/** @var string */
private $name;
/** @var array of [local, foreign, onDelete, onUpdate] */
private array $references;
private $references;
public function __construct(string $name, array $references)

View File

@@ -9,6 +9,7 @@ declare(strict_types=1);
namespace Dibi\Reflection;
use Dibi;
/**
@@ -21,8 +22,10 @@ namespace Dibi\Reflection;
*/
class Index
{
use Dibi\Strict;
/** @var array (name, columns, [unique], [primary]) */
private array $info;
private $info;
public function __construct(array $info)

View File

@@ -20,13 +20,16 @@ use Dibi;
*/
class Result
{
private Dibi\ResultDriver $driver;
use Dibi\Strict;
/** @var Dibi\ResultDriver */
private $driver;
/** @var Column[]|null */
private ?array $columns;
private $columns;
/** @var Column[]|null */
private ?array $names;
private $names;
public function __construct(Dibi\ResultDriver $driver)
@@ -51,7 +54,6 @@ class Result
foreach ($this->columns as $column) {
$res[] = $fullNames ? $column->getFullName() : $column->getName();
}
return $res;
}
@@ -78,7 +80,7 @@ class Result
protected function initColumns(): void
{
if (!isset($this->columns)) {
if ($this->columns === null) {
$this->columns = [];
$reflector = $this->driver instanceof Dibi\Reflector
? $this->driver

View File

@@ -25,19 +25,28 @@ use Dibi;
*/
class Table
{
private Dibi\Reflector $reflector;
private string $name;
private bool $view;
use Dibi\Strict;
/** @var Column[] */
private array $columns;
/** @var Dibi\Reflector */
private $reflector;
/** @var ForeignKey[] */
private array $foreignKeys;
/** @var string */
private $name;
/** @var Index[] */
private array $indexes;
private ?Index $primaryKey;
/** @var bool */
private $view;
/** @var Column[]|null */
private $columns;
/** @var ForeignKey[]|null */
private $foreignKeys;
/** @var Index[]|null */
private $indexes;
/** @var Index|null */
private $primaryKey;
public function __construct(Dibi\Reflector $reflector, array $info)
@@ -76,7 +85,6 @@ class Table
foreach ($this->columns as $column) {
$res[] = $column->getName();
}
return $res;
}
@@ -126,7 +134,7 @@ class Table
protected function initColumns(): void
{
if (!isset($this->columns)) {
if ($this->columns === null) {
$this->columns = [];
foreach ($this->reflector->getColumns($this->name) as $info) {
$this->columns[strtolower($info['name'])] = new Column($this->reflector, $info);
@@ -137,14 +145,13 @@ class Table
protected function initIndexes(): void
{
if (!isset($this->indexes)) {
if ($this->indexes === null) {
$this->initColumns();
$this->indexes = [];
foreach ($this->reflector->getIndexes($this->name) as $info) {
foreach ($info['columns'] as $key => $name) {
$info['columns'][$key] = $this->columns[strtolower($name)];
}
$this->indexes[strtolower($info['name'])] = new Index($info);
if (!empty($info['primary'])) {
$this->primaryKey = $this->indexes[strtolower($info['name'])];

View File

@@ -17,29 +17,34 @@ namespace Dibi;
*/
class Result implements IDataSource
{
private ?ResultDriver $driver;
use Strict;
/** Translate table */
private array $types = [];
private ?Reflection\Result $meta;
/** @var ResultDriver|null */
private $driver;
/** Already fetched? Used for allowance for first seek(0) */
private bool $fetched = false;
/** @var array Translate table */
private $types = [];
/** returned object class */
private ?string $rowClass = Row::class;
/** @var Reflection\Result|null */
private $meta;
/** @var bool Already fetched? Used for allowance for first seek(0) */
private $fetched = false;
/** @var string|null returned object class */
private $rowClass = Row::class;
/** @var callable|null returned object factory */
private $rowFactory;
private array $formats = [];
/** @var array format */
private $formats = [];
public function __construct(ResultDriver $driver, bool $normalize = true)
public function __construct(ResultDriver $driver)
{
$this->driver = $driver;
if ($normalize) {
$this->detectTypes();
}
$this->detectTypes();
}
@@ -126,7 +131,7 @@ class Result implements IDataSource
/**
* Set fetched object class. This class should extend the Row class.
*/
public function setRowClass(?string $class): static
public function setRowClass(?string $class): self
{
$this->rowClass = $class;
return $this;
@@ -145,7 +150,7 @@ class Result implements IDataSource
/**
* Set a factory to create fetched object instances. These should extend the Row class.
*/
public function setRowFactory(callable $callback): static
public function setRowFactory(callable $callback): self
{
$this->rowFactory = $callback;
return $this;
@@ -155,14 +160,14 @@ class Result implements IDataSource
/**
* Fetches the row at current position, process optional type conversion.
* and moves the internal cursor to the next position
* @return Row|array|null
*/
final public function fetch(): mixed
final public function fetch()
{
$row = $this->getResultDriver()->fetch(true);
if ($row === null) {
return null;
}
$this->fetched = true;
$this->normalize($row);
if ($this->rowFactory) {
@@ -170,22 +175,20 @@ class Result implements IDataSource
} elseif ($this->rowClass) {
return new $this->rowClass($row);
}
return $row;
}
/**
* Like fetch(), but returns only first field.
* Returns value on success, null if no next record
* @return mixed value on success, null if no next record
*/
final public function fetchSingle(): mixed
final public function fetchSingle()
{
$row = $this->getResultDriver()->fetch(true);
if ($row === null) {
return null;
}
$this->fetched = true;
$this->normalize($row);
return reset($row);
@@ -196,9 +199,9 @@ class Result implements IDataSource
* Fetches all records from table.
* @return Row[]|array[]
*/
final public function fetchAll(?int $offset = null, ?int $limit = null): array
final public function fetchAll(int $offset = null, int $limit = null): array
{
$limit ??= -1;
$limit = $limit ?? -1;
$this->seek($offset ?: 0);
$row = $this->fetch();
if (!$row) {
@@ -210,7 +213,6 @@ class Result implements IDataSource
if ($limit === 0) {
break;
}
$limit--;
$data[] = $row;
} while ($row = $this->fetch());
@@ -230,7 +232,7 @@ class Result implements IDataSource
*/
final public function fetchAssoc(string $assoc): array
{
if (str_contains($assoc, ',')) {
if (strpos($assoc, ',') !== false) {
return $this->oldFetchAssoc($assoc);
}
@@ -283,6 +285,7 @@ class Result implements IDataSource
} else {
$x = &$x->{$assoc[$i + 1]};
}
} elseif ($as !== '|') { // associative-array node
$x = &$x[(string) $row->$as];
}
@@ -340,6 +343,7 @@ class Result implements IDataSource
} else {
$x = &$x[$assoc[$i + 1]];
}
} elseif ($as === '@') { // "object" node
if ($x === null) {
$x = clone $row;
@@ -348,6 +352,7 @@ class Result implements IDataSource
} else {
$x = &$x->{$assoc[$i + 1]};
}
} else { // associative-array node
$x = &$x[(string) $row->$as];
}
@@ -369,7 +374,7 @@ class Result implements IDataSource
* Fetches all records from table like $key => $value pairs.
* @throws \InvalidArgumentException
*/
final public function fetchPairs(?string $key = null, ?string $value = null): array
final public function fetchPairs(string $key = null, string $value = null): array
{
$this->seek(0);
$row = $this->fetch();
@@ -391,7 +396,6 @@ class Result implements IDataSource
do {
$data[] = $row[$key];
} while ($row = $this->fetch());
return $data;
}
@@ -406,7 +410,6 @@ class Result implements IDataSource
do {
$data[] = $row[$value];
} while ($row = $this->fetch());
return $data;
}
@@ -450,14 +453,9 @@ class Result implements IDataSource
if (!isset($row[$key])) { // null
continue;
}
$value = $row[$key];
$format = $this->formats[$type] ?? null;
if ($type === null || $format === 'native') {
$row[$key] = $value;
} elseif ($type === Type::TEXT) {
if ($type === Type::TEXT) {
$row[$key] = (string) $value;
} elseif ($type === Type::INTEGER) {
@@ -474,11 +472,9 @@ class Result implements IDataSource
} elseif ($p !== false && $e !== false) {
$value = rtrim($value, '.');
}
if ($value === '' || $value[0] === '.') {
$value = '0' . $value;
}
$row[$key] = $value === str_replace(',', '.', (string) ($float = (float) $value))
? $float
: $value;
@@ -487,17 +483,19 @@ class Result implements IDataSource
$row[$key] = ((bool) $value) && $value !== 'f' && $value !== 'F';
} elseif ($type === Type::DATETIME || $type === Type::DATE || $type === Type::TIME) {
if ($value && !str_starts_with((string) $value, '0000-00')) { // '', null, false, '0000-00-00', ...
if ($value && substr((string) $value, 0, 3) !== '000') { // '', null, false, '0000-00-00', ...
$value = new DateTime($value);
$row[$key] = $format ? $value->format($format) : $value;
$row[$key] = empty($this->formats[$type])
? $value
: $value->format($this->formats[$type]);
} else {
$row[$key] = null;
}
} elseif ($type === Type::TIME_INTERVAL) {
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;
$row[$key] = new \DateInterval("PT$m[2]H$m[3]M$m[4]S");
$row[$key]->invert = (int) (bool) $m[1];
} elseif ($type === Type::BINARY) {
$row[$key] = is_string($value)
@@ -505,11 +503,15 @@ class Result implements IDataSource
: $value;
} elseif ($type === Type::JSON) {
if ($format === 'string') { // back compatibility with 'native'
if ($this->formats[$type] === 'string') {
$row[$key] = $value;
} else {
$row[$key] = json_decode($value, $format === 'array');
$row[$key] = json_decode($value, $this->formats[$type] === 'array');
}
} elseif ($type === null) {
$row[$key] = $value;
} else {
throw new \RuntimeException('Unexpected type ' . $type);
}
@@ -521,7 +523,7 @@ class Result implements IDataSource
* Define column type.
* @param string|null $type use constant Type::*
*/
final public function setType(string $column, ?string $type): static
final public function setType(string $column, ?string $type): self
{
$this->types[$column] = $type;
return $this;
@@ -547,25 +549,15 @@ class Result implements IDataSource
/**
* Sets type format.
* Sets date format.
*/
final public function setFormat(string $type, ?string $format): static
final public function setFormat(string $type, ?string $format): self
{
$this->formats[$type] = $format;
return $this;
}
/**
* Sets type formats.
*/
final public function setFormats(array $formats): static
{
$this->formats = $formats;
return $this;
}
/**
* Returns data format.
*/
@@ -583,10 +575,9 @@ class Result implements IDataSource
*/
public function getInfo(): Reflection\Result
{
if (!isset($this->meta)) {
if ($this->meta === null) {
$this->meta = new Reflection\Result($this->getResultDriver());
}
return $this->meta;
}

View File

@@ -15,9 +15,16 @@ namespace Dibi;
*/
class ResultIterator implements \Iterator, \Countable
{
private Result $result;
private mixed $row;
private int $pointer = 0;
use Strict;
/** @var Result */
private $result;
/** @var mixed */
private $row;
/** @var int */
private $pointer = 0;
public function __construct(Result $result)
@@ -39,9 +46,9 @@ class ResultIterator implements \Iterator, \Countable
/**
* Returns the key of the current element.
* @return mixed
*/
#[\ReturnTypeWillChange]
public function key(): mixed
public function key()
{
return $this->pointer;
}
@@ -49,9 +56,9 @@ class ResultIterator implements \Iterator, \Countable
/**
* Returns the current element.
* @return mixed
*/
#[\ReturnTypeWillChange]
public function current(): mixed
public function current()
{
return $this->row;
}

View File

@@ -13,7 +13,6 @@ namespace Dibi;
/**
* Result set single row.
*/
#[\AllowDynamicProperties]
class Row implements \ArrayAccess, \IteratorAggregate, \Countable
{
public function __construct(array $arr)
@@ -32,70 +31,62 @@ class Row implements \ArrayAccess, \IteratorAggregate, \Countable
/**
* Converts value to DateTime object.
* @return DateTime|string|null
*/
public function asDateTime(string $key, ?string $format = null): DateTime|string|null
public function asDateTime(string $key, string $format = null)
{
$time = $this[$key];
if (!$time instanceof DateTime) {
if (!$time || str_starts_with((string) $time, '0000-00')) { // '', null, false, '0000-00-00', ...
if (!$time || substr((string) $time, 0, 3) === '000') { // '', null, false, '0000-00-00', ...
return null;
}
$time = new DateTime($time);
}
return $format === null ? $time : $time->format($format);
}
public function __get(string $key): mixed
public function __get(string $key)
{
$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;
}
public function __isset(string $key): bool
{
return false;
}
/********************* interfaces ArrayAccess, Countable & IteratorAggregate ****************d*g**/
final public function count(): int
final public function count()
{
return count((array) $this);
}
final public function getIterator(): \ArrayIterator
final public function getIterator()
{
return new \ArrayIterator($this);
}
final public function offsetSet($nm, $val): void
final public function offsetSet($nm, $val)
{
$this->$nm = $val;
}
final public function offsetGet($nm): mixed
final public function offsetGet($nm)
{
return $this->$nm;
}
final public function offsetExists($nm): bool
final public function offsetExists($nm)
{
return isset($this->$nm);
}
final public function offsetUnset($nm): void
final public function offsetUnset($nm)
{
unset($this->$nm);
}

111
src/Dibi/Strict.php Normal file
View File

@@ -0,0 +1,111 @@
<?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;
use ReflectionClass;
use ReflectionMethod;
use ReflectionProperty;
/**
* Better OOP experience.
*/
trait Strict
{
/** @var array [method => [type => callback]] */
private static $extMethods;
/**
* Call to undefined method.
* @throws \LogicException
*/
public function __call(string $name, array $args)
{
$class = method_exists($this, $name) ? 'parent' : static::class;
$items = (new ReflectionClass($this))->getMethods(ReflectionMethod::IS_PUBLIC);
$items = array_map(function ($item) { return $item->getName(); }, $items);
$hint = ($t = Helpers::getSuggestion($items, $name))
? ", did you mean $t()?"
: '.';
throw new \LogicException("Call to undefined method $class::$name()$hint");
}
/**
* Call to undefined static method.
* @throws \LogicException
*/
public static function __callStatic(string $name, array $args)
{
$rc = new ReflectionClass(static::class);
$items = array_intersect($rc->getMethods(ReflectionMethod::IS_PUBLIC), $rc->getMethods(ReflectionMethod::IS_STATIC));
$items = array_map(function ($item) { return $item->getName(); }, $items);
$hint = ($t = Helpers::getSuggestion($items, $name))
? ", did you mean $t()?"
: '.';
throw new \LogicException("Call to undefined static method {$rc->getName()}::$name()$hint");
}
/**
* Access to undeclared property.
* @throws \LogicException
*/
public function &__get(string $name)
{
if ((method_exists($this, $m = 'get' . $name) || method_exists($this, $m = 'is' . $name))
&& (new ReflectionMethod($this, $m))->isPublic()
) { // back compatiblity
$ret = $this->$m();
return $ret;
}
$rc = new ReflectionClass($this);
$items = array_diff($rc->getProperties(ReflectionProperty::IS_PUBLIC), $rc->getProperties(ReflectionProperty::IS_STATIC));
$items = array_map(function ($item) { return $item->getName(); }, $items);
$hint = ($t = Helpers::getSuggestion($items, $name))
? ", did you mean $$t?"
: '.';
throw new \LogicException("Attempt to read undeclared property {$rc->getName()}::$$name$hint");
}
/**
* Access to undeclared property.
* @throws \LogicException
*/
public function __set(string $name, $value)
{
$rc = new ReflectionClass($this);
$items = array_diff($rc->getProperties(ReflectionProperty::IS_PUBLIC), $rc->getProperties(ReflectionProperty::IS_STATIC));
$items = array_map(function ($item) { return $item->getName(); }, $items);
$hint = ($t = Helpers::getSuggestion($items, $name))
? ", did you mean $$t?"
: '.';
throw new \LogicException("Attempt to write to undeclared property {$rc->getName()}::$$name$hint");
}
public function __isset(string $name): bool
{
return false;
}
/**
* Access to undeclared property.
* @throws \LogicException
*/
public function __unset(string $name)
{
$class = static::class;
throw new \LogicException("Attempt to unset undeclared property $class::$$name.");
}
}

View File

@@ -15,19 +15,40 @@ namespace Dibi;
*/
final class Translator
{
private Connection $connection;
private Driver $driver;
private int $cursor = 0;
private array $args;
use Strict;
/** @var Connection */
private $connection;
/** @var Driver */
private $driver;
/** @var int */
private $cursor = 0;
/** @var array */
private $args;
/** @var string[] */
private array $errors;
private bool $comment = false;
private int $ifLevel = 0;
private int $ifLevelStart = 0;
private ?int $limit = null;
private ?int $offset = null;
private HashMap $identifiers;
private $errors;
/** @var bool */
private $comment = false;
/** @var int */
private $ifLevel = 0;
/** @var int */
private $ifLevelStart = 0;
/** @var int|null */
private $limit;
/** @var int|null */
private $offset;
/** @var HashMap */
private $identifiers;
public function __construct(Connection $connection)
@@ -48,7 +69,6 @@ final class Translator
while (count($args) === 1 && is_array($args[0])) { // implicit array expansion
$args = array_values($args[0]);
}
$this->args = $args;
$this->errors = [];
@@ -75,27 +95,27 @@ final class Translator
// note: this can change $this->args & $this->cursor & ...
. preg_replace_callback(
<<<'XX'
/
(?=[`['":%?]) ## speed-up
(?:
`(.+?)`| ## 1) `identifier`
\[(.+?)\]| ## 2) [identifier]
(')((?:''|[^'])*)'| ## 3,4) string
(")((?:""|[^"])*)"| ## 5,6) "string"
('|")| ## 7) lone quote
:(\S*?:)([a-zA-Z0-9._]?)| ## 8,9) :substitution:
%([a-zA-Z~][a-zA-Z0-9~]{0,5})| ## 10) modifier
(\?) ## 11) placeholder
)/xs
XX,
/
(?=[`['":%?]) ## speed-up
(?:
`(.+?)`| ## 1) `identifier`
\[(.+?)\]| ## 2) [identifier]
(')((?:''|[^'])*)'| ## 3,4) string
(")((?:""|[^"])*)"| ## 5,6) "string"
('|")| ## 7) lone quote
:(\S*?:)([a-zA-Z0-9._]?)| ## 8,9) :substitution:
%([a-zA-Z~][a-zA-Z0-9~]{0,5})| ## 10) modifier
(\?) ## 11) placeholder
)/xs
XX
,
[$this, 'cb'],
substr($arg, $toSkip),
substr($arg, $toSkip)
);
if (preg_last_error()) {
throw new PcreException;
}
}
continue;
}
@@ -118,10 +138,8 @@ final class Translator
if ($lastArr === $cursor - 1) {
$sql[] = ',';
}
$sql[] = $this->formatValue($arg, $commandIns ? 'l' : 'a');
}
$lastArr = $cursor;
continue;
}
@@ -130,6 +148,7 @@ final class Translator
$sql[] = $this->formatValue($arg, null);
} // while
if ($comment) {
$sql[] = '*/';
}
@@ -151,8 +170,9 @@ final class Translator
/**
* Apply modifier to single value.
* @param mixed $value
*/
public function formatValue(mixed $value, ?string $modifier): string
public function formatValue($value, ?string $modifier): string
{
if ($this->comment) {
return '...';
@@ -187,21 +207,20 @@ final class Translator
$v = $this->formatValue($v, $pair[1]);
if ($pair[1] === 'l' || $pair[1] === 'in') {
$op = 'IN ';
} elseif (str_contains($pair[1], 'like')) {
} elseif (strpos($pair[1], 'like') !== false) {
$op = 'LIKE ';
} elseif ($v === 'NULL') {
$op = 'IS ';
} else {
$op = '= ';
}
$vx[] = $k . $op . $v;
}
} else {
$vx[] = $this->formatValue($v, 'ex');
}
}
return '(' . implode(') ' . strtoupper($modifier) . ' (', $vx) . ')';
case 'n': // key, key, ... identifier names
@@ -213,7 +232,6 @@ final class Translator
$vx[] = $this->identifiers->{$pair[0]};
}
}
return implode(', ', $vx);
@@ -223,7 +241,6 @@ final class Translator
$vx[] = $this->identifiers->{$pair[0]} . '='
. $this->formatValue($v, $pair[1] ?? (is_array($v) ? 'ex!' : null));
}
return implode(', ', $vx);
@@ -233,7 +250,6 @@ final class Translator
$pair = explode('%', (string) $k, 2); // split into identifier & modifier
$vx[] = $this->formatValue($v, $pair[1] ?? (is_array($v) ? 'ex!' : null));
}
return '(' . (($vx || $modifier === 'l') ? implode(', ', $vx) : 'NULL') . ')';
@@ -243,7 +259,6 @@ final class Translator
$kx[] = $this->identifiers->{$pair[0]};
$vx[] = $this->formatValue($v, $pair[1] ?? (is_array($v) ? 'ex!' : null));
}
return '(' . implode(', ', $kx) . ') VALUES (' . implode(', ', $vx) . ')';
case 'm': // (key, key, ...) VALUES (val, val, ...), (val, val, ...), ...
@@ -257,7 +272,7 @@ final class Translator
$proto = array_keys($v);
}
} else {
return $this->errors[] = '**Unexpected type ' . get_debug_type($v) . '**';
return $this->errors[] = '**Unexpected type ' . (is_object($v) ? get_class($v) : gettype($v)) . '**';
}
$pair = explode('%', $k, 2); // split into identifier & modifier
@@ -266,11 +281,9 @@ final class Translator
$vx[$k2][] = $this->formatValue($v2, $pair[1] ?? (is_array($v2) ? 'ex!' : null));
}
}
foreach ($vx as $k => $v) {
$vx[$k] = '(' . implode(', ', $v) . ')';
}
return '(' . implode(', ', $kx) . ') VALUES ' . implode(', ', $vx);
case 'by': // key ASC, key DESC
@@ -284,7 +297,6 @@ final class Translator
$vx[] = $this->identifiers->$v;
}
}
return implode(', ', $vx);
case 'ex!':
@@ -298,24 +310,10 @@ final class Translator
foreach ($value as $v) {
$vx[] = $this->formatValue($v, $modifier);
}
return implode(', ', $vx);
}
}
if (is_object($value)
&& $modifier === null
&& !$value instanceof Literal
&& !$value instanceof Expression
&& $result = $this->connection->translateObject($value)
) {
return $this->connection->translate(...$result->getValues());
}
// object-to-scalar procession
if ($value instanceof \BackedEnum && is_scalar($value->value)) {
$value = $value->value;
}
// with modifier procession
if ($modifier) {
@@ -335,7 +333,7 @@ final class Translator
) {
// continue
} else {
$type = get_debug_type($value);
$type = is_object($value) ? get_class($value) : gettype($value);
return $this->errors[] = "**Invalid combination of type $type and modifier %$modifier**";
}
}
@@ -402,7 +400,6 @@ final class Translator
} elseif (!$value instanceof \DateTimeInterface) {
$value = new DateTime($value);
}
return $modifier === 'd'
? $this->driver->escapeDate($value)
: $this->driver->escapeDateTime($value);
@@ -423,25 +420,25 @@ final class Translator
$value = substr($value, 0, $toSkip)
. preg_replace_callback(
<<<'XX'
/
(?=[`['":])
(?:
`(.+?)`|
\[(.+?)]|
(')((?:''|[^'])*)'|
(")((?:""|[^"])*)"|
(['"])|
:(\S*?:)([a-zA-Z0-9._]?)
)/sx
XX,
/
(?=[`['":])
(?:
`(.+?)`|
\[(.+?)\]|
(')((?:''|[^'])*)'|
(")((?:""|[^"])*)"|
('|")|
:(\S*?:)([a-zA-Z0-9._]?)
)/sx
XX
,
[$this, 'cb'],
substr($value, $toSkip),
substr($value, $toSkip)
);
if (preg_last_error()) {
throw new PcreException;
}
}
return $value;
case 'SQL': // preserve as real SQL (TODO: rename to %sql)
@@ -472,6 +469,7 @@ final class Translator
}
}
// without modifier procession
if (is_string($value)) {
return $this->driver->escapeText($value);
@@ -501,7 +499,7 @@ final class Translator
return $this->connection->translate(...$value->getValues());
} else {
$type = get_debug_type($value);
$type = is_object($value) ? get_class($value) : gettype($value);
return $this->errors[] = "**Unexpected $type**";
}
}
@@ -553,7 +551,6 @@ final class Translator
$this->comment = true;
return '/*';
}
return '';
} elseif ($mod === 'else') {
@@ -566,6 +563,7 @@ final class Translator
$this->comment = true;
return '/*';
}
} elseif ($mod === 'end') {
$this->ifLevel--;
if ($this->ifLevelStart === $this->ifLevel + 1) {
@@ -574,7 +572,6 @@ final class Translator
$this->comment = false;
return '*/';
}
return '';
} elseif ($mod === 'ex') { // array expansion
@@ -589,7 +586,6 @@ final class Translator
} else {
$this->limit = Helpers::intVal($arg);
}
return '';
} elseif ($mod === 'ofs') { // apply offset
@@ -600,7 +596,6 @@ final class Translator
} else {
$this->offset = Helpers::intVal($arg);
}
return '';
} else { // default processing
@@ -654,7 +649,6 @@ final class Translator
$v = $this->driver->escapeIdentifier($v);
}
}
return implode('.', $parts);
}
}

View File

@@ -25,7 +25,6 @@ declare(strict_types=1);
* @method static void begin(string $savepoint = null)
* @method static void commit(string $savepoint = null)
* @method static void rollback(string $savepoint = null)
* @method static mixed transaction(callable $callback)
* @method static Dibi\Reflection\Database getDatabaseInfo()
* @method static Dibi\Fluent command()
* @method static Dibi\Fluent select(...$args)
@@ -37,35 +36,38 @@ declare(strict_types=1);
*/
class dibi
{
use Dibi\Strict;
public const
AFFECTED_ROWS = 'a',
IDENTIFIER = 'n';
/** version */
public const VERSION = '5.0.1';
public const
VERSION = '4.1.3';
/** sorting order */
public const
ASC = 'ASC',
DESC = 'DESC';
/** Last SQL command @see dibi::query() */
public static ?string $sql = null;
/** @var string|null Last SQL command @see dibi::query() */
public static $sql;
/** Elapsed time for last query */
public static ?float $elapsedTime = null;
/** @var float|null Elapsed time for last query */
public static $elapsedTime;
/** Elapsed time for all queries */
public static float $totalTime = 0;
/** @var float Elapsed time for all queries */
public static $totalTime;
/** Number or queries */
public static int $numOfQueries = 0;
/** @var int Number or queries */
public static $numOfQueries = 0;
/** @var Dibi\Connection[] Connection registry storage for Dibi\Connection objects */
private static array $registry = [];
private static $registry = [];
/** Current connection */
private static Dibi\Connection $connection;
/** @var Dibi\Connection Current connection */
private static $connection;
/**
@@ -85,7 +87,7 @@ class dibi
* @param array $config connection parameters
* @throws Dibi\Exception
*/
public static function connect(array $config = [], string $name = '0'): Dibi\Connection
public static function connect($config = [], string $name = '0'): Dibi\Connection
{
return self::$connection = self::$registry[$name] = new Dibi\Connection($config, $name);
}
@@ -104,7 +106,7 @@ class dibi
* Retrieve active connection.
* @throws Dibi\Exception
*/
public static function getConnection(?string $name = null): Dibi\Connection
public static function getConnection(string $name = null): Dibi\Connection
{
if ($name === null) {
if (self::$connection === null) {
@@ -148,9 +150,10 @@ class dibi
/**
* Prints out a syntax highlighted version of the SQL command or Result.
* @param string|Dibi\Result $sql
* @param bool $return return output instead of printing it?
*/
public static function dump(string|Dibi\Result|null $sql = null, bool $return = false): ?string
public static function dump($sql = null, bool $return = false): ?string
{
return Dibi\Helpers::dump($sql, $return);
}
@@ -159,9 +162,9 @@ class dibi
/**
* Strips microseconds part.
*/
public static function stripMicroseconds(DateTimeInterface $dt): DateTimeInterface
public static function stripMicroseconds(\DateTimeInterface $dt): \DateTimeInterface
{
$class = $dt::class;
$class = get_class($dt);
return new $class($dt->format('Y-m-d H:i:s'), $dt->getTimezone());
}
}

View File

@@ -15,15 +15,15 @@ namespace Dibi;
*/
class Exception extends \Exception
{
private ?string $sql;
/** @var string|null */
private $sql;
public function __construct(
string $message = '',
int|string $code = 0,
?string $sql = null,
?\Throwable $previous = null,
) {
/**
* @param int|string $code
*/
public function __construct(string $message = '', $code = 0, string $sql = null, \Throwable $previous = null)
{
parent::__construct($message, 0, $previous);
$this->code = $code;
$this->sql = $sql;
@@ -56,9 +56,17 @@ class DriverException extends Exception
*/
class PcreException extends Exception
{
public function __construct()
public function __construct(string $message = '%msg.')
{
parent::__construct(preg_last_error_msg(), preg_last_error());
static $messages = [
PREG_INTERNAL_ERROR => 'Internal error',
PREG_BACKTRACK_LIMIT_ERROR => 'Backtrack limit was exhausted',
PREG_RECURSION_LIMIT_ERROR => 'Recursion limit was exhausted',
PREG_BAD_UTF8_ERROR => 'Malformed UTF-8 data',
5 => 'Offset didn\'t correspond to the begin of a valid UTF-8 code point', // PREG_BAD_UTF8_OFFSET_ERROR
];
$code = preg_last_error();
parent::__construct(str_replace('%msg', $messages[$code] ?? 'Unknown error', $message), $code);
}
}
@@ -78,13 +86,14 @@ class NotSupportedException extends Exception
*/
class ProcedureException extends Exception
{
protected string $severity;
/** @var string */
protected $severity;
/**
* Construct the exception.
*/
public function __construct(string $message = '', int $code = 0, string $severity = '', ?string $sql = null)
public function __construct(string $message = '', int $code = 0, string $severity = '', string $sql = null)
{
parent::__construct($message, $code, $sql);
$this->severity = $severity;

View File

@@ -51,24 +51,25 @@ interface Driver
* Begins a transaction (if supported).
* @throws DriverException
*/
function begin(?string $savepoint = null): void;
function begin(string $savepoint = null): void;
/**
* Commits statements in a transaction.
* @throws DriverException
*/
function commit(?string $savepoint = null): void;
function commit(string $savepoint = null): void;
/**
* Rollback changes in a transaction.
* @throws DriverException
*/
function rollback(?string $savepoint = null): void;
function rollback(string $savepoint = null): void;
/**
* Returns the connection resource.
* @return mixed
*/
function getResource(): mixed;
function getResource();
/**
* Returns the connection reflector.
@@ -116,6 +117,7 @@ interface ResultDriver
/**
* Moves cursor position without fetching row.
* @return bool true on success, false if unable to seek to specified record
* @throws Exception
*/
function seek(int $row): bool;
@@ -140,8 +142,9 @@ interface ResultDriver
/**
* Returns the result set resource.
* @return mixed
*/
function getResultResource(): mixed;
function getResultResource();
/**
* Decodes data from result set.
@@ -221,20 +224,20 @@ interface IConnection
* Retrieves the ID generated for an AUTO_INCREMENT column by the previous INSERT query.
* @throws Exception
*/
function getInsertId(?string $sequence = null): int;
function getInsertId(string $sequence = null): int;
/**
* Begins a transaction (if supported).
*/
function begin(?string $savepoint = null): void;
function begin(string $savepoint = null): void;
/**
* Commits statements in a transaction.
*/
function commit(?string $savepoint = null): void;
function commit(string $savepoint = null): void;
/**
* Rollback changes in a transaction.
*/
function rollback(?string $savepoint = null): void;
function rollback(string $savepoint = null): void;
}

View File

@@ -1,4 +1,4 @@
# for php-coveralls
service_name: github-actions
service_name: travis-ci
coverage_clover: coverage.xml
json_path: coverage.json

View File

@@ -13,13 +13,12 @@ driver = mysqli
host = 127.0.0.1
username = root
password = "Password12!"
database = dibi_test
charset = utf8
system = mysql
[mysql pdo]
driver = pdo
dsn = "mysql:host=127.0.0.1;dbname=dibi_test"
dsn = "mysql:host=127.0.0.1"
username = root
password = "Password12!"
system = mysql

View File

@@ -1,89 +0,0 @@
[sqlite] ; default
driver = sqlite
database = :memory:
system = sqlite
[sqlite pdo]
driver = pdo
dsn = "sqlite::memory:"
system = sqlite
[mysql 5.7]
driver = mysqli
host = "127.0.0.1"
database = dibi_test
username = root
password = root
port = 3306
system = mysql
[mysql 5.7pdo]
driver = pdo
dsn = "mysql:host=127.0.0.1;port=3306;dbname=dibi_test"
user = root
password = root
system = mysql
[mysql 8.0]
driver = mysqli
host = "127.0.0.1"
database = dibi_test
username = root
password = root
port = 3307
system = mysql
[mysql 8.0pdo]
driver = pdo
dsn = "mysql:host=127.0.0.1;port=3307;dbname=dibi_test"
user = root
password = root
system = mysql
[postgre 9.6]
driver = postgre
host = "127.0.0.1"
database = dibi_test
username = postgres
password = postgres
port = 5432
system = postgre
[postgre 9.6pdo]
driver = pdo
dsn = "pgsql:host=127.0.0.1;port=5432;dbname=dibi_test"
user = postgres
password = postgres
system = postgre
[postgre 13]
driver = postgre
host = "127.0.0.1"
database = dibi_test
username = postgres
password = postgres
port = 5433
system = postgre
[postgre 13pdo]
driver = pdo
dsn = "pgsql:host=127.0.0.1;port=5433;dbname=dibi_test"
user = postgres
password = postgres
system = postgre
[sqlsrv]
driver = sqlsrv
host = "localhost"
username = SA
password = "YourStrong!Passw0rd"
database = dibi_test
port = 1433
system = sqlsrv
;[sqlsrv pdo]
;driver = pdo
;dsn = "sqlsrv:Server=localhost,1433;Database=dibi_test"
;user = SA
;password = "YourStrong!Passw0rd"
;system = sqlsrv

View File

@@ -13,13 +13,12 @@ driver = mysqli
host = 127.0.0.1
username = root
password =
database = dibi_test
charset = utf8
system = mysql
[mysql pdo]
driver = pdo
dsn = "mysql:host=127.0.0.1;dbname=dibi_test"
dsn = "mysql:host=127.0.0.1"
username = root
password =
system = mysql

View File

@@ -0,0 +1,38 @@
[sqlite] ; default
driver = sqlite
database = :memory:
system = sqlite
[sqlite pdo]
driver = pdo
dsn = "sqlite::memory:"
system = sqlite
[mysql improved]
driver = mysqli
host = 127.0.0.1
username = root
password =
charset = utf8
system = mysql
[mysql pdo]
driver = pdo
dsn = "mysql:host=127.0.0.1"
username = root
password =
system = mysql
[postgre]
driver = postgre
host = 127.0.0.1
username = postgres
password =
system = postgre
[postgre pdo]
driver = pdo
dsn = "pgsql:host=127.0.0.1;dbname=dibi_test"
username = postgres
password =
system = postgre

View File

@@ -64,38 +64,24 @@ test('', function () use ($config) {
test('', function () use ($config) {
$conn = new Connection($config);
Assert::true($conn->isConnected());
Assert::exception(function () use ($config) {
new Connection($config + ['onConnect' => '']);
}, InvalidArgumentException::class, "Configuration option 'onConnect' must be array.");
$conn->__destruct();
Assert::false($conn->isConnected());
});
test('', function () use ($config) {
Assert::exception(
fn() => new Connection($config + ['onConnect' => '']),
InvalidArgumentException::class,
"Configuration option 'onConnect' must be array.",
);
$e = Assert::exception(
fn() => new Connection($config + ['onConnect' => ['STOP']]),
Dibi\DriverException::class,
);
$e = Assert::exception(function () use ($config) {
new Connection($config + ['onConnect' => ['STOP']]);
}, Dibi\DriverException::class);
Assert::same('STOP', $e->getSql());
$e = Assert::exception(
fn() => new Connection($config + ['onConnect' => [['STOP %i', 123]]]),
Dibi\DriverException::class,
);
$e = Assert::exception(function () use ($config) {
new Connection($config + ['onConnect' => [['STOP %i', 123]]]);
}, Dibi\DriverException::class);
Assert::same('STOP 123', $e->getSql());
// lazy
$conn = new Connection($config + ['lazy' => true, 'onConnect' => ['STOP']]);
$e = Assert::exception(
fn() => $conn->query('SELECT 1'),
Dibi\DriverException::class,
);
$e = Assert::exception(function () use ($conn) {
$conn->query('SELECT 1');
}, Dibi\DriverException::class);
Assert::same('STOP', $e->getSql());
});

View File

@@ -33,13 +33,13 @@ Assert::equal([
$res = $conn->query('SELECT * FROM [products] ORDER BY product_id');
Assert::same(
[1 => 'Chair', 'Table', 'Computer'],
$res->fetchPairs('product_id', 'title'),
$res->fetchPairs('product_id', 'title')
);
$res = $conn->query('SELECT * FROM [products] ORDER BY product_id');
Assert::same(
[1 => 'Chair', 'Table', 'Computer'],
$res->fetchPairs(),
$res->fetchPairs()
);

View File

@@ -1,155 +0,0 @@
<?php
/**
* @dataProvider ../databases.ini
*/
declare(strict_types=1);
use Tester\Assert;
require __DIR__ . '/bootstrap.php';
$conn = new Dibi\Connection($config + ['formatDateTime' => "'Y-m-d H:i:s.u'", 'formatDate' => "'Y-m-d'"]);
class Email
{
public $address = 'address@example.com';
}
class Time extends DateTimeImmutable
{
}
test('Without object translator', function () use ($conn) {
Assert::exception(
fn() => $conn->translate('?', new Email),
Dibi\Exception::class,
'SQL translate error: Unexpected Email',
);
});
test('Basics', function () use ($conn) {
$conn->setObjectTranslator(fn(Email $email) => new Dibi\Expression('?', $email->address));
Assert::same(
reformat([
'sqlsrv' => "N'address@example.com'",
"'address@example.com'",
]),
$conn->translate('?', new Email),
);
});
test('DateTime', function () use ($conn) {
$stamp = Time::createFromFormat('Y-m-d H:i:s', '2022-11-22 12:13:14');
// Without object translator, DateTime child is translated by driver
Assert::same(
$conn->getDriver()->escapeDateTime($stamp),
$conn->translate('?', $stamp),
);
// With object translator
$conn->setObjectTranslator(fn(Time $time) => new Dibi\Expression('OwnTime(?)', $time->format('H:i:s')));
Assert::same(
reformat([
'sqlsrv' => "OwnTime(N'12:13:14')",
"OwnTime('12:13:14')",
]),
$conn->translate('?', $stamp),
);
// With modifier, it is still translated by driver
Assert::same(
$conn->getDriver()->escapeDateTime($stamp),
$conn->translate('%dt', $stamp),
);
Assert::same(
$conn->getDriver()->escapeDateTime($stamp),
$conn->translate('%t', $stamp),
);
Assert::same(
$conn->getDriver()->escapeDate($stamp),
$conn->translate('%d', $stamp),
);
// DateTimeImmutable as a Time parent is not affected and still translated by driver
$dt = DateTimeImmutable::createFromFormat('Y-m-d H:i:s', '2022-11-22 12:13:14');
Assert::same(
$conn->getDriver()->escapeDateTime($dt),
$conn->translate('?', $dt),
);
// But DateTime translation can be overloaded
$conn->setObjectTranslator(fn(DateTimeInterface $dt) => new Dibi\Expression('OwnDateTime'));
Assert::same(
'OwnDateTime',
$conn->translate('?', $dt),
);
});
test('Complex structures', function () use ($conn) {
$conn->setObjectTranslator(fn(Email $email) => new Dibi\Expression('?', $email->address));
$conn->setObjectTranslator(fn(Time $time) => new Dibi\Expression('OwnTime(?)', $time->format('H:i:s')));
$conn->setObjectTranslator(fn(DateTimeInterface $dt) => new Dibi\Expression('OwnDateTime'));
$time = Time::createFromFormat('Y-m-d H:i:s', '2022-11-22 12:13:14');
Assert::same(
reformat([
'sqlsrv' => "([a], [b], [c], [d], [e], [f], [g]) VALUES (OwnTime(N'12:13:14'), '2022-11-22', CONVERT(DATETIME2(7), '2022-11-22 12:13:14.000000'), CONVERT(DATETIME2(7), '2022-11-22 12:13:14.000000'), N'address@example.com', OwnDateTime, OwnDateTime)",
'odbc' => "([a], [b], [c], [d], [e], [f], [g]) VALUES (OwnTime('12:13:14'), #11/22/2022#, #11/22/2022 12:13:14.000000#, #11/22/2022 12:13:14.000000#, 'address@example.com', OwnDateTime, OwnDateTime)",
"([a], [b], [c], [d], [e], [f], [g]) VALUES (OwnTime('12:13:14'), '2022-11-22', '2022-11-22 12:13:14.000000', '2022-11-22 12:13:14.000000', 'address@example.com', OwnDateTime, OwnDateTime)",
]),
$conn->translate('%v', [
'a' => $time,
'b%d' => $time,
'c%t' => $time,
'd%dt' => $time,
'e' => new Email,
'f' => new DateTime,
'g' => new DateTimeImmutable,
]),
);
});
test('Invalid translator', function () use ($conn) {
Assert::exception(
fn() => $conn->setObjectTranslator(fn($email) => 'foo'),
Dibi\Exception::class,
'Object translator must have exactly one parameter with class typehint.',
);
Assert::exception(
fn() => $conn->setObjectTranslator(fn(string $email) => 'foo'),
Dibi\Exception::class,
"Object translator must have exactly one parameter with non-nullable class typehint, got 'string'.",
);
Assert::exception(
fn() => $conn->setObjectTranslator(fn(Email|bool $email) => 'foo'),
Dibi\Exception::class,
"Object translator must have exactly one parameter with non-nullable class typehint, got 'bool'.",
);
Assert::exception(
fn() => $conn->setObjectTranslator(fn(Email|null $email) => 'foo'),
Dibi\Exception::class,
"Object translator must have exactly one parameter with non-nullable class typehint, got '?Email'.",
);
$conn->setObjectTranslator(fn(Email $email) => 'foo');
Assert::exception(
fn() => $conn->translate('?', new Email),
Dibi\Exception::class,
"Object translator for class 'Email' returned 'string' but Dibi\\Expression expected.",
);
});

View File

@@ -1,5 +1,4 @@
<?php
declare(strict_types=1);
use Tester\Assert;
@@ -14,27 +13,27 @@ $conn->getSubstitutes()->blog = 'wp_';
Assert::same(
reformat('UPDATE wp_items SET [val]=1'),
$conn->translate('UPDATE :blog:items SET [val]=1'),
$conn->translate('UPDATE :blog:items SET [val]=1')
);
Assert::same(
reformat('UPDATE [wp_items] SET [val]=1'),
$conn->translate('UPDATE [:blog:items] SET [val]=1'),
$conn->translate('UPDATE [:blog:items] SET [val]=1')
);
Assert::same(
reformat("UPDATE 'wp_' SET [val]=1"),
$conn->translate('UPDATE :blog: SET [val]=1'),
$conn->translate('UPDATE :blog: SET [val]=1')
);
Assert::same(
reformat("UPDATE ':blg:' SET [val]=1"),
$conn->translate('UPDATE :blg: SET [val]=1'),
$conn->translate('UPDATE :blg: SET [val]=1')
);
Assert::same(
reformat("UPDATE table SET [text]=':blog:a'"),
$conn->translate("UPDATE table SET [text]=':blog:a'"),
$conn->translate("UPDATE table SET [text]=':blog:a'")
);
@@ -43,14 +42,16 @@ $conn->getSubstitutes()->{''} = 'my_';
Assert::same(
reformat('UPDATE my_table SET [val]=1'),
$conn->translate('UPDATE ::table SET [val]=1'),
$conn->translate('UPDATE ::table SET [val]=1')
);
// create substitutions using fallback callback
$conn->getSubstitutes()->setCallback(fn($expr) => '_' . $expr . '_');
$conn->getSubstitutes()->setCallback(function ($expr) {
return '_' . $expr . '_';
});
Assert::same(
reformat('UPDATE _account_user SET [val]=1'),
$conn->translate('UPDATE :account:user SET [val]=1'),
$conn->translate('UPDATE :account:user SET [val]=1')
);

View File

@@ -15,131 +15,36 @@ $conn = new Dibi\Connection($config);
$conn->loadFile(__DIR__ . "/data/$config[system].sql");
/*
Assert::exception(
fn() => $conn->rollback(),
Dibi\Exception::class,
);
/*Assert::exception(function () use ($conn) {
$conn->rollback();
}, Dibi\Exception::class);
Assert::exception(
fn() => $conn->commit(),
Dibi\Exception::class,
);
Assert::exception(function () use ($conn) {
$conn->commit();
}, Dibi\Exception::class);
$conn->begin();
Assert::exception(
fn() => $conn->begin(),
Dibi\Exception::class,
);
Assert::exception(function () use ($conn) {
$conn->begin();
}, Dibi\Exception::class);
*/
test('begin() & rollback()', function () use ($conn) {
$conn->begin();
Assert::same(3, (int) $conn->query('SELECT COUNT(*) FROM [products]')->fetchSingle());
$conn->query('INSERT INTO [products]', [
'title' => 'Test product',
]);
Assert::same(4, (int) $conn->query('SELECT COUNT(*) FROM [products]')->fetchSingle());
$conn->rollback();
Assert::same(3, (int) $conn->query('SELECT COUNT(*) FROM [products]')->fetchSingle());
});
$conn->begin();
Assert::same(3, (int) $conn->query('SELECT COUNT(*) FROM [products]')->fetchSingle());
$conn->query('INSERT INTO [products]', [
'title' => 'Test product',
]);
Assert::same(4, (int) $conn->query('SELECT COUNT(*) FROM [products]')->fetchSingle());
$conn->rollback();
Assert::same(3, (int) $conn->query('SELECT COUNT(*) FROM [products]')->fetchSingle());
test('begin() & commit()', function () use ($conn) {
$conn->begin();
$conn->query('INSERT INTO [products]', [
'title' => 'Test product',
]);
$conn->commit();
Assert::same(4, (int) $conn->query('SELECT COUNT(*) FROM [products]')->fetchSingle());
});
test('transaction() fail', function () use ($conn) {
Assert::exception(
fn() => $conn->transaction(function (Dibi\Connection $connection) {
$connection->query('INSERT INTO [products]', [
'title' => 'Test product',
]);
throw new Exception('my exception');
}),
Throwable::class,
'my exception',
);
Assert::same(4, (int) $conn->query('SELECT COUNT(*) FROM [products]')->fetchSingle());
});
test('transaction() success', function () use ($conn) {
$conn->transaction(function (Dibi\Connection $connection) {
$connection->query('INSERT INTO [products]', [
'title' => 'Test product',
]);
});
Assert::same(5, (int) $conn->query('SELECT COUNT(*) FROM [products]')->fetchSingle());
});
test('nested transaction() call fail', function () use ($conn) {
Assert::exception(
fn() => $conn->transaction(function (Dibi\Connection $connection) {
$connection->query('INSERT INTO [products]', [
'title' => 'Test product',
]);
$connection->transaction(function (Dibi\Connection $connection2) {
$connection2->query('INSERT INTO [products]', [
'title' => 'Test product',
]);
throw new Exception('my exception');
});
}),
Throwable::class,
'my exception',
);
Assert::same(5, (int) $conn->query('SELECT COUNT(*) FROM [products]')->fetchSingle());
});
test('nested transaction() call success', function () use ($conn) {
$conn->transaction(function (Dibi\Connection $connection) {
$connection->query('INSERT INTO [products]', [
'title' => 'Test product',
]);
$connection->transaction(function (Dibi\Connection $connection2) {
$connection2->query('INSERT INTO [products]', [
'title' => 'Test product',
]);
});
});
Assert::same(7, (int) $conn->query('SELECT COUNT(*) FROM [products]')->fetchSingle());
});
test('begin(), commit() & rollback() calls are forbidden in transaction()', function () use ($conn) {
Assert::exception(
fn() => $conn->transaction(function (Dibi\Connection $connection) {
$connection->begin();
}),
LogicException::class,
Dibi\Connection::class . '::begin() call is forbidden inside a transaction() callback',
);
Assert::exception(
fn() => $conn->transaction(function (Dibi\Connection $connection) {
$connection->commit();
}),
LogicException::class,
Dibi\Connection::class . '::commit() call is forbidden inside a transaction() callback',
);
Assert::exception(
fn() => $conn->transaction(function (Dibi\Connection $connection) {
$connection->rollback();
}),
LogicException::class,
Dibi\Connection::class . '::rollback() call is forbidden inside a transaction() callback',
);
});
$conn->begin();
$conn->query('INSERT INTO [products]', [
'title' => 'Test product',
]);
$conn->commit();
Assert::same(4, (int) $conn->query('SELECT COUNT(*) FROM [products]')->fetchSingle());

View File

@@ -1,5 +1,4 @@
<?php
declare(strict_types=1);
use Dibi\Row;
@@ -17,7 +16,7 @@ Assert::match(
reformat('
SELECT *
FROM (SELECT * FROM products) t'),
(string) $ds,
(string) $ds
);
@@ -25,7 +24,7 @@ Assert::same(3, $ds->count());
Assert::same(3, $ds->getTotalCount());
Assert::same(
reformat('SELECT COUNT(*) FROM (SELECT * FROM products) t'),
dibi::$sql,
dibi::$sql
);
@@ -39,7 +38,7 @@ FROM (SELECT * FROM products) t
WHERE (title like '%a%')
ORDER BY [title] DESC
"),
(string) $ds,
(string) $ds
);
@@ -53,7 +52,7 @@ FROM (SELECT * FROM products) t
WHERE (title like '%a%') AND (product_id = 1)
ORDER BY [title] DESC, [product_id] ASC
"),
(string) $ds,
(string) $ds
);
@@ -67,7 +66,7 @@ FROM (SELECT * FROM products) t
WHERE (title like '%a%') AND (product_id = 1) AND (product_id = 1)
ORDER BY [product_id] ASC
"),
(string) $ds,
(string) $ds
);
@@ -95,7 +94,7 @@ FROM (SELECT * FROM products) t
WHERE (title like '%a%') AND (product_id = 1) AND (product_id = 1)
ORDER BY [product_id] ASC
"),
dibi::$sql,
dibi::$sql
);
@@ -108,7 +107,7 @@ FROM (SELECT * FROM products) t
WHERE (title like '%a%') AND (product_id = 1) AND (product_id = 1)
ORDER BY [product_id] ASC
) t"),
(string) $fluent,
(string) $fluent
);
@@ -117,7 +116,7 @@ Assert::match(
reformat('
SELECT *
FROM (SELECT [title] FROM [products]) t'),
(string) $ds,
(string) $ds
);
Assert::equal(new Row([
@@ -129,7 +128,7 @@ Assert::same(1, $conn->dataSource('SELECT * FROM products ORDER BY product_id')-
Assert::same(
[1 => 'Chair', 'Table', 'Computer'],
$conn->dataSource('SELECT * FROM products ORDER BY product_id')->fetchPairs(),
$conn->dataSource('SELECT * FROM products ORDER BY product_id')->fetchPairs()
);
Assert::equal([
@@ -154,7 +153,7 @@ Assert::match(
reformat('
SELECT *
FROM [products]'),
(string) $ds,
(string) $ds
);
Assert::same(3, $ds->count());

View File

@@ -10,10 +10,12 @@ require __DIR__ . '/bootstrap.php';
date_default_timezone_set('Europe/Prague');
Assert::same('1978-01-23 11:40:00.000000', (string) new DateTime(254_400_000));
Assert::same('1978-01-23 11:40:00.000000', (string) (new DateTime)->setTimestamp(254_400_000));
Assert::same(254_400_000, (new DateTime(254_400_000))->getTimestamp());
Assert::same('1978-01-23 11:40:00.000000', (string) new DateTime(254400000));
Assert::same('1978-01-23 11:40:00.000000', (string) (new DateTime)->setTimestamp(254400000));
Assert::same(254400000, (new DateTime(254400000))->getTimestamp());
Assert::same(is_int(2_544_000_000) ? 2_544_000_000 : '2544000000', (new DateTime(2_544_000_000))->getTimestamp()); // 64 bit
Assert::same('2050-08-13 11:40:00.000000', (string) new DateTime(2544000000));
Assert::same('2050-08-13 11:40:00.000000', (string) (new DateTime)->setTimestamp(2544000000));
Assert::same(is_int(2544000000) ? 2544000000 : '2544000000', (new DateTime(2544000000))->getTimestamp()); // 64 bit
Assert::same('1978-05-05 00:00:00.000000', (string) new DateTime('1978-05-05'));

View File

@@ -1,21 +0,0 @@
<?php
declare(strict_types=1);
use Tester\Assert;
require __DIR__ . '/bootstrap.php';
$conn = new Dibi\Connection($config);
$event = new Dibi\Event($conn, Dibi\Event::CONNECT);
Assert::same([__FILE__, __LINE__ - 1], $event->source);
eval('$event = new Dibi\Event($conn, Dibi\Event::CONNECT);');
Assert::same([__FILE__, __LINE__ - 1], $event->source);
array_map(function () use ($conn) {
$event = new Dibi\Event($conn, Dibi\Event::CONNECT);
Assert::same([__FILE__, __LINE__ - 1], $event->source);
}, [null]);

View File

@@ -1,5 +1,4 @@
<?php
declare(strict_types=1);
use Dibi\Fluent;

View File

@@ -1,5 +1,4 @@
<?php
declare(strict_types=1);
use Tester\Assert;
@@ -15,33 +14,33 @@ $fluent = $conn->delete('table')->as('bAlias')
Assert::same(
reformat('DELETE IGNORE FROM [table] AS [bAlias]'),
(string) $fluent,
(string) $fluent
);
$fluent->removeClause('from')->from('anotherTable');
Assert::same(
reformat('DELETE IGNORE FROM [anotherTable]'),
(string) $fluent,
(string) $fluent
);
$fluent->using('thirdTable');
Assert::same(
reformat('DELETE IGNORE FROM [anotherTable] USING [thirdTable]'),
(string) $fluent,
(string) $fluent
);
$fluent->setFlag('IGNORE', false);
Assert::same(
reformat('DELETE FROM [anotherTable] USING [thirdTable]'),
(string) $fluent,
(string) $fluent
);
$fluent->limit(10);
Assert::same(
reformat('DELETE FROM [anotherTable] USING [thirdTable] LIMIT 10'),
(string) $fluent,
(string) $fluent
);

View File

@@ -1,5 +1,4 @@
<?php
declare(strict_types=1);
use Tester\Assert;
@@ -58,28 +57,28 @@ $fluent = $conn->select('*')
Assert::same(
reformat('SELECT TOP (1) * FROM (SELECT * FROM [customers] ORDER BY [customer_id]) t'),
(string) $fluent,
(string) $fluent
);
$fluent->fetch();
Assert::same(
'SELECT TOP (1) * FROM (SELECT * FROM [customers] ORDER BY [customer_id]) t',
dibi::$sql,
dibi::$sql
);
$fluent->fetchSingle();
Assert::same(
reformat('SELECT TOP (1) * FROM (SELECT * FROM [customers] ORDER BY [customer_id]) t'),
dibi::$sql,
dibi::$sql
);
$fluent->fetchAll(0, 3);
Assert::same(
reformat('SELECT TOP (3) * FROM (SELECT * FROM [customers] ORDER BY [customer_id]) t'),
dibi::$sql,
dibi::$sql
);
Assert::same(
reformat('SELECT TOP (1) * FROM (SELECT * FROM [customers] ORDER BY [customer_id]) t'),
(string) $fluent,
(string) $fluent
);
@@ -87,16 +86,16 @@ $fluent->limit(0);
$fluent->fetch();
Assert::same(
reformat('SELECT TOP (0) * FROM (SELECT * FROM [customers] ORDER BY [customer_id]) t'),
dibi::$sql,
dibi::$sql
);
$fluent->fetchSingle();
Assert::same(
reformat('SELECT TOP (0) * FROM (SELECT * FROM [customers] ORDER BY [customer_id]) t'),
dibi::$sql,
dibi::$sql
);
Assert::same(
reformat('SELECT TOP (0) * FROM (SELECT * FROM [customers] ORDER BY [customer_id]) t'),
(string) $fluent,
(string) $fluent
);
@@ -105,14 +104,14 @@ $fluent->removeClause('offset');
$fluent->fetch();
Assert::same(
reformat('SELECT TOP (1) * FROM (SELECT * FROM [customers] ORDER BY [customer_id]) t'),
dibi::$sql,
dibi::$sql
);
$fluent->fetchSingle();
Assert::same(
reformat('SELECT TOP (1) * FROM (SELECT * FROM [customers] ORDER BY [customer_id]) t'),
dibi::$sql,
dibi::$sql
);
Assert::same(
reformat('SELECT * FROM [customers] ORDER BY [customer_id]'),
(string) $fluent,
(string) $fluent
);

View File

@@ -23,28 +23,28 @@ $fluent = $conn->select('*')
Assert::same(
reformat('SELECT * FROM [customers] ORDER BY [customer_id] LIMIT 1 OFFSET 3'),
(string) $fluent,
(string) $fluent
);
$fluent->fetch();
Assert::same(
reformat('SELECT * FROM [customers] ORDER BY [customer_id] LIMIT 1 OFFSET 3'),
dibi::$sql,
dibi::$sql
);
$fluent->fetchSingle();
Assert::same(
reformat('SELECT * FROM [customers] ORDER BY [customer_id] LIMIT 1 OFFSET 3'),
dibi::$sql,
dibi::$sql
);
$fluent->fetchAll(2, 3);
Assert::same(
reformat('SELECT * FROM [customers] ORDER BY [customer_id] LIMIT 3 OFFSET 2'),
dibi::$sql,
dibi::$sql
);
Assert::same(
reformat('SELECT * FROM [customers] ORDER BY [customer_id] LIMIT 1 OFFSET 3'),
(string) $fluent,
(string) $fluent
);
@@ -52,16 +52,16 @@ $fluent->limit(0);
$fluent->fetch();
Assert::same(
reformat('SELECT * FROM [customers] ORDER BY [customer_id] LIMIT 0 OFFSET 3'),
dibi::$sql,
dibi::$sql
);
$fluent->fetchSingle();
Assert::same(
reformat('SELECT * FROM [customers] ORDER BY [customer_id] LIMIT 0 OFFSET 3'),
dibi::$sql,
dibi::$sql
);
Assert::same(
reformat('SELECT * FROM [customers] ORDER BY [customer_id] LIMIT 0 OFFSET 3'),
(string) $fluent,
(string) $fluent
);
@@ -69,16 +69,16 @@ $fluent->removeClause('limit');
$fluent->fetch();
Assert::same(
reformat('SELECT * FROM [customers] ORDER BY [customer_id] LIMIT 1 OFFSET 3'),
dibi::$sql,
dibi::$sql
);
$fluent->fetchSingle();
Assert::same(
reformat('SELECT * FROM [customers] ORDER BY [customer_id] LIMIT 1 OFFSET 3'),
dibi::$sql,
dibi::$sql
);
Assert::same(
reformat('SELECT * FROM [customers] ORDER BY [customer_id] LIMIT 18446744073709551615 OFFSET 3'),
(string) $fluent,
(string) $fluent
);
@@ -86,14 +86,14 @@ $fluent->removeClause('offset');
$fluent->fetch();
Assert::same(
reformat('SELECT * FROM [customers] ORDER BY [customer_id] LIMIT 1'),
dibi::$sql,
dibi::$sql
);
$fluent->fetchSingle();
Assert::same(
reformat('SELECT * FROM [customers] ORDER BY [customer_id] LIMIT 1'),
dibi::$sql,
dibi::$sql
);
Assert::same(
reformat('SELECT * FROM [customers] ORDER BY [customer_id]'),
(string) $fluent,
(string) $fluent
);

View File

@@ -1,5 +1,4 @@
<?php
declare(strict_types=1);
use Tester\Assert;
@@ -21,40 +20,40 @@ $fluent = $conn->insert('table', $arr)
Assert::same(
reformat('INSERT IGNORE DELAYED INTO [table] ([title], [price], [brand]) VALUES (\'Super Product\', 12, NULL)'),
(string) $fluent,
(string) $fluent
);
$fluent->setFlag('IGNORE', false);
Assert::same(
reformat('INSERT DELAYED INTO [table] ([title], [price], [brand]) VALUES (\'Super Product\', 12, NULL)'),
(string) $fluent,
(string) $fluent
);
$fluent->setFlag('HIGH_priority');
Assert::same(
reformat('INSERT DELAYED HIGH_PRIORITY INTO [table] ([title], [price], [brand]) VALUES (\'Super Product\', 12, NULL)'),
(string) $fluent,
(string) $fluent
);
$fluent->into('anotherTable');
Assert::same(
reformat('INSERT DELAYED HIGH_PRIORITY INTO [anotherTable] VALUES (\'Super Product\', 12, NULL)'),
(string) $fluent,
(string) $fluent
);
$fluent->values('%l', $arr);
Assert::same(
reformat('INSERT DELAYED HIGH_PRIORITY INTO [anotherTable] VALUES (\'Super Product\', 12, NULL) , (\'Super Product\', 12, NULL)'),
(string) $fluent,
(string) $fluent
);
$fluent->values($arr);
Assert::same(
reformat('INSERT DELAYED HIGH_PRIORITY INTO [anotherTable] VALUES (\'Super Product\', 12, NULL) , (\'Super Product\', 12, NULL) , (\'Super Product\', 12, NULL)'),
(string) $fluent,
(string) $fluent
);

View File

@@ -25,7 +25,7 @@ $fluent = $conn->select('*')
Assert::same(
reformat('SELECT * , [a] , [b] AS [bAlias] , [c], [d], [e] , [d]'),
(string) $fluent,
(string) $fluent
);
$fluent->from('table')->as('table.Alias')
@@ -34,21 +34,21 @@ $fluent->from('table')->as('table.Alias')
Assert::same(
reformat('SELECT * , [a] , [b] AS [bAlias] , [c], [d], [e] , [d] FROM [table] AS [table.Alias] INNER JOIN [table1] ON table.col = table1.col INNER JOIN [table2] ON table.col = table2.col'),
(string) $fluent,
(string) $fluent
);
$fluent->from('anotherTable');
Assert::same(
reformat('SELECT * , [a] , [b] AS [bAlias] , [c], [d], [e] , [d] FROM [table] AS [table.Alias] INNER JOIN [table1] ON table.col = table1.col INNER JOIN [table2] ON table.col = table2.col , [anotherTable]'),
(string) $fluent,
(string) $fluent
);
$fluent->removeClause('from')->from('anotherTable');
Assert::same(
reformat('SELECT * , [a] , [b] AS [bAlias] , [c], [d], [e] , [d] FROM [anotherTable]'),
(string) $fluent,
(string) $fluent
);
$fluent->as('anotherAlias')
@@ -58,7 +58,7 @@ $fluent->as('anotherAlias')
Assert::same(
reformat('SELECT * , [a] , [b] AS [bAlias] , [c], [d], [e] , [d] FROM [anotherTable] AS [anotherAlias] INNER JOIN [table3] ON table.col = table3.col'),
(string) $fluent,
(string) $fluent
);
$fluent->where('col > %i', $max)
@@ -71,14 +71,14 @@ $fluent->where('col > %i', $max)
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) ORDER BY [val] ASC , [val2] DESC , [val3] DESC'),
(string) $fluent,
(string) $fluent
);
$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)'),
(string) $fluent,
(string) $fluent
);
@@ -86,7 +86,7 @@ $fluent = $conn->select('*')
->select(
$conn->select('count(*)')
->from('precteni')->as('P')
->where('P.id_clanku', '=', 'C.id_clanku'),
->where('P.id_clanku', '=', 'C.id_clanku')
)
->from('clanky')->as('C')
->where('id_clanku=%i', 123)
@@ -99,7 +99,7 @@ Assert::same(
'sqlsrv' => 'SELECT * , (SELECT count(*) FROM [precteni] AS [P] WHERE P.id_clanku = C.id_clanku) FROM [clanky] AS [C] WHERE id_clanku=123 OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY',
'SELECT * , (SELECT count(*) FROM [precteni] AS [P] WHERE P.id_clanku = C.id_clanku) FROM [clanky] AS [C] WHERE id_clanku=123 LIMIT 1',
]),
(string) $fluent,
(string) $fluent
);
@@ -112,7 +112,7 @@ $fluent = $conn->select('*')
Assert::same(
reformat('SELECT * , [x] AS [xAlias] FROM [products] INNER JOIN [orders] USING (product_id) INNER JOIN [customers] USING ([customer_id]) INNER JOIN [items] USING ([customer_id], [order_id])'),
(string) $fluent,
(string) $fluent
);
@@ -124,7 +124,7 @@ $fluent = $conn->command()->select()
Assert::same(
reformat('SELECT * FROM [products] INNER JOIN [orders] USING (product_id)'),
(string) $fluent,
(string) $fluent
);
@@ -138,7 +138,7 @@ Assert::same(
'sqlsrv' => "SELECT * FROM [me] AS [t] WHERE col > 10 AND ([x] = N'a') AND (b) AND (c)",
"SELECT * FROM [me] AS [t] WHERE col > 10 AND ([x] = 'a') AND (b) AND (c)",
]),
(string) $fluent,
(string) $fluent
);
@@ -147,11 +147,9 @@ if ($config['system'] === 'mysql') {
->limit(' 1; DROP TABLE users')
->offset(' 1; DROP TABLE users');
Assert::exception(
fn() => (string) $fluent,
Dibi\Exception::class,
"Expected number, ' 1; DROP TABLE users' given.",
);
Assert::error(function () use ($fluent) {
(string) $fluent;
}, E_USER_ERROR, "Expected number, ' 1; DROP TABLE users' given.");
}
@@ -160,5 +158,5 @@ $fluent = $conn->select('*')->from('abc')
Assert::same(
reformat('SELECT * FROM [abc] WHERE x IN ((SELECT [id] FROM [xyz]))'),
(string) $fluent,
(string) $fluent
);

View File

@@ -1,5 +1,4 @@
<?php
declare(strict_types=1);
use Tester\Assert;
@@ -21,14 +20,14 @@ $fluent = $conn->update('table', $arr)
Assert::same(
reformat('UPDATE IGNORE DELAYED [table] SET [title]=\'Super Product\', [price]=12, [brand]=NULL'),
(string) $fluent,
(string) $fluent
);
$fluent->set(['another' => 123]);
Assert::same(
reformat('UPDATE IGNORE DELAYED [table] SET [title]=\'Super Product\', [price]=12, [brand]=NULL , [another]=123'),
(string) $fluent,
(string) $fluent
);
@@ -40,5 +39,5 @@ $arr = [
$fluent = $conn->update(['table1', 'table2'], $arr);
Assert::same(
reformat('UPDATE [table1], [table2] SET [table1].[title]=\'Super Product\', [table2].[price]=12, [table2].[brand]=NULL'),
(string) $fluent,
(string) $fluent
);

View File

@@ -6,7 +6,9 @@ use Tester\Assert;
require __DIR__ . '/bootstrap.php';
$hash = new Dibi\HashMap(fn($v) => "b-$v-e");
$hash = new Dibi\HashMap(function ($v) {
return "b-$v-e";
});
Assert::same('b-X-e', $hash->{'X'});
Assert::same('b--e', $hash->{''});

View File

@@ -1,5 +1,4 @@
<?php
declare(strict_types=1);
use Dibi\Helpers;

View File

@@ -1,5 +1,4 @@
<?php
declare(strict_types=1);
use Dibi\Helpers;
@@ -12,32 +11,22 @@ Assert::same(0, Helpers::intVal(0));
Assert::same(0, Helpers::intVal('0'));
Assert::same(-10, Helpers::intVal('-10'));
Assert::exception(
fn() => Helpers::intVal('12345678901234567890123456879'),
Dibi\Exception::class,
'Number 12345678901234567890123456879 is greater than integer.',
);
Assert::exception(function () {
Helpers::intVal('12345678901234567890123456879');
}, Dibi\Exception::class, 'Number 12345678901234567890123456879 is greater than integer.');
Assert::exception(
fn() => Helpers::intVal('-12345678901234567890123456879'),
Dibi\Exception::class,
'Number -12345678901234567890123456879 is greater than integer.',
);
Assert::exception(function () {
Helpers::intVal('-12345678901234567890123456879');
}, Dibi\Exception::class, 'Number -12345678901234567890123456879 is greater than integer.');
Assert::exception(
fn() => Helpers::intVal(''),
Dibi\Exception::class,
"Expected number, '' given.",
);
Assert::exception(function () {
Helpers::intVal('');
}, Dibi\Exception::class, "Expected number, '' given.");
Assert::exception(
fn() => Helpers::intVal('not number'),
Dibi\Exception::class,
"Expected number, 'not number' given.",
);
Assert::exception(function () {
Helpers::intVal('not number');
}, Dibi\Exception::class, "Expected number, 'not number' given.");
Assert::exception(
fn() => Helpers::intVal(null),
Dibi\Exception::class,
"Expected number, '' given.",
);
Assert::exception(function () {
Helpers::intVal(null);
}, Dibi\Exception::class, "Expected number, '' given.");

View File

@@ -13,28 +13,22 @@ function buildPdoDriver(?int $errorMode)
if ($errorMode !== null) {
$pdo->setAttribute(PDO::ATTR_ERRMODE, $errorMode);
}
new Dibi\Drivers\PdoDriver(['resource' => $pdo]);
}
// PDO error mode: exception
Assert::exception(
fn() => buildPdoDriver(PDO::ERRMODE_EXCEPTION),
Dibi\DriverException::class,
'PDO connection in exception or warning error mode is not supported.',
);
Assert::exception(function () {
buildPdoDriver(PDO::ERRMODE_EXCEPTION);
}, Dibi\DriverException::class, 'PDO connection in exception or warning error mode is not supported.');
// PDO error mode: warning
Assert::exception(
fn() => buildPdoDriver(PDO::ERRMODE_WARNING),
Dibi\DriverException::class,
'PDO connection in exception or warning error mode is not supported.',
);
Assert::exception(function () {
buildPdoDriver(PDO::ERRMODE_WARNING);
}, Dibi\DriverException::class, 'PDO connection in exception or warning error mode is not supported.');
test(
'PDO error mode: explicitly set silent',
fn() => buildPdoDriver(PDO::ERRMODE_SILENT)
);
test('PDO error mode: explicitly set silent', function () {
buildPdoDriver(PDO::ERRMODE_SILENT);
});

View File

@@ -28,14 +28,14 @@ Assert::same(4, $res->getColumnCount());
Assert::same(
['product_id', 'order_id', 'name', 'xXx'],
$info->getColumnNames(),
$info->getColumnNames()
);
if (!in_array($config['driver'], ['sqlite', 'sqlite3', 'pdo', 'sqlsrv'], true)) {
if (!in_array($config['driver'], ['sqlite', 'pdo', 'sqlsrv'], true)) {
Assert::same(
['products.product_id', 'orders.order_id', 'customers.name', 'xXx'],
$info->getColumnNames(true),
$info->getColumnNames(true)
);
}
@@ -43,7 +43,7 @@ if (!in_array($config['driver'], ['sqlite', 'sqlite3', 'pdo', 'sqlsrv'], true))
$columns = $info->getColumns();
Assert::same('product_id', $columns[0]->getName());
if (!in_array($config['driver'], ['sqlite', 'sqlite3', 'pdo', 'sqlsrv'], true)) {
if (!in_array($config['driver'], ['sqlite', 'pdo', 'sqlsrv'], true)) {
Assert::same('products', $columns[0]->getTableName());
}
Assert::null($columns[0]->getVendorInfo('xxx'));

View File

@@ -1,5 +1,4 @@
<?php
declare(strict_types=1);
use Dibi\Type;
@@ -25,17 +24,6 @@ class MockResult extends Dibi\Result
}
test('', function () {
$result = new MockResult;
$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]));
Assert::same(['col' => false], $result->test(['col' => false]));
});
test('', function () {
$result = new MockResult;
$result->setType('col', Type::BOOL);
@@ -162,10 +150,9 @@ test('', function () {
if (PHP_VERSION_ID < 80000) {
Assert::same(['col' => 0], @$result->test(['col' => ''])); // triggers warning since PHP 7.1
} else {
Assert::exception(
fn() => Assert::same(['col' => 0], $result->test(['col' => ''])),
TypeError::class,
);
Assert::exception(function () use ($result) {
Assert::same(['col' => 0], $result->test(['col' => '']));
}, TypeError::class);
}
Assert::same(['col' => 0], $result->test(['col' => '0']));
@@ -190,10 +177,9 @@ test('', function () {
$result->setType('col', Type::DATETIME);
Assert::same(['col' => null], $result->test(['col' => null]));
Assert::exception(
fn() => $result->test(['col' => true]),
TypeError::class,
);
Assert::exception(function () use ($result) {
$result->test(['col' => true]);
}, TypeError::class);
Assert::same(['col' => null], $result->test(['col' => false]));
Assert::same(['col' => null], $result->test(['col' => '']));
@@ -210,10 +196,9 @@ test('', function () {
$result->setFormat(Type::DATETIME, 'Y-m-d H:i:s');
Assert::same(['col' => null], $result->test(['col' => null]));
Assert::exception(
fn() => $result->test(['col' => true]),
TypeError::class,
);
Assert::exception(function () use ($result) {
$result->test(['col' => true]);
}, TypeError::class);
Assert::same(['col' => null], $result->test(['col' => false]));
Assert::same(['col' => null], $result->test(['col' => '']));
@@ -229,10 +214,9 @@ test('', function () {
$result->setType('col', Type::DATE);
Assert::same(['col' => null], $result->test(['col' => null]));
Assert::exception(
fn() => $result->test(['col' => true]),
TypeError::class,
);
Assert::exception(function () use ($result) {
$result->test(['col' => true]);
}, TypeError::class);
Assert::same(['col' => null], $result->test(['col' => false]));
Assert::same(['col' => null], $result->test(['col' => '']));
@@ -246,10 +230,9 @@ test('', function () {
$result->setType('col', Type::TIME);
Assert::same(['col' => null], $result->test(['col' => null]));
Assert::exception(
fn() => $result->test(['col' => true]),
TypeError::class,
);
Assert::exception(function () use ($result) {
$result->test(['col' => true]);
}, TypeError::class);
Assert::same(['col' => null], $result->test(['col' => false]));
Assert::same(['col' => null], $result->test(['col' => '']));

View File

@@ -1,5 +1,4 @@
<?php
declare(strict_types=1);
use Tester\Assert;

View File

@@ -24,38 +24,26 @@ Assert::true(isset($row['title']));
// 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::error(function () use ($row) {
$x = $row['missing'];
}, E_USER_NOTICE, "Attempt to read missing column 'missing'.");
Assert::false(isset($row->missing));
Assert::false(isset($row['missing']));
// ??
Assert::same(123, $row->missing ?? 123);
Assert::same(123, $row['missing'] ?? 123);
// suggestions
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'?",
);
Assert::error(function () use ($row) {
$x = $row['tilte'];
}, E_USER_NOTICE, "Attempt to read missing column 'tilte', did you mean 'title'?");
// to array

View File

@@ -25,7 +25,7 @@ $conn->query(
'CREATE TRIGGER %n ON %n AFTER INSERT AS INSERT INTO %n DEFAULT VALUES',
'UpdAAB',
'aab',
'aaa',
'aaa'
);
$conn->query('INSERT INTO %n DEFAULT VALUES', 'aab');

View File

@@ -21,19 +21,19 @@ $tests = function ($conn) {
// Limit and offset
Assert::same(
'SELECT 1 OFFSET 10 ROWS FETCH NEXT 10 ROWS ONLY',
$conn->translate('SELECT 1 %ofs %lmt', 10, 10),
$conn->translate('SELECT 1 %ofs %lmt', 10, 10)
);
// Limit only
Assert::same(
'SELECT 1 OFFSET 0 ROWS FETCH NEXT 10 ROWS ONLY',
$conn->translate('SELECT 1 %lmt', 10),
$conn->translate('SELECT 1 %lmt', 10)
);
// Offset only
Assert::same(
'SELECT 1 OFFSET 10 ROWS',
$conn->translate('SELECT 1 %ofs', 10),
$conn->translate('SELECT 1 %ofs', 10)
);
// Offset invalid
@@ -42,7 +42,7 @@ $tests = function ($conn) {
$conn->translate('SELECT 1 %ofs', -10);
},
Dibi\NotSupportedException::class,
'Negative offset or limit.',
'Negative offset or limit.'
);
// Limit invalid
@@ -51,7 +51,7 @@ $tests = function ($conn) {
$conn->translate('SELECT 1 %lmt', -10);
},
Dibi\NotSupportedException::class,
'Negative offset or limit.',
'Negative offset or limit.'
);
// Limit invalid, offset valid
@@ -60,7 +60,7 @@ $tests = function ($conn) {
$conn->translate('SELECT 1 %ofs %lmt', 10, -10);
},
Dibi\NotSupportedException::class,
'Negative offset or limit.',
'Negative offset or limit.'
);
// Limit valid, offset invalid
@@ -69,22 +69,22 @@ $tests = function ($conn) {
$conn->translate('SELECT 1 %ofs %lmt', -10, 10);
},
Dibi\NotSupportedException::class,
'Negative offset or limit.',
'Negative offset or limit.'
);
} else {
Assert::same(
'SELECT TOP (1) * FROM (SELECT 1) t',
$conn->translate('SELECT 1 %lmt', 1),
$conn->translate('SELECT 1 %lmt', 1)
);
Assert::same(
'SELECT 1',
$conn->translate('SELECT 1 %lmt', -10),
$conn->translate('SELECT 1 %lmt', -10)
);
Assert::exception(
fn() => $conn->translate('SELECT 1 %ofs %lmt', 10, 10),
Dibi\NotSupportedException::class,
$conn->translate('SELECT 1 %ofs %lmt', 10, 10),
Dibi\NotSupportedException::class
);
}
};

150
tests/dibi/Strict.phpt Normal file
View File

@@ -0,0 +1,150 @@
<?php
declare(strict_types=1);
use Tester\Assert;
require __DIR__ . '/bootstrap.php';
class TestClass
{
use Dibi\Strict;
public $public;
public static $publicStatic;
protected $protected;
public function publicMethod()
{
}
public static function publicMethodStatic()
{
}
protected function protectedMethod()
{
}
protected static function protectedMethodS()
{
}
public function getBar()
{
return 123;
}
public function isFoo()
{
return 456;
}
}
class TestChild extends TestClass
{
public function callParent()
{
parent::callParent();
}
}
// calling
Assert::exception(function () {
$obj = new TestClass;
$obj->undeclared();
}, LogicException::class, 'Call to undefined method TestClass::undeclared().');
Assert::exception(function () {
TestClass::undeclared();
}, LogicException::class, 'Call to undefined static method TestClass::undeclared().');
Assert::exception(function () {
$obj = new TestChild;
$obj->callParent();
}, LogicException::class, 'Call to undefined method parent::callParent().');
Assert::exception(function () {
$obj = new TestClass;
$obj->publicMethodX();
}, LogicException::class, 'Call to undefined method TestClass::publicMethodX(), did you mean publicMethod()?');
Assert::exception(function () { // suggest static method
$obj = new TestClass;
$obj->publicMethodStaticX();
}, LogicException::class, 'Call to undefined method TestClass::publicMethodStaticX(), did you mean publicMethodStatic()?');
Assert::exception(function () { // suggest only public method
$obj = new TestClass;
$obj->protectedMethodX();
}, LogicException::class, 'Call to undefined method TestClass::protectedMethodX().');
// writing
Assert::exception(function () {
$obj = new TestClass;
$obj->undeclared = 'value';
}, LogicException::class, 'Attempt to write to undeclared property TestClass::$undeclared.');
Assert::exception(function () {
$obj = new TestClass;
$obj->publicX = 'value';
}, LogicException::class, 'Attempt to write to undeclared property TestClass::$publicX, did you mean $public?');
Assert::exception(function () { // suggest only non-static property
$obj = new TestClass;
$obj->publicStaticX = 'value';
}, LogicException::class, 'Attempt to write to undeclared property TestClass::$publicStaticX.');
Assert::exception(function () { // suggest only public property
$obj = new TestClass;
$obj->protectedX = 'value';
}, LogicException::class, 'Attempt to write to undeclared property TestClass::$protectedX.');
// property getter
$obj = new TestClass;
Assert::false(isset($obj->bar));
Assert::same(123, $obj->bar);
Assert::false(isset($obj->foo));
Assert::same(456, $obj->foo);
// reading
Assert::exception(function () {
$obj = new TestClass;
$val = $obj->undeclared;
}, LogicException::class, 'Attempt to read undeclared property TestClass::$undeclared.');
Assert::exception(function () {
$obj = new TestClass;
$val = $obj->publicX;
}, LogicException::class, 'Attempt to read undeclared property TestClass::$publicX, did you mean $public?');
Assert::exception(function () { // suggest only non-static property
$obj = new TestClass;
$val = $obj->publicStaticX;
}, LogicException::class, 'Attempt to read undeclared property TestClass::$publicStaticX.');
Assert::exception(function () { // suggest only public property
$obj = new TestClass;
$val = $obj->protectedX;
}, LogicException::class, 'Attempt to read undeclared property TestClass::$protectedX.');
// unset/isset
Assert::exception(function () {
$obj = new TestClass;
unset($obj->undeclared);
}, LogicException::class, 'Attempt to unset undeclared property TestClass::$undeclared.');
Assert::false(isset($obj->undeclared));

View File

@@ -13,16 +13,13 @@ switch ($config['system']) {
case 'mysql':
Assert::equal('10:20:30.0', $translator->formatValue(new DateInterval('PT10H20M30S'), null));
Assert::equal('-1:00:00.0', $translator->formatValue(DateInterval::createFromDateString('-1 hour'), null));
Assert::exception(
fn() => $translator->formatValue(new DateInterval('P2Y4DT6H8M'), null),
Dibi\NotSupportedException::class,
'Only time interval is supported.',
);
Assert::exception(function () use ($translator) {
$translator->formatValue(new DateInterval('P2Y4DT6H8M'), null);
}, Dibi\NotSupportedException::class, 'Only time interval is supported.');
break;
default:
Assert::exception(
fn() => $translator->formatValue(new DateInterval('PT10H20M30S'), null),
Dibi\Exception::class,
);
Assert::exception(function () use ($translator) {
$translator->formatValue(new DateInterval('PT10H20M30S'), null);
}, Dibi\Exception::class);
}

View File

@@ -27,8 +27,8 @@ FROM [customers]
isset($name),
'WHERE [name] LIKE %s',
'xxx',
'%end',
),
'%end'
)
);
@@ -42,8 +42,8 @@ FROM [customers] /* ... */'),
SELECT *
FROM %if',
true,
'[customers] %else [products]',
),
'[customers] %else [products]'
)
);
@@ -62,7 +62,7 @@ FROM [people]
WHERE [id] > 0
%if', false, 'AND [foo]=%i', 1, '
%else %if', true, 'AND [bar]=%i', 1, '
'),
')
);
@@ -97,8 +97,8 @@ WHERE
%if',
false,
'AND [admin]=1 %end
%else 1 LIMIT 10 %end',
),
%else 1 LIMIT 10 %end'
)
);
@@ -113,6 +113,6 @@ Assert::same(
3,
'%ofs',
5,
'%end',
),
'%end'
)
);

View File

@@ -1,41 +0,0 @@
<?php
/**
* @phpVersion 8.1
* @dataProvider ../databases.ini
*/
declare(strict_types=1);
use Tester\Assert;
require __DIR__ . '/bootstrap.php';
$conn = new Dibi\Connection($config);
$translator = new Dibi\Translator($conn);
enum EnumInt: int
{
case One = 1;
}
enum EnumString: string
{
case One = 'one';
}
enum PureEnum
{
case One;
}
Assert::equal('1', $translator->formatValue(EnumInt::One, null));
Assert::equal(match ($config['driver']) {
'sqlsrv' => "N'one'",
default => "'one'",
}, $translator->formatValue(EnumString::One, null));
Assert::equal('**Unexpected PureEnum**', $translator->formatValue(PureEnum::One, null));

Some files were not shown because too many files have changed in this diff Show More