mirror of
https://github.com/dg/dibi.git
synced 2025-08-31 17:51:43 +02:00
Compare commits
102 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
32b6976209 | ||
|
f484630e56 | ||
|
611e051c02 | ||
|
7595a6d5bd | ||
|
494d7c1c21 | ||
|
658dbe388a | ||
|
c7fe0fef21 | ||
|
9151d1eb9c | ||
|
d76f40c2a4 | ||
|
befde664fe | ||
|
e1c4cbaece | ||
|
1df20ced10 | ||
|
ce1ba4668b | ||
|
0f21a6ab3d | ||
|
78f552fe8e | ||
|
97053089e0 | ||
|
2c7b35c29d | ||
|
c04d2197e3 | ||
|
d342d8d78f | ||
|
7d8c39f42a | ||
|
29b58d64dd | ||
|
0a32bb5bdf | ||
|
d707b4ba0e | ||
|
490cf143ba | ||
|
12ffa0ffd1 | ||
|
23f65ef837 | ||
|
86a71dde28 | ||
|
bb1f7d4b93 | ||
|
680026747e | ||
|
7ca47508cb | ||
|
beba7b3592 | ||
|
6cc7ce8e44 | ||
|
92b8e6077e | ||
|
e45638eab4 | ||
|
8564217bc1 | ||
|
82c45c3076 | ||
|
df45bd3553 | ||
|
fe22e230ce | ||
|
8257532630 | ||
|
08dfc37492 | ||
|
7acef0c34b | ||
|
b01d97ac86 | ||
|
8915b0343c | ||
|
d1a3362321 | ||
|
b6ead80202 | ||
|
a640ac2a8f | ||
|
87e702d1fc | ||
|
cb0cf4ba2f | ||
|
8e7df8374b | ||
|
848ac76fed | ||
|
a0f2ca2fca | ||
|
cf14987b42 | ||
|
01c7ab63e3 | ||
|
520119740d | ||
|
7fa05f381b | ||
|
3a962de553 | ||
|
96e370b8fe | ||
|
ec0455ae00 | ||
|
b61737311e | ||
|
9d5d430d3d | ||
|
04bb5ede3d | ||
|
7d82ce2ff6 | ||
|
82150d120d | ||
|
0f045c0986 | ||
|
5646884899 | ||
|
af33a354d6 | ||
|
a0c86747dc | ||
|
d70e274244 | ||
|
e05eb01233 | ||
|
2ac618ffff | ||
|
1881fea0e5 | ||
|
cb82357cfb | ||
|
0a29fcb502 | ||
|
8270b7c1c3 | ||
|
73e16eb1a3 | ||
|
0b394a993d | ||
|
df3edee70b | ||
|
245da39a9f | ||
|
3df64fc3b3 | ||
|
95c3f72a17 | ||
|
d71caf0c75 | ||
|
3066fea2aa | ||
|
b00e556289 | ||
|
877dffd460 | ||
|
7049949b14 | ||
|
771e846a62 | ||
|
4e056c52dd | ||
|
40ad77cf5f | ||
|
d09b462eef | ||
|
4c796a0e0f | ||
|
304af5185d | ||
|
e046935137 | ||
|
1a77048225 | ||
|
ca74488636 | ||
|
07f994a0b5 | ||
|
5fa5acb724 | ||
|
98563d8165 | ||
|
c464960239 | ||
|
a9e90d0b22 | ||
|
decb30de1e | ||
|
f4e71e8855 | ||
|
39f59e0f08 |
4
.gitattributes
vendored
4
.gitattributes
vendored
@@ -1,8 +1,8 @@
|
||||
.gitattributes export-ignore
|
||||
.gitignore export-ignore
|
||||
.github export-ignore
|
||||
.travis.yml export-ignore
|
||||
ecs.php export-ignore
|
||||
appveyor.yml export-ignore
|
||||
ncs.* export-ignore
|
||||
phpstan.neon export-ignore
|
||||
tests/ export-ignore
|
||||
|
||||
|
31
.github/workflows/coding-style.yml
vendored
Normal file
31
.github/workflows/coding-style.yml
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
name: Coding Style
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
nette_cc:
|
||||
name: Nette Code Checker
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: 8.3
|
||||
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@v4
|
||||
- uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: 8.3
|
||||
coverage: none
|
||||
|
||||
- run: composer create-project nette/coding-standard temp/coding-standard ^3 --no-progress
|
||||
- run: php temp/coding-standard/ecs check
|
21
.github/workflows/static-analysis.yml
vendored
Normal file
21
.github/workflows/static-analysis.yml
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
name: Static Analysis (only informative)
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
phpstan:
|
||||
name: PHPStan
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: 8.2
|
||||
coverage: none
|
||||
|
||||
- run: composer install --no-progress --prefer-dist
|
||||
- run: composer phpstan -- --no-progress
|
||||
continue-on-error: true # is only informative
|
120
.github/workflows/tests.yml
vendored
Normal file
120
.github/workflows/tests.yml
vendored
Normal file
@@ -0,0 +1,120 @@
|
||||
name: Tests
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
env:
|
||||
php-extensions: mbstring, intl, mysqli, pgsql, sqlsrv-5.12.0, pdo_sqlsrv-5.12.0
|
||||
php-tools: "composer:v2, pecl"
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
php: ['8.2', '8.3', '8.4', '8.5']
|
||||
|
||||
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
|
||||
|
||||
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-tools18/bin/sqlcmd -S localhost -U SA -P 'YourStrong!Passw0rd' -Q 'SELECT 1' -N -C"
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- 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-tools18/bin/sqlcmd -S localhost -U SA -P 'YourStrong!Passw0rd' -Q 'CREATE DATABASE dibi_test' -N -C
|
||||
|
||||
- run: composer install --no-progress --prefer-dist
|
||||
- run: vendor/bin/tester -p phpdbg tests -s -C --coverage ./coverage.xml --coverage-src ./src
|
||||
- if: failure()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: output-${{ matrix.php }}
|
||||
path: tests/**/output
|
||||
|
||||
|
||||
- name: Save Code Coverage
|
||||
if: ${{ matrix.php == '8.2' }}
|
||||
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
77
.travis.yml
@@ -1,77 +0,0 @@
|
||||
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
|
29
appveyor.yml
29
appveyor.yml
@@ -1,31 +1,34 @@
|
||||
build: off
|
||||
cache:
|
||||
- c:\php7 -> appveyor.yml
|
||||
- c:\php -> appveyor.yml
|
||||
- '%LOCALAPPDATA%\Composer\files -> appveyor.yml'
|
||||
|
||||
clone_folder: c:\projects\dibi
|
||||
|
||||
environment:
|
||||
MYSQL_PWD: Password12!
|
||||
|
||||
services:
|
||||
- mssql2012sp1
|
||||
# - mssql2014
|
||||
- mysql
|
||||
|
||||
init:
|
||||
- SET PATH=c:\php7;%PATH%
|
||||
- SET PATH=c:\php;c:\Program Files\MySQL\MySQL Server 5.7\bin;%PATH%
|
||||
- SET ANSICON=121x90 (121x90)
|
||||
|
||||
install:
|
||||
# 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
|
||||
# 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
|
||||
- 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 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 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 del /Q *.zip
|
||||
|
||||
# Install Microsoft Access Database Engine x64
|
||||
@@ -40,8 +43,12 @@ 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:\php7\php -c tests\php-win.ini
|
||||
- vendor\bin\tester tests -s -p c:\php\php -c tests\php-win.ini
|
||||
|
||||
on_failure:
|
||||
# Print *.actual content
|
||||
|
@@ -11,27 +11,32 @@
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": ">=7.1"
|
||||
"php": "8.2 - 8.5"
|
||||
},
|
||||
"require-dev": {
|
||||
"tracy/tracy": "~2.2",
|
||||
"nette/tester": "~2.0",
|
||||
"nette/di": "^3.0",
|
||||
"phpstan/phpstan": "^0.12"
|
||||
"tracy/tracy": "^2.9",
|
||||
"nette/tester": "^2.5",
|
||||
"nette/di": "^3.1",
|
||||
"phpstan/phpstan-nette": "^2.0@stable",
|
||||
"jetbrains/phpstorm-attributes": "^1.0"
|
||||
},
|
||||
"replace": {
|
||||
"dg/dibi": "*"
|
||||
},
|
||||
"autoload": {
|
||||
"classmap": ["src/"]
|
||||
"classmap": ["src/"],
|
||||
"psr-4": {
|
||||
"Dibi\\": "src/Dibi"
|
||||
}
|
||||
},
|
||||
"minimum-stability": "dev",
|
||||
"scripts": {
|
||||
"phpstan": "phpstan analyse",
|
||||
"tester": "tester tests -s"
|
||||
},
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "4.1-dev"
|
||||
"dev-master": "5.1-dev"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
21
ecs.php
21
ecs.php
@@ -1,21 +0,0 @@
|
||||
<?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'],
|
||||
]);
|
||||
};
|
@@ -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
|
||||
|
||||
|
@@ -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 )
|
||||
|
||||
|
@@ -27,9 +27,9 @@ $dibi = new Dibi\Connection([
|
||||
// using manual hints
|
||||
$res = $dibi->query('SELECT * FROM [customers]');
|
||||
|
||||
$res->setType('customer_id', Type::INTEGER)
|
||||
->setType('added', Type::DATETIME)
|
||||
->setFormat(Type::DATETIME, 'Y-m-d H:i:s');
|
||||
$res->setType('customer_id', Type::Integer)
|
||||
->setType('added', Type::DateTime)
|
||||
->setFormat(Type::DateTime, 'Y-m-d H:i:s');
|
||||
|
||||
|
||||
Tracy\Dumper::dump($res->fetch());
|
||||
|
@@ -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')
|
||||
|
@@ -45,7 +45,7 @@ function substFallBack($expr)
|
||||
|
||||
|
||||
// define callback
|
||||
$dibi->getSubstitutes()->setCallback('substFallBack');
|
||||
$dibi->getSubstitutes()->setCallback(substFallBack(...));
|
||||
|
||||
// define substitutes as constants
|
||||
define('SUBST_ACCOUNT', 'eshop_');
|
||||
@@ -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
|
||||
|
18
readme.md
18
readme.md
@@ -2,7 +2,7 @@
|
||||
=========================================================
|
||||
|
||||
[](https://packagist.org/packages/dibi/dibi)
|
||||
[](https://travis-ci.org/dg/dibi)
|
||||
[](https://github.com/dg/dibi/actions)
|
||||
[](https://ci.appveyor.com/project/dg/dibi/branch/master)
|
||||
[](https://github.com/dg/dibi/releases)
|
||||
[](https://github.com/dg/dibi/blob/master/license.md)
|
||||
@@ -15,12 +15,14 @@ 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 Project
|
||||
---------------
|
||||
Support Me
|
||||
----------
|
||||
|
||||
Do you like Dibi? Are you looking forward to the new features?
|
||||
|
||||
[](https://nette.org/make-donation?to=dibi)
|
||||
[](https://github.com/sponsors/dg)
|
||||
|
||||
Thank you!
|
||||
|
||||
|
||||
Installation
|
||||
@@ -32,7 +34,7 @@ Install Dibi via Composer:
|
||||
composer require dibi/dibi
|
||||
```
|
||||
|
||||
The Dibi 4.1 requires PHP version 7.1 and supports PHP up to 8.0.
|
||||
The Dibi 5.1 requires PHP version 8.2 and supports PHP up to 8.5.
|
||||
|
||||
|
||||
Usage
|
||||
@@ -339,7 +341,7 @@ $database->query('INSERT INTO users', [
|
||||
There are three methods for dealing with transactions:
|
||||
|
||||
```php
|
||||
$database->beginTransaction();
|
||||
$database->begin();
|
||||
|
||||
$database->commit();
|
||||
|
||||
@@ -606,7 +608,7 @@ $database->query("UPDATE [:blog:items] SET [text]='Hello World'");
|
||||
Dibi automatically detects the types of query columns and converts fields them to native PHP types. We can also specify the type manually. You can find the possible types in the `Dibi\Type` class.
|
||||
|
||||
```php
|
||||
$result->setType('id', Dibi\Type::INTEGER); // id will be integer
|
||||
$result->setType('id', Dibi\Type::Integer); // id will be integer
|
||||
$row = $result->fetch();
|
||||
|
||||
is_int($row->id) // true
|
||||
@@ -637,7 +639,7 @@ In the configuration file, we will register the DI extensions and add the `dibi`
|
||||
|
||||
```neon
|
||||
extensions:
|
||||
dibi: Dibi\Bridges\Nette\DibiExtension22
|
||||
dibi: Dibi\Bridges\Nette\DibiExtension3
|
||||
|
||||
dibi:
|
||||
host: localhost
|
||||
|
@@ -19,13 +19,10 @@ use Tracy;
|
||||
*/
|
||||
class DibiExtension22 extends Nette\DI\CompilerExtension
|
||||
{
|
||||
/** @var bool|null */
|
||||
private $debugMode;
|
||||
|
||||
|
||||
public function __construct(bool $debugMode = null)
|
||||
{
|
||||
$this->debugMode = $debugMode;
|
||||
public function __construct(
|
||||
private ?bool $debugMode = null,
|
||||
private ?bool $cliMode = null,
|
||||
) {
|
||||
}
|
||||
|
||||
|
||||
@@ -38,7 +35,11 @@ class DibiExtension22 extends Nette\DI\CompilerExtension
|
||||
$this->debugMode = $container->parameters['debugMode'];
|
||||
}
|
||||
|
||||
$useProfiler = $config['profiler'] ?? (class_exists(Tracy\Debugger::class) && $this->debugMode);
|
||||
if ($this->cliMode === null) {
|
||||
$this->cliMode = $container->parameters['consoleMode'];
|
||||
}
|
||||
|
||||
$useProfiler = $config['profiler'] ?? (class_exists(Tracy\Debugger::class) && $this->debugMode && !$this->cliMode);
|
||||
|
||||
unset($config['profiler']);
|
||||
|
||||
@@ -47,6 +48,7 @@ class DibiExtension22 extends Nette\DI\CompilerExtension
|
||||
foreach ((array) $config['flags'] as $flag) {
|
||||
$flags |= constant($flag);
|
||||
}
|
||||
|
||||
$config['flags'] = $flags;
|
||||
}
|
||||
|
||||
@@ -57,9 +59,10 @@ 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, [
|
||||
|
93
src/Dibi/Bridges/Nette/DibiExtension3.php
Normal file
93
src/Dibi/Bridges/Nette/DibiExtension3.php
Normal file
@@ -0,0 +1,93 @@
|
||||
<?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;
|
||||
use function is_array;
|
||||
|
||||
|
||||
/**
|
||||
* Dibi extension for Nette Framework 3. Creates 'connection' & 'panel' services.
|
||||
*/
|
||||
class DibiExtension3 extends Nette\DI\CompilerExtension
|
||||
{
|
||||
public function __construct(
|
||||
private ?bool $debugMode = null,
|
||||
private ?bool $cliMode = null,
|
||||
) {
|
||||
}
|
||||
|
||||
|
||||
public function getConfigSchema(): Nette\Schema\Schema
|
||||
{
|
||||
return Expect::structure([
|
||||
'autowired' => Expect::bool(true),
|
||||
'flags' => Expect::anyOf(Expect::arrayOf('string'), Expect::type('dynamic')),
|
||||
'profiler' => Expect::bool(),
|
||||
'explain' => Expect::bool(true),
|
||||
'filter' => Expect::bool(true),
|
||||
'driver' => Expect::string()->dynamic(),
|
||||
'name' => Expect::string()->dynamic(),
|
||||
'lazy' => Expect::bool(false)->dynamic(),
|
||||
'onConnect' => Expect::array()->dynamic(),
|
||||
'substitutes' => Expect::arrayOf('string')->dynamic(),
|
||||
'result' => Expect::structure([
|
||||
'normalize' => Expect::bool(true),
|
||||
'formatDateTime' => Expect::string(),
|
||||
'formatTimeInterval' => Expect::string(),
|
||||
'formatJson' => Expect::string(),
|
||||
])->castTo('array'),
|
||||
])->otherItems(Expect::type('mixed'))
|
||||
->castTo('array');
|
||||
}
|
||||
|
||||
|
||||
public function loadConfiguration()
|
||||
{
|
||||
$container = $this->getContainerBuilder();
|
||||
$config = $this->getConfig();
|
||||
$this->debugMode ??= $container->parameters['debugMode'];
|
||||
$this->cliMode ??= $container->parameters['consoleMode'];
|
||||
|
||||
$useProfiler = $config['profiler'] ?? (class_exists(Tracy\Debugger::class) && $this->debugMode && !$this->cliMode);
|
||||
unset($config['profiler']);
|
||||
|
||||
if (is_array($config['flags'])) {
|
||||
$flags = 0;
|
||||
foreach ((array) $config['flags'] as $flag) {
|
||||
$flags |= constant($flag);
|
||||
}
|
||||
$config['flags'] = $flags;
|
||||
}
|
||||
|
||||
$connection = $container->addDefinition($this->prefix('connection'))
|
||||
->setCreator(Dibi\Connection::class, [$config])
|
||||
->setAutowired($config['autowired']);
|
||||
|
||||
if (class_exists(Tracy\Debugger::class)) {
|
||||
$connection->addSetup(
|
||||
[new Nette\DI\Definitions\Statement('Tracy\Debugger::getBlueScreen'), 'addPanel'],
|
||||
[[Dibi\Bridges\Tracy\Panel::class, 'renderException']],
|
||||
);
|
||||
}
|
||||
|
||||
if ($useProfiler) {
|
||||
$panel = $container->addDefinition($this->prefix('panel'))
|
||||
->setCreator(Dibi\Bridges\Tracy\Panel::class, [
|
||||
$config['explain'],
|
||||
$config['filter'] ? Dibi\Event::QUERY : Dibi\Event::ALL,
|
||||
]);
|
||||
$connection->addSetup([$panel, 'register'], [$connection]);
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,8 +1,8 @@
|
||||
# This will create service named 'dibi.connection'.
|
||||
# Requires Nette Framework 2.2 or later
|
||||
# Requires Nette Framework 3 or later
|
||||
|
||||
extensions:
|
||||
dibi: Dibi\Bridges\Nette\DibiExtension22
|
||||
dibi: Dibi\Bridges\Nette\DibiExtension3
|
||||
|
||||
dibi:
|
||||
host: localhost
|
||||
|
@@ -13,6 +13,7 @@ use Dibi;
|
||||
use Dibi\Event;
|
||||
use Dibi\Helpers;
|
||||
use Tracy;
|
||||
use function count, is_string, strlen;
|
||||
|
||||
|
||||
/**
|
||||
@@ -20,33 +21,23 @@ use Tracy;
|
||||
*/
|
||||
class Panel implements Tracy\IBarPanel
|
||||
{
|
||||
use Dibi\Strict;
|
||||
public static int $maxLength = 1000;
|
||||
|
||||
/** @var int maximum SQL length */
|
||||
public static $maxLength = 1000;
|
||||
|
||||
/** @var bool|string explain queries? */
|
||||
public $explain;
|
||||
|
||||
/** @var int */
|
||||
public $filter;
|
||||
|
||||
/** @var array */
|
||||
private $events = [];
|
||||
private array $events = [];
|
||||
|
||||
|
||||
public function __construct($explain = true, int $filter = null)
|
||||
{
|
||||
$this->filter = $filter ?: Event::QUERY;
|
||||
$this->explain = $explain;
|
||||
public function __construct(
|
||||
public bool|string $explain = true,
|
||||
public int $filter = Event::QUERY,
|
||||
) {
|
||||
}
|
||||
|
||||
|
||||
public function register(Dibi\Connection $connection): void
|
||||
{
|
||||
Tracy\Debugger::getBar()->addPanel($this);
|
||||
Tracy\Debugger::getBlueScreen()->addPanel([self::class, 'renderException']);
|
||||
$connection->onEvent[] = [$this, 'logEvent'];
|
||||
Tracy\Debugger::getBlueScreen()->addPanel(self::renderException(...));
|
||||
$connection->onEvent[] = $this->logEvent(...);
|
||||
}
|
||||
|
||||
|
||||
@@ -58,6 +49,7 @@ class Panel implements Tracy\IBarPanel
|
||||
if (($event->type & $this->filter) === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->events[] = $event;
|
||||
}
|
||||
|
||||
@@ -70,7 +62,7 @@ class Panel implements Tracy\IBarPanel
|
||||
if ($e instanceof Dibi\Exception && $e->getSql()) {
|
||||
return [
|
||||
'tab' => 'SQL',
|
||||
'panel' => Helpers::dump($e->getSql(), true),
|
||||
'panel' => Helpers::dump($e->getSql(), return: true),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -88,9 +80,10 @@ 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 . ' queries'
|
||||
. ($totalTime ? ' / ' . number_format($totalTime * 1000, 1, '.', ' ') . ' ms' : '')
|
||||
. $count . "\u{a0}queries"
|
||||
. ($totalTime ? ' / ' . number_format($totalTime * 1000, 1, '.', "\u{202f}") . "\u{202f}ms" : '')
|
||||
. '</span></span>';
|
||||
}
|
||||
|
||||
@@ -125,23 +118,25 @@ class Panel implements Tracy\IBarPanel
|
||||
? $this->explain
|
||||
: ($connection->getConfig('driver') === 'oracle' ? 'EXPLAIN PLAN FOR' : 'EXPLAIN');
|
||||
try {
|
||||
$explain = @Helpers::dump($connection->nativeQuery("$cmd $event->sql"), true);
|
||||
$explain = @Helpers::dump($connection->nativeQuery("$cmd $event->sql"), return: true);
|
||||
} catch (Dibi\Exception $e) {
|
||||
}
|
||||
|
||||
[$connection->onEvent, \dibi::$numOfQueries, \dibi::$totalTime] = $backup;
|
||||
}
|
||||
|
||||
$s .= '<tr><td data-order="' . $event->time . '">' . number_format($event->time * 1000, 3, '.', ' ');
|
||||
$s .= '<tr><td data-order="' . $event->time . '">' . number_format($event->time * 1000, 3, '.', "\u{202f}");
|
||||
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, true);
|
||||
$s .= '</td><td class="tracy-DibiProfiler-sql">' . Helpers::dump(strlen($event->sql) > self::$maxLength ? substr($event->sql, 0, self::$maxLength) . '...' : $event->sql, return: true);
|
||||
if ($explain) {
|
||||
$s .= "<div id='tracy-debug-DibiProfiler-row-$counter' class='tracy-collapsed'>{$explain}</div>";
|
||||
}
|
||||
|
||||
if ($event->source) {
|
||||
$s .= Tracy\Helpers::editorLink($event->source[0], $event->source[1]); //->class('tracy-DibiProfiler-source');
|
||||
}
|
||||
@@ -155,9 +150,9 @@ 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: ' . count($this->events)
|
||||
. ($totalTime === null ? '' : ', time: ' . number_format($totalTime * 1000, 1, '.', ' ') . ' ms') . ', '
|
||||
. htmlspecialchars($this->getConnectionName($singleConnection)) . '</h1>
|
||||
<h1>Queries:' . "\u{a0}" . count($this->events)
|
||||
. ($totalTime === null ? '' : ", time:\u{a0}" . number_format($totalTime * 1000, 1, '.', "\u{202f}") . "\u{202f}ms")
|
||||
. ($singleConnection === null ? '' : ', ' . htmlspecialchars($this->getConnectionName($singleConnection))) . '</h1>
|
||||
<div class="tracy-inner tracy-DibiProfiler">
|
||||
<table class="tracy-sortable">
|
||||
<tr><th>Time ms</th><th>SQL Statement</th><th>Rows</th>' . (!$singleConnection ? '<th>Connection</th>' : '') . '</tr>
|
||||
@@ -170,8 +165,8 @@ class Panel implements Tracy\IBarPanel
|
||||
private function getConnectionName(Dibi\Connection $connection): string
|
||||
{
|
||||
$driver = $connection->getConfig('driver');
|
||||
return (is_object($driver) ? get_class($driver) : $driver)
|
||||
return get_debug_type($driver)
|
||||
. ($connection->getConfig('name') ? '/' . $connection->getConfig('name') : '')
|
||||
. ($connection->getConfig('host') ? ' @ ' . $connection->getConfig('host') : '');
|
||||
. ($connection->getConfig('host') ? "\u{202f}@\u{202f}" . $connection->getConfig('host') : '');
|
||||
}
|
||||
}
|
||||
|
@@ -9,7 +9,10 @@ declare(strict_types=1);
|
||||
|
||||
namespace Dibi;
|
||||
|
||||
use JetBrains\PhpStorm\Language;
|
||||
use Traversable;
|
||||
use function array_key_exists, is_array, sprintf;
|
||||
use const PHP_SAPI;
|
||||
|
||||
|
||||
/**
|
||||
@@ -20,34 +23,39 @@ use Traversable;
|
||||
*/
|
||||
class Connection implements IConnection
|
||||
{
|
||||
use Strict;
|
||||
/** function (Event $event); Occurs after query is executed */
|
||||
public ?array $onEvent = [];
|
||||
private array $config;
|
||||
|
||||
/** @var array of function (Event $event); Occurs after query is executed */
|
||||
public $onEvent = [];
|
||||
/** @var string[] resultset formats */
|
||||
private array $formats;
|
||||
private ?Driver $driver = null;
|
||||
private ?Translator $translator = null;
|
||||
|
||||
/** @var array Current connection configuration */
|
||||
private $config;
|
||||
|
||||
/** @var Driver|null */
|
||||
private $driver;
|
||||
|
||||
/** @var Translator|null */
|
||||
private $translator;
|
||||
|
||||
/** @var HashMap Substitutes for identifiers */
|
||||
private $substitutes;
|
||||
/** @var array<string, callable(object): Expression | null> */
|
||||
private array $translators = [];
|
||||
private bool $sortTranslators = false;
|
||||
private HashMap $substitutes;
|
||||
private int $transactionDepth = 0;
|
||||
|
||||
|
||||
/**
|
||||
* Connection options: (see driver-specific options too)
|
||||
* - lazy (bool) => if true, connection will be established only when required
|
||||
* - result (array) => result set options
|
||||
* - 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
|
||||
* )
|
||||
* - 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
|
||||
* - profiler (array)
|
||||
* - run (bool) => enable profiler?
|
||||
* - file => file to log
|
||||
@@ -56,18 +64,24 @@ 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'] = $config['driver'] ?? 'mysqli';
|
||||
$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::TimeInterval => $this->config['result']['formatTimeInterval'] ?? null,
|
||||
];
|
||||
|
||||
// profiler
|
||||
if (isset($config['profiler']['file']) && (!isset($config['profiler']['run']) || $config['profiler']['run'])) {
|
||||
$filter = $config['profiler']['filter'] ?? Event::QUERY;
|
||||
@@ -75,7 +89,7 @@ class Connection implements IConnection
|
||||
$this->onEvent[] = [new Loggers\FileLogger($config['profiler']['file'], $filter, $errorsOnly), 'logEvent'];
|
||||
}
|
||||
|
||||
$this->substitutes = new HashMap(function (string $expr) { return ":$expr:"; });
|
||||
$this->substitutes = new HashMap(fn(string $expr) => ":$expr:");
|
||||
if (!empty($config['substitutes'])) {
|
||||
foreach ($config['substitutes'] as $key => $value) {
|
||||
$this->substitutes->$key = $value;
|
||||
@@ -132,16 +146,17 @@ 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;
|
||||
}
|
||||
}
|
||||
@@ -171,9 +186,8 @@ 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)
|
||||
final public function getConfig(?string $key = null, $default = null): mixed
|
||||
{
|
||||
return $key === null
|
||||
? $this->config
|
||||
@@ -189,48 +203,51 @@ 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(...$args): Result
|
||||
final public function query(#[Language('GenericSQL')] mixed ...$args): Result
|
||||
{
|
||||
return $this->nativeQuery($this->translateArgs($args));
|
||||
return $this->nativeQuery($this->translate(...$args));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Generates SQL query.
|
||||
* @param mixed ...$args
|
||||
* @throws Exception
|
||||
*/
|
||||
final public function translate(...$args): string
|
||||
final public function translate(#[Language('GenericSQL')] mixed ...$args): string
|
||||
{
|
||||
return $this->translateArgs($args);
|
||||
if (!$this->driver) {
|
||||
$this->connect();
|
||||
}
|
||||
|
||||
return (clone $this->translator)->translate($args);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Generates and prints SQL query.
|
||||
* @param mixed ...$args
|
||||
*/
|
||||
final public function test(...$args): bool
|
||||
final public function test(#[Language('GenericSQL')] mixed ...$args): bool
|
||||
{
|
||||
try {
|
||||
Helpers::dump($this->translateArgs($args));
|
||||
Helpers::dump($this->translate(...$args));
|
||||
return true;
|
||||
|
||||
} catch (Exception $e) {
|
||||
if ($e->getSql()) {
|
||||
Helpers::dump($e->getSql());
|
||||
} else {
|
||||
echo get_class($e) . ': ' . $e->getMessage() . (PHP_SAPI === 'cli' ? "\n" : '<br>');
|
||||
echo $e::class . ': ' . $e->getMessage() . (PHP_SAPI === 'cli' ? "\n" : '<br>');
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -238,24 +255,11 @@ class Connection implements IConnection
|
||||
|
||||
/**
|
||||
* Generates (translates) and returns SQL query as DataSource.
|
||||
* @param mixed ...$args
|
||||
* @throws Exception
|
||||
*/
|
||||
final public function dataSource(...$args): DataSource
|
||||
final public function dataSource(#[Language('GenericSQL')] mixed ...$args): DataSource
|
||||
{
|
||||
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);
|
||||
return new DataSource($this->translate(...$args), $this);
|
||||
}
|
||||
|
||||
|
||||
@@ -263,7 +267,7 @@ class Connection implements IConnection
|
||||
* Executes the SQL query.
|
||||
* @throws Exception
|
||||
*/
|
||||
final public function nativeQuery(string $sql): Result
|
||||
final public function nativeQuery(#[Language('SQL')] string $sql): Result
|
||||
{
|
||||
if (!$this->driver) {
|
||||
$this->connect();
|
||||
@@ -278,6 +282,7 @@ class Connection implements IConnection
|
||||
if ($event) {
|
||||
$this->onEvent($event->done($e));
|
||||
}
|
||||
|
||||
throw $e;
|
||||
}
|
||||
|
||||
@@ -285,6 +290,7 @@ class Connection implements IConnection
|
||||
if ($event) {
|
||||
$this->onEvent($event->done($res));
|
||||
}
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
@@ -298,10 +304,12 @@ 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;
|
||||
}
|
||||
|
||||
@@ -310,15 +318,17 @@ 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;
|
||||
}
|
||||
|
||||
@@ -326,22 +336,27 @@ 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;
|
||||
}
|
||||
}
|
||||
@@ -350,22 +365,27 @@ 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;
|
||||
}
|
||||
}
|
||||
@@ -374,36 +394,66 @@ 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
|
||||
{
|
||||
$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']);
|
||||
return (new Result($resultDriver, $this->config['result']['normalize'] ?? true))
|
||||
->setFormats($this->formats);
|
||||
}
|
||||
|
||||
|
||||
@@ -436,6 +486,7 @@ 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);
|
||||
}
|
||||
@@ -464,9 +515,77 @@ class Connection implements IConnection
|
||||
*/
|
||||
public function substitute(string $value): string
|
||||
{
|
||||
return strpos($value, ':') === false
|
||||
? $value
|
||||
: preg_replace_callback('#:([^:\s]*):#', function (array $m) { return $this->substitutes->{$m[1]}; }, $value);
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -475,10 +594,9 @@ class Connection implements IConnection
|
||||
|
||||
/**
|
||||
* Executes SQL query and fetch result - shortcut for query() & fetch().
|
||||
* @param mixed ...$args
|
||||
* @throws Exception
|
||||
*/
|
||||
public function fetch(...$args): ?Row
|
||||
public function fetch(#[Language('GenericSQL')] mixed ...$args): ?Row
|
||||
{
|
||||
return $this->query($args)->fetch();
|
||||
}
|
||||
@@ -486,11 +604,10 @@ 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(...$args): array
|
||||
public function fetchAll(#[Language('GenericSQL')] mixed ...$args): array
|
||||
{
|
||||
return $this->query($args)->fetchAll();
|
||||
}
|
||||
@@ -498,11 +615,9 @@ 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(...$args)
|
||||
public function fetchSingle(#[Language('GenericSQL')] mixed ...$args): mixed
|
||||
{
|
||||
return $this->query($args)->fetchSingle();
|
||||
}
|
||||
@@ -510,10 +625,9 @@ class Connection implements IConnection
|
||||
|
||||
/**
|
||||
* Executes SQL query and fetch pairs - shortcut for query() & fetchPairs().
|
||||
* @param mixed ...$args
|
||||
* @throws Exception
|
||||
*/
|
||||
public function fetchPairs(...$args): array
|
||||
public function fetchPairs(#[Language('GenericSQL')] mixed ...$args): array
|
||||
{
|
||||
return $this->query($args)->fetchPairs();
|
||||
}
|
||||
@@ -539,7 +653,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);
|
||||
}
|
||||
@@ -553,6 +667,7 @@ class Connection implements IConnection
|
||||
if (!$this->driver) {
|
||||
$this->connect();
|
||||
}
|
||||
|
||||
return new Reflection\Database($this->driver->getReflector(), $this->config['database'] ?? null);
|
||||
}
|
||||
|
||||
@@ -560,7 +675,7 @@ class Connection implements IConnection
|
||||
/**
|
||||
* Prevents unserialization.
|
||||
*/
|
||||
public function __wakeup()
|
||||
public function __unserialize($_)
|
||||
{
|
||||
throw new NotSupportedException('You cannot serialize or unserialize ' . static::class . ' instances.');
|
||||
}
|
||||
@@ -569,7 +684,7 @@ class Connection implements IConnection
|
||||
/**
|
||||
* Prevents serialization.
|
||||
*/
|
||||
public function __sleep()
|
||||
public function __serialize()
|
||||
{
|
||||
throw new NotSupportedException('You cannot serialize or unserialize ' . static::class . ' instances.');
|
||||
}
|
||||
|
@@ -9,43 +9,24 @@ declare(strict_types=1);
|
||||
|
||||
namespace Dibi;
|
||||
|
||||
use function func_get_args, is_array, strpbrk;
|
||||
|
||||
|
||||
/**
|
||||
* Default implementation of IDataSource.
|
||||
*/
|
||||
class DataSource implements IDataSource
|
||||
{
|
||||
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;
|
||||
private readonly Connection $connection;
|
||||
private readonly 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;
|
||||
|
||||
|
||||
/**
|
||||
@@ -65,13 +46,14 @@ class DataSource implements IDataSource
|
||||
* @param string|array $col column name or array of column names
|
||||
* @param string $as column alias
|
||||
*/
|
||||
public function select($col, string $as = null): self
|
||||
public function select(string|array $col, ?string $as = null): static
|
||||
{
|
||||
if (is_array($col)) {
|
||||
$this->cols = $col;
|
||||
} else {
|
||||
$this->cols[$col] = $as;
|
||||
}
|
||||
|
||||
$this->result = null;
|
||||
return $this;
|
||||
}
|
||||
@@ -80,7 +62,7 @@ class DataSource implements IDataSource
|
||||
/**
|
||||
* Adds conditions to query.
|
||||
*/
|
||||
public function where($cond): self
|
||||
public function where($cond): static
|
||||
{
|
||||
$this->conds[] = is_array($cond)
|
||||
? $cond // TODO: not consistent with select and orderBy
|
||||
@@ -94,13 +76,14 @@ class DataSource implements IDataSource
|
||||
* Selects columns to order by.
|
||||
* @param string|array $row column name or array of column names
|
||||
*/
|
||||
public function orderBy($row, string $direction = 'ASC'): self
|
||||
public function orderBy(string|array $row, string $direction = 'ASC'): static
|
||||
{
|
||||
if (is_array($row)) {
|
||||
$this->sorting = $row;
|
||||
} else {
|
||||
$this->sorting[$row] = $direction;
|
||||
}
|
||||
|
||||
$this->result = null;
|
||||
return $this;
|
||||
}
|
||||
@@ -109,7 +92,7 @@ class DataSource implements IDataSource
|
||||
/**
|
||||
* Limits number of rows.
|
||||
*/
|
||||
public function applyLimit(int $limit, int $offset = null): self
|
||||
public function applyLimit(int $limit, ?int $offset = null): static
|
||||
{
|
||||
$this->limit = $limit;
|
||||
$this->offset = $offset;
|
||||
@@ -135,6 +118,7 @@ class DataSource implements IDataSource
|
||||
if ($this->result === null) {
|
||||
$this->result = $this->connection->nativeQuery($this->__toString());
|
||||
}
|
||||
|
||||
return $this->result;
|
||||
}
|
||||
|
||||
@@ -158,7 +142,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()
|
||||
public function fetchSingle(): mixed
|
||||
{
|
||||
return $this->getResult()->fetchSingle();
|
||||
}
|
||||
@@ -185,7 +169,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);
|
||||
}
|
||||
@@ -226,24 +210,19 @@ class DataSource implements IDataSource
|
||||
*/
|
||||
public function __toString(): string
|
||||
{
|
||||
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 '';
|
||||
}
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -258,10 +237,11 @@ 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;
|
||||
}
|
||||
|
||||
@@ -273,9 +253,10 @@ 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;
|
||||
}
|
||||
}
|
||||
|
@@ -15,12 +15,7 @@ namespace Dibi;
|
||||
*/
|
||||
class DateTime extends \DateTimeImmutable
|
||||
{
|
||||
use Strict;
|
||||
|
||||
/**
|
||||
* @param string|int $time
|
||||
*/
|
||||
public function __construct($time = 'now', \DateTimeZone $timezone = null)
|
||||
public function __construct(string|int $time = 'now', ?\DateTimeZone $timezone = null)
|
||||
{
|
||||
$timezone = $timezone ?: new \DateTimeZone(date_default_timezone_get());
|
||||
if (is_numeric($time)) {
|
||||
|
@@ -17,8 +17,6 @@ use Dibi;
|
||||
*/
|
||||
class DummyDriver implements Dibi\Driver, Dibi\ResultDriver, Dibi\Reflector
|
||||
{
|
||||
use Dibi\Strict;
|
||||
|
||||
public function disconnect(): void
|
||||
{
|
||||
}
|
||||
@@ -42,22 +40,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()
|
||||
public function getResource(): mixed
|
||||
{
|
||||
return null;
|
||||
}
|
||||
@@ -171,8 +169,9 @@ class DummyDriver implements Dibi\Driver, Dibi\ResultDriver, Dibi\Reflector
|
||||
}
|
||||
|
||||
|
||||
public function getResultResource()
|
||||
public function getResultResource(): mixed
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
|
@@ -11,6 +11,7 @@ namespace Dibi\Drivers;
|
||||
|
||||
use Dibi;
|
||||
use Dibi\Helpers;
|
||||
use function is_resource;
|
||||
|
||||
|
||||
/**
|
||||
@@ -26,18 +27,17 @@ use Dibi\Helpers;
|
||||
*/
|
||||
class FirebirdDriver implements Dibi\Driver
|
||||
{
|
||||
use Dibi\Strict;
|
||||
public const ErrorExceptionThrown = -836;
|
||||
|
||||
public const ERROR_EXCEPTION_THROWN = -836;
|
||||
/** @deprecated use FirebirdDriver::ErrorExceptionThrown */
|
||||
public const ERROR_EXCEPTION_THROWN = self::ErrorExceptionThrown;
|
||||
|
||||
/** @var resource */
|
||||
private $connection;
|
||||
|
||||
/** @var resource|null */
|
||||
/** @var ?resource */
|
||||
private $transaction;
|
||||
|
||||
/** @var bool */
|
||||
private $inTransaction = false;
|
||||
private bool $inTransaction = false;
|
||||
|
||||
|
||||
/** @throws Dibi\NotSupportedException */
|
||||
@@ -101,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;
|
||||
}
|
||||
|
||||
@@ -131,11 +131,12 @@ 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;
|
||||
}
|
||||
@@ -145,7 +146,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.');
|
||||
@@ -163,7 +164,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.');
|
||||
@@ -190,7 +191,7 @@ class FirebirdDriver implements Dibi\Driver
|
||||
* Returns the connection resource.
|
||||
* @return resource|null
|
||||
*/
|
||||
public function getResource()
|
||||
public function getResource(): mixed
|
||||
{
|
||||
return is_resource($this->connection) ? $this->connection : null;
|
||||
}
|
||||
|
@@ -17,15 +17,9 @@ use Dibi;
|
||||
*/
|
||||
class FirebirdReflector implements Dibi\Reflector
|
||||
{
|
||||
use Dibi\Strict;
|
||||
|
||||
/** @var Dibi\Driver */
|
||||
private $driver;
|
||||
|
||||
|
||||
public function __construct(Dibi\Driver $driver)
|
||||
{
|
||||
$this->driver = $driver;
|
||||
public function __construct(
|
||||
private readonly Dibi\Driver $driver,
|
||||
) {
|
||||
}
|
||||
|
||||
|
||||
@@ -47,6 +41,7 @@ class FirebirdReflector implements Dibi\Reflector
|
||||
'view' => $row[1] === 'TRUE',
|
||||
];
|
||||
}
|
||||
|
||||
return $tables;
|
||||
}
|
||||
|
||||
@@ -99,6 +94,7 @@ class FirebirdReflector implements Dibi\Reflector
|
||||
'autoincrement' => false,
|
||||
];
|
||||
}
|
||||
|
||||
return $columns;
|
||||
}
|
||||
|
||||
@@ -131,6 +127,7 @@ class FirebirdReflector implements Dibi\Reflector
|
||||
$indexes[$key]['table'] = $table;
|
||||
$indexes[$key]['columns'][$row['FIELD_POSITION']] = $row['FIELD_NAME'];
|
||||
}
|
||||
|
||||
return $indexes;
|
||||
}
|
||||
|
||||
@@ -159,6 +156,7 @@ class FirebirdReflector implements Dibi\Reflector
|
||||
'table' => $table,
|
||||
];
|
||||
}
|
||||
|
||||
return $keys;
|
||||
}
|
||||
|
||||
@@ -179,6 +177,7 @@ class FirebirdReflector implements Dibi\Reflector
|
||||
while ($row = $res->fetch(false)) {
|
||||
$indices[] = $row[0];
|
||||
}
|
||||
|
||||
return $indices;
|
||||
}
|
||||
|
||||
@@ -201,6 +200,7 @@ class FirebirdReflector implements Dibi\Reflector
|
||||
while ($row = $res->fetch(false)) {
|
||||
$constraints[] = $row[0];
|
||||
}
|
||||
|
||||
return $constraints;
|
||||
}
|
||||
|
||||
@@ -209,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(
|
||||
"
|
||||
@@ -236,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)) {
|
||||
@@ -248,6 +248,7 @@ class FirebirdReflector implements Dibi\Reflector
|
||||
'enabled' => trim($row['TRIGGER_ENABLED']) === 'TRUE',
|
||||
];
|
||||
}
|
||||
|
||||
return $triggers;
|
||||
}
|
||||
|
||||
@@ -256,7 +257,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
|
||||
@@ -270,6 +271,7 @@ class FirebirdReflector implements Dibi\Reflector
|
||||
while ($row = $res->fetch(false)) {
|
||||
$triggers[] = $row[0];
|
||||
}
|
||||
|
||||
return $triggers;
|
||||
}
|
||||
|
||||
@@ -321,6 +323,7 @@ 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;
|
||||
}
|
||||
|
||||
@@ -338,6 +341,7 @@ class FirebirdReflector implements Dibi\Reflector
|
||||
while ($row = $res->fetch(false)) {
|
||||
$procedures[] = $row[0];
|
||||
}
|
||||
|
||||
return $procedures;
|
||||
}
|
||||
|
||||
@@ -356,6 +360,7 @@ class FirebirdReflector implements Dibi\Reflector
|
||||
while ($row = $res->fetch(false)) {
|
||||
$generators[] = $row[0];
|
||||
}
|
||||
|
||||
return $generators;
|
||||
}
|
||||
|
||||
@@ -374,6 +379,7 @@ class FirebirdReflector implements Dibi\Reflector
|
||||
while ($row = $res->fetch(false)) {
|
||||
$functions[] = $row[0];
|
||||
}
|
||||
|
||||
return $functions;
|
||||
}
|
||||
}
|
||||
|
@@ -11,6 +11,7 @@ namespace Dibi\Drivers;
|
||||
|
||||
use Dibi;
|
||||
use Dibi\Helpers;
|
||||
use function is_resource;
|
||||
|
||||
|
||||
/**
|
||||
@@ -18,32 +19,10 @@ use Dibi\Helpers;
|
||||
*/
|
||||
class FirebirdResult implements Dibi\ResultDriver
|
||||
{
|
||||
use Dibi\Strict;
|
||||
|
||||
/** @var resource */
|
||||
private $resultSet;
|
||||
|
||||
/** @var bool */
|
||||
private $autoFree = true;
|
||||
|
||||
|
||||
/**
|
||||
* @param resource $resultSet
|
||||
*/
|
||||
public function __construct($resultSet)
|
||||
{
|
||||
$this->resultSet = $resultSet;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Automatically frees the resources allocated for this result set.
|
||||
*/
|
||||
public function __destruct()
|
||||
{
|
||||
if ($this->autoFree && $this->getResultResource()) {
|
||||
$this->free();
|
||||
}
|
||||
public function __construct(
|
||||
/** @var resource */
|
||||
private $resultSet,
|
||||
) {
|
||||
}
|
||||
|
||||
|
||||
@@ -103,9 +82,8 @@ class FirebirdResult implements Dibi\ResultDriver
|
||||
* Returns the result set resource.
|
||||
* @return resource|null
|
||||
*/
|
||||
public function getResultResource()
|
||||
public function getResultResource(): mixed
|
||||
{
|
||||
$this->autoFree = false;
|
||||
return is_resource($this->resultSet) ? $this->resultSet : null;
|
||||
}
|
||||
|
||||
@@ -126,6 +104,7 @@ class FirebirdResult implements Dibi\ResultDriver
|
||||
'nativetype' => $row['type'],
|
||||
];
|
||||
}
|
||||
|
||||
return $columns;
|
||||
}
|
||||
|
||||
|
@@ -18,15 +18,9 @@ use Dibi;
|
||||
*/
|
||||
class MySqlReflector implements Dibi\Reflector
|
||||
{
|
||||
use Dibi\Strict;
|
||||
|
||||
/** @var Dibi\Driver */
|
||||
private $driver;
|
||||
|
||||
|
||||
public function __construct(Dibi\Driver $driver)
|
||||
{
|
||||
$this->driver = $driver;
|
||||
public function __construct(
|
||||
private readonly Dibi\Driver $driver,
|
||||
) {
|
||||
}
|
||||
|
||||
|
||||
@@ -43,6 +37,7 @@ class MySqlReflector implements Dibi\Reflector
|
||||
'view' => isset($row[1]) && $row[1] === 'VIEW',
|
||||
];
|
||||
}
|
||||
|
||||
return $tables;
|
||||
}
|
||||
|
||||
@@ -67,6 +62,7 @@ class MySqlReflector implements Dibi\Reflector
|
||||
'vendor' => $row,
|
||||
];
|
||||
}
|
||||
|
||||
return $columns;
|
||||
}
|
||||
|
||||
@@ -84,6 +80,7 @@ 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,6 +120,7 @@ class MySqlReflector implements Dibi\Reflector
|
||||
$foreignKeys[$keyName]['onDelete'] = $row['DELETE_RULE'];
|
||||
$foreignKeys[$keyName]['onUpdate'] = $row['UPDATE_RULE'];
|
||||
}
|
||||
|
||||
return array_values($foreignKeys);
|
||||
}
|
||||
}
|
||||
|
@@ -10,6 +10,8 @@ declare(strict_types=1);
|
||||
namespace Dibi\Drivers;
|
||||
|
||||
use Dibi;
|
||||
use function in_array;
|
||||
use const MYSQLI_REPORT_OFF, MYSQLI_STORE_RESULT, MYSQLI_USE_RESULT, PREG_SET_ORDER;
|
||||
|
||||
|
||||
/**
|
||||
@@ -32,19 +34,21 @@ use Dibi;
|
||||
*/
|
||||
class MySqliDriver implements Dibi\Driver
|
||||
{
|
||||
use Dibi\Strict;
|
||||
public const ErrorAccessDenied = 1045;
|
||||
public const ErrorDuplicateEntry = 1062;
|
||||
public const ErrorDataTruncated = 1265;
|
||||
|
||||
public const ERROR_ACCESS_DENIED = 1045;
|
||||
/** @deprecated use MySqliDriver::ErrorAccessDenied */
|
||||
public const ERROR_ACCESS_DENIED = self::ErrorAccessDenied;
|
||||
|
||||
public const ERROR_DUPLICATE_ENTRY = 1062;
|
||||
/** @deprecated use MySqliDriver::ErrorDuplicateEntry */
|
||||
public const ERROR_DUPLICATE_ENTRY = self::ErrorDuplicateEntry;
|
||||
|
||||
public const ERROR_DATA_TRUNCATED = 1265;
|
||||
/** @deprecated use MySqliDriver::ErrorDataTruncated */
|
||||
public const ERROR_DATA_TRUNCATED = self::ErrorDataTruncated;
|
||||
|
||||
/** @var \mysqli */
|
||||
private $connection;
|
||||
|
||||
/** @var bool Is buffered (seekable and countable)? */
|
||||
private $buffered;
|
||||
private \mysqli $connection;
|
||||
private bool $buffered = false;
|
||||
|
||||
|
||||
/** @throws Dibi\NotSupportedException */
|
||||
@@ -72,7 +76,7 @@ class MySqliDriver implements Dibi\Driver
|
||||
$host = ini_get('mysqli.default_host');
|
||||
if ($host) {
|
||||
$config['host'] = $host;
|
||||
$config['port'] = ini_get('mysqli.default_port');
|
||||
$config['port'] = (int) ini_get('mysqli.default_port');
|
||||
} else {
|
||||
$config['host'] = null;
|
||||
$config['port'] = null;
|
||||
@@ -88,6 +92,7 @@ class MySqliDriver implements Dibi\Driver
|
||||
$this->connection->options($key, $value);
|
||||
}
|
||||
}
|
||||
|
||||
@$this->connection->real_connect( // intentionally @
|
||||
(empty($config['persistent']) ? '' : 'p:') . $config['host'],
|
||||
$config['username'],
|
||||
@@ -95,7 +100,7 @@ class MySqliDriver implements Dibi\Driver
|
||||
$config['database'] ?? '',
|
||||
$config['port'] ?? 0,
|
||||
$config['socket'],
|
||||
$config['flags'] ?? 0
|
||||
$config['flags'] ?? 0,
|
||||
);
|
||||
|
||||
if ($this->connection->connect_errno) {
|
||||
@@ -153,14 +158,12 @@ class MySqliDriver implements Dibi\Driver
|
||||
} elseif ($res instanceof \mysqli_result) {
|
||||
return $this->createResultDriver($res);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param int|string $code
|
||||
*/
|
||||
public static function createException(string $message, $code, string $sql): Dibi\DriverException
|
||||
public static function createException(string $message, int|string $code, string $sql): Dibi\DriverException
|
||||
{
|
||||
if (in_array($code, [1216, 1217, 1451, 1452, 1701], true)) {
|
||||
return new Dibi\ForeignKeyConstraintViolationException($message, $code, $sql);
|
||||
@@ -191,6 +194,7 @@ class MySqliDriver implements Dibi\Driver
|
||||
foreach ($matches as $m) {
|
||||
$res[$m[1]] = (int) $m[2];
|
||||
}
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
@@ -219,7 +223,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');
|
||||
}
|
||||
@@ -229,7 +233,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');
|
||||
}
|
||||
@@ -239,7 +243,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');
|
||||
}
|
||||
@@ -250,7 +254,11 @@ class MySqliDriver implements Dibi\Driver
|
||||
*/
|
||||
public function getResource(): ?\mysqli
|
||||
{
|
||||
return @$this->connection->thread_id ? $this->connection : null;
|
||||
try {
|
||||
return @$this->connection->thread_id ? $this->connection : null;
|
||||
} catch (\Throwable $e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -319,6 +327,7 @@ 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'");
|
||||
}
|
||||
|
||||
|
@@ -10,6 +10,7 @@ declare(strict_types=1);
|
||||
namespace Dibi\Drivers;
|
||||
|
||||
use Dibi;
|
||||
use const MYSQLI_TYPE_LONG, MYSQLI_TYPE_SHORT, MYSQLI_TYPE_TIME, MYSQLI_TYPE_TINY;
|
||||
|
||||
|
||||
/**
|
||||
@@ -17,33 +18,10 @@ use Dibi;
|
||||
*/
|
||||
class MySqliResult implements Dibi\ResultDriver
|
||||
{
|
||||
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)
|
||||
{
|
||||
$this->resultSet = $resultSet;
|
||||
$this->buffered = $buffered;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Automatically frees the resources allocated for this result set.
|
||||
*/
|
||||
public function __destruct()
|
||||
{
|
||||
if ($this->autoFree && $this->getResultResource()) {
|
||||
@$this->free();
|
||||
}
|
||||
public function __construct(
|
||||
private readonly \mysqli_result $resultSet,
|
||||
private readonly bool $buffered,
|
||||
) {
|
||||
}
|
||||
|
||||
|
||||
@@ -55,6 +33,7 @@ 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;
|
||||
}
|
||||
|
||||
@@ -80,6 +59,7 @@ class MySqliResult implements Dibi\ResultDriver
|
||||
if (!$this->buffered) {
|
||||
throw new Dibi\NotSupportedException('Cannot seek an unbuffered result set.');
|
||||
}
|
||||
|
||||
return $this->resultSet->data_seek($row);
|
||||
}
|
||||
|
||||
@@ -107,6 +87,7 @@ class MySqliResult implements Dibi\ResultDriver
|
||||
$types[$value] = substr($key, 12);
|
||||
}
|
||||
}
|
||||
|
||||
$types[MYSQLI_TYPE_TINY] = $types[MYSQLI_TYPE_SHORT] = $types[MYSQLI_TYPE_LONG] = 'INT';
|
||||
}
|
||||
|
||||
@@ -119,10 +100,11 @@ class MySqliResult implements Dibi\ResultDriver
|
||||
'table' => $row['orgtable'],
|
||||
'fullname' => $row['table'] ? $row['table'] . '.' . $row['name'] : $row['name'],
|
||||
'nativetype' => $types[$row['type']] ?? $row['type'],
|
||||
'type' => $row['type'] === MYSQLI_TYPE_TIME ? Dibi\Type::TIME_INTERVAL : null,
|
||||
'type' => $row['type'] === MYSQLI_TYPE_TIME ? Dibi\Type::TimeInterval : null,
|
||||
'vendor' => $row,
|
||||
];
|
||||
}
|
||||
|
||||
return $columns;
|
||||
}
|
||||
|
||||
@@ -132,7 +114,6 @@ class MySqliResult implements Dibi\ResultDriver
|
||||
*/
|
||||
public function getResultResource(): \mysqli_result
|
||||
{
|
||||
$this->autoFree = false;
|
||||
return $this->resultSet;
|
||||
}
|
||||
|
||||
|
@@ -17,15 +17,9 @@ use Dibi;
|
||||
*/
|
||||
class NoDataResult implements Dibi\ResultDriver
|
||||
{
|
||||
use Dibi\Strict;
|
||||
|
||||
/** @var int */
|
||||
private $rows;
|
||||
|
||||
|
||||
public function __construct(int $rows)
|
||||
{
|
||||
$this->rows = $rows;
|
||||
public function __construct(
|
||||
private readonly int $rows,
|
||||
) {
|
||||
}
|
||||
|
||||
|
||||
@@ -61,7 +55,7 @@ class NoDataResult implements Dibi\ResultDriver
|
||||
}
|
||||
|
||||
|
||||
public function getResultResource()
|
||||
public function getResultResource(): mixed
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
@@ -10,6 +10,7 @@ declare(strict_types=1);
|
||||
namespace Dibi\Drivers;
|
||||
|
||||
use Dibi;
|
||||
use function is_resource;
|
||||
|
||||
|
||||
/**
|
||||
@@ -25,16 +26,10 @@ use Dibi;
|
||||
*/
|
||||
class OdbcDriver implements Dibi\Driver
|
||||
{
|
||||
use Dibi\Strict;
|
||||
|
||||
/** @var resource */
|
||||
private $connection;
|
||||
|
||||
/** @var int|null Affected rows */
|
||||
private $affectedRows;
|
||||
|
||||
/** @var bool */
|
||||
private $microseconds = true;
|
||||
private ?int $affectedRows;
|
||||
private bool $microseconds = true;
|
||||
|
||||
|
||||
/** @throws Dibi\NotSupportedException */
|
||||
@@ -96,6 +91,7 @@ class OdbcDriver implements Dibi\Driver
|
||||
? $this->createResultDriver($res)
|
||||
: null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -122,9 +118,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, PHP_VERSION_ID < 80000 ? 0 : false)) {
|
||||
if (!odbc_autocommit($this->connection)) {
|
||||
throw new Dibi\DriverException(odbc_errormsg($this->connection) . ' ' . odbc_error($this->connection));
|
||||
}
|
||||
}
|
||||
@@ -134,12 +130,13 @@ 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, PHP_VERSION_ID < 80000 ? 1 : true);
|
||||
|
||||
odbc_autocommit($this->connection, true);
|
||||
}
|
||||
|
||||
|
||||
@@ -147,12 +144,13 @@ 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, PHP_VERSION_ID < 80000 ? 1 : true);
|
||||
|
||||
odbc_autocommit($this->connection, true);
|
||||
}
|
||||
|
||||
|
||||
@@ -169,7 +167,7 @@ class OdbcDriver implements Dibi\Driver
|
||||
* Returns the connection resource.
|
||||
* @return resource|null
|
||||
*/
|
||||
public function getResource()
|
||||
public function getResource(): mixed
|
||||
{
|
||||
return is_resource($this->connection) ? $this->connection : null;
|
||||
}
|
||||
|
@@ -17,15 +17,9 @@ use Dibi;
|
||||
*/
|
||||
class OdbcReflector implements Dibi\Reflector
|
||||
{
|
||||
use Dibi\Strict;
|
||||
|
||||
/** @var Dibi\Driver */
|
||||
private $driver;
|
||||
|
||||
|
||||
public function __construct(Dibi\Driver $driver)
|
||||
{
|
||||
$this->driver = $driver;
|
||||
public function __construct(
|
||||
private readonly Dibi\Driver $driver,
|
||||
) {
|
||||
}
|
||||
|
||||
|
||||
@@ -44,6 +38,7 @@ class OdbcReflector implements Dibi\Reflector
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
odbc_free_result($res);
|
||||
return $tables;
|
||||
}
|
||||
@@ -68,6 +63,7 @@ class OdbcReflector implements Dibi\Reflector
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
odbc_free_result($res);
|
||||
return $columns;
|
||||
}
|
||||
|
@@ -10,6 +10,7 @@ declare(strict_types=1);
|
||||
namespace Dibi\Drivers;
|
||||
|
||||
use Dibi;
|
||||
use function is_resource;
|
||||
|
||||
|
||||
/**
|
||||
@@ -17,35 +18,13 @@ use Dibi;
|
||||
*/
|
||||
class OdbcResult implements Dibi\ResultDriver
|
||||
{
|
||||
use Dibi\Strict;
|
||||
|
||||
/** @var resource */
|
||||
private $resultSet;
|
||||
|
||||
/** @var bool */
|
||||
private $autoFree = true;
|
||||
|
||||
/** @var int Cursor */
|
||||
private $row = 0;
|
||||
private int $row = 0;
|
||||
|
||||
|
||||
/**
|
||||
* @param resource $resultSet
|
||||
*/
|
||||
public function __construct($resultSet)
|
||||
{
|
||||
$this->resultSet = $resultSet;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Automatically frees the resources allocated for this result set.
|
||||
*/
|
||||
public function __destruct()
|
||||
{
|
||||
if ($this->autoFree && $this->getResultResource()) {
|
||||
$this->free();
|
||||
}
|
||||
public function __construct(
|
||||
/** @var resource */
|
||||
private $resultSet,
|
||||
) {
|
||||
}
|
||||
|
||||
|
||||
@@ -72,11 +51,13 @@ 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;
|
||||
}
|
||||
}
|
||||
@@ -116,6 +97,7 @@ class OdbcResult implements Dibi\ResultDriver
|
||||
'nativetype' => odbc_field_type($this->resultSet, $i),
|
||||
];
|
||||
}
|
||||
|
||||
return $columns;
|
||||
}
|
||||
|
||||
@@ -124,9 +106,8 @@ class OdbcResult implements Dibi\ResultDriver
|
||||
* Returns the result set resource.
|
||||
* @return resource|null
|
||||
*/
|
||||
public function getResultResource()
|
||||
public function getResultResource(): mixed
|
||||
{
|
||||
$this->autoFree = false;
|
||||
return is_resource($this->resultSet) ? $this->resultSet : null;
|
||||
}
|
||||
|
||||
|
@@ -10,6 +10,7 @@ declare(strict_types=1);
|
||||
namespace Dibi\Drivers;
|
||||
|
||||
use Dibi;
|
||||
use function in_array, is_resource;
|
||||
|
||||
|
||||
/**
|
||||
@@ -27,19 +28,11 @@ use Dibi;
|
||||
*/
|
||||
class OracleDriver implements Dibi\Driver
|
||||
{
|
||||
use Dibi\Strict;
|
||||
|
||||
/** @var resource */
|
||||
private $connection;
|
||||
|
||||
/** @var bool */
|
||||
private $autocommit = true;
|
||||
|
||||
/** @var bool use native datetime format */
|
||||
private $nativeDate;
|
||||
|
||||
/** @var int|null Number of affected rows */
|
||||
private $affectedRows;
|
||||
private bool $autocommit = true;
|
||||
private bool $nativeDate;
|
||||
private ?int $affectedRows;
|
||||
|
||||
|
||||
/** @throws Dibi\NotSupportedException */
|
||||
@@ -104,6 +97,7 @@ class OracleDriver implements Dibi\Driver
|
||||
$err = oci_error($this->connection);
|
||||
throw new Dibi\DriverException($err['message'], $err['code'], $sql);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -147,7 +141,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;
|
||||
}
|
||||
@@ -157,12 +151,13 @@ 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;
|
||||
}
|
||||
|
||||
@@ -171,12 +166,13 @@ 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;
|
||||
}
|
||||
|
||||
@@ -185,7 +181,7 @@ class OracleDriver implements Dibi\Driver
|
||||
* Returns the connection resource.
|
||||
* @return resource|null
|
||||
*/
|
||||
public function getResource()
|
||||
public function getResource(): mixed
|
||||
{
|
||||
return is_resource($this->connection) ? $this->connection : null;
|
||||
}
|
||||
|
@@ -17,15 +17,9 @@ use Dibi;
|
||||
*/
|
||||
class OracleReflector implements Dibi\Reflector
|
||||
{
|
||||
use Dibi\Strict;
|
||||
|
||||
/** @var Dibi\Driver */
|
||||
private $driver;
|
||||
|
||||
|
||||
public function __construct(Dibi\Driver $driver)
|
||||
{
|
||||
$this->driver = $driver;
|
||||
public function __construct(
|
||||
private readonly Dibi\Driver $driver,
|
||||
) {
|
||||
}
|
||||
|
||||
|
||||
@@ -44,6 +38,7 @@ class OracleReflector implements Dibi\Reflector
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $tables;
|
||||
}
|
||||
|
||||
@@ -66,6 +61,7 @@ class OracleReflector implements Dibi\Reflector
|
||||
'vendor' => $row,
|
||||
];
|
||||
}
|
||||
|
||||
return $columns;
|
||||
}
|
||||
|
||||
|
@@ -10,6 +10,7 @@ declare(strict_types=1);
|
||||
namespace Dibi\Drivers;
|
||||
|
||||
use Dibi;
|
||||
use function is_resource;
|
||||
|
||||
|
||||
/**
|
||||
@@ -17,32 +18,10 @@ use Dibi;
|
||||
*/
|
||||
class OracleResult implements Dibi\ResultDriver
|
||||
{
|
||||
use Dibi\Strict;
|
||||
|
||||
/** @var resource */
|
||||
private $resultSet;
|
||||
|
||||
/** @var bool */
|
||||
private $autoFree = true;
|
||||
|
||||
|
||||
/**
|
||||
* @param resource $resultSet
|
||||
*/
|
||||
public function __construct($resultSet)
|
||||
{
|
||||
$this->resultSet = $resultSet;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Automatically frees the resources allocated for this result set.
|
||||
*/
|
||||
public function __destruct()
|
||||
{
|
||||
if ($this->autoFree && $this->getResultResource()) {
|
||||
$this->free();
|
||||
}
|
||||
public function __construct(
|
||||
/** @var resource */
|
||||
private $resultSet,
|
||||
) {
|
||||
}
|
||||
|
||||
|
||||
@@ -96,10 +75,11 @@ class OracleResult implements Dibi\ResultDriver
|
||||
'name' => oci_field_name($this->resultSet, $i),
|
||||
'table' => null,
|
||||
'fullname' => oci_field_name($this->resultSet, $i),
|
||||
'type' => $type === 'LONG' ? Dibi\Type::TEXT : null,
|
||||
'type' => $type === 'LONG' ? Dibi\Type::Text : null,
|
||||
'nativetype' => $type === 'NUMBER' && oci_field_scale($this->resultSet, $i) === 0 ? 'INTEGER' : $type,
|
||||
];
|
||||
}
|
||||
|
||||
return $columns;
|
||||
}
|
||||
|
||||
@@ -108,9 +88,8 @@ class OracleResult implements Dibi\ResultDriver
|
||||
* Returns the result set resource.
|
||||
* @return resource|null
|
||||
*/
|
||||
public function getResultResource()
|
||||
public function getResultResource(): mixed
|
||||
{
|
||||
$this->autoFree = false;
|
||||
return is_resource($this->resultSet) ? $this->resultSet : null;
|
||||
}
|
||||
|
||||
|
@@ -12,6 +12,7 @@ namespace Dibi\Drivers;
|
||||
use Dibi;
|
||||
use Dibi\Helpers;
|
||||
use PDO;
|
||||
use function sprintf;
|
||||
|
||||
|
||||
/**
|
||||
@@ -23,23 +24,12 @@ use PDO;
|
||||
* - password (or pass)
|
||||
* - options (array) => driver specific options {@see PDO::__construct}
|
||||
* - resource (PDO) => existing connection
|
||||
* - version
|
||||
*/
|
||||
class PdoDriver implements Dibi\Driver
|
||||
{
|
||||
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 = '';
|
||||
private ?PDO $connection;
|
||||
private ?int $affectedRows;
|
||||
private string $driverName;
|
||||
|
||||
|
||||
/** @throws Dibi\NotSupportedException */
|
||||
@@ -60,7 +50,6 @@ 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']);
|
||||
@@ -69,12 +58,12 @@ 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());
|
||||
}
|
||||
}
|
||||
|
||||
$this->driverName = $this->connection->getAttribute(PDO::ATTR_DRIVER_NAME);
|
||||
$this->serverVersion = (string) ($config['version'] ?? @$this->connection->getAttribute(PDO::ATTR_SERVER_VERSION)); // @ - may be not supported
|
||||
}
|
||||
|
||||
|
||||
@@ -102,23 +91,15 @@ class PdoDriver implements Dibi\Driver
|
||||
$this->affectedRows = null;
|
||||
|
||||
[$sqlState, $code, $message] = $this->connection->errorInfo();
|
||||
$code ??= 0;
|
||||
$message = "SQLSTATE[$sqlState]: $message";
|
||||
switch ($this->driverName) {
|
||||
case 'mysql':
|
||||
throw MySqliDriver::createException($message, $code, $sql);
|
||||
|
||||
case 'oci':
|
||||
throw OracleDriver::createException($message, $code, $sql);
|
||||
|
||||
case 'pgsql':
|
||||
throw PostgreDriver::createException($message, $sqlState, $sql);
|
||||
|
||||
case 'sqlite':
|
||||
throw SqliteDriver::createException($message, $code, $sql);
|
||||
|
||||
default:
|
||||
throw new Dibi\DriverException($message, $code, $sql);
|
||||
}
|
||||
throw match ($this->driverName) {
|
||||
'mysql' => MySqliDriver::createException($message, $code, $sql),
|
||||
'oci' => OracleDriver::createException($message, $code, $sql),
|
||||
'pgsql' => PostgreDriver::createException($message, $sqlState, $sql),
|
||||
'sqlite' => SqliteDriver::createException($message, $code, $sql),
|
||||
default => new Dibi\DriverException($message, $code, $sql),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -144,11 +125,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]);
|
||||
throw new Dibi\DriverException("SQLSTATE[$err[0]]: $err[2]", $err[1] ?? 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,11 +138,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]);
|
||||
throw new Dibi\DriverException("SQLSTATE[$err[0]]: $err[2]", $err[1] ?? 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -170,11 +151,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]);
|
||||
throw new Dibi\DriverException("SQLSTATE[$err[0]]: $err[2]", $err[1] ?? 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -193,27 +174,14 @@ class PdoDriver implements Dibi\Driver
|
||||
*/
|
||||
public function getReflector(): Dibi\Reflector
|
||||
{
|
||||
switch ($this->driverName) {
|
||||
case 'mysql':
|
||||
return new MySqlReflector($this);
|
||||
|
||||
case 'oci':
|
||||
return new OracleReflector($this);
|
||||
|
||||
case 'pgsql':
|
||||
return new PostgreReflector($this, $this->connection->getAttribute(PDO::ATTR_SERVER_VERSION));
|
||||
|
||||
case 'sqlite':
|
||||
return new SqliteReflector($this);
|
||||
|
||||
case 'mssql':
|
||||
case 'dblib':
|
||||
case 'sqlsrv':
|
||||
return new SqlsrvReflector($this);
|
||||
|
||||
default:
|
||||
throw new Dibi\NotSupportedException;
|
||||
}
|
||||
return match ($this->driverName) {
|
||||
'mysql' => new MySqlReflector($this),
|
||||
'oci' => new OracleReflector($this),
|
||||
'pgsql' => new PostgreReflector($this),
|
||||
'sqlite' => new SqliteReflector($this),
|
||||
'mssql', 'dblib', 'sqlsrv' => new SqlsrvReflector($this),
|
||||
default => throw new Dibi\NotSupportedException,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -234,44 +202,34 @@ class PdoDriver implements Dibi\Driver
|
||||
*/
|
||||
public function escapeText(string $value): string
|
||||
{
|
||||
return $this->driverName === 'odbc'
|
||||
? "'" . str_replace("'", "''", $value) . "'"
|
||||
: $this->connection->quote($value, PDO::PARAM_STR);
|
||||
return match ($this->driverName) {
|
||||
'odbc' => "'" . str_replace("'", "''", $value) . "'",
|
||||
'sqlsrv' => "N'" . str_replace("'", "''", $value) . "'",
|
||||
default => $this->connection->quote($value, PDO::PARAM_STR),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
public function escapeBinary(string $value): string
|
||||
{
|
||||
return $this->driverName === 'odbc'
|
||||
? "'" . str_replace("'", "''", $value) . "'"
|
||||
: $this->connection->quote($value, PDO::PARAM_LOB);
|
||||
return match ($this->driverName) {
|
||||
'odbc' => "'" . str_replace("'", "''", $value) . "'",
|
||||
'sqlsrv' => '0x' . bin2hex($value),
|
||||
default => $this->connection->quote($value, PDO::PARAM_LOB),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
public function escapeIdentifier(string $value): string
|
||||
{
|
||||
switch ($this->driverName) {
|
||||
case 'mysql':
|
||||
return '`' . str_replace('`', '``', $value) . '`';
|
||||
|
||||
case 'oci':
|
||||
case 'pgsql':
|
||||
return '"' . str_replace('"', '""', $value) . '"';
|
||||
|
||||
case 'sqlite':
|
||||
return '[' . strtr($value, '[]', ' ') . ']';
|
||||
|
||||
case 'odbc':
|
||||
case 'mssql':
|
||||
return '[' . str_replace(['[', ']'], ['[[', ']]'], $value) . ']';
|
||||
|
||||
case 'dblib':
|
||||
case 'sqlsrv':
|
||||
return '[' . str_replace(']', ']]', $value) . ']';
|
||||
|
||||
default:
|
||||
return $value;
|
||||
}
|
||||
return match ($this->driverName) {
|
||||
'mysql' => '`' . str_replace('`', '``', $value) . '`',
|
||||
'oci', 'pgsql' => '"' . str_replace('"', '""', $value) . '"',
|
||||
'sqlite' => '[' . strtr($value, '[]', ' ') . ']',
|
||||
'odbc', 'mssql' => '[' . str_replace(['[', ']'], ['[[', ']]'], $value) . ']',
|
||||
'dblib', 'sqlsrv' => '[' . str_replace(']', ']]', $value) . ']',
|
||||
default => $value,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -293,16 +251,11 @@ class PdoDriver implements Dibi\Driver
|
||||
|
||||
public function escapeDateTime(\DateTimeInterface $value): string
|
||||
{
|
||||
switch ($this->driverName) {
|
||||
case 'odbc':
|
||||
return $value->format('#m/d/Y H:i:s.u#');
|
||||
case 'mssql':
|
||||
case 'dblib':
|
||||
case 'sqlsrv':
|
||||
return 'CONVERT(DATETIME2(7), ' . $value->format("'Y-m-d H:i:s.u'") . ')';
|
||||
default:
|
||||
return $value->format("'Y-m-d H:i:s.u'");
|
||||
}
|
||||
return match ($this->driverName) {
|
||||
'odbc' => $value->format('#m/d/Y H:i:s.u#'),
|
||||
'mssql', 'dblib', 'sqlsrv' => 'CONVERT(DATETIME2(7), ' . $value->format("'Y-m-d H:i:s.u'") . ')',
|
||||
default => $value->format("'Y-m-d H:i:s.u'"),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -366,15 +319,18 @@ 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':
|
||||
@@ -382,6 +338,7 @@ class PdoDriver implements Dibi\Driver
|
||||
$sql .= ' LIMIT ' . ($limit ?? '-1')
|
||||
. ($offset ? ' OFFSET ' . $offset : '');
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case 'oci':
|
||||
@@ -394,21 +351,20 @@ class PdoDriver implements Dibi\Driver
|
||||
} elseif ($limit !== null) {
|
||||
$sql = 'SELECT * FROM (' . $sql . ') WHERE ROWNUM <= ' . $limit;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case 'mssql':
|
||||
case 'sqlsrv':
|
||||
case 'dblib':
|
||||
if (version_compare($this->serverVersion, '11.0') >= 0) { // 11 == SQL Server 2012
|
||||
// requires ORDER BY, see https://technet.microsoft.com/en-us/library/gg699618(v=sql.110).aspx
|
||||
if ($limit !== null) {
|
||||
$sql = sprintf('%s OFFSET %d ROWS FETCH NEXT %d ROWS ONLY', rtrim($sql), $offset, $limit);
|
||||
} elseif ($offset) {
|
||||
$sql = sprintf('%s OFFSET %d ROWS', rtrim($sql), $offset);
|
||||
}
|
||||
break;
|
||||
// requires ORDER BY, see https://technet.microsoft.com/en-us/library/gg699618(v=sql.110).aspx
|
||||
if ($limit !== null) {
|
||||
$sql = sprintf('%s OFFSET %d ROWS FETCH NEXT %d ROWS ONLY', rtrim($sql), $offset, $limit);
|
||||
} elseif ($offset) {
|
||||
$sql = sprintf('%s OFFSET %d ROWS', rtrim($sql), $offset);
|
||||
}
|
||||
// break omitted
|
||||
|
||||
break;
|
||||
|
||||
case 'odbc':
|
||||
if ($offset) {
|
||||
@@ -419,7 +375,6 @@ class PdoDriver implements Dibi\Driver
|
||||
break;
|
||||
}
|
||||
// break omitted
|
||||
|
||||
default:
|
||||
throw new Dibi\NotSupportedException('PDO or driver does not support applying limit or offset.');
|
||||
}
|
||||
|
@@ -19,19 +19,10 @@ use PDO;
|
||||
*/
|
||||
class PdoResult implements Dibi\ResultDriver
|
||||
{
|
||||
use Dibi\Strict;
|
||||
|
||||
/** @var \PDOStatement|null */
|
||||
private $resultSet;
|
||||
|
||||
/** @var string */
|
||||
private $driverName;
|
||||
|
||||
|
||||
public function __construct(\PDOStatement $resultSet, string $driverName)
|
||||
{
|
||||
$this->resultSet = $resultSet;
|
||||
$this->driverName = $driverName;
|
||||
public function __construct(
|
||||
private ?\PDOStatement $resultSet,
|
||||
private readonly string $driverName,
|
||||
) {
|
||||
}
|
||||
|
||||
|
||||
@@ -85,6 +76,7 @@ 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',
|
||||
@@ -94,11 +86,12 @@ class PdoResult implements Dibi\ResultDriver
|
||||
'name' => $row['name'],
|
||||
'table' => $row['table'],
|
||||
'nativetype' => $row['native_type'],
|
||||
'type' => $row['native_type'] === 'TIME' && $this->driverName === 'mysql' ? Dibi\Type::TIME_INTERVAL : null,
|
||||
'type' => $row['native_type'] === 'TIME' && $this->driverName === 'mysql' ? Dibi\Type::TimeInterval : null,
|
||||
'fullname' => $row['table'] ? $row['table'] . '.' . $row['name'] : $row['name'],
|
||||
'vendor' => $row,
|
||||
];
|
||||
}
|
||||
|
||||
return $columns;
|
||||
}
|
||||
|
||||
|
@@ -11,6 +11,8 @@ namespace Dibi\Drivers;
|
||||
|
||||
use Dibi;
|
||||
use Dibi\Helpers;
|
||||
use PgSql;
|
||||
use function in_array, is_array, is_resource, strlen;
|
||||
|
||||
|
||||
/**
|
||||
@@ -22,18 +24,13 @@ use Dibi\Helpers;
|
||||
* - schema => the schema search path
|
||||
* - charset => character encoding to set (default is utf8)
|
||||
* - persistent (bool) => try to find a persistent link?
|
||||
* - resource (resource) => existing connection resource
|
||||
* - resource (PgSql\Connection) => existing connection resource
|
||||
* - connect_type (int) => see pg_connect()
|
||||
*/
|
||||
class PostgreDriver implements Dibi\Driver
|
||||
{
|
||||
use Dibi\Strict;
|
||||
|
||||
/** @var resource */
|
||||
private $connection;
|
||||
|
||||
/** @var int|null Affected rows */
|
||||
private $affectedRows;
|
||||
private PgSql\Connection $connection;
|
||||
private ?int $affectedRows;
|
||||
|
||||
|
||||
/** @throws Dibi\NotSupportedException */
|
||||
@@ -63,6 +60,7 @@ class PostgreDriver implements Dibi\Driver
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$connectType = $config['connect_type'] ?? PGSQL_CONNECT_FORCE_NEW;
|
||||
|
||||
set_error_handler(function (int $severity, string $message) use (&$error) {
|
||||
@@ -74,7 +72,7 @@ class PostgreDriver implements Dibi\Driver
|
||||
restore_error_handler();
|
||||
}
|
||||
|
||||
if (!is_resource($this->connection)) {
|
||||
if (!$this->connection instanceof PgSql\Connection) {
|
||||
throw new Dibi\DriverException($error ?: 'Connecting error.');
|
||||
}
|
||||
|
||||
@@ -120,24 +118,25 @@ class PostgreDriver implements Dibi\Driver
|
||||
if ($res === false) {
|
||||
throw static::createException(pg_last_error($this->connection), null, $sql);
|
||||
|
||||
} elseif (is_resource($res)) {
|
||||
} elseif ($res instanceof PgSql\Result) {
|
||||
$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' && strpos($message, 'truncate') !== false) {
|
||||
if ($code === '0A000' && str_contains($message, 'truncate')) {
|
||||
return new Dibi\ForeignKeyConstraintViolationException($message, $code, $sql);
|
||||
|
||||
} elseif ($code === '23502') {
|
||||
@@ -186,9 +185,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 $savepoint" : 'START TRANSACTION');
|
||||
$this->query($savepoint ? "SAVEPOINT {$this->escapeIdentifier($savepoint)}" : 'START TRANSACTION');
|
||||
}
|
||||
|
||||
|
||||
@@ -196,9 +195,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 $savepoint" : 'COMMIT');
|
||||
$this->query($savepoint ? "RELEASE SAVEPOINT {$this->escapeIdentifier($savepoint)}" : 'COMMIT');
|
||||
}
|
||||
|
||||
|
||||
@@ -206,9 +205,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 $savepoint" : 'ROLLBACK');
|
||||
$this->query($savepoint ? "ROLLBACK TO SAVEPOINT {$this->escapeIdentifier($savepoint)}" : 'ROLLBACK');
|
||||
}
|
||||
|
||||
|
||||
@@ -223,11 +222,10 @@ class PostgreDriver implements Dibi\Driver
|
||||
|
||||
/**
|
||||
* Returns the connection resource.
|
||||
* @return resource|null
|
||||
*/
|
||||
public function getResource()
|
||||
public function getResource(): PgSql\Connection
|
||||
{
|
||||
return is_resource($this->connection) ? $this->connection : null;
|
||||
return $this->connection;
|
||||
}
|
||||
|
||||
|
||||
@@ -236,15 +234,14 @@ class PostgreDriver implements Dibi\Driver
|
||||
*/
|
||||
public function getReflector(): Dibi\Reflector
|
||||
{
|
||||
return new PostgreReflector($this, pg_parameter_status($this->connection, 'server_version'));
|
||||
return new PostgreReflector($this);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Result set driver factory.
|
||||
* @param resource $resource
|
||||
*/
|
||||
public function createResultDriver($resource): PostgreResult
|
||||
public function createResultDriver(PgSql\Result $resource): PostgreResult
|
||||
{
|
||||
return new PostgreResult($resource);
|
||||
}
|
||||
@@ -258,18 +255,20 @@ class PostgreDriver implements Dibi\Driver
|
||||
*/
|
||||
public function escapeText(string $value): string
|
||||
{
|
||||
if (!is_resource($this->connection)) {
|
||||
if (!$this->getResource()) {
|
||||
throw new Dibi\Exception('Lost connection to server.');
|
||||
}
|
||||
|
||||
return "'" . pg_escape_string($this->connection, $value) . "'";
|
||||
}
|
||||
|
||||
|
||||
public function escapeBinary(string $value): string
|
||||
{
|
||||
if (!is_resource($this->connection)) {
|
||||
if (!$this->getResource()) {
|
||||
throw new Dibi\Exception('Lost connection to server.');
|
||||
}
|
||||
|
||||
return "'" . pg_escape_bytea($this->connection, $value) . "'";
|
||||
}
|
||||
|
||||
@@ -325,9 +324,11 @@ 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;
|
||||
}
|
||||
|
@@ -17,22 +17,9 @@ use Dibi;
|
||||
*/
|
||||
class PostgreReflector implements Dibi\Reflector
|
||||
{
|
||||
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;
|
||||
public function __construct(
|
||||
private readonly Dibi\Driver $driver,
|
||||
) {
|
||||
}
|
||||
|
||||
|
||||
@@ -51,24 +38,22 @@ class PostgreReflector implements Dibi\Reflector
|
||||
FROM
|
||||
information_schema.tables
|
||||
WHERE
|
||||
table_schema = ANY (current_schemas(false))";
|
||||
table_schema = ANY (current_schemas(false))
|
||||
|
||||
if ($this->version >= 9.3) {
|
||||
$query .= '
|
||||
UNION ALL
|
||||
SELECT
|
||||
matviewname, 1
|
||||
FROM
|
||||
pg_matviews
|
||||
WHERE
|
||||
schemaname = ANY (current_schemas(false))';
|
||||
}
|
||||
UNION ALL
|
||||
SELECT
|
||||
matviewname, 1
|
||||
FROM
|
||||
pg_matviews
|
||||
WHERE
|
||||
schemaname = ANY (current_schemas(false))";
|
||||
|
||||
$res = $this->driver->query($query);
|
||||
$tables = [];
|
||||
while ($row = $res->fetch(true)) {
|
||||
$tables[] = $row;
|
||||
}
|
||||
|
||||
return $tables;
|
||||
}
|
||||
|
||||
@@ -79,13 +64,6 @@ class PostgreReflector implements Dibi\Reflector
|
||||
public function getColumns(string $table): array
|
||||
{
|
||||
$_table = $this->driver->escapeText($this->driver->escapeIdentifier($table));
|
||||
$res = $this->driver->query("
|
||||
SELECT indkey
|
||||
FROM pg_class
|
||||
LEFT JOIN pg_index on pg_class.oid = pg_index.indrelid AND pg_index.indisprimary
|
||||
WHERE pg_class.oid = $_table::regclass
|
||||
");
|
||||
$primary = (int) $res->fetch(true)['indkey'];
|
||||
|
||||
$res = $this->driver->query("
|
||||
SELECT *
|
||||
@@ -105,7 +83,8 @@ class PostgreReflector implements Dibi\Reflector
|
||||
a.atttypmod-4 AS character_maximum_length,
|
||||
NOT a.attnotnull AS is_nullable,
|
||||
a.attnum AS ordinal_position,
|
||||
adef.adsrc AS column_default
|
||||
pg_get_expr(adef.adbin, adef.adrelid) AS column_default,
|
||||
CASE WHEN a.attidentity IN ('a', 'd') THEN 'YES' ELSE 'NO' END AS is_identity
|
||||
FROM
|
||||
pg_attribute a
|
||||
JOIN pg_type ON a.atttypid = pg_type.oid
|
||||
@@ -130,10 +109,11 @@ class PostgreReflector implements Dibi\Reflector
|
||||
'size' => $size > 0 ? $size : null,
|
||||
'nullable' => $row['is_nullable'] === 'YES' || $row['is_nullable'] === 't' || $row['is_nullable'] === true,
|
||||
'default' => $row['column_default'],
|
||||
'autoincrement' => (int) $row['ordinal_position'] === $primary && substr($row['column_default'] ?? '', 0, 7) === 'nextval',
|
||||
'autoincrement' => $row['is_identity'] === 'YES' || str_starts_with($row['column_default'] ?? '', 'nextval('),
|
||||
'vendor' => $row,
|
||||
];
|
||||
}
|
||||
|
||||
return $columns;
|
||||
}
|
||||
|
||||
@@ -183,6 +163,7 @@ class PostgreReflector implements Dibi\Reflector
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return array_values($indexes);
|
||||
}
|
||||
|
||||
|
@@ -11,6 +11,7 @@ namespace Dibi\Drivers;
|
||||
|
||||
use Dibi;
|
||||
use Dibi\Helpers;
|
||||
use PgSql;
|
||||
|
||||
|
||||
/**
|
||||
@@ -18,32 +19,9 @@ use Dibi\Helpers;
|
||||
*/
|
||||
class PostgreResult implements Dibi\ResultDriver
|
||||
{
|
||||
use Dibi\Strict;
|
||||
|
||||
/** @var resource */
|
||||
private $resultSet;
|
||||
|
||||
/** @var bool */
|
||||
private $autoFree = true;
|
||||
|
||||
|
||||
/**
|
||||
* @param resource $resultSet
|
||||
*/
|
||||
public function __construct($resultSet)
|
||||
{
|
||||
$this->resultSet = $resultSet;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Automatically frees the resources allocated for this result set.
|
||||
*/
|
||||
public function __destruct()
|
||||
{
|
||||
if ($this->autoFree && $this->getResultResource()) {
|
||||
$this->free();
|
||||
}
|
||||
public function __construct(
|
||||
private readonly PgSql\Result $resultSet,
|
||||
) {
|
||||
}
|
||||
|
||||
|
||||
@@ -102,18 +80,17 @@ class PostgreResult implements Dibi\ResultDriver
|
||||
: $row['name'];
|
||||
$columns[] = $row;
|
||||
}
|
||||
|
||||
return $columns;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the result set resource.
|
||||
* @return resource|null
|
||||
*/
|
||||
public function getResultResource()
|
||||
public function getResultResource(): PgSql\Result
|
||||
{
|
||||
$this->autoFree = false;
|
||||
return is_resource($this->resultSet) ? $this->resultSet : null;
|
||||
return $this->resultSet;
|
||||
}
|
||||
|
||||
|
||||
|
@@ -25,16 +25,9 @@ use SQLite3;
|
||||
*/
|
||||
class SqliteDriver implements Dibi\Driver
|
||||
{
|
||||
use Dibi\Strict;
|
||||
|
||||
/** @var SQLite3 */
|
||||
private $connection;
|
||||
|
||||
/** @var string Date format */
|
||||
private $fmtDate;
|
||||
|
||||
/** @var string Datetime format */
|
||||
private $fmtDateTime;
|
||||
private SQLite3 $connection;
|
||||
private string $fmtDate;
|
||||
private string $fmtDateTime;
|
||||
|
||||
|
||||
/** @throws Dibi\NotSupportedException */
|
||||
@@ -57,16 +50,13 @@ class SqliteDriver implements Dibi\Driver
|
||||
} else {
|
||||
try {
|
||||
$this->connection = new SQLite3($config['database']);
|
||||
} catch (\Exception $e) {
|
||||
} catch (\Throwable $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();
|
||||
if ($version['versionNumber'] >= '3006019') {
|
||||
$this->query('PRAGMA foreign_keys = ON');
|
||||
}
|
||||
$this->query('PRAGMA foreign_keys = ON');
|
||||
}
|
||||
|
||||
|
||||
@@ -92,6 +82,7 @@ class SqliteDriver implements Dibi\Driver
|
||||
} elseif ($res instanceof \SQLite3Result && $res->numColumns()) {
|
||||
return $this->createResultDriver($res);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -101,19 +92,19 @@ class SqliteDriver implements Dibi\Driver
|
||||
if ($code !== 19) {
|
||||
return new Dibi\DriverException($message, $code, $sql);
|
||||
|
||||
} elseif (strpos($message, 'must be unique') !== false
|
||||
|| strpos($message, 'is not unique') !== false
|
||||
|| strpos($message, 'UNIQUE constraint failed') !== false
|
||||
} elseif (str_contains($message, 'must be unique')
|
||||
|| str_contains($message, 'is not unique')
|
||||
|| str_contains($message, 'UNIQUE constraint failed')
|
||||
) {
|
||||
return new Dibi\UniqueConstraintViolationException($message, $code, $sql);
|
||||
|
||||
} elseif (strpos($message, 'may not be null') !== false
|
||||
|| strpos($message, 'NOT NULL constraint failed') !== false
|
||||
} elseif (str_contains($message, 'may not be null')
|
||||
|| str_contains($message, 'NOT NULL constraint failed')
|
||||
) {
|
||||
return new Dibi\NotNullConstraintViolationException($message, $code, $sql);
|
||||
|
||||
} elseif (strpos($message, 'foreign key constraint failed') !== false
|
||||
|| strpos($message, 'FOREIGN KEY constraint failed') !== false
|
||||
} elseif (str_contains($message, 'foreign key constraint failed')
|
||||
|| str_contains($message, 'FOREIGN KEY constraint failed')
|
||||
) {
|
||||
return new Dibi\ForeignKeyConstraintViolationException($message, $code, $sql);
|
||||
|
||||
@@ -145,7 +136,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');
|
||||
}
|
||||
@@ -155,7 +146,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');
|
||||
}
|
||||
@@ -165,7 +156,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');
|
||||
}
|
||||
@@ -290,8 +281,9 @@ 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);
|
||||
}
|
||||
}
|
||||
|
@@ -17,15 +17,9 @@ use Dibi;
|
||||
*/
|
||||
class SqliteReflector implements Dibi\Reflector
|
||||
{
|
||||
use Dibi\Strict;
|
||||
|
||||
/** @var Dibi\Driver */
|
||||
private $driver;
|
||||
|
||||
|
||||
public function __construct(Dibi\Driver $driver)
|
||||
{
|
||||
$this->driver = $driver;
|
||||
public function __construct(
|
||||
private readonly Dibi\Driver $driver,
|
||||
) {
|
||||
}
|
||||
|
||||
|
||||
@@ -44,6 +38,7 @@ class SqliteReflector implements Dibi\Reflector
|
||||
while ($row = $res->fetch(true)) {
|
||||
$tables[] = $row;
|
||||
}
|
||||
|
||||
return $tables;
|
||||
}
|
||||
|
||||
@@ -70,6 +65,7 @@ class SqliteReflector implements Dibi\Reflector
|
||||
'vendor' => $row,
|
||||
];
|
||||
}
|
||||
|
||||
return $columns;
|
||||
}
|
||||
|
||||
@@ -103,8 +99,10 @@ 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']) {
|
||||
@@ -142,6 +140,7 @@ class SqliteReflector implements Dibi\Reflector
|
||||
$keys[$row['id']]['foreign'] = null;
|
||||
}
|
||||
}
|
||||
|
||||
return array_values($keys);
|
||||
}
|
||||
}
|
||||
|
@@ -11,6 +11,7 @@ namespace Dibi\Drivers;
|
||||
|
||||
use Dibi;
|
||||
use Dibi\Helpers;
|
||||
use const SQLITE3_ASSOC, SQLITE3_BLOB, SQLITE3_FLOAT, SQLITE3_INTEGER, SQLITE3_NULL, SQLITE3_NUM, SQLITE3_TEXT;
|
||||
|
||||
|
||||
/**
|
||||
@@ -18,29 +19,9 @@ use Dibi\Helpers;
|
||||
*/
|
||||
class SqliteResult implements Dibi\ResultDriver
|
||||
{
|
||||
use Dibi\Strict;
|
||||
|
||||
/** @var \SQLite3Result */
|
||||
private $resultSet;
|
||||
|
||||
/** @var bool */
|
||||
private $autoFree = true;
|
||||
|
||||
|
||||
public function __construct(\SQLite3Result $resultSet)
|
||||
{
|
||||
$this->resultSet = $resultSet;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Automatically frees the resources allocated for this result set.
|
||||
*/
|
||||
public function __destruct()
|
||||
{
|
||||
if ($this->autoFree && $this->getResultResource()) {
|
||||
@$this->free();
|
||||
}
|
||||
public function __construct(
|
||||
private readonly \SQLite3Result $resultSet,
|
||||
) {
|
||||
}
|
||||
|
||||
|
||||
@@ -90,7 +71,7 @@ class SqliteResult implements Dibi\ResultDriver
|
||||
{
|
||||
$count = $this->resultSet->numColumns();
|
||||
$columns = [];
|
||||
static $types = [SQLITE3_INTEGER => 'int', SQLITE3_FLOAT => 'float', SQLITE3_TEXT => 'text', SQLITE3_BLOB => 'blob', SQLITE3_NULL => 'null'];
|
||||
$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),
|
||||
@@ -99,6 +80,7 @@ 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;
|
||||
}
|
||||
|
||||
@@ -108,7 +90,6 @@ class SqliteResult implements Dibi\ResultDriver
|
||||
*/
|
||||
public function getResultResource(): \SQLite3Result
|
||||
{
|
||||
$this->autoFree = false;
|
||||
return $this->resultSet;
|
||||
}
|
||||
|
||||
|
@@ -11,6 +11,7 @@ namespace Dibi\Drivers;
|
||||
|
||||
use Dibi;
|
||||
use Dibi\Helpers;
|
||||
use function is_resource, sprintf;
|
||||
|
||||
|
||||
/**
|
||||
@@ -27,16 +28,9 @@ use Dibi\Helpers;
|
||||
*/
|
||||
class SqlsrvDriver implements Dibi\Driver
|
||||
{
|
||||
use Dibi\Strict;
|
||||
|
||||
/** @var resource */
|
||||
private $connection;
|
||||
|
||||
/** @var int|null Affected rows */
|
||||
private $affectedRows;
|
||||
|
||||
/** @var string */
|
||||
private $version = '';
|
||||
private ?int $affectedRows;
|
||||
|
||||
|
||||
/** @throws Dibi\NotSupportedException */
|
||||
@@ -53,24 +47,27 @@ 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'] = $options['CharacterSet'] ?? 'UTF-8';
|
||||
$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']);
|
||||
}
|
||||
|
||||
if (!is_resource($this->connection)) {
|
||||
$info = sqlsrv_errors();
|
||||
throw new Dibi\DriverException($info[0]['message'], $info[0]['code']);
|
||||
sqlsrv_configure('WarningsReturnAsErrors', 1);
|
||||
}
|
||||
$this->version = sqlsrv_server_info($this->connection)['SQLServerVersion'];
|
||||
}
|
||||
|
||||
|
||||
@@ -102,6 +99,7 @@ class SqlsrvDriver implements Dibi\Driver
|
||||
? $this->createResultDriver($res)
|
||||
: null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -125,6 +123,7 @@ class SqlsrvDriver implements Dibi\Driver
|
||||
$row = sqlsrv_fetch_array($res, SQLSRV_FETCH_NUMERIC);
|
||||
return Dibi\Helpers::intVal($row[0]);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -133,7 +132,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);
|
||||
}
|
||||
@@ -143,7 +142,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);
|
||||
}
|
||||
@@ -153,7 +152,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);
|
||||
}
|
||||
@@ -163,7 +162,7 @@ class SqlsrvDriver implements Dibi\Driver
|
||||
* Returns the connection resource.
|
||||
* @return resource|null
|
||||
*/
|
||||
public function getResource()
|
||||
public function getResource(): mixed
|
||||
{
|
||||
return is_resource($this->connection) ? $this->connection : null;
|
||||
}
|
||||
@@ -255,14 +254,6 @@ class SqlsrvDriver implements Dibi\Driver
|
||||
if ($limit < 0 || $offset < 0) {
|
||||
throw new Dibi\NotSupportedException('Negative offset or limit.');
|
||||
|
||||
} elseif (version_compare($this->version, '11', '<')) { // 11 == SQL Server 2012
|
||||
if ($offset) {
|
||||
throw new Dibi\NotSupportedException('Offset is not supported by this database.');
|
||||
|
||||
} 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);
|
||||
|
@@ -10,6 +10,7 @@ declare(strict_types=1);
|
||||
namespace Dibi\Drivers;
|
||||
|
||||
use Dibi;
|
||||
use function sprintf;
|
||||
|
||||
|
||||
/**
|
||||
@@ -17,15 +18,9 @@ use Dibi;
|
||||
*/
|
||||
class SqlsrvReflector implements Dibi\Reflector
|
||||
{
|
||||
use Dibi\Strict;
|
||||
|
||||
/** @var Dibi\Driver */
|
||||
private $driver;
|
||||
|
||||
|
||||
public function __construct(Dibi\Driver $driver)
|
||||
{
|
||||
$this->driver = $driver;
|
||||
public function __construct(
|
||||
private readonly Dibi\Driver $driver,
|
||||
) {
|
||||
}
|
||||
|
||||
|
||||
@@ -42,6 +37,7 @@ class SqlsrvReflector implements Dibi\Reflector
|
||||
'view' => isset($row[1]) && $row[1] === 'VIEW',
|
||||
];
|
||||
}
|
||||
|
||||
return $tables;
|
||||
}
|
||||
|
||||
@@ -91,6 +87,7 @@ class SqlsrvReflector implements Dibi\Reflector
|
||||
'vendor' => $row,
|
||||
];
|
||||
}
|
||||
|
||||
return $columns;
|
||||
}
|
||||
|
||||
@@ -114,6 +111,7 @@ 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);
|
||||
}
|
||||
|
||||
|
@@ -10,6 +10,7 @@ declare(strict_types=1);
|
||||
namespace Dibi\Drivers;
|
||||
|
||||
use Dibi;
|
||||
use function is_resource;
|
||||
|
||||
|
||||
/**
|
||||
@@ -17,32 +18,10 @@ use Dibi;
|
||||
*/
|
||||
class SqlsrvResult implements Dibi\ResultDriver
|
||||
{
|
||||
use Dibi\Strict;
|
||||
|
||||
/** @var resource */
|
||||
private $resultSet;
|
||||
|
||||
/** @var bool */
|
||||
private $autoFree = true;
|
||||
|
||||
|
||||
/**
|
||||
* @param resource $resultSet
|
||||
*/
|
||||
public function __construct($resultSet)
|
||||
{
|
||||
$this->resultSet = $resultSet;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Automatically frees the resources allocated for this result set.
|
||||
*/
|
||||
public function __destruct()
|
||||
{
|
||||
if ($this->autoFree && $this->getResultResource()) {
|
||||
$this->free();
|
||||
}
|
||||
public function __construct(
|
||||
/** @var resource */
|
||||
private $resultSet,
|
||||
) {
|
||||
}
|
||||
|
||||
|
||||
@@ -96,6 +75,7 @@ class SqlsrvResult implements Dibi\ResultDriver
|
||||
'nativetype' => $fieldMetadata['Type'],
|
||||
];
|
||||
}
|
||||
|
||||
return $columns;
|
||||
}
|
||||
|
||||
@@ -104,9 +84,8 @@ class SqlsrvResult implements Dibi\ResultDriver
|
||||
* Returns the result set resource.
|
||||
* @return resource|null
|
||||
*/
|
||||
public function getResultResource()
|
||||
public function getResultResource(): mixed
|
||||
{
|
||||
$this->autoFree = false;
|
||||
return is_resource($this->resultSet) ? $this->resultSet : null;
|
||||
}
|
||||
|
||||
|
@@ -9,14 +9,15 @@ declare(strict_types=1);
|
||||
|
||||
namespace Dibi;
|
||||
|
||||
use function count, dirname, microtime, preg_match, str_starts_with, strtoupper, trim;
|
||||
use const DIRECTORY_SEPARATOR;
|
||||
|
||||
|
||||
/**
|
||||
* Profiler & logger event.
|
||||
*/
|
||||
class Event
|
||||
{
|
||||
use Strict;
|
||||
|
||||
/** event type */
|
||||
public const
|
||||
CONNECT = 1,
|
||||
@@ -31,29 +32,16 @@ class Event
|
||||
TRANSACTION = 448, // BEGIN | COMMIT | ROLLBACK
|
||||
ALL = 1023;
|
||||
|
||||
/** @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 readonly Connection $connection;
|
||||
public int $type;
|
||||
public readonly string $sql;
|
||||
public readonly Result|DriverException|null $result;
|
||||
public float $time;
|
||||
public ?int $count = null;
|
||||
public ?array $source = null;
|
||||
|
||||
|
||||
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;
|
||||
@@ -61,7 +49,7 @@ class Event
|
||||
$this->time = -microtime(true);
|
||||
|
||||
if ($type === self::QUERY && preg_match('#\(?\s*(SELECT|UPDATE|INSERT|DELETE)#iA', $this->sql, $matches)) {
|
||||
static $types = [
|
||||
$types = [
|
||||
'SELECT' => self::SELECT, 'UPDATE' => self::UPDATE,
|
||||
'INSERT' => self::INSERT, 'DELETE' => self::DELETE,
|
||||
];
|
||||
@@ -70,7 +58,11 @@ class Event
|
||||
|
||||
$dibiDir = dirname((new \ReflectionClass('dibi'))->getFileName()) . DIRECTORY_SEPARATOR;
|
||||
foreach (debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS) as $row) {
|
||||
if (isset($row['file']) && is_file($row['file']) && strpos($row['file'], $dibiDir) !== 0) {
|
||||
if (
|
||||
isset($row['file'])
|
||||
&& preg_match('~\.(php.?|phtml)$~', $row['file'])
|
||||
&& !str_starts_with($row['file'], $dibiDir)
|
||||
) {
|
||||
$this->source = [$row['file'], (int) $row['line']];
|
||||
break;
|
||||
}
|
||||
@@ -82,10 +74,7 @@ class Event
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param Result|DriverException|null $result
|
||||
*/
|
||||
public function done($result = null): self
|
||||
public function done(Result|DriverException|null $result = null): static
|
||||
{
|
||||
$this->result = $result;
|
||||
try {
|
||||
|
@@ -15,10 +15,7 @@ namespace Dibi;
|
||||
*/
|
||||
class Expression
|
||||
{
|
||||
use Strict;
|
||||
|
||||
/** @var array */
|
||||
private $values;
|
||||
private readonly array $values;
|
||||
|
||||
|
||||
public function __construct(...$values)
|
||||
|
@@ -9,6 +9,8 @@ declare(strict_types=1);
|
||||
|
||||
namespace Dibi;
|
||||
|
||||
use function array_key_exists, count, func_get_args, is_array, is_string;
|
||||
|
||||
|
||||
/**
|
||||
* SQL builder via fluent interfaces.
|
||||
@@ -27,6 +29,8 @@ 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)
|
||||
@@ -43,12 +47,15 @@ namespace Dibi;
|
||||
*/
|
||||
class Fluent implements IDataSource
|
||||
{
|
||||
use Strict;
|
||||
public const
|
||||
AffectedRows = 'a',
|
||||
Identifier = 'n',
|
||||
Remove = false;
|
||||
|
||||
public const REMOVE = false;
|
||||
/** @deprecated use Fluent::Remove */
|
||||
public const REMOVE = self::Remove;
|
||||
|
||||
/** @var array */
|
||||
public static $masks = [
|
||||
public static array $masks = [
|
||||
'SELECT' => ['SELECT', 'DISTINCT', 'FROM', 'WHERE', 'GROUP BY',
|
||||
'HAVING', 'ORDER BY', 'LIMIT', 'OFFSET', ],
|
||||
'UPDATE' => ['UPDATE', 'SET', 'WHERE', 'ORDER BY', 'LIMIT'],
|
||||
@@ -56,8 +63,8 @@ class Fluent implements IDataSource
|
||||
'DELETE' => ['DELETE', 'FROM', 'USING', 'WHERE', 'ORDER BY', 'LIMIT'],
|
||||
];
|
||||
|
||||
/** @var array default modifiers for arrays */
|
||||
public static $modifiers = [
|
||||
/** default modifiers for arrays */
|
||||
public static array $modifiers = [
|
||||
'SELECT' => '%n',
|
||||
'FROM' => '%n',
|
||||
'IN' => '%in',
|
||||
@@ -69,8 +76,8 @@ class Fluent implements IDataSource
|
||||
'GROUP BY' => '%by',
|
||||
];
|
||||
|
||||
/** @var array clauses separators */
|
||||
public static $separators = [
|
||||
/** clauses separators */
|
||||
public static array $separators = [
|
||||
'SELECT' => ',',
|
||||
'FROM' => ',',
|
||||
'WHERE' => 'AND',
|
||||
@@ -84,42 +91,31 @@ class Fluent implements IDataSource
|
||||
'INTO' => false,
|
||||
];
|
||||
|
||||
/** @var array clauses */
|
||||
public static $clauseSwitches = [
|
||||
/** clauses */
|
||||
public static array $clauseSwitches = [
|
||||
'JOIN' => 'FROM',
|
||||
'INNER JOIN' => 'FROM',
|
||||
'LEFT JOIN' => 'FROM',
|
||||
'RIGHT JOIN' => 'FROM',
|
||||
];
|
||||
|
||||
/** @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 readonly Connection $connection;
|
||||
private array $setups = [];
|
||||
private ?string $command = null;
|
||||
private array $clauses = [];
|
||||
private array $flags = [];
|
||||
private $cursor;
|
||||
|
||||
/** @var HashMap normalized clauses */
|
||||
private static $normalizer;
|
||||
/** normalized clauses */
|
||||
private static HashMap $normalizer;
|
||||
|
||||
|
||||
public function __construct(Connection $connection)
|
||||
{
|
||||
$this->connection = $connection;
|
||||
|
||||
if (self::$normalizer === null) {
|
||||
self::$normalizer = new HashMap([self::class, '_formatClause']);
|
||||
if (!isset(self::$normalizer)) {
|
||||
self::$normalizer = new HashMap(self::_formatClause(...));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,7 +123,7 @@ class Fluent implements IDataSource
|
||||
/**
|
||||
* Appends new argument to the clause.
|
||||
*/
|
||||
public function __call(string $clause, array $args): self
|
||||
public function __call(string $clause, array $args): static
|
||||
{
|
||||
$clause = self::$normalizer->$clause;
|
||||
|
||||
@@ -136,6 +132,7 @@ 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;
|
||||
@@ -151,7 +148,7 @@ class Fluent implements IDataSource
|
||||
$this->cursor = &$this->clauses[$clause];
|
||||
|
||||
// TODO: really delete?
|
||||
if ($args === [self::REMOVE]) {
|
||||
if ($args === [self::Remove]) {
|
||||
$this->cursor = null;
|
||||
return $this;
|
||||
}
|
||||
@@ -165,10 +162,9 @@ class Fluent implements IDataSource
|
||||
$this->cursor[] = $sep;
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
// append to currect flow
|
||||
if ($args === [self::REMOVE]) {
|
||||
if ($args === [self::Remove]) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
@@ -203,6 +199,7 @@ class Fluent implements IDataSource
|
||||
if ($arg instanceof self) {
|
||||
$arg = new Literal("($arg)");
|
||||
}
|
||||
|
||||
$this->cursor[] = $arg;
|
||||
}
|
||||
|
||||
@@ -213,7 +210,7 @@ class Fluent implements IDataSource
|
||||
/**
|
||||
* Switch to a clause.
|
||||
*/
|
||||
public function clause(string $clause): self
|
||||
public function clause(string $clause): static
|
||||
{
|
||||
$this->cursor = &$this->clauses[self::$normalizer->$clause];
|
||||
if ($this->cursor === null) {
|
||||
@@ -227,7 +224,7 @@ class Fluent implements IDataSource
|
||||
/**
|
||||
* Removes a clause.
|
||||
*/
|
||||
public function removeClause(string $clause): self
|
||||
public function removeClause(string $clause): static
|
||||
{
|
||||
$this->clauses[self::$normalizer->$clause] = null;
|
||||
return $this;
|
||||
@@ -237,7 +234,7 @@ class Fluent implements IDataSource
|
||||
/**
|
||||
* Change a SQL flag.
|
||||
*/
|
||||
public function setFlag(string $flag, bool $value = true): self
|
||||
public function setFlag(string $flag, bool $value = true): static
|
||||
{
|
||||
$flag = strtoupper($flag);
|
||||
if ($value) {
|
||||
@@ -245,6 +242,7 @@ class Fluent implements IDataSource
|
||||
} else {
|
||||
unset($this->flags[$flag]);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
@@ -276,7 +274,7 @@ class Fluent implements IDataSource
|
||||
/**
|
||||
* Adds Result setup.
|
||||
*/
|
||||
public function setupResult(string $method): self
|
||||
public function setupResult(string $method): static
|
||||
{
|
||||
$this->setups[] = func_get_args();
|
||||
return $this;
|
||||
@@ -288,28 +286,25 @@ class Fluent implements IDataSource
|
||||
|
||||
/**
|
||||
* Generates and executes SQL query.
|
||||
* @return Result|int|null result set or number of affected rows
|
||||
* Returns result set or number of affected rows
|
||||
* @return ($return is self::Identifier|self::AffectedRows ? int : Result)
|
||||
* @throws Exception
|
||||
*/
|
||||
public function execute(string $return = null)
|
||||
public function execute(?string $return = null): Result|int|null
|
||||
{
|
||||
$res = $this->query($this->_export());
|
||||
switch ($return) {
|
||||
case \dibi::IDENTIFIER:
|
||||
return $this->connection->getInsertId();
|
||||
case \dibi::AFFECTED_ROWS:
|
||||
return $this->connection->getAffectedRows();
|
||||
default:
|
||||
return $res;
|
||||
}
|
||||
return match ($return) {
|
||||
self::Identifier => $this->connection->getInsertId(),
|
||||
self::AffectedRows => $this->connection->getAffectedRows(),
|
||||
default => $res,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Generates, executes SQL query and fetches the single row.
|
||||
* @return Row|array|null
|
||||
*/
|
||||
public function fetch()
|
||||
public function fetch(): Row|array|null
|
||||
{
|
||||
return $this->command === 'SELECT' && !$this->clauses['LIMIT']
|
||||
? $this->query($this->_export(null, ['%lmt', 1]))->fetch()
|
||||
@@ -319,9 +314,9 @@ class Fluent implements IDataSource
|
||||
|
||||
/**
|
||||
* Like fetch(), but returns only first field.
|
||||
* @return mixed value on success, null if no next record
|
||||
* Returns value on success, null if no next record
|
||||
*/
|
||||
public function fetchSingle()
|
||||
public function fetchSingle(): mixed
|
||||
{
|
||||
return $this->command === 'SELECT' && !$this->clauses['LIMIT']
|
||||
? $this->query($this->_export(null, ['%lmt', 1]))->fetchSingle()
|
||||
@@ -332,7 +327,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();
|
||||
}
|
||||
@@ -351,7 +346,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);
|
||||
}
|
||||
@@ -360,7 +355,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();
|
||||
}
|
||||
@@ -369,7 +364,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));
|
||||
}
|
||||
@@ -390,6 +385,7 @@ class Fluent implements IDataSource
|
||||
$method = array_shift($setup);
|
||||
$res->$method(...$setup);
|
||||
}
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
@@ -408,19 +404,14 @@ class Fluent implements IDataSource
|
||||
*/
|
||||
final public function __toString(): string
|
||||
{
|
||||
try {
|
||||
return $this->connection->translate($this->_export());
|
||||
} catch (\Throwable $e) {
|
||||
trigger_error($e->getMessage(), E_USER_ERROR);
|
||||
return '';
|
||||
}
|
||||
return $this->connection->translate($this->_export());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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;
|
||||
@@ -428,7 +419,6 @@ 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)) {
|
||||
@@ -444,6 +434,7 @@ class Fluent implements IDataSource
|
||||
if ($clause === $this->command && $this->flags) {
|
||||
$args[] = implode(' ', array_keys($this->flags));
|
||||
}
|
||||
|
||||
foreach ($statement as $arg) {
|
||||
$args[] = $arg;
|
||||
}
|
||||
@@ -464,6 +455,7 @@ 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));
|
||||
}
|
||||
|
||||
@@ -475,6 +467,7 @@ class Fluent implements IDataSource
|
||||
$this->clauses[$clause] = &$val;
|
||||
unset($val);
|
||||
}
|
||||
|
||||
$this->cursor = &$foo;
|
||||
}
|
||||
}
|
||||
|
@@ -14,6 +14,7 @@ namespace Dibi;
|
||||
* Lazy cached storage.
|
||||
* @internal
|
||||
*/
|
||||
#[\AllowDynamicProperties]
|
||||
abstract class HashMapBase
|
||||
{
|
||||
/** @var callable */
|
||||
@@ -45,20 +46,21 @@ abstract class HashMapBase
|
||||
*/
|
||||
final class HashMap extends HashMapBase
|
||||
{
|
||||
public function __set(string $nm, $val)
|
||||
public function __set(string $nm, mixed $val): void
|
||||
{
|
||||
if ($nm === '') {
|
||||
$nm = "\xFF";
|
||||
}
|
||||
|
||||
$this->$nm = $val;
|
||||
}
|
||||
|
||||
|
||||
public function __get(string $nm)
|
||||
public function __get(string $nm): mixed
|
||||
{
|
||||
if ($nm === '') {
|
||||
$nm = "\xFF";
|
||||
return isset($this->$nm) ? $this->$nm : $this->$nm = $this->getCallback()('');
|
||||
return isset($this->$nm) && true ? $this->$nm : $this->$nm = $this->getCallback()('');
|
||||
} else {
|
||||
return $this->$nm = $this->getCallback()($nm);
|
||||
}
|
||||
|
@@ -9,24 +9,23 @@ declare(strict_types=1);
|
||||
|
||||
namespace Dibi;
|
||||
|
||||
use function array_map, array_unique, explode, fclose, fgets, fopen, fstat, getenv, htmlspecialchars, is_float, is_int, is_string, levenshtein, max, mb_strlen, ob_end_flush, ob_get_clean, ob_start, preg_match, preg_replace, preg_replace_callback, rtrim, set_time_limit, str_ends_with, str_repeat, str_starts_with, strlen, strtoupper, substr, trim, wordwrap;
|
||||
use const PHP_SAPI;
|
||||
|
||||
|
||||
class Helpers
|
||||
{
|
||||
use Strict;
|
||||
|
||||
/** @var HashMap */
|
||||
private static $types;
|
||||
private static HashMap $types;
|
||||
|
||||
|
||||
/**
|
||||
* Prints out a syntax highlighted version of the SQL command or Result.
|
||||
* @param string|Result $sql
|
||||
*/
|
||||
public static function dump($sql = null, bool $return = false): ?string
|
||||
public static function dump(string|Result|null $sql = null, bool $return = false): ?string
|
||||
{
|
||||
ob_start();
|
||||
if ($sql instanceof Result && PHP_SAPI === 'cli') {
|
||||
$hasColors = (substr((string) getenv('TERM'), 0, 5) === 'xterm');
|
||||
$hasColors = (str_starts_with((string) getenv('TERM'), 'xterm'));
|
||||
$maxLen = 0;
|
||||
foreach ($sql as $i => $row) {
|
||||
if ($i === 0) {
|
||||
@@ -41,6 +40,7 @@ class Helpers
|
||||
$spaces = $maxLen - mb_strlen($col) + 2;
|
||||
echo "$col" . str_repeat(' ', $spaces) . "$val\n";
|
||||
}
|
||||
|
||||
echo "\n";
|
||||
}
|
||||
|
||||
@@ -53,6 +53,7 @@ class Helpers
|
||||
foreach ($row as $col => $foo) {
|
||||
echo "\t\t<th>" . htmlspecialchars((string) $col) . "</th>\n";
|
||||
}
|
||||
|
||||
echo "\t</tr>\n</thead>\n<tbody>\n";
|
||||
}
|
||||
|
||||
@@ -60,6 +61,7 @@ class Helpers
|
||||
foreach ($row as $col) {
|
||||
echo "\t\t<td>", htmlspecialchars((string) $col), "</td>\n";
|
||||
}
|
||||
|
||||
echo "\t</tr>\n";
|
||||
}
|
||||
|
||||
@@ -72,8 +74,8 @@ class Helpers
|
||||
$sql = \dibi::$sql;
|
||||
}
|
||||
|
||||
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';
|
||||
$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';
|
||||
|
||||
// insert new lines
|
||||
$sql = " $sql ";
|
||||
@@ -88,7 +90,7 @@ class Helpers
|
||||
// syntax highlight
|
||||
$highlighter = "#(/\\*.+?\\*/)|(\\*\\*.+?\\*\\*)|(?<=[\\s,(])($keywords1)(?=[\\s,)])|(?<=[\\s,(=])($keywords2)(?=[\\s,)=])#is";
|
||||
if (PHP_SAPI === 'cli') {
|
||||
if (substr((string) getenv('TERM'), 0, 5) === 'xterm') {
|
||||
if (str_starts_with((string) getenv('TERM'), 'xterm')) {
|
||||
$sql = preg_replace_callback($highlighter, function (array $m) {
|
||||
if (!empty($m[1])) { // comment
|
||||
return "\033[1;30m" . $m[1] . "\033[0m";
|
||||
@@ -104,6 +106,7 @@ class Helpers
|
||||
}
|
||||
}, $sql);
|
||||
}
|
||||
|
||||
echo trim($sql) . "\n\n";
|
||||
|
||||
} else {
|
||||
@@ -150,6 +153,7 @@ class Helpers
|
||||
$best = $item;
|
||||
}
|
||||
}
|
||||
|
||||
return $best;
|
||||
}
|
||||
|
||||
@@ -157,13 +161,13 @@ class Helpers
|
||||
/** @internal */
|
||||
public static function escape(Driver $driver, $value, string $type): string
|
||||
{
|
||||
static $types = [
|
||||
Type::TEXT => 'text',
|
||||
Type::BINARY => 'binary',
|
||||
Type::BOOL => 'bool',
|
||||
Type::DATE => 'date',
|
||||
Type::DATETIME => 'datetime',
|
||||
\dibi::IDENTIFIER => 'identifier',
|
||||
$types = [
|
||||
Type::Text => 'text',
|
||||
Type::Binary => 'binary',
|
||||
Type::Bool => 'bool',
|
||||
Type::Date => 'date',
|
||||
Type::DateTime => 'datetime',
|
||||
Fluent::Identifier => 'identifier',
|
||||
];
|
||||
if (isset($types[$type])) {
|
||||
return $driver->{'escape' . $types[$type]}($value);
|
||||
@@ -179,16 +183,17 @@ class Helpers
|
||||
*/
|
||||
public static function detectType(string $type): ?string
|
||||
{
|
||||
static $patterns = [
|
||||
'^_' => Type::TEXT, // PostgreSQL arrays
|
||||
'BYTEA|BLOB|BIN' => Type::BINARY,
|
||||
'TEXT|CHAR|POINT|INTERVAL|STRING' => Type::TEXT,
|
||||
'YEAR|BYTE|COUNTER|SERIAL|INT|LONG|SHORT|^TINY$' => Type::INTEGER,
|
||||
'CURRENCY|REAL|MONEY|FLOAT|DOUBLE|DECIMAL|NUMERIC|NUMBER' => Type::FLOAT,
|
||||
'^TIME$' => Type::TIME,
|
||||
'TIME' => Type::DATETIME, // DATETIME, TIMESTAMP
|
||||
'DATE' => Type::DATE,
|
||||
'BOOL' => Type::BOOL,
|
||||
$patterns = [
|
||||
'^_' => Type::Text, // PostgreSQL arrays
|
||||
'RANGE$' => Type::Text, // PostgreSQL range types
|
||||
'BYTEA|BLOB|BIN' => Type::Binary,
|
||||
'TEXT|CHAR|POINT|INTERVAL|STRING' => Type::Text,
|
||||
'YEAR|BYTE|COUNTER|SERIAL|INT|LONG|SHORT|^TINY$' => Type::Integer,
|
||||
'CURRENCY|REAL|MONEY|FLOAT|DOUBLE|DECIMAL|NUMERIC|NUMBER' => Type::Float,
|
||||
'^TIME$' => Type::Time,
|
||||
'TIME' => Type::DateTime, // DATETIME, TIMESTAMP
|
||||
'DATE' => Type::Date,
|
||||
'BOOL' => Type::Bool,
|
||||
'JSON' => Type::JSON,
|
||||
];
|
||||
|
||||
@@ -197,6 +202,7 @@ class Helpers
|
||||
return $val;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -204,9 +210,10 @@ class Helpers
|
||||
/** @internal */
|
||||
public static function getTypeCache(): HashMap
|
||||
{
|
||||
if (self::$types === null) {
|
||||
self::$types = new HashMap([self::class, 'detectType']);
|
||||
if (!isset(self::$types)) {
|
||||
self::$types = new HashMap(self::detectType(...));
|
||||
}
|
||||
|
||||
return self::$types;
|
||||
}
|
||||
|
||||
@@ -230,9 +237,9 @@ class Helpers
|
||||
|
||||
/**
|
||||
* Import SQL dump from file.
|
||||
* @return int count of sql commands
|
||||
* Returns 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 @
|
||||
|
||||
@@ -251,7 +258,7 @@ class Helpers
|
||||
if (strtoupper(substr($s, 0, 10)) === 'DELIMITER ') {
|
||||
$delimiter = trim(substr($s, 10));
|
||||
|
||||
} elseif (substr($ts = rtrim($s), -strlen($delimiter)) === $delimiter) {
|
||||
} elseif (str_ends_with($ts = rtrim($s), $delimiter)) {
|
||||
$sql .= substr($ts, 0, -strlen($delimiter));
|
||||
$driver->query($sql);
|
||||
$sql = '';
|
||||
@@ -259,7 +266,6 @@ class Helpers
|
||||
if ($onProgress) {
|
||||
$onProgress($count, isset($stat['size']) ? $size * 100 / $stat['size'] : null);
|
||||
}
|
||||
|
||||
} else {
|
||||
$sql .= $s;
|
||||
}
|
||||
@@ -272,20 +278,21 @@ class Helpers
|
||||
$onProgress($count, isset($stat['size']) ? 100 : null);
|
||||
}
|
||||
}
|
||||
|
||||
fclose($handle);
|
||||
return $count;
|
||||
}
|
||||
|
||||
|
||||
/** @internal */
|
||||
public static function false2Null($val)
|
||||
public static function false2Null(mixed $val): mixed
|
||||
{
|
||||
return $val === false ? null : $val;
|
||||
}
|
||||
|
||||
|
||||
/** @internal */
|
||||
public static function intVal($value): int
|
||||
public static function intVal(mixed $value): int
|
||||
{
|
||||
if (is_int($value)) {
|
||||
return $value;
|
||||
@@ -293,6 +300,7 @@ 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.");
|
||||
|
@@ -15,10 +15,7 @@ namespace Dibi;
|
||||
*/
|
||||
class Literal
|
||||
{
|
||||
use Strict;
|
||||
|
||||
/** @var string */
|
||||
private $value;
|
||||
private readonly string $value;
|
||||
|
||||
|
||||
public function __construct($value)
|
||||
|
@@ -10,6 +10,8 @@ declare(strict_types=1);
|
||||
namespace Dibi\Loggers;
|
||||
|
||||
use Dibi;
|
||||
use function sprintf;
|
||||
use const FILE_APPEND, LOCK_EX;
|
||||
|
||||
|
||||
/**
|
||||
@@ -17,23 +19,11 @@ use Dibi;
|
||||
*/
|
||||
class FileLogger
|
||||
{
|
||||
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)
|
||||
{
|
||||
$this->file = $file;
|
||||
$this->filter = $filter ?: Dibi\Event::QUERY;
|
||||
$this->errorsOnly = $errorsOnly;
|
||||
public function __construct(
|
||||
public string $file,
|
||||
public int $filter = Dibi\Event::QUERY,
|
||||
private bool $errorsOnly = false,
|
||||
) {
|
||||
}
|
||||
|
||||
|
||||
@@ -54,10 +44,11 @@ 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(
|
||||
@@ -65,7 +56,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),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -75,7 +66,7 @@ class FileLogger
|
||||
{
|
||||
$driver = $event->connection->getConfig('driver');
|
||||
$message .=
|
||||
"\n-- driver: " . (is_object($driver) ? get_class($driver) : $driver) . '/' . $event->connection->getConfig('name')
|
||||
"\n-- driver: " . get_debug_type($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);
|
||||
|
@@ -27,19 +27,10 @@ use Dibi;
|
||||
*/
|
||||
class Column
|
||||
{
|
||||
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 $info;
|
||||
|
||||
|
||||
public function __construct(Dibi\Reflector $reflector = null, array $info)
|
||||
{
|
||||
$this->reflector = $reflector;
|
||||
$this->info = $info;
|
||||
public function __construct(
|
||||
private readonly ?Dibi\Reflector $reflector,
|
||||
private array $info,
|
||||
) {
|
||||
}
|
||||
|
||||
|
||||
@@ -66,6 +57,7 @@ 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']]);
|
||||
}
|
||||
|
||||
@@ -108,15 +100,13 @@ class Column
|
||||
}
|
||||
|
||||
|
||||
/** @return mixed */
|
||||
public function getDefault()
|
||||
public function getDefault(): mixed
|
||||
{
|
||||
return $this->info['default'] ?? null;
|
||||
}
|
||||
|
||||
|
||||
/** @return mixed */
|
||||
public function getVendorInfo(string $key)
|
||||
public function getVendorInfo(string $key): mixed
|
||||
{
|
||||
return $this->info['vendor'][$key] ?? null;
|
||||
}
|
||||
|
@@ -10,6 +10,7 @@ declare(strict_types=1);
|
||||
namespace Dibi\Reflection;
|
||||
|
||||
use Dibi;
|
||||
use function array_values, strtolower;
|
||||
|
||||
|
||||
/**
|
||||
@@ -21,22 +22,14 @@ use Dibi;
|
||||
*/
|
||||
class Database
|
||||
{
|
||||
use Dibi\Strict;
|
||||
|
||||
/** @var Dibi\Reflector */
|
||||
private $reflector;
|
||||
|
||||
/** @var string|null */
|
||||
private $name;
|
||||
|
||||
/** @var Table[]|null */
|
||||
private $tables;
|
||||
/** @var Table[] */
|
||||
private array $tables;
|
||||
|
||||
|
||||
public function __construct(Dibi\Reflector $reflector, string $name = null)
|
||||
{
|
||||
$this->reflector = $reflector;
|
||||
$this->name = $name;
|
||||
public function __construct(
|
||||
private readonly Dibi\Reflector $reflector,
|
||||
private ?string $name = null,
|
||||
) {
|
||||
}
|
||||
|
||||
|
||||
@@ -62,6 +55,7 @@ class Database
|
||||
foreach ($this->tables as $table) {
|
||||
$res[] = $table->getName();
|
||||
}
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
@@ -88,7 +82,7 @@ class Database
|
||||
|
||||
protected function init(): void
|
||||
{
|
||||
if ($this->tables === null) {
|
||||
if (!isset($this->tables)) {
|
||||
$this->tables = [];
|
||||
foreach ($this->reflector->getTables() as $info) {
|
||||
$this->tables[strtolower($info['name'])] = new Table($this->reflector, $info);
|
||||
|
@@ -9,7 +9,6 @@ declare(strict_types=1);
|
||||
|
||||
namespace Dibi\Reflection;
|
||||
|
||||
use Dibi;
|
||||
|
||||
|
||||
/**
|
||||
@@ -20,19 +19,10 @@ use Dibi;
|
||||
*/
|
||||
class ForeignKey
|
||||
{
|
||||
use Dibi\Strict;
|
||||
|
||||
/** @var string */
|
||||
private $name;
|
||||
|
||||
/** @var array of [local, foreign, onDelete, onUpdate] */
|
||||
private $references;
|
||||
|
||||
|
||||
public function __construct(string $name, array $references)
|
||||
{
|
||||
$this->name = $name;
|
||||
$this->references = $references;
|
||||
public function __construct(
|
||||
private readonly string $name,
|
||||
private readonly array $references,
|
||||
) {
|
||||
}
|
||||
|
||||
|
||||
|
@@ -9,7 +9,6 @@ declare(strict_types=1);
|
||||
|
||||
namespace Dibi\Reflection;
|
||||
|
||||
use Dibi;
|
||||
|
||||
|
||||
/**
|
||||
@@ -22,15 +21,9 @@ use Dibi;
|
||||
*/
|
||||
class Index
|
||||
{
|
||||
use Dibi\Strict;
|
||||
|
||||
/** @var array (name, columns, [unique], [primary]) */
|
||||
private $info;
|
||||
|
||||
|
||||
public function __construct(array $info)
|
||||
{
|
||||
$this->info = $info;
|
||||
public function __construct(
|
||||
private readonly array $info,
|
||||
) {
|
||||
}
|
||||
|
||||
|
||||
|
@@ -10,6 +10,7 @@ declare(strict_types=1);
|
||||
namespace Dibi\Reflection;
|
||||
|
||||
use Dibi;
|
||||
use function array_values, strtolower;
|
||||
|
||||
|
||||
/**
|
||||
@@ -20,21 +21,16 @@ use Dibi;
|
||||
*/
|
||||
class Result
|
||||
{
|
||||
use Dibi\Strict;
|
||||
|
||||
/** @var Dibi\ResultDriver */
|
||||
private $driver;
|
||||
/** @var Column[]|null */
|
||||
private ?array $columns;
|
||||
|
||||
/** @var Column[]|null */
|
||||
private $columns;
|
||||
|
||||
/** @var Column[]|null */
|
||||
private $names;
|
||||
private ?array $names;
|
||||
|
||||
|
||||
public function __construct(Dibi\ResultDriver $driver)
|
||||
{
|
||||
$this->driver = $driver;
|
||||
public function __construct(
|
||||
private readonly Dibi\ResultDriver $driver,
|
||||
) {
|
||||
}
|
||||
|
||||
|
||||
@@ -54,6 +50,7 @@ class Result
|
||||
foreach ($this->columns as $column) {
|
||||
$res[] = $fullNames ? $column->getFullName() : $column->getName();
|
||||
}
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
@@ -80,7 +77,7 @@ class Result
|
||||
|
||||
protected function initColumns(): void
|
||||
{
|
||||
if ($this->columns === null) {
|
||||
if (!isset($this->columns)) {
|
||||
$this->columns = [];
|
||||
$reflector = $this->driver instanceof Dibi\Reflector
|
||||
? $this->driver
|
||||
|
@@ -10,6 +10,7 @@ declare(strict_types=1);
|
||||
namespace Dibi\Reflection;
|
||||
|
||||
use Dibi;
|
||||
use function array_values, strtolower;
|
||||
|
||||
|
||||
/**
|
||||
@@ -25,28 +26,19 @@ use Dibi;
|
||||
*/
|
||||
class Table
|
||||
{
|
||||
use Dibi\Strict;
|
||||
private readonly Dibi\Reflector $reflector;
|
||||
private string $name;
|
||||
private bool $view;
|
||||
|
||||
/** @var Dibi\Reflector */
|
||||
private $reflector;
|
||||
/** @var Column[] */
|
||||
private array $columns;
|
||||
|
||||
/** @var string */
|
||||
private $name;
|
||||
/** @var ForeignKey[] */
|
||||
private array $foreignKeys;
|
||||
|
||||
/** @var bool */
|
||||
private $view;
|
||||
|
||||
/** @var Column[]|null */
|
||||
private $columns;
|
||||
|
||||
/** @var ForeignKey[]|null */
|
||||
private $foreignKeys;
|
||||
|
||||
/** @var Index[]|null */
|
||||
private $indexes;
|
||||
|
||||
/** @var Index|null */
|
||||
private $primaryKey;
|
||||
/** @var Index[] */
|
||||
private array $indexes;
|
||||
private ?Index $primaryKey;
|
||||
|
||||
|
||||
public function __construct(Dibi\Reflector $reflector, array $info)
|
||||
@@ -85,6 +77,7 @@ class Table
|
||||
foreach ($this->columns as $column) {
|
||||
$res[] = $column->getName();
|
||||
}
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
@@ -134,7 +127,7 @@ class Table
|
||||
|
||||
protected function initColumns(): void
|
||||
{
|
||||
if ($this->columns === null) {
|
||||
if (!isset($this->columns)) {
|
||||
$this->columns = [];
|
||||
foreach ($this->reflector->getColumns($this->name) as $info) {
|
||||
$this->columns[strtolower($info['name'])] = new Column($this->reflector, $info);
|
||||
@@ -145,13 +138,14 @@ class Table
|
||||
|
||||
protected function initIndexes(): void
|
||||
{
|
||||
if ($this->indexes === null) {
|
||||
if (!isset($this->indexes)) {
|
||||
$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'])];
|
||||
|
@@ -9,6 +9,9 @@ declare(strict_types=1);
|
||||
|
||||
namespace Dibi;
|
||||
|
||||
use function array_keys, array_pop, count, explode, is_float, is_string, json_decode, ltrim, preg_match, preg_split, property_exists, reset, rtrim, str_contains, str_replace, str_starts_with, strpos;
|
||||
use const PREG_SPLIT_DELIM_CAPTURE, PREG_SPLIT_NO_EMPTY;
|
||||
|
||||
|
||||
/**
|
||||
* Query result.
|
||||
@@ -17,34 +20,29 @@ namespace Dibi;
|
||||
*/
|
||||
class Result implements IDataSource
|
||||
{
|
||||
use Strict;
|
||||
private ?ResultDriver $driver;
|
||||
|
||||
/** @var ResultDriver|null */
|
||||
private $driver;
|
||||
/** Translate table */
|
||||
private array $types = [];
|
||||
private ?Reflection\Result $meta;
|
||||
|
||||
/** @var array Translate table */
|
||||
private $types = [];
|
||||
/** Already fetched? Used for allowance for first seek(0) */
|
||||
private bool $fetched = false;
|
||||
|
||||
/** @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;
|
||||
/** returned object class */
|
||||
private ?string $rowClass = Row::class;
|
||||
|
||||
/** @var callable|null returned object factory */
|
||||
private $rowFactory;
|
||||
|
||||
/** @var array format */
|
||||
private $formats = [];
|
||||
private array $formats = [];
|
||||
|
||||
|
||||
public function __construct(ResultDriver $driver)
|
||||
public function __construct(ResultDriver $driver, bool $normalize = true)
|
||||
{
|
||||
$this->driver = $driver;
|
||||
$this->detectTypes();
|
||||
if ($normalize) {
|
||||
$this->detectTypes();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -131,7 +129,7 @@ class Result implements IDataSource
|
||||
/**
|
||||
* Set fetched object class. This class should extend the Row class.
|
||||
*/
|
||||
public function setRowClass(?string $class): self
|
||||
public function setRowClass(?string $class): static
|
||||
{
|
||||
$this->rowClass = $class;
|
||||
return $this;
|
||||
@@ -150,7 +148,7 @@ class Result implements IDataSource
|
||||
/**
|
||||
* Set a factory to create fetched object instances. These should extend the Row class.
|
||||
*/
|
||||
public function setRowFactory(callable $callback): self
|
||||
public function setRowFactory(callable $callback): static
|
||||
{
|
||||
$this->rowFactory = $callback;
|
||||
return $this;
|
||||
@@ -160,14 +158,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()
|
||||
final public function fetch(): mixed
|
||||
{
|
||||
$row = $this->getResultDriver()->fetch(true);
|
||||
if ($row === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$this->fetched = true;
|
||||
$this->normalize($row);
|
||||
if ($this->rowFactory) {
|
||||
@@ -175,20 +173,22 @@ class Result implements IDataSource
|
||||
} elseif ($this->rowClass) {
|
||||
return new $this->rowClass($row);
|
||||
}
|
||||
|
||||
return $row;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Like fetch(), but returns only first field.
|
||||
* @return mixed value on success, null if no next record
|
||||
* Returns value on success, null if no next record
|
||||
*/
|
||||
final public function fetchSingle()
|
||||
final public function fetchSingle(): mixed
|
||||
{
|
||||
$row = $this->getResultDriver()->fetch(true);
|
||||
if ($row === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$this->fetched = true;
|
||||
$this->normalize($row);
|
||||
return reset($row);
|
||||
@@ -199,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 = $limit ?? -1;
|
||||
$limit ??= -1;
|
||||
$this->seek($offset ?: 0);
|
||||
$row = $this->fetch();
|
||||
if (!$row) {
|
||||
@@ -213,6 +213,7 @@ class Result implements IDataSource
|
||||
if ($limit === 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
$limit--;
|
||||
$data[] = $row;
|
||||
} while ($row = $this->fetch());
|
||||
@@ -232,7 +233,7 @@ class Result implements IDataSource
|
||||
*/
|
||||
final public function fetchAssoc(string $assoc): array
|
||||
{
|
||||
if (strpos($assoc, ',') !== false) {
|
||||
if (str_contains($assoc, ',')) {
|
||||
return $this->oldFetchAssoc($assoc);
|
||||
}
|
||||
|
||||
@@ -285,7 +286,6 @@ class Result implements IDataSource
|
||||
} else {
|
||||
$x = &$x->{$assoc[$i + 1]};
|
||||
}
|
||||
|
||||
} elseif ($as !== '|') { // associative-array node
|
||||
$x = &$x[(string) $row->$as];
|
||||
}
|
||||
@@ -343,7 +343,6 @@ class Result implements IDataSource
|
||||
} else {
|
||||
$x = &$x[$assoc[$i + 1]];
|
||||
}
|
||||
|
||||
} elseif ($as === '@') { // "object" node
|
||||
if ($x === null) {
|
||||
$x = clone $row;
|
||||
@@ -352,7 +351,6 @@ class Result implements IDataSource
|
||||
} else {
|
||||
$x = &$x->{$assoc[$i + 1]};
|
||||
}
|
||||
|
||||
} else { // associative-array node
|
||||
$x = &$x[(string) $row->$as];
|
||||
}
|
||||
@@ -374,7 +372,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();
|
||||
@@ -396,6 +394,7 @@ class Result implements IDataSource
|
||||
do {
|
||||
$data[] = $row[$key];
|
||||
} while ($row = $this->fetch());
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
@@ -410,6 +409,7 @@ class Result implements IDataSource
|
||||
do {
|
||||
$data[] = $row[$value];
|
||||
} while ($row = $this->fetch());
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
@@ -453,18 +453,29 @@ class Result implements IDataSource
|
||||
if (!isset($row[$key])) { // null
|
||||
continue;
|
||||
}
|
||||
$value = $row[$key];
|
||||
|
||||
if ($type === Type::TEXT) {
|
||||
$value = $row[$key];
|
||||
$format = $this->formats[$type] ?? null;
|
||||
|
||||
if ($type === null || $format === 'native') {
|
||||
$row[$key] = $value;
|
||||
|
||||
} elseif ($type === Type::Text) {
|
||||
$row[$key] = (string) $value;
|
||||
|
||||
} elseif ($type === Type::INTEGER) {
|
||||
} elseif ($type === Type::Integer) {
|
||||
$row[$key] = is_float($tmp = $value * 1)
|
||||
? (is_string($value) ? $value : (int) $value)
|
||||
: $tmp;
|
||||
|
||||
} elseif ($type === Type::FLOAT) {
|
||||
$value = ltrim((string) $value, '0');
|
||||
} elseif ($type === Type::Float) {
|
||||
if (!is_string($value)) {
|
||||
$row[$key] = (float) $value;
|
||||
continue;
|
||||
}
|
||||
|
||||
$negative = ($value[0] ?? null) === '-';
|
||||
$value = ltrim($value, '0-');
|
||||
$p = strpos($value, '.');
|
||||
$e = strpos($value, 'e');
|
||||
if ($p !== false && $e === false) {
|
||||
@@ -472,46 +483,46 @@ class Result implements IDataSource
|
||||
} elseif ($p !== false && $e !== false) {
|
||||
$value = rtrim($value, '.');
|
||||
}
|
||||
|
||||
if ($value === '' || $value[0] === '.') {
|
||||
$value = '0' . $value;
|
||||
}
|
||||
|
||||
if ($negative) {
|
||||
$value = '-' . $value;
|
||||
}
|
||||
|
||||
$row[$key] = $value === str_replace(',', '.', (string) ($float = (float) $value))
|
||||
? $float
|
||||
: $value;
|
||||
|
||||
} elseif ($type === Type::BOOL) {
|
||||
} elseif ($type === Type::Bool) {
|
||||
$row[$key] = ((bool) $value) && $value !== 'f' && $value !== 'F';
|
||||
|
||||
} elseif ($type === Type::DATETIME || $type === Type::DATE || $type === Type::TIME) {
|
||||
if ($value && substr((string) $value, 0, 3) !== '000') { // '', null, false, '0000-00-00', ...
|
||||
} elseif ($type === Type::DateTime || $type === Type::Date || $type === Type::Time) {
|
||||
if ($value && !str_starts_with((string) $value, '0000-00')) { // '', null, false, '0000-00-00', ...
|
||||
$value = new DateTime($value);
|
||||
$row[$key] = empty($this->formats[$type])
|
||||
? $value
|
||||
: $value->format($this->formats[$type]);
|
||||
$row[$key] = $format ? $value->format($format) : $value;
|
||||
} else {
|
||||
$row[$key] = null;
|
||||
}
|
||||
|
||||
} elseif ($type === Type::TIME_INTERVAL) {
|
||||
} elseif ($type === Type::TimeInterval) {
|
||||
preg_match('#^(-?)(\d+)\D(\d+)\D(\d+)\z#', $value, $m);
|
||||
$row[$key] = new \DateInterval("PT$m[2]H$m[3]M$m[4]S");
|
||||
$row[$key]->invert = (int) (bool) $m[1];
|
||||
$value = new \DateInterval("PT$m[2]H$m[3]M$m[4]S");
|
||||
$value->invert = (int) (bool) $m[1];
|
||||
$row[$key] = $format ? $value->format($format) : $value;
|
||||
|
||||
} elseif ($type === Type::BINARY) {
|
||||
} elseif ($type === Type::Binary) {
|
||||
$row[$key] = is_string($value)
|
||||
? $this->getResultDriver()->unescapeBinary($value)
|
||||
: $value;
|
||||
|
||||
} elseif ($type === Type::JSON) {
|
||||
if ($this->formats[$type] === 'string') {
|
||||
if ($format === 'string') { // back compatibility with 'native'
|
||||
$row[$key] = $value;
|
||||
} else {
|
||||
$row[$key] = json_decode($value, $this->formats[$type] === 'array');
|
||||
$row[$key] = json_decode($value, $format === 'array');
|
||||
}
|
||||
|
||||
} elseif ($type === null) {
|
||||
$row[$key] = $value;
|
||||
|
||||
} else {
|
||||
throw new \RuntimeException('Unexpected type ' . $type);
|
||||
}
|
||||
@@ -523,7 +534,7 @@ class Result implements IDataSource
|
||||
* Define column type.
|
||||
* @param string|null $type use constant Type::*
|
||||
*/
|
||||
final public function setType(string $column, ?string $type): self
|
||||
final public function setType(string $column, ?string $type): static
|
||||
{
|
||||
$this->types[$column] = $type;
|
||||
return $this;
|
||||
@@ -549,15 +560,25 @@ class Result implements IDataSource
|
||||
|
||||
|
||||
/**
|
||||
* Sets date format.
|
||||
* Sets type format.
|
||||
*/
|
||||
final public function setFormat(string $type, ?string $format): self
|
||||
final public function setFormat(string $type, ?string $format): static
|
||||
{
|
||||
$this->formats[$type] = $format;
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sets type formats.
|
||||
*/
|
||||
final public function setFormats(array $formats): static
|
||||
{
|
||||
$this->formats = $formats;
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns data format.
|
||||
*/
|
||||
@@ -575,9 +596,10 @@ class Result implements IDataSource
|
||||
*/
|
||||
public function getInfo(): Reflection\Result
|
||||
{
|
||||
if ($this->meta === null) {
|
||||
if (!isset($this->meta)) {
|
||||
$this->meta = new Reflection\Result($this->getResultDriver());
|
||||
}
|
||||
|
||||
return $this->meta;
|
||||
}
|
||||
|
||||
|
@@ -15,21 +15,13 @@ namespace Dibi;
|
||||
*/
|
||||
class ResultIterator implements \Iterator, \Countable
|
||||
{
|
||||
use Strict;
|
||||
|
||||
/** @var Result */
|
||||
private $result;
|
||||
|
||||
/** @var mixed */
|
||||
private $row;
|
||||
|
||||
/** @var int */
|
||||
private $pointer = 0;
|
||||
private mixed $row;
|
||||
private int $pointer = 0;
|
||||
|
||||
|
||||
public function __construct(Result $result)
|
||||
{
|
||||
$this->result = $result;
|
||||
public function __construct(
|
||||
private readonly Result $result,
|
||||
) {
|
||||
}
|
||||
|
||||
|
||||
@@ -46,9 +38,9 @@ class ResultIterator implements \Iterator, \Countable
|
||||
|
||||
/**
|
||||
* Returns the key of the current element.
|
||||
* @return mixed
|
||||
*/
|
||||
public function key()
|
||||
#[\ReturnTypeWillChange]
|
||||
public function key(): mixed
|
||||
{
|
||||
return $this->pointer;
|
||||
}
|
||||
@@ -56,9 +48,9 @@ class ResultIterator implements \Iterator, \Countable
|
||||
|
||||
/**
|
||||
* Returns the current element.
|
||||
* @return mixed
|
||||
*/
|
||||
public function current()
|
||||
#[\ReturnTypeWillChange]
|
||||
public function current(): mixed
|
||||
{
|
||||
return $this->row;
|
||||
}
|
||||
|
@@ -9,10 +9,13 @@ declare(strict_types=1);
|
||||
|
||||
namespace Dibi;
|
||||
|
||||
use function array_keys, count, str_starts_with;
|
||||
|
||||
|
||||
/**
|
||||
* Result set single row.
|
||||
*/
|
||||
#[\AllowDynamicProperties]
|
||||
class Row implements \ArrayAccess, \IteratorAggregate, \Countable
|
||||
{
|
||||
public function __construct(array $arr)
|
||||
@@ -31,62 +34,70 @@ class Row implements \ArrayAccess, \IteratorAggregate, \Countable
|
||||
|
||||
/**
|
||||
* Converts value to DateTime object.
|
||||
* @return DateTime|string|null
|
||||
*/
|
||||
public function asDateTime(string $key, string $format = null)
|
||||
public function asDateTime(string $key, ?string $format = null): DateTime|string|null
|
||||
{
|
||||
$time = $this[$key];
|
||||
if (!$time instanceof DateTime) {
|
||||
if (!$time || substr((string) $time, 0, 3) === '000') { // '', null, false, '0000-00-00', ...
|
||||
if (!$time || str_starts_with((string) $time, '0000-00')) { // '', null, false, '0000-00-00', ...
|
||||
return null;
|
||||
}
|
||||
|
||||
$time = new DateTime($time);
|
||||
}
|
||||
|
||||
return $format === null ? $time : $time->format($format);
|
||||
}
|
||||
|
||||
|
||||
public function __get(string $key)
|
||||
public function __get(string $key): mixed
|
||||
{
|
||||
$hint = Helpers::getSuggestion(array_keys((array) $this), $key);
|
||||
trigger_error("Attempt to read missing column '$key'" . ($hint ? ", did you mean '$hint'?" : '.'), E_USER_NOTICE);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
public function __isset(string $key): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/********************* interfaces ArrayAccess, Countable & IteratorAggregate ****************d*g**/
|
||||
|
||||
|
||||
final public function count()
|
||||
final public function count(): int
|
||||
{
|
||||
return count((array) $this);
|
||||
}
|
||||
|
||||
|
||||
final public function getIterator()
|
||||
final public function getIterator(): \ArrayIterator
|
||||
{
|
||||
return new \ArrayIterator($this);
|
||||
}
|
||||
|
||||
|
||||
final public function offsetSet($nm, $val)
|
||||
final public function offsetSet($nm, $val): void
|
||||
{
|
||||
$this->$nm = $val;
|
||||
}
|
||||
|
||||
|
||||
final public function offsetGet($nm)
|
||||
final public function offsetGet($nm): mixed
|
||||
{
|
||||
return $this->$nm;
|
||||
}
|
||||
|
||||
|
||||
final public function offsetExists($nm)
|
||||
final public function offsetExists($nm): bool
|
||||
{
|
||||
return isset($this->$nm);
|
||||
}
|
||||
|
||||
|
||||
final public function offsetUnset($nm)
|
||||
final public function offsetUnset($nm): void
|
||||
{
|
||||
unset($this->$nm);
|
||||
}
|
||||
|
@@ -1,111 +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;
|
||||
|
||||
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.");
|
||||
}
|
||||
}
|
@@ -9,53 +9,34 @@ declare(strict_types=1);
|
||||
|
||||
namespace Dibi;
|
||||
|
||||
use function array_filter, array_keys, array_splice, array_values, count, explode, get_debug_type, gettype, implode, is_array, is_bool, is_float, is_int, is_numeric, is_object, is_scalar, is_string, iterator_to_array, key, ltrim, number_format, preg_last_error, preg_match, preg_replace_callback, reset, rtrim, str_contains, str_replace, strcspn, strlen, strncasecmp, strtoupper, substr, trim;
|
||||
|
||||
|
||||
/**
|
||||
* SQL translator.
|
||||
*/
|
||||
final class Translator
|
||||
{
|
||||
use Strict;
|
||||
|
||||
/** @var Connection */
|
||||
private $connection;
|
||||
|
||||
/** @var Driver */
|
||||
private $driver;
|
||||
|
||||
/** @var int */
|
||||
private $cursor = 0;
|
||||
|
||||
/** @var array */
|
||||
private $args;
|
||||
private readonly Connection $connection;
|
||||
private readonly Driver $driver;
|
||||
private int $cursor = 0;
|
||||
private array $args;
|
||||
|
||||
/** @var string[] */
|
||||
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;
|
||||
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;
|
||||
|
||||
|
||||
public function __construct(Connection $connection)
|
||||
{
|
||||
$this->connection = $connection;
|
||||
$this->driver = $connection->getDriver();
|
||||
$this->identifiers = new HashMap([$this, 'delimite']);
|
||||
$this->identifiers = new HashMap($this->delimite(...));
|
||||
}
|
||||
|
||||
|
||||
@@ -69,6 +50,7 @@ final class Translator
|
||||
while (count($args) === 1 && is_array($args[0])) { // implicit array expansion
|
||||
$args = array_values($args[0]);
|
||||
}
|
||||
|
||||
$this->args = $args;
|
||||
$this->errors = [];
|
||||
|
||||
@@ -95,27 +77,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
|
||||
,
|
||||
[$this, 'cb'],
|
||||
substr($arg, $toSkip)
|
||||
/
|
||||
(?=[`['":%?]) ## 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),
|
||||
);
|
||||
if (preg_last_error()) {
|
||||
throw new PcreException;
|
||||
}
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -138,8 +120,10 @@ XX
|
||||
if ($lastArr === $cursor - 1) {
|
||||
$sql[] = ',';
|
||||
}
|
||||
|
||||
$sql[] = $this->formatValue($arg, $commandIns ? 'l' : 'a');
|
||||
}
|
||||
|
||||
$lastArr = $cursor;
|
||||
continue;
|
||||
}
|
||||
@@ -148,7 +132,6 @@ XX
|
||||
$sql[] = $this->formatValue($arg, null);
|
||||
} // while
|
||||
|
||||
|
||||
if ($comment) {
|
||||
$sql[] = '*/';
|
||||
}
|
||||
@@ -170,9 +153,8 @@ XX
|
||||
|
||||
/**
|
||||
* Apply modifier to single value.
|
||||
* @param mixed $value
|
||||
*/
|
||||
public function formatValue($value, ?string $modifier): string
|
||||
public function formatValue(mixed $value, ?string $modifier): string
|
||||
{
|
||||
if ($this->comment) {
|
||||
return '...';
|
||||
@@ -207,20 +189,21 @@ XX
|
||||
$v = $this->formatValue($v, $pair[1]);
|
||||
if ($pair[1] === 'l' || $pair[1] === 'in') {
|
||||
$op = 'IN ';
|
||||
} elseif (strpos($pair[1], 'like') !== false) {
|
||||
} elseif (str_contains($pair[1], 'like')) {
|
||||
$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
|
||||
@@ -232,15 +215,17 @@ XX
|
||||
$vx[] = $this->identifiers->{$pair[0]};
|
||||
}
|
||||
}
|
||||
|
||||
return implode(', ', $vx);
|
||||
|
||||
|
||||
case 'a': // key=val, key=val, ...
|
||||
foreach ($value as $k => $v) {
|
||||
$pair = explode('%', $k, 2); // split into identifier & modifier
|
||||
$pair = explode('%', (string) $k, 2); // split into identifier & modifier
|
||||
$vx[] = $this->identifiers->{$pair[0]} . '='
|
||||
. $this->formatValue($v, $pair[1] ?? (is_array($v) ? 'ex!' : null));
|
||||
}
|
||||
|
||||
return implode(', ', $vx);
|
||||
|
||||
|
||||
@@ -250,6 +235,7 @@ XX
|
||||
$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') . ')';
|
||||
|
||||
|
||||
@@ -259,6 +245,7 @@ XX
|
||||
$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, ...), ...
|
||||
@@ -272,7 +259,7 @@ XX
|
||||
$proto = array_keys($v);
|
||||
}
|
||||
} else {
|
||||
return $this->errors[] = '**Unexpected type ' . (is_object($v) ? get_class($v) : gettype($v)) . '**';
|
||||
return $this->errors[] = '**Unexpected type ' . get_debug_type($v) . '**';
|
||||
}
|
||||
|
||||
$pair = explode('%', $k, 2); // split into identifier & modifier
|
||||
@@ -281,9 +268,11 @@ XX
|
||||
$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
|
||||
@@ -297,6 +286,7 @@ XX
|
||||
$vx[] = $this->identifiers->$v;
|
||||
}
|
||||
}
|
||||
|
||||
return implode(', ', $vx);
|
||||
|
||||
case 'ex!':
|
||||
@@ -310,10 +300,24 @@ XX
|
||||
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) {
|
||||
@@ -333,7 +337,7 @@ XX
|
||||
) {
|
||||
// continue
|
||||
} else {
|
||||
$type = is_object($value) ? get_class($value) : gettype($value);
|
||||
$type = get_debug_type($value);
|
||||
return $this->errors[] = "**Invalid combination of type $type and modifier %$modifier**";
|
||||
}
|
||||
}
|
||||
@@ -400,6 +404,7 @@ XX
|
||||
} elseif (!$value instanceof \DateTimeInterface) {
|
||||
$value = new DateTime($value);
|
||||
}
|
||||
|
||||
return $modifier === 'd'
|
||||
? $this->driver->escapeDate($value)
|
||||
: $this->driver->escapeDateTime($value);
|
||||
@@ -420,25 +425,25 @@ XX
|
||||
$value = substr($value, 0, $toSkip)
|
||||
. preg_replace_callback(
|
||||
<<<'XX'
|
||||
/
|
||||
(?=[`['":])
|
||||
(?:
|
||||
`(.+?)`|
|
||||
\[(.+?)\]|
|
||||
(')((?:''|[^'])*)'|
|
||||
(")((?:""|[^"])*)"|
|
||||
('|")|
|
||||
:(\S*?:)([a-zA-Z0-9._]?)
|
||||
)/sx
|
||||
XX
|
||||
,
|
||||
[$this, 'cb'],
|
||||
substr($value, $toSkip)
|
||||
/
|
||||
(?=[`['":])
|
||||
(?:
|
||||
`(.+?)`|
|
||||
\[(.+?)]|
|
||||
(')((?:''|[^'])*)'|
|
||||
(")((?:""|[^"])*)"|
|
||||
(['"])|
|
||||
:(\S*?:)([a-zA-Z0-9._]?)
|
||||
)/sx
|
||||
XX,
|
||||
$this->cb(...),
|
||||
substr($value, $toSkip),
|
||||
);
|
||||
if (preg_last_error()) {
|
||||
throw new PcreException;
|
||||
}
|
||||
}
|
||||
|
||||
return $value;
|
||||
|
||||
case 'SQL': // preserve as real SQL (TODO: rename to %sql)
|
||||
@@ -469,7 +474,6 @@ XX
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// without modifier procession
|
||||
if (is_string($value)) {
|
||||
return $this->driver->escapeText($value);
|
||||
@@ -499,7 +503,7 @@ XX
|
||||
return $this->connection->translate(...$value->getValues());
|
||||
|
||||
} else {
|
||||
$type = is_object($value) ? get_class($value) : gettype($value);
|
||||
$type = get_debug_type($value);
|
||||
return $this->errors[] = "**Unexpected $type**";
|
||||
}
|
||||
}
|
||||
@@ -551,6 +555,7 @@ XX
|
||||
$this->comment = true;
|
||||
return '/*';
|
||||
}
|
||||
|
||||
return '';
|
||||
|
||||
} elseif ($mod === 'else') {
|
||||
@@ -563,7 +568,6 @@ XX
|
||||
$this->comment = true;
|
||||
return '/*';
|
||||
}
|
||||
|
||||
} elseif ($mod === 'end') {
|
||||
$this->ifLevel--;
|
||||
if ($this->ifLevelStart === $this->ifLevel + 1) {
|
||||
@@ -572,6 +576,7 @@ XX
|
||||
$this->comment = false;
|
||||
return '*/';
|
||||
}
|
||||
|
||||
return '';
|
||||
|
||||
} elseif ($mod === 'ex') { // array expansion
|
||||
@@ -586,6 +591,7 @@ XX
|
||||
} else {
|
||||
$this->limit = Helpers::intVal($arg);
|
||||
}
|
||||
|
||||
return '';
|
||||
|
||||
} elseif ($mod === 'ofs') { // apply offset
|
||||
@@ -596,6 +602,7 @@ XX
|
||||
} else {
|
||||
$this->offset = Helpers::intVal($arg);
|
||||
}
|
||||
|
||||
return '';
|
||||
|
||||
} else { // default processing
|
||||
@@ -649,6 +656,7 @@ XX
|
||||
$v = $this->driver->escapeIdentifier($v);
|
||||
}
|
||||
}
|
||||
|
||||
return implode('.', $parts);
|
||||
}
|
||||
}
|
||||
|
@@ -16,16 +16,43 @@ namespace Dibi;
|
||||
class Type
|
||||
{
|
||||
public const
|
||||
TEXT = 's', // as 'string'
|
||||
BINARY = 'bin',
|
||||
Text = 's', // as 'string'
|
||||
Binary = 'bin',
|
||||
JSON = 'json',
|
||||
BOOL = 'b',
|
||||
INTEGER = 'i',
|
||||
FLOAT = 'f',
|
||||
DATE = 'd',
|
||||
DATETIME = 'dt',
|
||||
TIME = 't',
|
||||
TIME_INTERVAL = 'ti';
|
||||
Bool = 'b',
|
||||
Integer = 'i',
|
||||
Float = 'f',
|
||||
Date = 'd',
|
||||
DateTime = 'dt',
|
||||
Time = 't',
|
||||
TimeInterval = 'ti';
|
||||
|
||||
/** @deprecated use Type::Text */
|
||||
public const TEXT = self::Text;
|
||||
|
||||
/** @deprecated use Type::Binary */
|
||||
public const BINARY = self::Binary;
|
||||
|
||||
/** @deprecated use Type::Bool */
|
||||
public const BOOL = self::Bool;
|
||||
|
||||
/** @deprecated use Type::Integer */
|
||||
public const INTEGER = self::Integer;
|
||||
|
||||
/** @deprecated use Type::Float */
|
||||
public const FLOAT = self::Float;
|
||||
|
||||
/** @deprecated use Type::Date */
|
||||
public const DATE = self::Date;
|
||||
|
||||
/** @deprecated use Type::DateTime */
|
||||
public const DATETIME = self::DateTime;
|
||||
|
||||
/** @deprecated use Type::Time */
|
||||
public const TIME = self::Time;
|
||||
|
||||
/** @deprecated use Type::TimeInterval */
|
||||
public const TIME_INTERVAL = self::TimeInterval;
|
||||
|
||||
|
||||
final public function __construct()
|
||||
|
@@ -25,6 +25,7 @@ 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)
|
||||
@@ -36,38 +37,39 @@ declare(strict_types=1);
|
||||
*/
|
||||
class dibi
|
||||
{
|
||||
use Dibi\Strict;
|
||||
public const Version = '5.1-dev';
|
||||
|
||||
public const
|
||||
AFFECTED_ROWS = 'a',
|
||||
IDENTIFIER = 'n';
|
||||
/** @deprecated use dibi::Version */
|
||||
public const VERSION = self::Version;
|
||||
|
||||
/** version */
|
||||
public const
|
||||
VERSION = '4.1.3';
|
||||
/** @deprecated use Dibi\Fluent::AffectedRows */
|
||||
public const AFFECTED_ROWS = Dibi\Fluent::AffectedRows;
|
||||
|
||||
/** @deprecated use Dibi\Fluent::Identifier */
|
||||
public const IDENTIFIER = Dibi\Fluent::Identifier;
|
||||
|
||||
/** sorting order */
|
||||
public const
|
||||
ASC = 'ASC',
|
||||
DESC = 'DESC';
|
||||
|
||||
/** @var string|null Last SQL command @see dibi::query() */
|
||||
public static $sql;
|
||||
/** Last SQL command @see dibi::query() */
|
||||
public static ?string $sql = null;
|
||||
|
||||
/** @var float|null Elapsed time for last query */
|
||||
public static $elapsedTime;
|
||||
/** Elapsed time for last query */
|
||||
public static ?float $elapsedTime = null;
|
||||
|
||||
/** @var float Elapsed time for all queries */
|
||||
public static $totalTime;
|
||||
/** Elapsed time for all queries */
|
||||
public static float $totalTime = 0;
|
||||
|
||||
/** @var int Number or queries */
|
||||
public static $numOfQueries = 0;
|
||||
/** Number or queries */
|
||||
public static int $numOfQueries = 0;
|
||||
|
||||
/** @var Dibi\Connection[] Connection registry storage for Dibi\Connection objects */
|
||||
private static $registry = [];
|
||||
private static array $registry = [];
|
||||
|
||||
/** @var Dibi\Connection Current connection */
|
||||
private static $connection;
|
||||
/** Current connection */
|
||||
private static Dibi\Connection $connection;
|
||||
|
||||
|
||||
/**
|
||||
@@ -87,7 +89,7 @@ class dibi
|
||||
* @param array $config connection parameters
|
||||
* @throws Dibi\Exception
|
||||
*/
|
||||
public static function connect($config = [], string $name = '0'): Dibi\Connection
|
||||
public static function connect(array $config = [], string $name = '0'): Dibi\Connection
|
||||
{
|
||||
return self::$connection = self::$registry[$name] = new Dibi\Connection($config, $name);
|
||||
}
|
||||
@@ -106,7 +108,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) {
|
||||
@@ -150,10 +152,9 @@ 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($sql = null, bool $return = false): ?string
|
||||
public static function dump(string|Dibi\Result|null $sql = null, bool $return = false): ?string
|
||||
{
|
||||
return Dibi\Helpers::dump($sql, $return);
|
||||
}
|
||||
@@ -162,9 +163,9 @@ class dibi
|
||||
/**
|
||||
* Strips microseconds part.
|
||||
*/
|
||||
public static function stripMicroseconds(\DateTimeInterface $dt): \DateTimeInterface
|
||||
public static function stripMicroseconds(DateTimeInterface $dt): DateTimeInterface
|
||||
{
|
||||
$class = get_class($dt);
|
||||
$class = $dt::class;
|
||||
return new $class($dt->format('Y-m-d H:i:s'), $dt->getTimezone());
|
||||
}
|
||||
}
|
||||
|
@@ -11,19 +11,19 @@ namespace Dibi;
|
||||
|
||||
|
||||
/**
|
||||
* Dibi common exception.
|
||||
* A database operation failed.
|
||||
*/
|
||||
class Exception extends \Exception
|
||||
{
|
||||
/** @var string|null */
|
||||
private $sql;
|
||||
private ?string $sql;
|
||||
|
||||
|
||||
/**
|
||||
* @param int|string $code
|
||||
*/
|
||||
public function __construct(string $message = '', $code = 0, string $sql = null, \Throwable $previous = null)
|
||||
{
|
||||
public function __construct(
|
||||
string $message = '',
|
||||
int|string $code = 0,
|
||||
?string $sql = null,
|
||||
?\Throwable $previous = null,
|
||||
) {
|
||||
parent::__construct($message, 0, $previous);
|
||||
$this->code = $code;
|
||||
$this->sql = $sql;
|
||||
@@ -44,7 +44,7 @@ class Exception extends \Exception
|
||||
|
||||
|
||||
/**
|
||||
* database server exception.
|
||||
* The database server reported an error.
|
||||
*/
|
||||
class DriverException extends Exception
|
||||
{
|
||||
@@ -52,48 +52,45 @@ class DriverException extends Exception
|
||||
|
||||
|
||||
/**
|
||||
* PCRE exception.
|
||||
* Regular expression pattern or execution failed.
|
||||
*/
|
||||
class PcreException extends Exception
|
||||
{
|
||||
public function __construct(string $message = '%msg.')
|
||||
public function __construct()
|
||||
{
|
||||
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);
|
||||
parent::__construct(preg_last_error_msg(), preg_last_error());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* The requested feature is not implemented.
|
||||
*/
|
||||
class NotImplementedException extends Exception
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* The requested operation is not supported.
|
||||
*/
|
||||
class NotSupportedException extends Exception
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Database procedure exception.
|
||||
* A database stored procedure failed.
|
||||
*/
|
||||
class ProcedureException extends Exception
|
||||
{
|
||||
/** @var string */
|
||||
protected $severity;
|
||||
protected string $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;
|
||||
@@ -111,7 +108,7 @@ class ProcedureException extends Exception
|
||||
|
||||
|
||||
/**
|
||||
* Base class for all constraint violation related exceptions.
|
||||
* A database constraint was violated.
|
||||
*/
|
||||
class ConstraintViolationException extends DriverException
|
||||
{
|
||||
@@ -119,7 +116,7 @@ class ConstraintViolationException extends DriverException
|
||||
|
||||
|
||||
/**
|
||||
* Exception for a foreign key constraint violation.
|
||||
* The foreign key constraint check failed.
|
||||
*/
|
||||
class ForeignKeyConstraintViolationException extends ConstraintViolationException
|
||||
{
|
||||
@@ -127,7 +124,7 @@ class ForeignKeyConstraintViolationException extends ConstraintViolationExceptio
|
||||
|
||||
|
||||
/**
|
||||
* Exception for a NOT NULL constraint violation.
|
||||
* The NOT NULL constraint check failed.
|
||||
*/
|
||||
class NotNullConstraintViolationException extends ConstraintViolationException
|
||||
{
|
||||
@@ -135,7 +132,7 @@ class NotNullConstraintViolationException extends ConstraintViolationException
|
||||
|
||||
|
||||
/**
|
||||
* Exception for a unique constraint violation.
|
||||
* The unique constraint check failed.
|
||||
*/
|
||||
class UniqueConstraintViolationException extends ConstraintViolationException
|
||||
{
|
||||
|
@@ -51,25 +51,24 @@ 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();
|
||||
function getResource(): mixed;
|
||||
|
||||
/**
|
||||
* Returns the connection reflector.
|
||||
@@ -117,7 +116,6 @@ 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;
|
||||
@@ -142,9 +140,8 @@ interface ResultDriver
|
||||
|
||||
/**
|
||||
* Returns the result set resource.
|
||||
* @return mixed
|
||||
*/
|
||||
function getResultResource();
|
||||
function getResultResource(): mixed;
|
||||
|
||||
/**
|
||||
* Decodes data from result set.
|
||||
@@ -224,20 +221,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;
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
# for php-coveralls
|
||||
service_name: travis-ci
|
||||
service_name: github-actions
|
||||
coverage_clover: coverage.xml
|
||||
json_path: coverage.json
|
||||
|
@@ -13,12 +13,13 @@ 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"
|
||||
dsn = "mysql:host=127.0.0.1;dbname=dibi_test"
|
||||
username = root
|
||||
password = "Password12!"
|
||||
system = mysql
|
||||
|
89
tests/databases.github.ini
Normal file
89
tests/databases.github.ini
Normal file
@@ -0,0 +1,89 @@
|
||||
[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
|
@@ -13,12 +13,13 @@ 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"
|
||||
dsn = "mysql:host=127.0.0.1;dbname=dibi_test"
|
||||
username = root
|
||||
password =
|
||||
system = mysql
|
||||
|
@@ -1,38 +0,0 @@
|
||||
[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
|
@@ -12,7 +12,7 @@ use Tester\Assert;
|
||||
require __DIR__ . '/bootstrap.php';
|
||||
|
||||
|
||||
test('', function () use ($config) {
|
||||
test('immediate connection and disconnection state', function () use ($config) {
|
||||
$conn = new Connection($config);
|
||||
Assert::true($conn->isConnected());
|
||||
|
||||
@@ -21,7 +21,7 @@ test('', function () use ($config) {
|
||||
});
|
||||
|
||||
|
||||
test('lazy', function () use ($config) {
|
||||
test('lazy connection initiated on first query', function () use ($config) {
|
||||
$conn = new Connection($config + ['lazy' => true]);
|
||||
Assert::false($conn->isConnected());
|
||||
|
||||
@@ -30,7 +30,7 @@ test('lazy', function () use ($config) {
|
||||
});
|
||||
|
||||
|
||||
test('', function () use ($config) {
|
||||
test('config retrieval and driver instance access', function () use ($config) {
|
||||
$conn = new Connection($config);
|
||||
Assert::true($conn->isConnected());
|
||||
|
||||
@@ -40,7 +40,7 @@ test('', function () use ($config) {
|
||||
});
|
||||
|
||||
|
||||
test('', function () use ($config) {
|
||||
test('idempotent disconnect calls', function () use ($config) {
|
||||
$conn = new Connection($config);
|
||||
Assert::true($conn->isConnected());
|
||||
|
||||
@@ -52,7 +52,7 @@ test('', function () use ($config) {
|
||||
});
|
||||
|
||||
|
||||
test('', function () use ($config) {
|
||||
test('reconnect after disconnection', function () use ($config) {
|
||||
$conn = new Connection($config);
|
||||
Assert::equal('hello', $conn->query('SELECT %s', 'hello')->fetchSingle());
|
||||
|
||||
@@ -63,25 +63,39 @@ test('', function () use ($config) {
|
||||
});
|
||||
|
||||
|
||||
test('', function () use ($config) {
|
||||
Assert::exception(function () use ($config) {
|
||||
new Connection($config + ['onConnect' => '']);
|
||||
}, InvalidArgumentException::class, "Configuration option 'onConnect' must be array.");
|
||||
test('destructor disconnects active connection', function () use ($config) {
|
||||
$conn = new Connection($config);
|
||||
Assert::true($conn->isConnected());
|
||||
|
||||
$e = Assert::exception(function () use ($config) {
|
||||
new Connection($config + ['onConnect' => ['STOP']]);
|
||||
}, Dibi\DriverException::class);
|
||||
$conn->__destruct();
|
||||
Assert::false($conn->isConnected());
|
||||
});
|
||||
|
||||
|
||||
test('invalid onConnect option triggers exceptions', 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,
|
||||
);
|
||||
Assert::same('STOP', $e->getSql());
|
||||
|
||||
$e = Assert::exception(function () use ($config) {
|
||||
new Connection($config + ['onConnect' => [['STOP %i', 123]]]);
|
||||
}, Dibi\DriverException::class);
|
||||
$e = Assert::exception(
|
||||
fn() => 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(function () use ($conn) {
|
||||
$conn->query('SELECT 1');
|
||||
}, Dibi\DriverException::class);
|
||||
$e = Assert::exception(
|
||||
fn() => $conn->query('SELECT 1'),
|
||||
Dibi\DriverException::class,
|
||||
);
|
||||
Assert::same('STOP', $e->getSql());
|
||||
});
|
||||
|
@@ -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(),
|
||||
);
|
||||
|
||||
|
||||
|
155
tests/dibi/Connection.objectTranslator.phpt
Normal file
155
tests/dibi/Connection.objectTranslator.phpt
Normal file
@@ -0,0 +1,155 @@
|
||||
<?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.",
|
||||
);
|
||||
});
|
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Tester\Assert;
|
||||
@@ -13,27 +14,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'"),
|
||||
);
|
||||
|
||||
|
||||
@@ -42,16 +43,14 @@ $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(function ($expr) {
|
||||
return '_' . $expr . '_';
|
||||
});
|
||||
$conn->getSubstitutes()->setCallback(fn($expr) => '_' . $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'),
|
||||
);
|
||||
|
@@ -15,36 +15,131 @@ $conn = new Dibi\Connection($config);
|
||||
$conn->loadFile(__DIR__ . "/data/$config[system].sql");
|
||||
|
||||
|
||||
/*Assert::exception(function () use ($conn) {
|
||||
$conn->rollback();
|
||||
}, Dibi\Exception::class);
|
||||
/*
|
||||
Assert::exception(
|
||||
fn() => $conn->rollback(),
|
||||
Dibi\Exception::class,
|
||||
);
|
||||
|
||||
Assert::exception(function () use ($conn) {
|
||||
$conn->commit();
|
||||
}, Dibi\Exception::class);
|
||||
Assert::exception(
|
||||
fn() => $conn->commit(),
|
||||
Dibi\Exception::class,
|
||||
);
|
||||
|
||||
$conn->begin();
|
||||
Assert::exception(function () use ($conn) {
|
||||
$conn->begin();
|
||||
}, Dibi\Exception::class);
|
||||
Assert::exception(
|
||||
fn() => $conn->begin(),
|
||||
Dibi\Exception::class,
|
||||
);
|
||||
*/
|
||||
|
||||
|
||||
$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() & 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());
|
||||
});
|
||||
|
||||
|
||||
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());
|
||||
});
|
||||
|
||||
|
||||
$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',
|
||||
);
|
||||
});
|
||||
|
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Dibi\Row;
|
||||
@@ -16,7 +17,7 @@ Assert::match(
|
||||
reformat('
|
||||
SELECT *
|
||||
FROM (SELECT * FROM products) t'),
|
||||
(string) $ds
|
||||
(string) $ds,
|
||||
);
|
||||
|
||||
|
||||
@@ -24,7 +25,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,
|
||||
);
|
||||
|
||||
|
||||
@@ -38,7 +39,7 @@ FROM (SELECT * FROM products) t
|
||||
WHERE (title like '%a%')
|
||||
ORDER BY [title] DESC
|
||||
"),
|
||||
(string) $ds
|
||||
(string) $ds,
|
||||
);
|
||||
|
||||
|
||||
@@ -52,7 +53,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,
|
||||
);
|
||||
|
||||
|
||||
@@ -66,7 +67,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,
|
||||
);
|
||||
|
||||
|
||||
@@ -94,7 +95,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,
|
||||
);
|
||||
|
||||
|
||||
@@ -107,7 +108,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,
|
||||
);
|
||||
|
||||
|
||||
@@ -116,7 +117,7 @@ Assert::match(
|
||||
reformat('
|
||||
SELECT *
|
||||
FROM (SELECT [title] FROM [products]) t'),
|
||||
(string) $ds
|
||||
(string) $ds,
|
||||
);
|
||||
|
||||
Assert::equal(new Row([
|
||||
@@ -128,7 +129,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([
|
||||
@@ -153,7 +154,7 @@ Assert::match(
|
||||
reformat('
|
||||
SELECT *
|
||||
FROM [products]'),
|
||||
(string) $ds
|
||||
(string) $ds,
|
||||
);
|
||||
|
||||
Assert::same(3, $ds->count());
|
||||
|
@@ -10,12 +10,10 @@ require __DIR__ . '/bootstrap.php';
|
||||
|
||||
date_default_timezone_set('Europe/Prague');
|
||||
|
||||
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('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('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(is_int(2_544_000_000) ? 2_544_000_000 : '2544000000', (new DateTime(2_544_000_000))->getTimestamp()); // 64 bit
|
||||
|
||||
Assert::same('1978-05-05 00:00:00.000000', (string) new DateTime('1978-05-05'));
|
||||
|
21
tests/dibi/Event.source.phpt
Normal file
21
tests/dibi/Event.source.phpt
Normal file
@@ -0,0 +1,21 @@
|
||||
<?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]);
|
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Dibi\Fluent;
|
||||
|
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Tester\Assert;
|
||||
@@ -14,33 +15,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,
|
||||
);
|
||||
|
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Tester\Assert;
|
||||
@@ -56,46 +57,46 @@ $fluent = $conn->select('*')
|
||||
->orderBy('customer_id');
|
||||
|
||||
Assert::same(
|
||||
reformat('SELECT TOP (1) * FROM (SELECT * FROM [customers] ORDER BY [customer_id]) t'),
|
||||
(string) $fluent
|
||||
reformat('SELECT * FROM [customers] ORDER BY [customer_id] OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY'),
|
||||
(string) $fluent,
|
||||
);
|
||||
|
||||
|
||||
$fluent->fetch();
|
||||
Assert::same(
|
||||
'SELECT TOP (1) * FROM (SELECT * FROM [customers] ORDER BY [customer_id]) t',
|
||||
dibi::$sql
|
||||
'SELECT * FROM [customers] ORDER BY [customer_id] OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY',
|
||||
dibi::$sql,
|
||||
);
|
||||
$fluent->fetchSingle();
|
||||
Assert::same(
|
||||
reformat('SELECT TOP (1) * FROM (SELECT * FROM [customers] ORDER BY [customer_id]) t'),
|
||||
dibi::$sql
|
||||
reformat('SELECT * FROM [customers] ORDER BY [customer_id] OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY'),
|
||||
dibi::$sql,
|
||||
);
|
||||
$fluent->fetchAll(0, 3);
|
||||
Assert::same(
|
||||
reformat('SELECT TOP (3) * FROM (SELECT * FROM [customers] ORDER BY [customer_id]) t'),
|
||||
dibi::$sql
|
||||
reformat('SELECT * FROM [customers] ORDER BY [customer_id] OFFSET 0 ROWS FETCH NEXT 3 ROWS ONLY'),
|
||||
dibi::$sql,
|
||||
);
|
||||
Assert::same(
|
||||
reformat('SELECT TOP (1) * FROM (SELECT * FROM [customers] ORDER BY [customer_id]) t'),
|
||||
(string) $fluent
|
||||
reformat('SELECT * FROM [customers] ORDER BY [customer_id] OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY'),
|
||||
(string) $fluent,
|
||||
);
|
||||
|
||||
|
||||
$fluent->limit(0);
|
||||
$fluent->fetch();
|
||||
Assert::same(
|
||||
reformat('SELECT TOP (0) * FROM (SELECT * FROM [customers] ORDER BY [customer_id]) t'),
|
||||
dibi::$sql
|
||||
reformat('SELECT * FROM [customers] ORDER BY [customer_id] OFFSET 0 ROWS FETCH NEXT 0 ROWS ONLY'),
|
||||
dibi::$sql,
|
||||
);
|
||||
$fluent->fetchSingle();
|
||||
Assert::same(
|
||||
reformat('SELECT TOP (0) * FROM (SELECT * FROM [customers] ORDER BY [customer_id]) t'),
|
||||
dibi::$sql
|
||||
reformat('SELECT * FROM [customers] ORDER BY [customer_id] OFFSET 0 ROWS FETCH NEXT 0 ROWS ONLY'),
|
||||
dibi::$sql,
|
||||
);
|
||||
Assert::same(
|
||||
reformat('SELECT TOP (0) * FROM (SELECT * FROM [customers] ORDER BY [customer_id]) t'),
|
||||
(string) $fluent
|
||||
reformat('SELECT * FROM [customers] ORDER BY [customer_id] OFFSET 0 ROWS FETCH NEXT 0 ROWS ONLY'),
|
||||
(string) $fluent,
|
||||
);
|
||||
|
||||
|
||||
@@ -103,15 +104,15 @@ $fluent->removeClause('limit');
|
||||
$fluent->removeClause('offset');
|
||||
$fluent->fetch();
|
||||
Assert::same(
|
||||
reformat('SELECT TOP (1) * FROM (SELECT * FROM [customers] ORDER BY [customer_id]) t'),
|
||||
dibi::$sql
|
||||
reformat('SELECT * FROM [customers] ORDER BY [customer_id] OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY'),
|
||||
dibi::$sql,
|
||||
);
|
||||
$fluent->fetchSingle();
|
||||
Assert::same(
|
||||
reformat('SELECT TOP (1) * FROM (SELECT * FROM [customers] ORDER BY [customer_id]) t'),
|
||||
dibi::$sql
|
||||
reformat('SELECT * FROM [customers] ORDER BY [customer_id] OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY'),
|
||||
dibi::$sql,
|
||||
);
|
||||
Assert::same(
|
||||
reformat('SELECT * FROM [customers] ORDER BY [customer_id]'),
|
||||
(string) $fluent
|
||||
(string) $fluent,
|
||||
);
|
||||
|
@@ -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,
|
||||
);
|
||||
|
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Tester\Assert;
|
||||
@@ -20,40 +21,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,
|
||||
);
|
||||
|
@@ -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);
|
||||
$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,9 +147,11 @@ if ($config['system'] === 'mysql') {
|
||||
->limit(' 1; DROP TABLE users')
|
||||
->offset(' 1; DROP TABLE users');
|
||||
|
||||
Assert::error(function () use ($fluent) {
|
||||
(string) $fluent;
|
||||
}, E_USER_ERROR, "Expected number, ' 1; DROP TABLE users' given.");
|
||||
Assert::exception(
|
||||
fn() => (string) $fluent,
|
||||
Dibi\Exception::class,
|
||||
"Expected number, ' 1; DROP TABLE users' given.",
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -158,5 +160,5 @@ $fluent = $conn->select('*')->from('abc')
|
||||
|
||||
Assert::same(
|
||||
reformat('SELECT * FROM [abc] WHERE x IN ((SELECT [id] FROM [xyz]))'),
|
||||
(string) $fluent
|
||||
(string) $fluent,
|
||||
);
|
||||
|
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Tester\Assert;
|
||||
@@ -20,14 +21,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,
|
||||
);
|
||||
|
||||
|
||||
@@ -39,5 +40,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,
|
||||
);
|
||||
|
@@ -6,9 +6,7 @@ use Tester\Assert;
|
||||
|
||||
require __DIR__ . '/bootstrap.php';
|
||||
|
||||
$hash = new Dibi\HashMap(function ($v) {
|
||||
return "b-$v-e";
|
||||
});
|
||||
$hash = new Dibi\HashMap(fn($v) => "b-$v-e");
|
||||
|
||||
Assert::same('b-X-e', $hash->{'X'});
|
||||
Assert::same('b--e', $hash->{''});
|
||||
|
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Dibi\Helpers;
|
||||
|
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Dibi\Helpers;
|
||||
@@ -11,22 +12,32 @@ Assert::same(0, Helpers::intVal(0));
|
||||
Assert::same(0, Helpers::intVal('0'));
|
||||
Assert::same(-10, Helpers::intVal('-10'));
|
||||
|
||||
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('-12345678901234567890123456879'),
|
||||
Dibi\Exception::class,
|
||||
'Number -12345678901234567890123456879 is greater than integer.',
|
||||
);
|
||||
|
||||
Assert::exception(function () {
|
||||
Helpers::intVal('');
|
||||
}, Dibi\Exception::class, "Expected number, '' given.");
|
||||
Assert::exception(
|
||||
fn() => Helpers::intVal(''),
|
||||
Dibi\Exception::class,
|
||||
"Expected number, '' given.",
|
||||
);
|
||||
|
||||
Assert::exception(function () {
|
||||
Helpers::intVal('not number');
|
||||
}, Dibi\Exception::class, "Expected number, 'not number' given.");
|
||||
Assert::exception(
|
||||
fn() => Helpers::intVal('not number'),
|
||||
Dibi\Exception::class,
|
||||
"Expected number, 'not number' given.",
|
||||
);
|
||||
|
||||
Assert::exception(function () {
|
||||
Helpers::intVal(null);
|
||||
}, Dibi\Exception::class, "Expected number, '' given.");
|
||||
Assert::exception(
|
||||
fn() => Helpers::intVal(null),
|
||||
Dibi\Exception::class,
|
||||
"Expected number, '' given.",
|
||||
);
|
||||
|
@@ -13,22 +13,28 @@ 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(function () {
|
||||
buildPdoDriver(PDO::ERRMODE_EXCEPTION);
|
||||
}, Dibi\DriverException::class, 'PDO connection in exception or warning error mode is not supported.');
|
||||
Assert::exception(
|
||||
fn() => buildPdoDriver(PDO::ERRMODE_EXCEPTION),
|
||||
Dibi\DriverException::class,
|
||||
'PDO connection in exception or warning error mode is not supported.',
|
||||
);
|
||||
|
||||
|
||||
// PDO error mode: warning
|
||||
Assert::exception(function () {
|
||||
buildPdoDriver(PDO::ERRMODE_WARNING);
|
||||
}, Dibi\DriverException::class, 'PDO connection in exception or warning error mode is not supported.');
|
||||
Assert::exception(
|
||||
fn() => 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', function () {
|
||||
buildPdoDriver(PDO::ERRMODE_SILENT);
|
||||
});
|
||||
test(
|
||||
'PDO error mode: explicitly set silent',
|
||||
fn() => buildPdoDriver(PDO::ERRMODE_SILENT),
|
||||
);
|
||||
|
@@ -18,9 +18,9 @@ $tests = function ($conn) {
|
||||
Assert::false($conn->query("SELECT 'AAxBB' LIKE %~like~", 'A%B')->fetchSingle());
|
||||
Assert::true($conn->query("SELECT 'AA%BB' LIKE %~like~", 'A%B')->fetchSingle());
|
||||
|
||||
Assert::same('AA\\BB', $conn->query("SELECT 'AA\\BB'")->fetchSingle());
|
||||
Assert::false($conn->query("SELECT 'AAxBB' LIKE %~like~", 'A\\B')->fetchSingle());
|
||||
Assert::true($conn->query("SELECT 'AA\\BB' LIKE %~like~", 'A\\B')->fetchSingle());
|
||||
Assert::same('AA\BB', $conn->query("SELECT 'AA\\BB'")->fetchSingle());
|
||||
Assert::false($conn->query("SELECT 'AAxBB' LIKE %~like~", 'A\B')->fetchSingle());
|
||||
Assert::true($conn->query("SELECT 'AA\\BB' LIKE %~like~", 'A\B')->fetchSingle());
|
||||
};
|
||||
|
||||
$conn = new Dibi\Connection($config);
|
||||
|
@@ -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', 'pdo', 'sqlsrv'], true)) {
|
||||
if (!in_array($config['driver'], ['sqlite', 'sqlite3', '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', 'pdo', 'sqlsrv'], true)) {
|
||||
$columns = $info->getColumns();
|
||||
|
||||
Assert::same('product_id', $columns[0]->getName());
|
||||
if (!in_array($config['driver'], ['sqlite', 'pdo', 'sqlsrv'], true)) {
|
||||
if (!in_array($config['driver'], ['sqlite', 'sqlite3', 'pdo', 'sqlsrv'], true)) {
|
||||
Assert::same('products', $columns[0]->getTableName());
|
||||
}
|
||||
Assert::null($columns[0]->getVendorInfo('xxx'));
|
||||
|
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Dibi\Type;
|
||||
@@ -24,9 +25,20 @@ class MockResult extends Dibi\Result
|
||||
}
|
||||
|
||||
|
||||
test('', function () {
|
||||
test('native text conversion preserves boolean values', function () {
|
||||
$result = new MockResult;
|
||||
$result->setType('col', Type::BOOL);
|
||||
$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('boolean conversion from diverse representations', function () {
|
||||
$result = new MockResult;
|
||||
$result->setType('col', Type::Bool);
|
||||
|
||||
Assert::same(['col' => null], $result->test(['col' => null]));
|
||||
Assert::same(['col' => true], $result->test(['col' => true]));
|
||||
@@ -46,9 +58,9 @@ test('', function () {
|
||||
});
|
||||
|
||||
|
||||
test('', function () {
|
||||
test('text conversion of booleans and numerics', function () {
|
||||
$result = new MockResult;
|
||||
$result->setType('col', Type::TEXT);
|
||||
$result->setType('col', Type::Text);
|
||||
|
||||
Assert::same(['col' => null], $result->test(['col' => null]));
|
||||
Assert::same(['col' => '1'], $result->test(['col' => true]));
|
||||
@@ -62,9 +74,9 @@ test('', function () {
|
||||
});
|
||||
|
||||
|
||||
test('', function () {
|
||||
test('float conversion with various numeric formats', function () {
|
||||
$result = new MockResult;
|
||||
$result->setType('col', Type::FLOAT);
|
||||
$result->setType('col', Type::Float);
|
||||
|
||||
Assert::same(['col' => null], $result->test(['col' => null]));
|
||||
Assert::same(['col' => 1.0], $result->test(['col' => true]));
|
||||
@@ -105,6 +117,37 @@ test('', function () {
|
||||
Assert::same(['col' => '1.1e+10'], $result->test(['col' => '001.1e+10']));
|
||||
Assert::notSame(['col' => '1.1e+1'], $result->test(['col' => '1.1e+10']));
|
||||
|
||||
// negative
|
||||
Assert::same(['col' => -0.0], $result->test(['col' => '-']));
|
||||
Assert::same(['col' => -0.0], $result->test(['col' => '-0']));
|
||||
Assert::same(['col' => -1.0], $result->test(['col' => '-1']));
|
||||
Assert::same(['col' => -0.0], $result->test(['col' => '-.0']));
|
||||
Assert::same(['col' => -0.1], $result->test(['col' => '-.1']));
|
||||
Assert::same(['col' => -0.0], $result->test(['col' => '-0.0']));
|
||||
Assert::same(['col' => -0.1], $result->test(['col' => '-0.1']));
|
||||
Assert::same(['col' => -0.0], $result->test(['col' => '-0.000']));
|
||||
Assert::same(['col' => -0.1], $result->test(['col' => '-0.100']));
|
||||
Assert::same(['col' => -1.0], $result->test(['col' => '-1.0']));
|
||||
Assert::same(['col' => -1.1], $result->test(['col' => '-1.1']));
|
||||
Assert::same(['col' => -1.0], $result->test(['col' => '-1.000']));
|
||||
Assert::same(['col' => -1.1], $result->test(['col' => '-1.100']));
|
||||
Assert::same(['col' => -1.0], $result->test(['col' => '-001.000']));
|
||||
Assert::same(['col' => -1.1], $result->test(['col' => '-001.100']));
|
||||
Assert::same(['col' => -10.0], $result->test(['col' => '-10']));
|
||||
Assert::same(['col' => -11.0], $result->test(['col' => '-11']));
|
||||
Assert::same(['col' => -10.0], $result->test(['col' => '-0010']));
|
||||
Assert::same(['col' => -11.0], $result->test(['col' => '-0011']));
|
||||
Assert::same(['col' => '-0.00000000000000000001'], $result->test(['col' => '-0.00000000000000000001']));
|
||||
Assert::same(['col' => '-12345678901234567890'], $result->test(['col' => '-12345678901234567890']));
|
||||
Assert::same(['col' => '-12345678901234567890'], $result->test(['col' => '-012345678901234567890']));
|
||||
Assert::same(['col' => '-12345678901234567890'], $result->test(['col' => '-12345678901234567890.000']));
|
||||
Assert::same(['col' => '-12345678901234567890.1'], $result->test(['col' => '-012345678901234567890.100']));
|
||||
|
||||
Assert::same(['col' => '-1.1e+10'], $result->test(['col' => '-1.1e+10']));
|
||||
Assert::same(['col' => '-1.1e-10'], $result->test(['col' => '-1.1e-10']));
|
||||
Assert::same(['col' => '-1.1e+10'], $result->test(['col' => '-001.1e+10']));
|
||||
Assert::notSame(['col' => '-1.1e+1'], $result->test(['col' => '-1.1e+10']));
|
||||
|
||||
setlocale(LC_ALL, 'de_DE@euro', 'de_DE', 'deu_deu');
|
||||
Assert::same(['col' => 0.0], $result->test(['col' => '']));
|
||||
Assert::same(['col' => 0.0], $result->test(['col' => '0']));
|
||||
@@ -135,25 +178,54 @@ test('', function () {
|
||||
Assert::same(['col' => 0.0], $result->test(['col' => 0.0]));
|
||||
Assert::same(['col' => 1.0], $result->test(['col' => 1]));
|
||||
Assert::same(['col' => 1.0], $result->test(['col' => 1.0]));
|
||||
|
||||
// Same but negative
|
||||
Assert::same(['col' => -0.0], $result->test(['col' => '-']));
|
||||
Assert::same(['col' => -0.0], $result->test(['col' => '-0']));
|
||||
Assert::same(['col' => -1.0], $result->test(['col' => '-1']));
|
||||
Assert::same(['col' => -0.0], $result->test(['col' => '-.0']));
|
||||
Assert::same(['col' => -0.1], $result->test(['col' => '-.1']));
|
||||
Assert::same(['col' => -0.0], $result->test(['col' => '-0.0']));
|
||||
Assert::same(['col' => -0.1], $result->test(['col' => '-0.1']));
|
||||
Assert::same(['col' => -0.0], $result->test(['col' => '-0.000']));
|
||||
Assert::same(['col' => -0.1], $result->test(['col' => '-0.100']));
|
||||
Assert::same(['col' => -1.0], $result->test(['col' => '-1.0']));
|
||||
Assert::same(['col' => -1.1], $result->test(['col' => '-1.1']));
|
||||
Assert::same(['col' => -1.0], $result->test(['col' => '-1.000']));
|
||||
Assert::same(['col' => -1.1], $result->test(['col' => '-1.100']));
|
||||
Assert::same(['col' => -1.0], $result->test(['col' => '-001.000']));
|
||||
Assert::same(['col' => -1.1], $result->test(['col' => '-001.100']));
|
||||
Assert::same(['col' => -10.0], $result->test(['col' => '-10']));
|
||||
Assert::same(['col' => -11.0], $result->test(['col' => '-11']));
|
||||
Assert::same(['col' => -10.0], $result->test(['col' => '-0010']));
|
||||
Assert::same(['col' => -11.0], $result->test(['col' => '-0011']));
|
||||
Assert::same(['col' => '-0.00000000000000000001'], $result->test(['col' => '-0.00000000000000000001']));
|
||||
Assert::same(['col' => '-12345678901234567890'], $result->test(['col' => '-12345678901234567890']));
|
||||
Assert::same(['col' => '-12345678901234567890'], $result->test(['col' => '-012345678901234567890']));
|
||||
Assert::same(['col' => '-12345678901234567890'], $result->test(['col' => '-12345678901234567890.000']));
|
||||
Assert::same(['col' => '-12345678901234567890.1'], $result->test(['col' => '-012345678901234567890.100']));
|
||||
|
||||
Assert::same(['col' => -0.0], $result->test(['col' => -0]));
|
||||
Assert::same(['col' => -0.0], $result->test(['col' => -0.0]));
|
||||
Assert::same(['col' => -1.0], $result->test(['col' => -1]));
|
||||
Assert::same(['col' => -1.0], $result->test(['col' => -1.0]));
|
||||
|
||||
setlocale(LC_NUMERIC, 'C');
|
||||
});
|
||||
|
||||
|
||||
test('', function () {
|
||||
test('strict integer conversion with error on empty string', function () {
|
||||
$result = new MockResult;
|
||||
$result->setType('col', Type::INTEGER);
|
||||
$result->setType('col', Type::Integer);
|
||||
|
||||
Assert::same(['col' => null], $result->test(['col' => null]));
|
||||
Assert::same(['col' => 1], $result->test(['col' => true]));
|
||||
Assert::same(['col' => 0], $result->test(['col' => false]));
|
||||
|
||||
if (PHP_VERSION_ID < 80000) {
|
||||
Assert::same(['col' => 0], @$result->test(['col' => ''])); // triggers warning since PHP 7.1
|
||||
} else {
|
||||
Assert::exception(function () use ($result) {
|
||||
Assert::same(['col' => 0], $result->test(['col' => '']));
|
||||
}, TypeError::class);
|
||||
}
|
||||
Assert::exception(
|
||||
fn() => Assert::same(['col' => 0], $result->test(['col' => ''])),
|
||||
TypeError::class,
|
||||
);
|
||||
|
||||
Assert::same(['col' => 0], $result->test(['col' => '0']));
|
||||
Assert::same(['col' => 1], $result->test(['col' => '1']));
|
||||
@@ -172,14 +244,15 @@ test('', function () {
|
||||
});
|
||||
|
||||
|
||||
test('', function () {
|
||||
test('dateTime conversion with object instantiation', function () {
|
||||
$result = new MockResult;
|
||||
$result->setType('col', Type::DATETIME);
|
||||
$result->setType('col', Type::DateTime);
|
||||
|
||||
Assert::same(['col' => null], $result->test(['col' => null]));
|
||||
Assert::exception(function () use ($result) {
|
||||
$result->test(['col' => true]);
|
||||
}, TypeError::class);
|
||||
Assert::exception(
|
||||
fn() => $result->test(['col' => true]),
|
||||
TypeError::class,
|
||||
);
|
||||
Assert::same(['col' => null], $result->test(['col' => false]));
|
||||
|
||||
Assert::same(['col' => null], $result->test(['col' => '']));
|
||||
@@ -190,15 +263,16 @@ test('', function () {
|
||||
});
|
||||
|
||||
|
||||
test('', function () {
|
||||
test('dateTime conversion using custom format', function () {
|
||||
$result = new MockResult;
|
||||
$result->setType('col', Type::DATETIME);
|
||||
$result->setFormat(Type::DATETIME, 'Y-m-d H:i:s');
|
||||
$result->setType('col', Type::DateTime);
|
||||
$result->setFormat(Type::DateTime, 'Y-m-d H:i:s');
|
||||
|
||||
Assert::same(['col' => null], $result->test(['col' => null]));
|
||||
Assert::exception(function () use ($result) {
|
||||
$result->test(['col' => true]);
|
||||
}, TypeError::class);
|
||||
Assert::exception(
|
||||
fn() => $result->test(['col' => true]),
|
||||
TypeError::class,
|
||||
);
|
||||
Assert::same(['col' => null], $result->test(['col' => false]));
|
||||
|
||||
Assert::same(['col' => null], $result->test(['col' => '']));
|
||||
@@ -209,14 +283,15 @@ test('', function () {
|
||||
});
|
||||
|
||||
|
||||
test('', function () {
|
||||
test('date conversion to DateTime instance', function () {
|
||||
$result = new MockResult;
|
||||
$result->setType('col', Type::DATE);
|
||||
$result->setType('col', Type::Date);
|
||||
|
||||
Assert::same(['col' => null], $result->test(['col' => null]));
|
||||
Assert::exception(function () use ($result) {
|
||||
$result->test(['col' => true]);
|
||||
}, TypeError::class);
|
||||
Assert::exception(
|
||||
fn() => $result->test(['col' => true]),
|
||||
TypeError::class,
|
||||
);
|
||||
Assert::same(['col' => null], $result->test(['col' => false]));
|
||||
|
||||
Assert::same(['col' => null], $result->test(['col' => '']));
|
||||
@@ -225,14 +300,15 @@ test('', function () {
|
||||
});
|
||||
|
||||
|
||||
test('', function () {
|
||||
test('time conversion to DateTime instance', function () {
|
||||
$result = new MockResult;
|
||||
$result->setType('col', Type::TIME);
|
||||
$result->setType('col', Type::Time);
|
||||
|
||||
Assert::same(['col' => null], $result->test(['col' => null]));
|
||||
Assert::exception(function () use ($result) {
|
||||
$result->test(['col' => true]);
|
||||
}, TypeError::class);
|
||||
Assert::exception(
|
||||
fn() => $result->test(['col' => true]),
|
||||
TypeError::class,
|
||||
);
|
||||
Assert::same(['col' => null], $result->test(['col' => false]));
|
||||
|
||||
Assert::same(['col' => null], $result->test(['col' => '']));
|
||||
|
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Tester\Assert;
|
||||
@@ -11,7 +12,7 @@ $conn->loadFile(__DIR__ . "/data/$config[system].sql");
|
||||
$res = $conn->query('SELECT * FROM [customers]');
|
||||
|
||||
// auto-converts this column to integer
|
||||
$res->setType('customer_id', Dibi\Type::DATETIME);
|
||||
$res->setType('customer_id', Dibi\Type::DateTime);
|
||||
|
||||
Assert::equal(new Dibi\Row([
|
||||
'customer_id' => new Dibi\DateTime('1970-01-01 01:00:01'),
|
||||
|
@@ -24,26 +24,38 @@ Assert::true(isset($row['title']));
|
||||
|
||||
|
||||
// missing
|
||||
Assert::error(function () use ($row) {
|
||||
$x = $row->missing;
|
||||
}, E_USER_NOTICE, "Attempt to read missing column 'missing'.");
|
||||
Assert::error(
|
||||
fn() => $x = $row->missing,
|
||||
E_USER_NOTICE,
|
||||
"Attempt to read missing column 'missing'.",
|
||||
);
|
||||
|
||||
Assert::error(function () use ($row) {
|
||||
$x = $row['missing'];
|
||||
}, E_USER_NOTICE, "Attempt to read missing column 'missing'.");
|
||||
Assert::error(
|
||||
fn() => $x = $row['missing'],
|
||||
E_USER_NOTICE,
|
||||
"Attempt to read missing column 'missing'.",
|
||||
);
|
||||
|
||||
Assert::false(isset($row->missing));
|
||||
Assert::false(isset($row['missing']));
|
||||
|
||||
// ??
|
||||
Assert::same(123, $row->missing ?? 123);
|
||||
Assert::same(123, $row['missing'] ?? 123);
|
||||
|
||||
|
||||
// suggestions
|
||||
Assert::error(function () use ($row) {
|
||||
$x = $row->tilte;
|
||||
}, E_USER_NOTICE, "Attempt to read missing column 'tilte', did you mean 'title'?");
|
||||
Assert::error(
|
||||
fn() => $x = $row->tilte,
|
||||
E_USER_NOTICE,
|
||||
"Attempt to read missing column 'tilte', did you mean 'title'?",
|
||||
);
|
||||
|
||||
Assert::error(function () use ($row) {
|
||||
$x = $row['tilte'];
|
||||
}, E_USER_NOTICE, "Attempt to read missing column 'tilte', did you mean 'title'?");
|
||||
Assert::error(
|
||||
fn() => $x = $row['tilte'],
|
||||
E_USER_NOTICE,
|
||||
"Attempt to read missing column 'tilte', did you mean 'title'?",
|
||||
);
|
||||
|
||||
|
||||
// to array
|
||||
|
@@ -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');
|
||||
|
@@ -11,82 +11,59 @@ use Tester\Assert;
|
||||
require __DIR__ . '/bootstrap.php';
|
||||
|
||||
$tests = function ($conn) {
|
||||
$resource = $conn->getDriver()->getResource();
|
||||
$version = is_resource($resource)
|
||||
? sqlsrv_server_info($resource)['SQLServerVersion']
|
||||
: $resource->getAttribute(PDO::ATTR_SERVER_VERSION);
|
||||
// Limit and offset
|
||||
Assert::same(
|
||||
'SELECT 1 OFFSET 10 ROWS FETCH NEXT 10 ROWS ONLY',
|
||||
$conn->translate('SELECT 1 %ofs %lmt', 10, 10),
|
||||
);
|
||||
|
||||
// MsSQL2012+
|
||||
if (version_compare($version, '11.0') >= 0) {
|
||||
// Limit and offset
|
||||
Assert::same(
|
||||
'SELECT 1 OFFSET 10 ROWS FETCH NEXT 10 ROWS ONLY',
|
||||
$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),
|
||||
);
|
||||
|
||||
// Limit only
|
||||
Assert::same(
|
||||
'SELECT 1 OFFSET 0 ROWS FETCH NEXT 10 ROWS ONLY',
|
||||
$conn->translate('SELECT 1 %lmt', 10)
|
||||
);
|
||||
// Offset only
|
||||
Assert::same(
|
||||
'SELECT 1 OFFSET 10 ROWS',
|
||||
$conn->translate('SELECT 1 %ofs', 10),
|
||||
);
|
||||
|
||||
// Offset only
|
||||
Assert::same(
|
||||
'SELECT 1 OFFSET 10 ROWS',
|
||||
$conn->translate('SELECT 1 %ofs', 10)
|
||||
);
|
||||
// Offset invalid
|
||||
Assert::error(
|
||||
function () use ($conn) {
|
||||
$conn->translate('SELECT 1 %ofs', -10);
|
||||
},
|
||||
Dibi\NotSupportedException::class,
|
||||
'Negative offset or limit.',
|
||||
);
|
||||
|
||||
// Offset invalid
|
||||
Assert::error(
|
||||
function () use ($conn) {
|
||||
$conn->translate('SELECT 1 %ofs', -10);
|
||||
},
|
||||
Dibi\NotSupportedException::class,
|
||||
'Negative offset or limit.'
|
||||
);
|
||||
// Limit invalid
|
||||
Assert::error(
|
||||
function () use ($conn) {
|
||||
$conn->translate('SELECT 1 %lmt', -10);
|
||||
},
|
||||
Dibi\NotSupportedException::class,
|
||||
'Negative offset or limit.',
|
||||
);
|
||||
|
||||
// Limit invalid
|
||||
Assert::error(
|
||||
function () use ($conn) {
|
||||
$conn->translate('SELECT 1 %lmt', -10);
|
||||
},
|
||||
Dibi\NotSupportedException::class,
|
||||
'Negative offset or limit.'
|
||||
);
|
||||
// Limit invalid, offset valid
|
||||
Assert::error(
|
||||
function () use ($conn) {
|
||||
$conn->translate('SELECT 1 %ofs %lmt', 10, -10);
|
||||
},
|
||||
Dibi\NotSupportedException::class,
|
||||
'Negative offset or limit.',
|
||||
);
|
||||
|
||||
// Limit invalid, offset valid
|
||||
Assert::error(
|
||||
function () use ($conn) {
|
||||
$conn->translate('SELECT 1 %ofs %lmt', 10, -10);
|
||||
},
|
||||
Dibi\NotSupportedException::class,
|
||||
'Negative offset or limit.'
|
||||
);
|
||||
|
||||
// Limit valid, offset invalid
|
||||
Assert::error(
|
||||
function () use ($conn) {
|
||||
$conn->translate('SELECT 1 %ofs %lmt', -10, 10);
|
||||
},
|
||||
Dibi\NotSupportedException::class,
|
||||
'Negative offset or limit.'
|
||||
);
|
||||
} else {
|
||||
Assert::same(
|
||||
'SELECT TOP (1) * FROM (SELECT 1) t',
|
||||
$conn->translate('SELECT 1 %lmt', 1)
|
||||
);
|
||||
|
||||
Assert::same(
|
||||
'SELECT 1',
|
||||
$conn->translate('SELECT 1 %lmt', -10)
|
||||
);
|
||||
|
||||
Assert::exception(
|
||||
$conn->translate('SELECT 1 %ofs %lmt', 10, 10),
|
||||
Dibi\NotSupportedException::class
|
||||
);
|
||||
}
|
||||
// Limit valid, offset invalid
|
||||
Assert::error(
|
||||
function () use ($conn) {
|
||||
$conn->translate('SELECT 1 %ofs %lmt', -10, 10);
|
||||
},
|
||||
Dibi\NotSupportedException::class,
|
||||
'Negative offset or limit.',
|
||||
);
|
||||
};
|
||||
|
||||
$conn = new Dibi\Connection($config);
|
||||
|
@@ -1,150 +0,0 @@
|
||||
<?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));
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user