1
0
mirror of https://github.com/dg/dibi.git synced 2025-08-30 17:29:53 +02:00

Compare commits

..

90 Commits

Author SHA1 Message Date
Marek Štípek
7fa05f381b Event: detecting source without filesystem check (#428)
Co-authored-by: Marek Stipek <stipek@shoptet.cz>
2022-11-18 04:40:51 +01:00
David Grudl
3a962de553 cs 2022-10-13 03:58:51 +02:00
David Grudl
96e370b8fe updated github workflow 2022-10-13 03:51:11 +02:00
Jirka Hrazdil
ec0455ae00 Panel: typo (#421) 2022-09-06 04:32:10 +02:00
David Grudl
b61737311e support for PHP 8.2 2022-09-06 04:26:58 +02:00
David Grudl
9d5d430d3d Released version 4.2.6 2022-01-19 18:38:15 +01:00
Miloslav Hůla
04bb5ede3d Translator: convert BackedEnum to scalar
also closes (#412)
2022-01-18 16:15:30 +01:00
Andrej Rypo
7d82ce2ff6 MySqliDriver::getResource() fixed access to resource being closed prior to the call in PHP 8 (#410) 2021-12-12 18:00:04 +01:00
David Grudl
82150d120d cs nullable typehints 2021-12-12 17:46:56 +01:00
David Grudl
0f045c0986 cs whitespace 2021-12-12 03:52:44 +01:00
David Grudl
5646884899 cs 2021-12-12 03:52:44 +01:00
David Grudl
af33a354d6 removed ecs.php 2021-12-12 03:52:44 +01:00
David Grudl
a0c86747dc GitHub Actions updates 2021-12-06 19:05:06 +01:00
David Grudl
d70e274244 Released version 4.2.5 2021-11-29 13:48:16 +01:00
David Grudl
e05eb01233 fixed PHP 7.2 compatibility 2021-11-28 15:09:41 +01:00
David Grudl
2ac618ffff Date 0000-01-01 is valid [Closes #402] 2021-11-24 18:34:20 +01:00
Milan Otáhal
1881fea0e5 Profiler is not used in CLI mode 2021-11-24 18:34:20 +01:00
Jan Rössler
cb82357cfb Helpers::detectType(): detect PostgreSQL range types as Type::TEXT 2021-11-24 18:34:20 +01:00
Jan Rössler
0a29fcb502 PostgreReflector: fix reflection of matview columns on PostgreSQL 12+ 2021-11-24 18:34:20 +01:00
David Grudl
8270b7c1c3 support for PHP 8.1 2021-11-24 18:34:20 +01:00
David Grudl
73e16eb1a3 Released version 4.2.3 2021-07-23 10:49:27 +02:00
David Grudl
0b394a993d SqlsrvDriver: fixed after 40ad77cf [Closes #391][Closes #392] 2021-06-30 12:47:45 +02:00
David Grudl
df3edee70b removed travis 2021-04-23 20:54:50 +02:00
David Grudl
245da39a9f added github workflows 2021-04-23 20:53:38 +02:00
David Grudl
3df64fc3b3 fixed tests 2021-04-23 20:48:29 +02:00
David Grudl
95c3f72a17 Released version 4.2.2 2021-04-21 13:56:00 +02:00
David Grudl
d71caf0c75 Row: fixed ?? usage 2021-04-21 13:54:59 +02:00
Miloslav Hůla
3066fea2aa Connection: begin(), commit() & rollback() calls are forbidden in transaction() 2021-04-05 16:50:15 +02:00
Miloslav Hůla
b00e556289 Connection::transtaction() call can be nested 2021-04-05 16:50:15 +02:00
Miloslav Hůla
877dffd460 tests: use test() helper 2021-04-05 13:37:16 +02:00
Miloslav Hůla
7049949b14 Connection::transaction(): pass self as a callback argument 2021-04-05 13:37:16 +02:00
Miloslav Hůla
771e846a62 tests: Sqlite3 driver fix 2021-04-05 13:37:16 +02:00
David Grudl
4e056c52dd updated appveyor.yml 2021-03-10 16:53:34 +01:00
David Grudl
40ad77cf5f SqlsrvDriver: workaround for "Driver's SQLSetConnectAttr failed on ODBC <=13" bug 2021-03-10 16:42:33 +01:00
David Grudl
d09b462eef Released version 4.2.1 2021-03-01 15:17:15 +01:00
David Grudl
4c796a0e0f readme: added support me 2021-02-05 21:53:00 +01:00
Jan Kuchař
304af5185d PostgreSQL driver: escaping of save point name (#383) 2021-02-05 21:52:45 +01:00
David Grudl
e046935137 Strict: refactoring 2020-12-30 12:44:04 +01:00
Jan Kuchař
1a77048225 PostgreReflector: removed version check (#381) 2020-12-23 10:31:03 +01:00
David Grudl
ca74488636 Released version 4.2.0 2020-11-25 21:02:22 +01:00
David Grudl
07f994a0b5 added Connection::transaction() 2020-11-25 20:59:58 +01:00
David Grudl
5fa5acb724 Connection: added option [result][formatTimeInterval] that sets time-interval column decoding 2020-11-02 15:28:28 +01:00
David Grudl
98563d8165 Connection: added option [result][normalize] [Closes #367] 2020-11-02 15:28:28 +01:00
David Grudl
c464960239 Connection, Result: added format 'native' 2020-11-02 15:28:28 +01:00
David Grudl
a9e90d0b22 Connection, Results: refactorings, added Result::setFormats() 2020-11-02 15:28:28 +01:00
David Grudl
decb30de1e Connection::translateArg() removed (BC break) 2020-11-02 15:28:28 +01:00
David Grudl
f4e71e8855 requires PHP 7.2 2020-11-02 15:28:28 +01:00
David Grudl
39f59e0f08 opened 4.2-dev 2020-11-02 15:28:28 +01:00
David Grudl
0d2f643795 strict comparison 2020-11-02 15:27:54 +01:00
David Grudl
70d4246866 Tracy\Panel: table is sortable 2020-11-02 15:05:55 +01:00
David Grudl
34a1665915 coding style 2020-11-02 15:05:55 +01:00
David Grudl
b27db4a9aa updated gitattributes 2020-11-02 15:05:55 +01:00
David Grudl
212dd1ae55 updated phpstan 2020-11-02 15:05:55 +01:00
David Grudl
b9683f8a3c updated nette/coding-standard 2020-11-02 15:05:55 +01:00
David Grudl
177a800bff PdoDriver: changes error mode do ERRMODE_SILENT (for PHP 8.0) 2020-11-02 15:05:55 +01:00
David Grudl
fa6a1203a9 fixed compatibility with PHP 8 [Closes #379] 2020-11-02 15:05:55 +01:00
David Grudl
3f7171c7a4 tested on PHP 8.0 2020-10-30 13:50:54 +01:00
David Grudl
e4b6e769ee Dibi\Helpers::getSuggestion(): item may be an int, type cast fix (#378) 2020-10-14 12:00:23 +02:00
David Grudl
24d0b069d8 Helpers::getSuggestion(): support for reflection objects moved to Strict 2020-10-14 12:00:04 +02:00
David Grudl
34bc742245 refactoring 2020-10-08 16:49:47 +02:00
Miloslav Hůla
f5fa2255ff FileLogger: fixed object to string conversion with custom driver (#376) (#376)
Co-authored-by: Miloslav Hůla <miloslav.hula@fsv.cvut.cz>
2020-10-08 16:38:35 +02:00
Miloslav Hůla
7b02296f3e Tracy\Panel: fixed object to string converion error with custom driver (#373)
Co-authored-by: Miloslav Hůla <miloslav.hula@fsv.cvut.cz>
2020-10-08 16:38:35 +02:00
David Grudl
df4cddac1f Tracy\Panel: supports multiple connections [Closes #365]
Partially reverts "Tracy\Panel: one panel is used per connection"

This reverts commit ae6c8756b6.
2020-10-08 16:38:35 +02:00
groupnet
cc37121390 Postgre driver - add connect_type config parameter (#370)
- adds the option to pass the connect_type as config parameter
 - PHP default is 0, but to make this change non-breaking defauls to PGSQL_CONNECT_FORCE_NEW
 - see PHP docs: https://www.php.net/manual/en/function.pg-connect.php
2020-10-08 15:55:11 +02:00
LHavlicek
a95b409231 MySqliDriver: fixed DateInterval encoding (#371) 2020-10-08 15:55:11 +02:00
Jakub Bouček
3b057c2e35 MySqliDriver: added support for ping (#372) 2020-10-08 15:55:11 +02:00
David Grudl
f444b5d993 typo 2020-10-08 15:55:11 +02:00
David Grudl
6e41c4223b tests: test() with description 2020-10-08 15:55:11 +02:00
David Grudl
0ee4628712 SqlsrvDriver: improved escapeBinary [Closes #287] 2020-10-07 03:19:03 +02:00
David Grudl
ab3677203c added funding.yml 2020-10-07 03:19:03 +02:00
Jan Barášek
1bdf6e93d0 PhpStan fixes (#363) 2020-05-07 21:41:28 +02:00
David Grudl
ed2a827419 Released version 4.1.3 2020-03-26 04:10:39 +01:00
David Grudl
e46be6cee6 SqliteResult: workaround for PHP bug 79414 2020-03-26 04:10:39 +01:00
David Grudl
0f69d5d32c Result: does not drop the value if detection fails 2020-03-26 04:10:39 +01:00
Adam Klvač
e826e3a719 MySqliDriver: coalesced password to an empty string (#360)
mysqli::real_connect() expects parameter 3 to be string, yields an error on NULL.
2020-03-26 03:32:03 +01:00
David Grudl
6eac117f5f Result::fetchAssoc() DateTime in key is converted to string [Closes #359] 2020-03-03 17:18:37 +01:00
David Grudl
2a2c814b0a readme.md: updated 2020-03-03 16:50:43 +01:00
David Grudl
dfab3d711c added DummyDriver 2020-03-03 16:21:21 +01:00
David Grudl
34e16031f7 added phpstan.neon 2020-02-23 19:15:59 +01:00
David Grudl
73160e9418 travis: uses PHP 7.4 for coding checks 2020-02-23 19:08:21 +01:00
David Grudl
f18056a066 Released version 4.1.2 2020-02-23 18:50:27 +01:00
Milan Pála
0bd222b3f1 tests: added reconnect test (#352) 2020-02-23 18:50:27 +01:00
David Grudl
9f71f39470 Connection: translator is created/destructed in connect/disconnect [Closes #352][Closes #354] 2020-02-23 18:50:27 +01:00
Enrico Dias
0b0d805040 FileLogger: refactoring (#351) 2020-02-09 17:22:55 +01:00
Enrico Dias
8c761eac5c FileLogger: Add option to log errors only (#351) 2020-02-09 17:17:05 +01:00
groupnet
2f857c28d6 PostgreDriver: fixed persistent connections (#348)
- https://www.php.net/manual/en/function.pg-pconnect.php
2020-01-24 13:41:57 +01:00
David Grudl
efe1cbdc20 cs 2020-01-12 13:46:23 +01:00
David Grudl
21dad1d846 composer: license clarification 2020-01-07 11:53:19 +01:00
David Grudl
7d55fd03b0 composer: added PHPStan 2019-12-11 21:05:37 +01:00
Ashus
294787a26e Add support for escapeLike without % on any side (#346) (BC break) 2019-11-25 14:01:24 +01:00
91 changed files with 1608 additions and 681 deletions

6
.gitattributes vendored
View File

@@ -2,5 +2,9 @@
.gitignore export-ignore
.github export-ignore
.travis.yml export-ignore
appveyor.yml export-ignore
ecs.php export-ignore
phpstan.neon export-ignore
tests/ export-ignore
*.sh eol=lf
*.php* diff=php linguist-language=PHP

1
.github/funding.yml vendored Normal file
View File

@@ -0,0 +1 @@
github: dg

31
.github/workflows/coding-style.yml vendored Normal file
View 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@v3
- uses: shivammathur/setup-php@v2
with:
php-version: 8.0
coverage: none
- run: composer create-project nette/code-checker temp/code-checker ^3 --no-progress
- run: php temp/code-checker/code-checker --strict-types --no-progress --ignore "tests/*/fixtures"
nette_cs:
name: Nette Coding Standard
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: shivammathur/setup-php@v2
with:
php-version: 8.0
coverage: none
- run: composer create-project nette/coding-standard temp/coding-standard ^3 --no-progress
- run: php temp/coding-standard/ecs check

21
.github/workflows/static-analysis.yml vendored Normal file
View 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@v3
- uses: shivammathur/setup-php@v2
with:
php-version: 8.0
coverage: none
- run: composer install --no-progress --prefer-dist
- run: composer phpstan -- --no-progress
continue-on-error: true # is only informative

121
.github/workflows/tests.yml vendored Normal file
View File

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

View File

@@ -1,77 +0,0 @@
language: php
php:
- 7.1
- 7.2
- 7.3
- 7.4
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
install:
- travis_retry composer create-project nette/coding-standard temp/coding-standard ^2 --no-progress
script:
- php temp/coding-standard/ecs check src tests examples --config tests/coding-standard.yml
- stage: Static Analysis (informative)
install:
# Install PHPStan
- travis_retry composer create-project phpstan/phpstan-shim temp/phpstan --no-progress
- travis_retry composer install --no-progress --prefer-dist
script:
- php temp/phpstan/phpstan.phar analyse --autoload-file vendor/autoload.php --level 5 src
- stage: Code Coverage
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
sudo: false
cache:
directories:
- $HOME/.composer/cache
notifications:
email: false

View File

@@ -15,17 +15,17 @@ init:
- SET ANSICON=121x90 (121x90)
install:
# Install PHP 7.1
# Install PHP 7.2
- IF EXIST c:\php7 (SET PHP=0) ELSE (SET PHP=1)
- IF %PHP%==1 mkdir c:\php7
- IF %PHP%==1 cd c:\php7
- IF %PHP%==1 curl https://windows.php.net/downloads/releases/archives/php-7.1.5-Win32-VC14-x64.zip --output php.zip
- IF %PHP%==1 curl https://windows.php.net/downloads/releases/archives/php-7.2.18-Win32-VC15-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.8.0/Windows-7.2.zip -L --output sqlsrv.zip
- IF %PHP%==1 7z x sqlsrv.zip >nul
- IF %PHP%==1 copy Windows-7.2\x64\php_sqlsrv_72_ts.dll ext\php_sqlsrv_ts.dll
- IF %PHP%==1 del /Q *.zip
# Install Microsoft Access Database Engine x64

View File

@@ -3,7 +3,7 @@
"description": "Dibi is Database Abstraction Library for PHP",
"keywords": ["database", "dbal", "mysql", "postgresql", "sqlite", "mssql", "sqlsrv", "oracle", "access", "pdo", "odbc"],
"homepage": "https://dibiphp.com",
"license": ["BSD-3-Clause", "GPL-2.0", "GPL-3.0"],
"license": ["BSD-3-Clause", "GPL-2.0-only", "GPL-3.0-only"],
"authors": [
{
"name": "David Grudl",
@@ -11,11 +11,13 @@
}
],
"require": {
"php": ">=7.1"
"php": ">=7.2"
},
"require-dev": {
"tracy/tracy": "~2.2",
"nette/tester": "~2.0"
"nette/tester": "~2.0",
"nette/di": "^3.0",
"phpstan/phpstan": "^0.12"
},
"replace": {
"dg/dibi": "*"
@@ -23,9 +25,13 @@
"autoload": {
"classmap": ["src/"]
},
"scripts": {
"phpstan": "phpstan analyse",
"tester": "tester tests -s"
},
"extra": {
"branch-alias": {
"dev-master": "4.1-dev"
"dev-master": "4.2-dev"
}
}
}

View File

@@ -20,6 +20,7 @@ $dibi = new Dibi\Connection([
// enable query logging to this file
'profiler' => [
'file' => 'log/log.sql',
'errorsOnly' => false,
],
]);

9
phpstan.neon Normal file
View File

@@ -0,0 +1,9 @@
parameters:
level: 5
paths:
- src
ignoreErrors:
# The namespace is referenced, not the class.
- '#Class dibi referenced with incorrect case: Dibi#'

View File

@@ -2,7 +2,7 @@
=========================================================
[![Downloads this Month](https://img.shields.io/packagist/dm/dibi/dibi.svg)](https://packagist.org/packages/dibi/dibi)
[![Build Status](https://travis-ci.org/dg/dibi.svg?branch=master)](https://travis-ci.org/dg/dibi)
[![Tests](https://github.com/dg/dibi/workflows/Tests/badge.svg?branch=master)](https://github.com/dg/dibi/actions)
[![Build Status Windows](https://ci.appveyor.com/api/projects/status/github/dg/dibi?branch=master&svg=true)](https://ci.appveyor.com/project/dg/dibi/branch/master)
[![Latest Stable Version](https://poser.pugx.org/dibi/dibi/v/stable)](https://github.com/dg/dibi/releases)
[![License](https://img.shields.io/badge/license-New%20BSD-blue.svg)](https://github.com/dg/dibi/blob/master/license.md)
@@ -14,7 +14,15 @@ Introduction
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.
If you like Dibi, **[please make a donation now](https://nette.org/make-donation?to=dibi)**. Thank you!
Support Me
----------
Do you like Dibi? Are you looking forward to the new features?
[![Buy me a coffee](https://files.nette.org/icons/donation-3.svg)](https://github.com/sponsors/dg)
Thank you!
Installation
@@ -26,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 7.4.
The Dibi 4.2 requires PHP version 7.2 and supports PHP up to 8.2.
Usage
@@ -42,11 +50,11 @@ The database connection is represented by the object `Dibi\Connection`:
```php
$database = new Dibi\Connection([
'driver' => 'mysqli',
'host' => 'localhost',
'username' => 'root',
'password' => '***',
'database' => 'table',
'driver' => 'mysqli',
'host' => 'localhost',
'username' => 'root',
'password' => '***',
'database' => 'table',
]);
$result = $database->query('SELECT * FROM users');
@@ -56,12 +64,12 @@ Alternatively, you can use the `dibi` static register, which maintains a connect
```php
dibi::connect([
'driver' => 'mysqli',
'host' => 'localhost',
'username' => 'root',
'password' => '***',
'database' => 'test',
'charset' => 'utf8',
'driver' => 'mysqli',
'host' => 'localhost',
'username' => 'root',
'password' => '***',
'database' => 'test',
'charset' => 'utf8',
]);
$result = dibi::query('SELECT * FROM users');
@@ -75,6 +83,8 @@ In the event of a connection error, it throws `Dibi\Exception`.
We query the database queries by the method `query()` which returns `Dibi\Result`. Rows are objects `Dibi\Row`.
You can try all the examples [online at the playground](https://repl.it/@DavidGrudl/dibi-playground).
```php
$result = $database->query('SELECT * FROM users');
@@ -183,7 +193,7 @@ $result = $database->query('SELECT * FROM users WHERE id IN (%i)', $ids);
// SELECT * FROM users WHERE id IN (10, 20, 30)
```
The modifier '%n' is used if the table or column name is a variable. (Beware, do not allow the user to manipulate the content of such a variable):
The modifier `%n` is used if the table or column name is a variable. (Beware, do not allow the user to manipulate the content of such a variable):
```php
$table = 'blog.users';
@@ -199,6 +209,7 @@ Three special modifiers are available for LIKE:
| `%like~` | the expression starts with a string
| `%~like` | the expression ends with a string
| `%~like~` | the expression contains a string
| `%like` | the expression matches a string
Search for names beginning with a string:
@@ -226,8 +237,8 @@ Example:
```php
$arr = [
'a' => 'hello',
'b' => true,
'a' => 'hello',
'b' => true,
];
$database->query('INSERT INTO table %v', $arr);
@@ -499,7 +510,7 @@ $all = $result->fetchAssoc('customer_id|order_id');
// we will iterate like this:
foreach ($all as $customerId => $orders) {
foreach ($orders as $orderId => $order) {
...
...
}
}
```
@@ -531,7 +542,7 @@ $all = $result->fetchAssoc('name[]order_id');
// we get all the Arnolds in the results
foreach ($all['Arnold Rimmer'] as $arnoldOrders) {
foreach ($arnoldOrders as $orderId => $order) {
...
...
}
}
```
@@ -545,8 +556,8 @@ foreach ($all as $customerId => $orders) {
echo "Customer $customerId":
foreach ($orders as $orderId => $order) {
echo "ID number: $order->number";
// customer name is in $order->name
echo "ID number: $order->number";
// customer name is in $order->name
}
}
```
@@ -568,7 +579,7 @@ foreach ($all as $customerId => $row) {
echo "Customer $row->name":
foreach ($row->order_id as $orderId => $order) {
echo "ID number: $order->number";
echo "ID number: $order->number";
}
}
```

View File

@@ -22,10 +22,14 @@ class DibiExtension22 extends Nette\DI\CompilerExtension
/** @var bool|null */
private $debugMode;
/** @var bool|null */
private $cliMode;
public function __construct(bool $debugMode = null)
public function __construct(?bool $debugMode = null, ?bool $cliMode = null)
{
$this->debugMode = $debugMode;
$this->cliMode = $cliMode;
}
@@ -38,7 +42,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 +55,7 @@ class DibiExtension22 extends Nette\DI\CompilerExtension
foreach ((array) $config['flags'] as $flag) {
$flags |= constant($flag);
}
$config['flags'] = $flags;
}
@@ -60,6 +69,7 @@ class DibiExtension22 extends Nette\DI\CompilerExtension
[[Dibi\Bridges\Tracy\Panel::class, 'renderException']]
);
}
if ($useProfiler) {
$panel = $container->addDefinition($this->prefix('panel'))
->setFactory(Dibi\Bridges\Tracy\Panel::class, [

View File

@@ -35,7 +35,7 @@ class Panel implements Tracy\IBarPanel
private $events = [];
public function __construct($explain = true, int $filter = null)
public function __construct($explain = true, ?int $filter = null)
{
$this->filter = $filter ?: Event::QUERY;
$this->explain = $explain;
@@ -45,7 +45,7 @@ class Panel implements Tracy\IBarPanel
public function register(Dibi\Connection $connection): void
{
Tracy\Debugger::getBar()->addPanel($this);
Tracy\Debugger::getBlueScreen()->addPanel([__CLASS__, 'renderException']);
Tracy\Debugger::getBlueScreen()->addPanel([self::class, 'renderException']);
$connection->onEvent[] = [$this, 'logEvent'];
}
@@ -58,6 +58,7 @@ class Panel implements Tracy\IBarPanel
if (($event->type & $this->filter) === 0) {
return;
}
$this->events[] = $event;
}
@@ -88,9 +89,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>';
}
@@ -105,6 +107,15 @@ class Panel implements Tracy\IBarPanel
}
$totalTime = $s = null;
$singleConnection = reset($this->events)->connection;
foreach ($this->events as $event) {
if ($event->connection !== $singleConnection) {
$singleConnection = null;
break;
}
}
foreach ($this->events as $event) {
$totalTime += $event->time;
$connection = $event->connection;
@@ -112,15 +123,18 @@ class Panel implements Tracy\IBarPanel
if ($this->explain && $event->type === Event::SELECT) {
$backup = [$connection->onEvent, \dibi::$numOfQueries, \dibi::$totalTime];
$connection->onEvent = null;
$cmd = is_string($this->explain) ? $this->explain : ($connection->getConfig('driver') === 'oracle' ? 'EXPLAIN PLAN FOR' : 'EXPLAIN');
$cmd = is_string($this->explain)
? $this->explain
: ($connection->getConfig('driver') === 'oracle' ? 'EXPLAIN PLAN FOR' : 'EXPLAIN');
try {
$explain = @Helpers::dump($connection->nativeQuery("$cmd $event->sql"), true);
} catch (Dibi\Exception $e) {
}
[$connection->onEvent, \dibi::$numOfQueries, \dibi::$totalTime] = $backup;
}
$s .= '<tr><td>' . 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++;
@@ -131,24 +145,37 @@ class Panel implements Tracy\IBarPanel
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');
$s .= Tracy\Helpers::editorLink($event->source[0], $event->source[1]); //->class('tracy-DibiProfiler-source');
}
$s .= "</td><td>{$event->count}</td></tr>";
$s .= "</td><td>{$event->count}</td>";
if (!$singleConnection) {
$s .= '<td>' . htmlspecialchars($this->getConnectionName($connection)) . '</td></tr>';
}
}
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($connection->getConfig('driver') . ($connection->getConfig('name') ? '/' . $connection->getConfig('name') : '')
. ($connection->getConfig('host') ? '@' . $connection->getConfig('host') : '')) . '</h1>
<h1>Queries:' . "\u{a0}" . count($this->events)
. ($totalTime === null ? '' : ", time:\u{a0}" . number_format($totalTime * 1000, 1, '.', "\u{202f}") . "\u{202f}ms") . ', '
. htmlspecialchars($this->getConnectionName($singleConnection)) . '</h1>
<div class="tracy-inner tracy-DibiProfiler">
<table>
<tr><th>Time&nbsp;ms</th><th>SQL Statement</th><th>Rows</th></tr>' . $s . '
<table class="tracy-sortable">
<tr><th>Time&nbsp;ms</th><th>SQL Statement</th><th>Rows</th>' . (!$singleConnection ? '<th>Connection</th>' : '') . '</tr>
' . $s . '
</table>
</div>';
}
private function getConnectionName(Dibi\Connection $connection): string
{
$driver = $connection->getConfig('driver');
return (is_object($driver) ? get_class($driver) : $driver)
. ($connection->getConfig('name') ? '/' . $connection->getConfig('name') : '')
. ($connection->getConfig('host') ? "\u{202f}@\u{202f}" . $connection->getConfig('host') : '');
}
}

View File

@@ -28,6 +28,9 @@ class Connection implements IConnection
/** @var array Current connection configuration */
private $config;
/** @var string[] resultset formats */
private $formats;
/** @var Driver|null */
private $driver;
@@ -37,25 +40,35 @@ class Connection implements IConnection
/** @var HashMap Substitutes for identifiers */
private $substitutes;
private $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
* - errorsOnly (bool) => log only errors
* - substitutes (array) => map of driver specific substitutes (under development)
* - 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');
@@ -64,13 +77,20 @@ class Connection implements IConnection
Helpers::alias($config, 'result|formatDateTime', 'resultDateTime');
$config['driver'] = $config['driver'] ?? 'mysqli';
$config['name'] = $name;
$config['result']['formatJson'] = $config['result']['formatJson'] ?? 'array';
$this->config = $config;
$this->formats = [
Type::DATE => $this->config['result']['formatDate'],
Type::DATETIME => $this->config['result']['formatDateTime'],
Type::JSON => $this->config['result']['formatJson'] ?? 'array',
Type::TIME_INTERVAL => $this->config['result']['formatTimeInterval'] ?? null,
];
// profiler
if (isset($config['profiler']['file']) && (!isset($config['profiler']['run']) || $config['profiler']['run'])) {
$filter = $config['profiler']['filter'] ?? Event::QUERY;
$this->onEvent[] = [new Loggers\FileLogger($config['profiler']['file'], $filter), 'logEvent'];
$errorsOnly = $config['profiler']['errorsOnly'] ?? false;
$this->onEvent[] = [new Loggers\FileLogger($config['profiler']['file'], $filter, $errorsOnly), 'logEvent'];
}
$this->substitutes = new HashMap(function (string $expr) { return ":$expr:"; });
@@ -108,6 +128,7 @@ class Connection implements IConnection
{
if ($this->config['driver'] instanceof Driver) {
$this->driver = $this->config['driver'];
$this->translator = new Translator($this);
return;
} elseif (is_subclass_of($this->config['driver'], Driver::class)) {
@@ -124,19 +145,22 @@ class Connection implements IConnection
$event = $this->onEvent ? new Event($this, Event::CONNECT) : null;
try {
$this->driver = new $class($this->config);
$this->translator = new Translator($this);
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;
}
}
@@ -149,7 +173,7 @@ class Connection implements IConnection
{
if ($this->driver) {
$this->driver->disconnect();
$this->driver = null;
$this->driver = $this->translator = null;
}
}
@@ -168,7 +192,7 @@ class Connection implements IConnection
* @see self::__construct
* @return mixed
*/
final public function getConfig(string $key = null, $default = null)
final public function getConfig(?string $key = null, $default = null)
{
return $key === null
? $this->config
@@ -184,6 +208,7 @@ class Connection implements IConnection
if (!$this->driver) {
$this->connect();
}
return $this->driver;
}
@@ -195,7 +220,7 @@ class Connection implements IConnection
*/
final public function query(...$args): Result
{
return $this->nativeQuery($this->translateArgs($args));
return $this->nativeQuery($this->translate(...$args));
}
@@ -206,7 +231,11 @@ class Connection implements IConnection
*/
final public function translate(...$args): string
{
return $this->translateArgs($args);
if (!$this->driver) {
$this->connect();
}
return (clone $this->translator)->translate($args);
}
@@ -217,7 +246,7 @@ class Connection implements IConnection
final public function test(...$args): bool
{
try {
Helpers::dump($this->translateArgs($args));
Helpers::dump($this->translate(...$args));
return true;
} catch (Exception $e) {
@@ -226,6 +255,7 @@ class Connection implements IConnection
} else {
echo get_class($e) . ': ' . $e->getMessage() . (PHP_SAPI === 'cli' ? "\n" : '<br>');
}
return false;
}
}
@@ -238,23 +268,7 @@ class Connection implements IConnection
*/
final public function dataSource(...$args): DataSource
{
return new DataSource($this->translateArgs($args), $this);
}
/**
* Generates SQL query.
*/
protected function translateArgs(array $args): string
{
if (!$this->driver) {
$this->connect();
}
if (!$this->translator) {
$this->translator = new Translator($this);
}
$translator = clone $this->translator;
return $translator->translate($args);
return new DataSource($this->translate(...$args), $this);
}
@@ -277,6 +291,7 @@ class Connection implements IConnection
if ($event) {
$this->onEvent($event->done($e));
}
throw $e;
}
@@ -284,6 +299,7 @@ class Connection implements IConnection
if ($event) {
$this->onEvent($event->done($res));
}
return $res;
}
@@ -297,10 +313,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;
}
@@ -309,15 +327,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;
}
@@ -325,22 +345,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;
}
}
@@ -349,22 +374,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;
}
}
@@ -373,36 +403,69 @@ 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;
}
}
/**
* @return mixed
*/
public function transaction(callable $callback)
{
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);
}
@@ -435,6 +498,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);
}
@@ -538,7 +602,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);
}
@@ -552,6 +616,7 @@ class Connection implements IConnection
if (!$this->driver) {
$this->connect();
}
return new Reflection\Database($this->driver->getReflector(), $this->config['database'] ?? null);
}
@@ -561,7 +626,7 @@ class Connection implements IConnection
*/
public function __wakeup()
{
throw new NotSupportedException('You cannot serialize or unserialize ' . get_class($this) . ' instances.');
throw new NotSupportedException('You cannot serialize or unserialize ' . static::class . ' instances.');
}
@@ -570,7 +635,7 @@ class Connection implements IConnection
*/
public function __sleep()
{
throw new NotSupportedException('You cannot serialize or unserialize ' . get_class($this) . ' instances.');
throw new NotSupportedException('You cannot serialize or unserialize ' . static::class . ' instances.');
}

View File

@@ -53,11 +53,9 @@ class DataSource implements IDataSource
*/
public function __construct(string $sql, Connection $connection)
{
if (strpbrk($sql, " \t\r\n") === false) {
$this->sql = $connection->getDriver()->escapeIdentifier($sql); // table name
} else {
$this->sql = '(' . $sql . ') t'; // SQL command
}
$this->sql = strpbrk($sql, " \t\r\n") === false
? $connection->getDriver()->escapeIdentifier($sql) // table name
: '(' . $sql . ') t'; // SQL command
$this->connection = $connection;
}
@@ -67,13 +65,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($col, ?string $as = null): self
{
if (is_array($col)) {
$this->cols = $col;
} else {
$this->cols[$col] = $as;
}
$this->result = null;
return $this;
}
@@ -84,12 +83,9 @@ class DataSource implements IDataSource
*/
public function where($cond): self
{
if (is_array($cond)) {
// TODO: not consistent with select and orderBy
$this->conds[] = $cond;
} else {
$this->conds[] = func_get_args();
}
$this->conds[] = is_array($cond)
? $cond // TODO: not consistent with select and orderBy
: func_get_args();
$this->result = $this->count = null;
return $this;
}
@@ -106,6 +102,7 @@ class DataSource implements IDataSource
} else {
$this->sorting[$row] = $direction;
}
$this->result = null;
return $this;
}
@@ -114,7 +111,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): self
{
$this->limit = $limit;
$this->offset = $offset;
@@ -140,6 +137,7 @@ class DataSource implements IDataSource
if ($this->result === null) {
$this->result = $this->connection->nativeQuery($this->__toString());
}
return $this->result;
}
@@ -190,7 +188,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);
}
@@ -232,12 +230,18 @@ class DataSource implements IDataSource
public function __toString(): string
{
try {
return $this->connection->translate('
SELECT %n', (empty($this->cols) ? '*' : $this->cols), '
FROM %SQL', $this->sql, '
%ex', $this->conds ? ['WHERE %and', $this->conds] : null, '
%ex', $this->sorting ? ['ORDER BY %by', $this->sorting] : null, '
%ofs %lmt', $this->offset, $this->limit
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);
@@ -261,6 +265,7 @@ FROM %SQL', $this->sql, '
)->fetchSingle())
: $this->getTotalCount();
}
return $this->count;
}
@@ -275,6 +280,7 @@ FROM %SQL', $this->sql, '
'SELECT COUNT(*) FROM ' . $this->sql
)->fetchSingle());
}
return $this->totalCount;
}
}

View File

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

View File

@@ -0,0 +1,219 @@
<?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\Drivers;
use Dibi;
/**
* The dummy driver for testing purposes.
*/
class DummyDriver implements Dibi\Driver, Dibi\ResultDriver, Dibi\Reflector
{
use Dibi\Strict;
public function disconnect(): void
{
}
public function query(string $sql): ?Dibi\ResultDriver
{
return null;
}
public function getAffectedRows(): ?int
{
return null;
}
public function getInsertId(?string $sequence): ?int
{
return null;
}
public function begin(?string $savepoint = null): void
{
}
public function commit(?string $savepoint = null): void
{
}
public function rollback(?string $savepoint = null): void
{
}
public function getResource()
{
return null;
}
/**
* Returns the connection reflector.
*/
public function getReflector(): Dibi\Reflector
{
return $this;
}
/********************* SQL ****************d*g**/
/**
* Encodes data for use in a SQL statement.
*/
public function escapeText(string $value): string
{
return "'" . str_replace("'", "''", $value) . "'";
}
public function escapeBinary(string $value): string
{
return "N'" . str_replace("'", "''", $value) . "'";
}
public function escapeIdentifier(string $value): string
{
return '[' . strtr($value, '[]', ' ') . ']';
}
public function escapeBool(bool $value): string
{
return $value ? '1' : '0';
}
public function escapeDate(\DateTimeInterface $value): string
{
return $value->format("'Y-m-d'");
}
public function escapeDateTime(\DateTimeInterface $value): string
{
return $value->format("'Y-m-d H:i:s.u'");
}
public function escapeDateInterval(\DateInterval $value): string
{
throw new Dibi\NotImplementedException;
}
/**
* Encodes string for use in a LIKE statement.
*/
public function escapeLike(string $value, int $pos): string
{
$value = strtr($value, ["'" => "''", '%' => '[%]', '_' => '[_]', '[' => '[[]']);
return ($pos & 1 ? "'%" : "'") . $value . ($pos & 2 ? "%'" : "'");
}
/**
* Injects LIMIT/OFFSET to the SQL query.
*/
public function applyLimit(string &$sql, ?int $limit, ?int $offset): void
{
if ($limit < 0 || $offset < 0) {
throw new Dibi\NotSupportedException('Negative offset or limit.');
} elseif ($limit !== null || $offset) {
$sql .= ' LIMIT ' . ($limit ?? '-1')
. ($offset ? ' OFFSET ' . $offset : '');
}
}
/********************* Result ****************d*g**/
public function getRowCount(): int
{
return 0;
}
public function fetch(bool $assoc): ?array
{
return null;
}
public function seek(int $row): bool
{
return false;
}
public function free(): void
{
}
public function getResultResource()
{
}
public function getResultColumns(): array
{
return [];
}
/**
* Decodes data from result set.
*/
public function unescapeBinary(string $value): string
{
return $value;
}
/********************* Reflector ****************d*g**/
public function getTables(): array
{
return [];
}
public function getColumns(string $table): array
{
return [];
}
public function getIndexes(string $table): array
{
return [];
}
public function getForeignKeys(string $table): array
{
return [];
}
}

View File

@@ -40,9 +40,7 @@ class FirebirdDriver implements Dibi\Driver
private $inTransaction = false;
/**
* @throws Dibi\NotSupportedException
*/
/** @throws Dibi\NotSupportedException */
public function __construct(array $config)
{
if (!extension_loaded('interbase')) {
@@ -64,11 +62,9 @@ class FirebirdDriver implements Dibi\Driver
'buffers' => 0,
];
if (empty($config['persistent'])) {
$this->connection = @ibase_connect($config['database'], $config['username'], $config['password'], $config['charset'], $config['buffers']); // intentionally @
} else {
$this->connection = @ibase_pconnect($config['database'], $config['username'], $config['password'], $config['charset'], $config['buffers']); // intentionally @
}
$this->connection = empty($config['persistent'])
? @ibase_connect($config['database'], $config['username'], $config['password'], $config['charset'], $config['buffers']) // intentionally @
: @ibase_pconnect($config['database'], $config['username'], $config['password'], $config['charset'], $config['buffers']); // intentionally @
if (!is_resource($this->connection)) {
throw new Dibi\DriverException(ibase_errmsg(), ibase_errcode());
@@ -92,21 +88,23 @@ class FirebirdDriver implements Dibi\Driver
*/
public function query(string $sql): ?Dibi\ResultDriver
{
$resource = $this->inTransaction ? $this->transaction : $this->connection;
$resource = $this->inTransaction
? $this->transaction
: $this->connection;
$res = ibase_query($resource, $sql);
if ($res === false) {
if (ibase_errcode() == self::ERROR_EXCEPTION_THROWN) {
if (ibase_errcode() === self::ERROR_EXCEPTION_THROWN) {
preg_match('/exception (\d+) (\w+) (.*)/i', ibase_errmsg(), $match);
throw new Dibi\ProcedureException($match[3], $match[1], $match[2], $sql);
} else {
throw new Dibi\DriverException(ibase_errmsg(), ibase_errcode(), $sql);
}
} elseif (is_resource($res)) {
return $this->createResultDriver($res);
}
return null;
}
@@ -133,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;
}
@@ -147,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.');
@@ -165,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.');
@@ -271,7 +270,7 @@ class FirebirdDriver implements Dibi\Driver
public function escapeLike(string $value, int $pos): string
{
$value = addcslashes($this->escapeText($value), '%_\\');
return ($pos <= 0 ? "'%" : "'") . $value . ($pos >= 0 ? "%'" : "'") . " ESCAPE '\\'";
return ($pos & 1 ? "'%" : "'") . $value . ($pos & 2 ? "%'" : "'") . " ESCAPE '\\'";
}

View File

@@ -38,8 +38,8 @@ class FirebirdReflector implements Dibi\Reflector
SELECT TRIM(RDB\$RELATION_NAME),
CASE RDB\$VIEW_BLR WHEN NULL THEN 'TRUE' ELSE 'FALSE' END
FROM RDB\$RELATIONS
WHERE RDB\$SYSTEM_FLAG = 0;"
);
WHERE RDB\$SYSTEM_FLAG = 0;
");
$tables = [];
while ($row = $res->fetch(false)) {
$tables[] = [
@@ -47,6 +47,7 @@ class FirebirdReflector implements Dibi\Reflector
'view' => $row[1] === 'TRUE',
];
}
return $tables;
}
@@ -84,9 +85,8 @@ class FirebirdReflector implements Dibi\Reflector
FROM RDB\$RELATION_FIELDS r
LEFT JOIN RDB\$FIELDS f ON r.RDB\$FIELD_SOURCE = f.RDB\$FIELD_NAME
WHERE r.RDB\$RELATION_NAME = '$table'
ORDER BY r.RDB\$FIELD_POSITION;"
);
ORDER BY r.RDB\$FIELD_POSITION;
");
$columns = [];
while ($row = $res->fetch(true)) {
$key = $row['FIELD_NAME'];
@@ -100,6 +100,7 @@ class FirebirdReflector implements Dibi\Reflector
'autoincrement' => false,
];
}
return $columns;
}
@@ -121,8 +122,8 @@ class FirebirdReflector implements Dibi\Reflector
LEFT JOIN RDB\$INDICES i ON i.RDB\$INDEX_NAME = s.RDB\$INDEX_NAME
LEFT JOIN RDB\$RELATION_CONSTRAINTS r ON r.RDB\$INDEX_NAME = s.RDB\$INDEX_NAME
WHERE UPPER(i.RDB\$RELATION_NAME) = '$table'
ORDER BY s.RDB\$FIELD_POSITION"
);
ORDER BY s.RDB\$FIELD_POSITION
");
$indexes = [];
while ($row = $res->fetch(true)) {
$key = $row['INDEX_NAME'];
@@ -132,6 +133,7 @@ class FirebirdReflector implements Dibi\Reflector
$indexes[$key]['table'] = $table;
$indexes[$key]['columns'][$row['FIELD_POSITION']] = $row['FIELD_NAME'];
}
return $indexes;
}
@@ -149,8 +151,8 @@ class FirebirdReflector implements Dibi\Reflector
LEFT JOIN RDB\$RELATION_CONSTRAINTS r ON r.RDB\$INDEX_NAME = s.RDB\$INDEX_NAME
WHERE UPPER(i.RDB\$RELATION_NAME) = '$table'
AND r.RDB\$CONSTRAINT_TYPE = 'FOREIGN KEY'
ORDER BY s.RDB\$FIELD_POSITION"
);
ORDER BY s.RDB\$FIELD_POSITION
");
$keys = [];
while ($row = $res->fetch(true)) {
$key = $row['INDEX_NAME'];
@@ -160,6 +162,7 @@ class FirebirdReflector implements Dibi\Reflector
'table' => $table,
];
}
return $keys;
}
@@ -174,12 +177,13 @@ class FirebirdReflector implements Dibi\Reflector
FROM RDB\$INDICES
WHERE RDB\$RELATION_NAME = UPPER('$table')
AND RDB\$UNIQUE_FLAG IS NULL
AND RDB\$FOREIGN_KEY IS NULL;"
);
AND RDB\$FOREIGN_KEY IS NULL;
");
$indices = [];
while ($row = $res->fetch(false)) {
$indices[] = $row[0];
}
return $indices;
}
@@ -196,12 +200,13 @@ class FirebirdReflector implements Dibi\Reflector
AND (
RDB\$UNIQUE_FLAG IS NOT NULL
OR RDB\$FOREIGN_KEY IS NOT NULL
);"
);
);
");
$constraints = [];
while ($row = $res->fetch(false)) {
$constraints[] = $row[0];
}
return $constraints;
}
@@ -210,9 +215,10 @@ 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("
$res = $this->driver->query(
"
SELECT TRIM(RDB\$TRIGGER_NAME) AS TRIGGER_NAME,
TRIM(RDB\$RELATION_NAME) AS TABLE_NAME,
CASE RDB\$TRIGGER_TYPE
@@ -248,6 +254,7 @@ class FirebirdReflector implements Dibi\Reflector
'enabled' => trim($row['TRIGGER_ENABLED']) === 'TRUE',
];
}
return $triggers;
}
@@ -256,18 +263,21 @@ 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
WHERE RDB$SYSTEM_FLAG = 0';
$q .= $table === null ? ';' : " AND RDB\$RELATION_NAME = UPPER('$table')";
$q .= $table === null
? ';'
: " AND RDB\$RELATION_NAME = UPPER('$table')";
$res = $this->driver->query($q);
$triggers = [];
while ($row = $res->fetch(false)) {
$triggers[] = $row[0];
}
return $triggers;
}
@@ -307,8 +317,8 @@ class FirebirdReflector implements Dibi\Reflector
p.RDB\$PARAMETER_NUMBER AS PARAMETER_NUMBER
FROM RDB\$PROCEDURE_PARAMETERS p
LEFT JOIN RDB\$FIELDS f ON f.RDB\$FIELD_NAME = p.RDB\$FIELD_SOURCE
ORDER BY p.RDB\$PARAMETER_TYPE, p.RDB\$PARAMETER_NUMBER;"
);
ORDER BY p.RDB\$PARAMETER_TYPE, p.RDB\$PARAMETER_NUMBER;
");
$procedures = [];
while ($row = $res->fetch(true)) {
$key = $row['PROCEDURE_NAME'];
@@ -319,6 +329,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;
}
@@ -330,12 +341,13 @@ class FirebirdReflector implements Dibi\Reflector
{
$res = $this->driver->query('
SELECT TRIM(RDB$PROCEDURE_NAME)
FROM RDB$PROCEDURES;'
);
FROM RDB$PROCEDURES;
');
$procedures = [];
while ($row = $res->fetch(false)) {
$procedures[] = $row[0];
}
return $procedures;
}
@@ -348,12 +360,13 @@ class FirebirdReflector implements Dibi\Reflector
$res = $this->driver->query('
SELECT TRIM(RDB$GENERATOR_NAME)
FROM RDB$GENERATORS
WHERE RDB$SYSTEM_FLAG = 0;'
);
WHERE RDB$SYSTEM_FLAG = 0;
');
$generators = [];
while ($row = $res->fetch(false)) {
$generators[] = $row[0];
}
return $generators;
}
@@ -366,12 +379,13 @@ class FirebirdReflector implements Dibi\Reflector
$res = $this->driver->query('
SELECT TRIM(RDB$FUNCTION_NAME)
FROM RDB$FUNCTIONS
WHERE RDB$SYSTEM_FLAG = 0;'
);
WHERE RDB$SYSTEM_FLAG = 0;
');
$functions = [];
while ($row = $res->fetch(false)) {
$functions[] = $row[0];
}
return $functions;
}
}

View File

@@ -62,10 +62,12 @@ class FirebirdResult implements Dibi\ResultDriver
*/
public function fetch(bool $assoc): ?array
{
$result = $assoc ? @ibase_fetch_assoc($this->resultSet, IBASE_TEXT) : @ibase_fetch_row($this->resultSet, IBASE_TEXT); // intentionally @
$result = $assoc
? @ibase_fetch_assoc($this->resultSet, IBASE_TEXT)
: @ibase_fetch_row($this->resultSet, IBASE_TEXT); // intentionally @
if (ibase_errcode()) {
if (ibase_errcode() == FirebirdDriver::ERROR_EXCEPTION_THROWN) {
if (ibase_errcode() === FirebirdDriver::ERROR_EXCEPTION_THROWN) {
preg_match('/exception (\d+) (\w+) (.*)/is', ibase_errmsg(), $match);
throw new Dibi\ProcedureException($match[3], $match[1], $match[2]);
@@ -124,6 +126,7 @@ class FirebirdResult implements Dibi\ResultDriver
'nativetype' => $row['type'],
];
}
return $columns;
}

View File

@@ -43,6 +43,7 @@ class MySqlReflector implements Dibi\Reflector
'view' => isset($row[1]) && $row[1] === 'VIEW',
];
}
return $tables;
}
@@ -67,6 +68,7 @@ class MySqlReflector implements Dibi\Reflector
'vendor' => $row,
];
}
return $columns;
}
@@ -84,6 +86,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 +126,7 @@ class MySqlReflector implements Dibi\Reflector
$foreignKeys[$keyName]['onDelete'] = $row['DELETE_RULE'];
$foreignKeys[$keyName]['onUpdate'] = $row['UPDATE_RULE'];
}
return array_values($foreignKeys);
}
}

View File

@@ -47,9 +47,7 @@ class MySqliDriver implements Dibi\Driver
private $buffered;
/**
* @throws Dibi\NotSupportedException
*/
/** @throws Dibi\NotSupportedException */
public function __construct(array $config)
{
if (!extension_loaded('mysqli')) {
@@ -74,7 +72,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;
@@ -90,10 +88,11 @@ class MySqliDriver implements Dibi\Driver
$this->connection->options($key, $value);
}
}
@$this->connection->real_connect( // intentionally @
(empty($config['persistent']) ? '' : 'p:') . $config['host'],
$config['username'],
$config['password'],
$config['password'] ?? '',
$config['database'] ?? '',
$config['port'] ?? 0,
$config['socket'],
@@ -132,6 +131,15 @@ class MySqliDriver implements Dibi\Driver
}
/**
* Pings a server connection, or tries to reconnect if the connection has gone down.
*/
public function ping(): bool
{
return $this->connection->ping();
}
/**
* Executes the SQL query.
* @throws Dibi\DriverException
@@ -146,10 +154,14 @@ 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
{
if (in_array($code, [1216, 1217, 1451, 1452, 1701], true)) {
@@ -181,6 +193,7 @@ class MySqliDriver implements Dibi\Driver
foreach ($matches as $m) {
$res[$m[1]] = (int) $m[2];
}
return $res;
}
@@ -190,7 +203,9 @@ class MySqliDriver implements Dibi\Driver
*/
public function getAffectedRows(): ?int
{
return $this->connection->affected_rows === -1 ? null : $this->connection->affected_rows;
return $this->connection->affected_rows === -1
? null
: $this->connection->affected_rows;
}
@@ -207,7 +222,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');
}
@@ -217,7 +232,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');
}
@@ -227,7 +242,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');
}
@@ -238,7 +253,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;
}
}
@@ -307,7 +326,8 @@ 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');
return $value->format("'%r%H:%I:%S.%f'");
}
@@ -317,7 +337,7 @@ class MySqliDriver implements Dibi\Driver
public function escapeLike(string $value, int $pos): string
{
$value = addcslashes(str_replace('\\', '\\\\', $value), "\x00\n\r\\'%_");
return ($pos <= 0 ? "'%" : "'") . $value . ($pos >= 0 ? "%'" : "'");
return ($pos & 1 ? "'%" : "'") . $value . ($pos & 2 ? "%'" : "'");
}

View File

@@ -55,6 +55,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 +81,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 +109,7 @@ class MySqliResult implements Dibi\ResultDriver
$types[$value] = substr($key, 12);
}
}
$types[MYSQLI_TYPE_TINY] = $types[MYSQLI_TYPE_SHORT] = $types[MYSQLI_TYPE_LONG] = 'INT';
}
@@ -123,6 +126,7 @@ class MySqliResult implements Dibi\ResultDriver
'vendor' => $row,
];
}
return $columns;
}

View File

@@ -37,9 +37,7 @@ class OdbcDriver implements Dibi\Driver
private $microseconds = true;
/**
* @throws Dibi\NotSupportedException
*/
/** @throws Dibi\NotSupportedException */
public function __construct(array $config)
{
if (!extension_loaded('odbc')) {
@@ -56,11 +54,9 @@ class OdbcDriver implements Dibi\Driver
'dsn' => ini_get('odbc.default_db'),
];
if (empty($config['persistent'])) {
$this->connection = @odbc_connect($config['dsn'], $config['username'] ?? '', $config['password'] ?? ''); // intentionally @
} else {
$this->connection = @odbc_pconnect($config['dsn'], $config['username'] ?? '', $config['password'] ?? ''); // intentionally @
}
$this->connection = empty($config['persistent'])
? @odbc_connect($config['dsn'], $config['username'] ?? '', $config['password'] ?? '') // intentionally @
: @odbc_pconnect($config['dsn'], $config['username'] ?? '', $config['password'] ?? ''); // intentionally @
}
if (!is_resource($this->connection)) {
@@ -96,8 +92,11 @@ class OdbcDriver implements Dibi\Driver
} elseif (is_resource($res)) {
$this->affectedRows = Dibi\Helpers::false2Null(odbc_num_rows($res));
return odbc_num_fields($res) ? $this->createResultDriver($res) : null;
return odbc_num_fields($res)
? $this->createResultDriver($res)
: null;
}
return null;
}
@@ -124,9 +123,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, 0/*false*/)) {
if (!odbc_autocommit($this->connection, PHP_VERSION_ID < 80000 ? 0 : false)) {
throw new Dibi\DriverException(odbc_errormsg($this->connection) . ' ' . odbc_error($this->connection));
}
}
@@ -136,12 +135,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, 1/*true*/);
odbc_autocommit($this->connection, PHP_VERSION_ID < 80000 ? 1 : true);
}
@@ -149,12 +149,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, 1/*true*/);
odbc_autocommit($this->connection, PHP_VERSION_ID < 80000 ? 1 : true);
}
@@ -250,7 +251,7 @@ class OdbcDriver implements Dibi\Driver
public function escapeLike(string $value, int $pos): string
{
$value = strtr($value, ["'" => "''", '%' => '[%]', '_' => '[_]', '[' => '[[]']);
return ($pos <= 0 ? "'%" : "'") . $value . ($pos >= 0 ? "%'" : "'");
return ($pos & 1 ? "'%" : "'") . $value . ($pos & 2 ? "%'" : "'");
}

View File

@@ -44,6 +44,7 @@ class OdbcReflector implements Dibi\Reflector
];
}
}
odbc_free_result($res);
return $tables;
}
@@ -68,6 +69,7 @@ class OdbcReflector implements Dibi\Reflector
];
}
}
odbc_free_result($res);
return $columns;
}

View File

@@ -72,11 +72,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 +118,7 @@ class OdbcResult implements Dibi\ResultDriver
'nativetype' => odbc_field_type($this->resultSet, $i),
];
}
return $columns;
}

View File

@@ -42,9 +42,7 @@ class OracleDriver implements Dibi\Driver
private $affectedRows;
/**
* @throws Dibi\NotSupportedException
*/
/** @throws Dibi\NotSupportedException */
public function __construct(array $config)
{
if (!extension_loaded('oci8')) {
@@ -98,12 +96,15 @@ class OracleDriver implements Dibi\Driver
} elseif (is_resource($res)) {
$this->affectedRows = Dibi\Helpers::false2Null(oci_num_rows($res));
return oci_num_fields($res) ? $this->createResultDriver($res) : null;
return oci_num_fields($res)
? $this->createResultDriver($res)
: null;
}
} else {
$err = oci_error($this->connection);
throw new Dibi\DriverException($err['message'], $err['code'], $sql);
}
return null;
}
@@ -147,7 +148,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 +158,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 +173,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;
}
@@ -270,7 +273,7 @@ class OracleDriver implements Dibi\Driver
{
$value = addcslashes(str_replace('\\', '\\\\', $value), "\x00\\%_");
$value = str_replace("'", "''", $value);
return ($pos <= 0 ? "'%" : "'") . $value . ($pos >= 0 ? "%'" : "'");
return ($pos & 1 ? "'%" : "'") . $value . ($pos & 2 ? "%'" : "'");
}

View File

@@ -44,6 +44,7 @@ class OracleReflector implements Dibi\Reflector
];
}
}
return $tables;
}
@@ -66,6 +67,7 @@ class OracleReflector implements Dibi\Reflector
'vendor' => $row,
];
}
return $columns;
}

View File

@@ -100,6 +100,7 @@ class OracleResult implements Dibi\ResultDriver
'nativetype' => $type === 'NUMBER' && oci_field_scale($this->resultSet, $i) === 0 ? 'INTEGER' : $type,
];
}
return $columns;
}

View File

@@ -42,9 +42,7 @@ class PdoDriver implements Dibi\Driver
private $serverVersion = '';
/**
* @throws Dibi\NotSupportedException
*/
/** @throws Dibi\NotSupportedException */
public function __construct(array $config)
{
if (!extension_loaded('pdo')) {
@@ -58,21 +56,23 @@ class PdoDriver implements Dibi\Driver
if ($config['resource'] instanceof PDO) {
$this->connection = $config['resource'];
unset($config['resource'], $config['pdo']);
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']);
$this->connection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT);
} catch (\PDOException $e) {
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());
}
}
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.');
}
$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
}
@@ -144,7 +144,7 @@ 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();
@@ -157,7 +157,7 @@ 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();
@@ -170,7 +170,7 @@ 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();
@@ -234,21 +234,17 @@ class PdoDriver implements Dibi\Driver
*/
public function escapeText(string $value): string
{
if ($this->driverName === 'odbc') {
return "'" . str_replace("'", "''", $value) . "'";
} else {
return $this->connection->quote($value, PDO::PARAM_STR);
}
return $this->driverName === 'odbc'
? "'" . str_replace("'", "''", $value) . "'"
: $this->connection->quote($value, PDO::PARAM_STR);
}
public function escapeBinary(string $value): string
{
if ($this->driverName === 'odbc') {
return "'" . str_replace("'", "''", $value) . "'";
} else {
return $this->connection->quote($value, PDO::PARAM_LOB);
}
return $this->driverName === 'odbc'
? "'" . str_replace("'", "''", $value) . "'"
: $this->connection->quote($value, PDO::PARAM_LOB);
}
@@ -324,29 +320,29 @@ class PdoDriver implements Dibi\Driver
switch ($this->driverName) {
case 'mysql':
$value = addcslashes(str_replace('\\', '\\\\', $value), "\x00\n\r\\'%_");
return ($pos <= 0 ? "'%" : "'") . $value . ($pos >= 0 ? "%'" : "'");
return ($pos & 1 ? "'%" : "'") . $value . ($pos & 2 ? "%'" : "'");
case 'oci':
$value = addcslashes(str_replace('\\', '\\\\', $value), "\x00\\%_");
$value = str_replace("'", "''", $value);
return ($pos <= 0 ? "'%" : "'") . $value . ($pos >= 0 ? "%'" : "'");
return ($pos & 1 ? "'%" : "'") . $value . ($pos & 2 ? "%'" : "'");
case 'pgsql':
$bs = substr($this->connection->quote('\\', PDO::PARAM_STR), 1, -1); // standard_conforming_strings = on/off
$value = substr($this->connection->quote($value, PDO::PARAM_STR), 1, -1);
$value = strtr($value, ['%' => $bs . '%', '_' => $bs . '_', '\\' => '\\\\']);
return ($pos <= 0 ? "'%" : "'") . $value . ($pos >= 0 ? "%'" : "'");
return ($pos & 1 ? "'%" : "'") . $value . ($pos & 2 ? "%'" : "'");
case 'sqlite':
$value = addcslashes(substr($this->connection->quote($value, PDO::PARAM_STR), 1, -1), '%_\\');
return ($pos <= 0 ? "'%" : "'") . $value . ($pos >= 0 ? "%'" : "'") . " ESCAPE '\\'";
return ($pos & 1 ? "'%" : "'") . $value . ($pos & 2 ? "%'" : "'") . " ESCAPE '\\'";
case 'odbc':
case 'mssql':
case 'dblib':
case 'sqlsrv':
$value = strtr($value, ["'" => "''", '%' => '[%]', '_' => '[_]', '[' => '[[]']);
return ($pos <= 0 ? "'%" : "'") . $value . ($pos >= 0 ? "%'" : "'");
return ($pos & 1 ? "'%" : "'") . $value . ($pos & 2 ? "%'" : "'");
default:
throw new Dibi\NotImplementedException;
@@ -370,15 +366,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':
@@ -386,6 +385,7 @@ class PdoDriver implements Dibi\Driver
$sql .= ' LIMIT ' . ($limit ?? '-1')
. ($offset ? ' OFFSET ' . $offset : '');
}
break;
case 'oci':
@@ -398,6 +398,7 @@ class PdoDriver implements Dibi\Driver
} elseif ($limit !== null) {
$sql = 'SELECT * FROM (' . $sql . ') WHERE ROWNUM <= ' . $limit;
}
break;
case 'mssql':
@@ -410,10 +411,10 @@ class PdoDriver implements Dibi\Driver
} elseif ($offset) {
$sql = sprintf('%s OFFSET %d ROWS', rtrim($sql), $offset);
}
break;
}
// break omitted
case 'odbc':
if ($offset) {
throw new Dibi\NotSupportedException('Offset is not supported by this database.');
@@ -423,7 +424,6 @@ class PdoDriver implements Dibi\Driver
break;
}
// break omitted
default:
throw new Dibi\NotSupportedException('PDO or driver does not support applying limit or offset.');
}

View File

@@ -85,7 +85,8 @@ class PdoResult implements Dibi\ResultDriver
if ($row === false) {
throw new Dibi\NotSupportedException('Driver does not support meta data.');
}
$row = $row + [
$row += [
'table' => null,
'native_type' => 'VAR_STRING',
];
@@ -99,6 +100,7 @@ class PdoResult implements Dibi\ResultDriver
'vendor' => $row,
];
}
return $columns;
}

View File

@@ -11,6 +11,7 @@ namespace Dibi\Drivers;
use Dibi;
use Dibi\Helpers;
use PgSql;
/**
@@ -23,21 +24,20 @@ use Dibi\Helpers;
* - charset => character encoding to set (default is utf8)
* - persistent (bool) => try to find a persistent link?
* - resource (resource) => existing connection resource
* - connect_type (int) => see pg_connect()
*/
class PostgreDriver implements Dibi\Driver
{
use Dibi\Strict;
/** @var resource */
/** @var resource|PgSql\Connection */
private $connection;
/** @var int|null Affected rows */
private $affectedRows;
/**
* @throws Dibi\NotSupportedException
*/
/** @throws Dibi\NotSupportedException */
public function __construct(array $config)
{
if (!extension_loaded('pgsql')) {
@@ -65,18 +65,18 @@ class PostgreDriver implements Dibi\Driver
}
}
$connectType = $config['connect_type'] ?? PGSQL_CONNECT_FORCE_NEW;
set_error_handler(function (int $severity, string $message) use (&$error) {
$error = $message;
});
if (empty($config['persistent'])) {
$this->connection = pg_connect($string, PGSQL_CONNECT_FORCE_NEW);
} else {
$this->connection = pg_pconnect($string, PGSQL_CONNECT_FORCE_NEW);
}
$this->connection = empty($config['persistent'])
? pg_connect($string, $connectType)
: pg_pconnect($string, $connectType);
restore_error_handler();
}
if (!is_resource($this->connection)) {
if (!is_resource($this->connection) && !$this->connection instanceof PgSql\Connection) {
throw new Dibi\DriverException($error ?: 'Connecting error.');
}
@@ -122,17 +122,18 @@ class PostgreDriver implements Dibi\Driver
if ($res === false) {
throw static::createException(pg_last_error($this->connection), null, $sql);
} elseif (is_resource($res)) {
} elseif (is_resource($res) || $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];
@@ -171,12 +172,9 @@ class PostgreDriver implements Dibi\Driver
*/
public function getInsertId(?string $sequence): ?int
{
if ($sequence === null) {
// PostgreSQL 8.1 is needed
$res = $this->query('SELECT LASTVAL()');
} else {
$res = $this->query("SELECT CURRVAL('$sequence')");
}
$res = $sequence === null
? $this->query('SELECT LASTVAL()') // PostgreSQL 8.1 is needed
: $this->query("SELECT CURRVAL('$sequence')");
if (!$res) {
return null;
@@ -191,9 +189,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');
}
@@ -201,9 +199,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');
}
@@ -211,9 +209,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');
}
@@ -232,7 +230,9 @@ class PostgreDriver implements Dibi\Driver
*/
public function getResource()
{
return is_resource($this->connection) ? $this->connection : null;
return is_resource($this->connection) || $this->connection instanceof PgSql\Connection
? $this->connection
: null;
}
@@ -263,18 +263,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) . "'";
}
@@ -318,7 +320,7 @@ class PostgreDriver implements Dibi\Driver
$bs = pg_escape_string($this->connection, '\\'); // standard_conforming_strings = on/off
$value = pg_escape_string($this->connection, $value);
$value = strtr($value, ['%' => $bs . '%', '_' => $bs . '_', '\\' => '\\\\']);
return ($pos <= 0 ? "'%" : "'") . $value . ($pos >= 0 ? "%'" : "'");
return ($pos & 1 ? "'%" : "'") . $value . ($pos & 2 ? "%'" : "'");
}
@@ -330,9 +332,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;
}

View File

@@ -28,9 +28,6 @@ class PostgreReflector implements Dibi\Reflector
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;
}
@@ -69,6 +66,7 @@ class PostgreReflector implements Dibi\Reflector
while ($row = $res->fetch(true)) {
$tables[] = $row;
}
return $tables;
}
@@ -105,7 +103,7 @@ 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
FROM
pg_attribute a
JOIN pg_type ON a.atttypid = pg_type.oid
@@ -134,6 +132,7 @@ class PostgreReflector implements Dibi\Reflector
'vendor' => $row,
];
}
return $columns;
}
@@ -183,6 +182,7 @@ class PostgreReflector implements Dibi\Reflector
}
}
}
return array_values($indexes);
}
@@ -251,7 +251,10 @@ class PostgreReflector implements Dibi\Reflector
$references[$row['name']] = array_combine($l, $f);
}
if (isset($references[$row['name']][$row['lnum']]) && $references[$row['name']][$row['lnum']] === $row['fnum']) {
if (
isset($references[$row['name']][$row['lnum']])
&& $references[$row['name']][$row['lnum']] === $row['fnum']
) {
$fKeys[$row['name']]['local'][] = $row['local'];
$fKeys[$row['name']]['foreign'][] = $row['foreign'];
}

View File

@@ -11,6 +11,7 @@ namespace Dibi\Drivers;
use Dibi;
use Dibi\Helpers;
use PgSql;
/**
@@ -20,7 +21,7 @@ class PostgreResult implements Dibi\ResultDriver
{
use Dibi\Strict;
/** @var resource */
/** @var resource|PgSql\Result */
private $resultSet;
/** @var bool */
@@ -28,7 +29,7 @@ class PostgreResult implements Dibi\ResultDriver
/**
* @param resource $resultSet
* @param resource|PgSql\Result $resultSet
*/
public function __construct($resultSet)
{
@@ -97,21 +98,26 @@ class PostgreResult implements Dibi\ResultDriver
'table' => pg_field_table($this->resultSet, $i),
'nativetype' => pg_field_type($this->resultSet, $i),
];
$row['fullname'] = $row['table'] ? $row['table'] . '.' . $row['name'] : $row['name'];
$row['fullname'] = $row['table']
? $row['table'] . '.' . $row['name']
: $row['name'];
$columns[] = $row;
}
return $columns;
}
/**
* Returns the result set resource.
* @return resource|null
* @return resource|PgSql\Result|null
*/
public function getResultResource()
{
$this->autoFree = false;
return is_resource($this->resultSet) ? $this->resultSet : null;
return is_resource($this->resultSet) || $this->resultSet instanceof PgSql\Result
? $this->resultSet
: null;
}

View File

@@ -37,9 +37,7 @@ class SqliteDriver implements Dibi\Driver
private $fmtDateTime;
/**
* @throws Dibi\NotSupportedException
*/
/** @throws Dibi\NotSupportedException */
public function __construct(array $config)
{
if (!extension_loaded('sqlite3')) {
@@ -59,7 +57,7 @@ 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());
}
}
@@ -94,6 +92,7 @@ class SqliteDriver implements Dibi\Driver
} elseif ($res instanceof \SQLite3Result && $res->numColumns()) {
return $this->createResultDriver($res);
}
return null;
}
@@ -147,7 +146,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');
}
@@ -157,7 +156,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');
}
@@ -167,7 +166,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');
}
@@ -254,7 +253,7 @@ class SqliteDriver implements Dibi\Driver
public function escapeLike(string $value, int $pos): string
{
$value = addcslashes($this->connection->escapeString($value), '%_\\');
return ($pos <= 0 ? "'%" : "'") . $value . ($pos >= 0 ? "%'" : "'") . " ESCAPE '\\'";
return ($pos & 1 ? "'%" : "'") . $value . ($pos & 2 ? "%'" : "'") . " ESCAPE '\\'";
}
@@ -288,7 +287,12 @@ class SqliteDriver implements Dibi\Driver
/**
* Registers an aggregating user defined function for use in SQL statements.
*/
public function registerAggregateFunction(string $name, callable $rowCallback, callable $agrCallback, int $numArgs = -1): void
public function registerAggregateFunction(
string $name,
callable $rowCallback,
callable $agrCallback,
int $numArgs = -1
): void
{
$this->connection->createAggregate($name, $rowCallback, $agrCallback, $numArgs);
}

View File

@@ -44,6 +44,7 @@ class SqliteReflector implements Dibi\Reflector
while ($row = $res->fetch(true)) {
$tables[] = $row;
}
return $tables;
}
@@ -64,12 +65,13 @@ class SqliteReflector implements Dibi\Reflector
'fullname' => "$table.$column",
'nativetype' => strtoupper($type[0]),
'size' => isset($type[1]) ? (int) $type[1] : null,
'nullable' => $row['notnull'] == '0',
'nullable' => $row['notnull'] === 0,
'default' => $row['dflt_value'],
'autoincrement' => $row['pk'] && $type[0] === 'INTEGER',
'vendor' => $row,
];
}
return $columns;
}
@@ -98,13 +100,15 @@ class SqliteReflector implements Dibi\Reflector
$column = $indexes[$index]['columns'][0];
$primary = false;
foreach ($columns as $info) {
if ($column == $info['name']) {
if ($column === $info['name']) {
$primary = $info['vendor']['pk'];
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 +146,7 @@ class SqliteReflector implements Dibi\Reflector
$keys[$row['id']]['foreign'] = null;
}
}
return array_values($keys);
}
}

View File

@@ -96,9 +96,10 @@ class SqliteResult implements Dibi\ResultDriver
'name' => $this->resultSet->columnName($i),
'table' => null,
'fullname' => $this->resultSet->columnName($i),
'nativetype' => $types[$this->resultSet->columnType($i)],
'nativetype' => $types[$this->resultSet->columnType($i)] ?? null, // buggy in PHP 7.4.4 & 7.3.16, bug 79414
];
}
return $columns;
}

View File

@@ -39,9 +39,7 @@ class SqlsrvDriver implements Dibi\Driver
private $version = '';
/**
* @throws Dibi\NotSupportedException
*/
/** @throws Dibi\NotSupportedException */
public function __construct(array $config)
{
if (!extension_loaded('sqlsrv')) {
@@ -55,7 +53,9 @@ 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'];
@@ -65,13 +65,16 @@ class SqlsrvDriver implements Dibi\Driver
$options['UID'] = (string) $options['UID'];
$options['Database'] = (string) $options['Database'];
sqlsrv_configure('WarningsReturnAsErrors', 0);
$this->connection = sqlsrv_connect($config['host'], $options);
if (!is_resource($this->connection)) {
$info = sqlsrv_errors(SQLSRV_ERR_ERRORS);
throw new Dibi\DriverException($info[0]['message'], $info[0]['code']);
}
sqlsrv_configure('WarningsReturnAsErrors', 1);
}
if (!is_resource($this->connection)) {
$info = sqlsrv_errors();
throw new Dibi\DriverException($info[0]['message'], $info[0]['code']);
}
$this->version = sqlsrv_server_info($this->connection)['SQLServerVersion'];
}
@@ -100,8 +103,11 @@ class SqlsrvDriver implements Dibi\Driver
} elseif (is_resource($res)) {
$this->affectedRows = Helpers::false2Null(sqlsrv_rows_affected($res));
return sqlsrv_num_fields($res) ? $this->createResultDriver($res) : null;
return sqlsrv_num_fields($res)
? $this->createResultDriver($res)
: null;
}
return null;
}
@@ -125,6 +131,7 @@ class SqlsrvDriver implements Dibi\Driver
$row = sqlsrv_fetch_array($res, SQLSRV_FETCH_NUMERIC);
return Dibi\Helpers::intVal($row[0]);
}
return null;
}
@@ -133,7 +140,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 +150,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 +160,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);
}
@@ -202,7 +209,7 @@ class SqlsrvDriver implements Dibi\Driver
public function escapeBinary(string $value): string
{
return "'" . str_replace("'", "''", $value) . "'";
return '0x' . bin2hex($value);
}
@@ -243,7 +250,7 @@ class SqlsrvDriver implements Dibi\Driver
public function escapeLike(string $value, int $pos): string
{
$value = strtr($value, ["'" => "''", '%' => '[%]', '_' => '[_]', '[' => '[[]']);
return ($pos <= 0 ? "'%" : "'") . $value . ($pos >= 0 ? "%'" : "'");
return ($pos & 1 ? "'%" : "'") . $value . ($pos & 2 ? "%'" : "'");
}
@@ -262,7 +269,6 @@ class SqlsrvDriver implements Dibi\Driver
} elseif ($limit !== null) {
$sql = sprintf('SELECT TOP (%d) * FROM (%s) t', $limit, $sql);
}
} elseif ($limit !== null) {
// requires ORDER BY, see https://technet.microsoft.com/en-us/library/gg699618(v=sql.110).aspx
$sql = sprintf('%s OFFSET %d ROWS FETCH NEXT %d ROWS ONLY', rtrim($sql), $offset, $limit);

View File

@@ -42,6 +42,7 @@ class SqlsrvReflector implements Dibi\Reflector
'view' => isset($row[1]) && $row[1] === 'VIEW',
];
}
return $tables;
}
@@ -91,6 +92,7 @@ class SqlsrvReflector implements Dibi\Reflector
'vendor' => $row,
];
}
return $columns;
}
@@ -114,6 +116,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);
}

View File

@@ -96,6 +96,7 @@ class SqlsrvResult implements Dibi\ResultDriver
'nativetype' => $fieldMetadata['Type'],
];
}
return $columns;
}

View File

@@ -53,7 +53,7 @@ class Event
public $source;
public function __construct(Connection $connection, int $type, string $sql = null)
public function __construct(Connection $connection, int $type, ?string $sql = null)
{
$this->connection = $connection;
$this->type = $type;
@@ -70,7 +70,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'])
&& substr($row['file'], 0, strlen($dibiDir)) !== $dibiDir
) {
$this->source = [$row['file'], (int) $row['line']];
break;
}

View File

@@ -32,6 +32,12 @@ namespace Dibi;
* @method Fluent and(...$cond)
* @method Fluent or(...$cond)
* @method Fluent using(...$cond)
* @method Fluent update(...$cond)
* @method Fluent insert(...$cond)
* @method Fluent delete(...$cond)
* @method Fluent into(...$cond)
* @method Fluent values(...$cond)
* @method Fluent set(...$args)
* @method Fluent asc()
* @method Fluent desc()
*/
@@ -113,7 +119,7 @@ class Fluent implements IDataSource
$this->connection = $connection;
if (self::$normalizer === null) {
self::$normalizer = new HashMap([__CLASS__, '_formatClause']);
self::$normalizer = new HashMap([self::class, '_formatClause']);
}
}
@@ -130,6 +136,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;
@@ -159,7 +166,6 @@ class Fluent implements IDataSource
$this->cursor[] = $sep;
}
}
} else {
// append to currect flow
if ($args === [self::REMOVE]) {
@@ -197,6 +203,7 @@ class Fluent implements IDataSource
if ($arg instanceof self) {
$arg = new Literal("($arg)");
}
$this->cursor[] = $arg;
}
@@ -239,6 +246,7 @@ class Fluent implements IDataSource
} else {
unset($this->flags[$flag]);
}
return $this;
}
@@ -285,7 +293,7 @@ class Fluent implements IDataSource
* @return Result|int|null result set or number of affected rows
* @throws Exception
*/
public function execute(string $return = null)
public function execute(?string $return = null)
{
$res = $this->query($this->_export());
switch ($return) {
@@ -305,11 +313,9 @@ class Fluent implements IDataSource
*/
public function fetch()
{
if ($this->command === 'SELECT' && !$this->clauses['LIMIT']) {
return $this->query($this->_export(null, ['%lmt', 1]))->fetch();
} else {
return $this->query($this->_export())->fetch();
}
return $this->command === 'SELECT' && !$this->clauses['LIMIT']
? $this->query($this->_export(null, ['%lmt', 1]))->fetch()
: $this->query($this->_export())->fetch();
}
@@ -319,18 +325,16 @@ class Fluent implements IDataSource
*/
public function fetchSingle()
{
if ($this->command === 'SELECT' && !$this->clauses['LIMIT']) {
return $this->query($this->_export(null, ['%lmt', 1]))->fetchSingle();
} else {
return $this->query($this->_export())->fetchSingle();
}
return $this->command === 'SELECT' && !$this->clauses['LIMIT']
? $this->query($this->_export(null, ['%lmt', 1]))->fetchSingle()
: $this->query($this->_export())->fetchSingle();
}
/**
* 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();
}
@@ -349,7 +353,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);
}
@@ -358,7 +362,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();
}
@@ -367,7 +371,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));
}
@@ -388,6 +392,7 @@ class Fluent implements IDataSource
$method = array_shift($setup);
$res->$method(...$setup);
}
return $res;
}
@@ -418,7 +423,7 @@ class Fluent implements IDataSource
/**
* 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;
@@ -426,7 +431,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)) {
@@ -442,6 +446,7 @@ class Fluent implements IDataSource
if ($clause === $this->command && $this->flags) {
$args[] = implode(' ', array_keys($this->flags));
}
foreach ($statement as $arg) {
$args[] = $arg;
}
@@ -462,6 +467,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));
}
@@ -473,6 +479,7 @@ class Fluent implements IDataSource
$this->clauses[$clause] = &$val;
unset($val);
}
$this->cursor = &$foo;
}
}

View File

@@ -14,6 +14,7 @@ namespace Dibi;
* Lazy cached storage.
* @internal
*/
#[\AllowDynamicProperties]
abstract class HashMapBase
{
/** @var callable */
@@ -50,6 +51,7 @@ final class HashMap extends HashMapBase
if ($nm === '') {
$nm = "\xFF";
}
$this->$nm = $val;
}
@@ -58,7 +60,7 @@ final class HashMap extends HashMapBase
{
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);
}

View File

@@ -41,6 +41,7 @@ class Helpers
$spaces = $maxLen - mb_strlen($col) + 2;
echo "$col" . str_repeat(' ', $spaces) . "$val\n";
}
echo "\n";
}
@@ -53,6 +54,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 +62,7 @@ class Helpers
foreach ($row as $col) {
echo "\t\t<td>", htmlspecialchars((string) $col), "</td>\n";
}
echo "\t</tr>\n";
}
@@ -104,6 +107,7 @@ class Helpers
}
}, $sql);
}
echo trim($sql) . "\n\n";
} else {
@@ -143,13 +147,14 @@ class Helpers
{
$best = null;
$min = (strlen($value) / 4 + 1) * 10 + .1;
foreach (array_unique($items, SORT_REGULAR) as $item) {
$item = is_object($item) ? $item->getName() : $item;
$items = array_map('strval', $items);
foreach (array_unique($items) as $item) {
if (($len = levenshtein($item, $value, 10, 11, 10)) > 0 && $len < $min) {
$min = $len;
$best = $item;
}
}
return $best;
}
@@ -181,6 +186,7 @@ class Helpers
{
static $patterns = [
'^_' => Type::TEXT, // PostgreSQL arrays
'RANGE$' => Type::TEXT, // PostgreSQL range types
'BYTEA|BLOB|BIN' => Type::BINARY,
'TEXT|CHAR|POINT|INTERVAL|STRING' => Type::TEXT,
'YEAR|BYTE|COUNTER|SERIAL|INT|LONG|SHORT|^TINY$' => Type::INTEGER,
@@ -197,18 +203,18 @@ class Helpers
return $val;
}
}
return null;
}
/**
* @internal
*/
/** @internal */
public static function getTypeCache(): HashMap
{
if (self::$types === null) {
self::$types = new HashMap([__CLASS__, 'detectType']);
self::$types = new HashMap([self::class, 'detectType']);
}
return self::$types;
}
@@ -234,7 +240,7 @@ class Helpers
* Import SQL dump from file.
* @return int count of sql commands
*/
public static function loadFromFile(Connection $connection, string $file, callable $onProgress = null): int
public static function loadFromFile(Connection $connection, string $file, ?callable $onProgress = null): int
{
@set_time_limit(0); // intentionally @
@@ -261,7 +267,6 @@ class Helpers
if ($onProgress) {
$onProgress($count, isset($stat['size']) ? $size * 100 / $stat['size'] : null);
}
} else {
$sql .= $s;
}
@@ -274,23 +279,20 @@ class Helpers
$onProgress($count, isset($stat['size']) ? 100 : null);
}
}
fclose($handle);
return $count;
}
/**
* @internal
*/
/** @internal */
public static function false2Null($val)
{
return $val === false ? null : $val;
}
/**
* @internal
*/
/** @internal */
public static function intVal($value): int
{
if (is_int($value)) {
@@ -299,6 +301,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.");

View File

@@ -25,11 +25,15 @@ class FileLogger
/** @var int */
public $filter;
/** @var bool */
private $errorsOnly;
public function __construct(string $file, int $filter = null)
public function __construct(string $file, ?int $filter = null, bool $errorsOnly = false)
{
$this->file = $file;
$this->filter = $filter ?: Dibi\Event::QUERY;
$this->errorsOnly = $errorsOnly;
}
@@ -38,39 +42,43 @@ class FileLogger
*/
public function logEvent(Dibi\Event $event): void
{
if (($event->type & $this->filter) === 0) {
if (
(($event->type & $this->filter) === 0)
|| ($this->errorsOnly === true && !$event->result instanceof \Exception)
) {
return;
}
$handle = fopen($this->file, 'a');
if (!$handle) {
return; // or throw exception?
}
flock($handle, LOCK_EX);
if ($event->result instanceof \Exception) {
$message = $event->result->getMessage();
if ($code = $event->result->getCode()) {
$message = "[$code] $message";
}
fwrite($handle,
$this->writeToFile(
$event,
"ERROR: $message"
. "\n-- SQL: " . $event->sql
. "\n-- driver: " . $event->connection->getConfig('driver') . '/' . $event->connection->getConfig('name')
. ";\n-- " . date('Y-m-d H:i:s')
. "\n\n"
. "\n-- SQL: " . $event->sql
);
} else {
fwrite($handle,
$this->writeToFile(
$event,
'OK: ' . $event->sql
. ($event->count ? ";\n-- rows: " . $event->count : '')
. "\n-- takes: " . sprintf('%0.3f ms', $event->time * 1000)
. "\n-- source: " . implode(':', $event->source)
. "\n-- driver: " . $event->connection->getConfig('driver') . '/' . $event->connection->getConfig('name')
. "\n-- " . date('Y-m-d H:i:s')
. "\n\n"
. ($event->count ? ";\n-- rows: " . $event->count : '')
. "\n-- takes: " . sprintf('%0.3f ms', $event->time * 1000)
. "\n-- source: " . implode(':', $event->source)
);
}
fclose($handle);
}
private function writeToFile(Dibi\Event $event, string $message): void
{
$driver = $event->connection->getConfig('driver');
$message .=
"\n-- driver: " . (is_object($driver) ? get_class($driver) : $driver) . '/' . $event->connection->getConfig('name')
. "\n-- " . date('Y-m-d H:i:s')
. "\n\n";
file_put_contents($this->file, $message, FILE_APPEND | LOCK_EX);
}
}

View File

@@ -36,7 +36,7 @@ class Column
private $info;
public function __construct(Dibi\Reflector $reflector = null, array $info)
public function __construct(?Dibi\Reflector $reflector, array $info)
{
$this->reflector = $reflector;
$this->info = $info;
@@ -66,13 +66,16 @@ 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']]);
}
public function getTableName(): ?string
{
return isset($this->info['table']) && $this->info['table'] != null ? $this->info['table'] : null; // intentionally ==
return isset($this->info['table']) && $this->info['table'] != null // intentionally ==
? $this->info['table']
: null;
}
@@ -106,18 +109,14 @@ class Column
}
/**
* @return mixed
*/
/** @return mixed */
public function getDefault()
{
return $this->info['default'] ?? null;
}
/**
* @return mixed
*/
/** @return mixed */
public function getVendorInfo(string $key)
{
return $this->info['vendor'][$key] ?? null;

View File

@@ -33,7 +33,7 @@ class Database
private $tables;
public function __construct(Dibi\Reflector $reflector, string $name = null)
public function __construct(Dibi\Reflector $reflector, ?string $name = null)
{
$this->reflector = $reflector;
$this->name = $name;
@@ -46,9 +46,7 @@ class Database
}
/**
* @return Table[]
*/
/** @return Table[] */
public function getTables(): array
{
$this->init();
@@ -56,9 +54,7 @@ class Database
}
/**
* @return string[]
*/
/** @return string[] */
public function getTableNames(): array
{
$this->init();
@@ -66,6 +62,7 @@ class Database
foreach ($this->tables as $table) {
$res[] = $table->getName();
}
return $res;
}

View File

@@ -38,9 +38,7 @@ class Result
}
/**
* @return Column[]
*/
/** @return Column[] */
public function getColumns(): array
{
$this->initColumns();
@@ -48,9 +46,7 @@ class Result
}
/**
* @return string[]
*/
/** @return string[] */
public function getColumnNames(bool $fullNames = false): array
{
$this->initColumns();
@@ -58,6 +54,7 @@ class Result
foreach ($this->columns as $column) {
$res[] = $fullNames ? $column->getFullName() : $column->getName();
}
return $res;
}
@@ -86,7 +83,9 @@ class Result
{
if ($this->columns === null) {
$this->columns = [];
$reflector = $this->driver instanceof Dibi\Reflector ? $this->driver : null;
$reflector = $this->driver instanceof Dibi\Reflector
? $this->driver
: null;
foreach ($this->driver->getResultColumns() as $info) {
$this->columns[] = $this->names[strtolower($info['name'])] = new Column($reflector, $info);
}

View File

@@ -69,9 +69,7 @@ class Table
}
/**
* @return Column[]
*/
/** @return Column[] */
public function getColumns(): array
{
$this->initColumns();
@@ -79,9 +77,7 @@ class Table
}
/**
* @return string[]
*/
/** @return string[] */
public function getColumnNames(): array
{
$this->initColumns();
@@ -89,6 +85,7 @@ class Table
foreach ($this->columns as $column) {
$res[] = $column->getName();
}
return $res;
}
@@ -113,9 +110,7 @@ class Table
}
/**
* @return ForeignKey[]
*/
/** @return ForeignKey[] */
public function getForeignKeys(): array
{
$this->initForeignKeys();
@@ -123,9 +118,7 @@ class Table
}
/**
* @return Index[]
*/
/** @return Index[] */
public function getIndexes(): array
{
$this->initIndexes();
@@ -160,6 +153,7 @@ class Table
foreach ($info['columns'] as $key => $name) {
$info['columns'][$key] = $this->columns[strtolower($name)];
}
$this->indexes[strtolower($info['name'])] = new Index($info);
if (!empty($info['primary'])) {
$this->primaryKey = $this->indexes[strtolower($info['name'])];

View File

@@ -19,7 +19,7 @@ class Result implements IDataSource
{
use Strict;
/** @var ResultDriver */
/** @var ResultDriver|null */
private $driver;
/** @var array Translate table */
@@ -41,10 +41,12 @@ class Result implements IDataSource
private $formats = [];
public function __construct(ResultDriver $driver)
public function __construct(ResultDriver $driver, bool $normalize = true)
{
$this->driver = $driver;
$this->detectTypes();
if ($normalize) {
$this->detectTypes();
}
}
@@ -83,7 +85,9 @@ class Result implements IDataSource
*/
final public function seek(int $row): bool
{
return ($row !== 0 || $this->fetched) ? $this->getResultDriver()->seek($row) : true;
return ($row !== 0 || $this->fetched)
? $this->getResultDriver()->seek($row)
: true;
}
@@ -166,6 +170,7 @@ class Result implements IDataSource
if ($row === null) {
return null;
}
$this->fetched = true;
$this->normalize($row);
if ($this->rowFactory) {
@@ -173,6 +178,7 @@ class Result implements IDataSource
} elseif ($this->rowClass) {
return new $this->rowClass($row);
}
return $row;
}
@@ -187,6 +193,7 @@ class Result implements IDataSource
if ($row === null) {
return null;
}
$this->fetched = true;
$this->normalize($row);
return reset($row);
@@ -197,9 +204,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 === null ? -1 : $limit;
$limit = $limit ?? -1;
$this->seek($offset ?: 0);
$row = $this->fetch();
if (!$row) {
@@ -211,6 +218,7 @@ class Result implements IDataSource
if ($limit === 0) {
break;
}
$limit--;
$data[] = $row;
} while ($row = $this->fetch());
@@ -242,6 +250,9 @@ class Result implements IDataSource
$data = null;
$assoc = preg_split('#(\[\]|->|=|\|)#', $assoc, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
if (!$assoc) {
throw new \InvalidArgumentException("Invalid descriptor '$assoc'.");
}
// check columns
foreach ($assoc as $as) {
@@ -280,9 +291,8 @@ class Result implements IDataSource
} else {
$x = &$x->{$assoc[$i + 1]};
}
} elseif ($as !== '|') { // associative-array node
$x = &$x[$row->$as];
$x = &$x[(string) $row->$as];
}
}
@@ -292,13 +302,12 @@ class Result implements IDataSource
} while ($row = $this->fetch());
unset($x);
/** @var mixed[] $data */
return $data;
}
/**
* @deprecated
*/
/** @deprecated */
private function oldFetchAssoc(string $assoc)
{
$this->seek(0);
@@ -339,7 +348,6 @@ class Result implements IDataSource
} else {
$x = &$x[$assoc[$i + 1]];
}
} elseif ($as === '@') { // "object" node
if ($x === null) {
$x = clone $row;
@@ -348,18 +356,15 @@ class Result implements IDataSource
} else {
$x = &$x->{$assoc[$i + 1]};
}
} else { // associative-array node
$x = &$x[$row->$as];
$x = &$x[(string) $row->$as];
}
}
if ($x === null) { // build leaf
if ($leaf === '=') {
$x = $row->toArray();
} else {
$x = $row;
}
$x = $leaf === '='
? $row->toArray()
: $row;
}
} while ($row = $this->fetch());
@@ -372,7 +377,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();
@@ -394,6 +399,7 @@ class Result implements IDataSource
do {
$data[] = $row[$key];
} while ($row = $this->fetch());
return $data;
}
@@ -408,6 +414,7 @@ class Result implements IDataSource
do {
$data[] = $row[$value];
} while ($row = $this->fetch());
return $data;
}
@@ -451,8 +458,14 @@ class Result implements IDataSource
if (!isset($row[$key])) { // null
continue;
}
$value = $row[$key];
if ($type === Type::TEXT) {
$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) {
@@ -469,9 +482,11 @@ class Result implements IDataSource
} elseif ($p !== false && $e !== false) {
$value = rtrim($value, '.');
}
if ($value === '' || $value[0] === '.') {
$value = '0' . $value;
}
$row[$key] = $value === str_replace(',', '.', (string) ($float = (float) $value))
? $float
: $value;
@@ -480,27 +495,31 @@ class Result implements IDataSource
$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', ...
if ($value && substr((string) $value, 0, 7) !== '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) {
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) {
$row[$key] = is_string($value) ? $this->getResultDriver()->unescapeBinary($value) : $value;
$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');
}
} else {
throw new \RuntimeException('Unexpected type ' . $type);
}
}
}
@@ -536,7 +555,7 @@ class Result implements IDataSource
/**
* Sets date format.
* Sets type format.
*/
final public function setFormat(string $type, ?string $format): self
{
@@ -545,6 +564,16 @@ class Result implements IDataSource
}
/**
* Sets type formats.
*/
final public function setFormats(array $formats): self
{
$this->formats = $formats;
return $this;
}
/**
* Returns data format.
*/
@@ -565,13 +594,12 @@ class Result implements IDataSource
if ($this->meta === null) {
$this->meta = new Reflection\Result($this->getResultDriver());
}
return $this->meta;
}
/**
* @return Reflection\Column[]
*/
/** @return Reflection\Column[] */
final public function getColumns(): array
{
return $this->getInfo()->getColumns();

View File

@@ -44,6 +44,7 @@ class ResultIterator implements \Iterator, \Countable
}
#[\ReturnTypeWillChange]
/**
* Returns the key of the current element.
* @return mixed
@@ -54,6 +55,7 @@ class ResultIterator implements \Iterator, \Countable
}
#[\ReturnTypeWillChange]
/**
* Returns the current element.
* @return mixed

View File

@@ -13,6 +13,7 @@ namespace Dibi;
/**
* Result set single row.
*/
#[\AllowDynamicProperties]
class Row implements \ArrayAccess, \IteratorAggregate, \Countable
{
public function __construct(array $arr)
@@ -33,15 +34,17 @@ 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)
{
$time = $this[$key];
if (!$time instanceof DateTime) {
if (!$time || substr((string) $time, 0, 3) === '000') { // '', null, false, '0000-00-00', ...
if (!$time || substr((string) $time, 0, 7) === '0000-00') { // '', null, false, '0000-00-00', ...
return null;
}
$time = new DateTime($time);
}
return $format === null ? $time : $time->format($format);
}
@@ -53,40 +56,47 @@ class Row implements \ArrayAccess, \IteratorAggregate, \Countable
}
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;
}
#[\ReturnTypeWillChange]
final public function offsetGet($nm)
{
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);
}

View File

@@ -29,9 +29,12 @@ trait Strict
*/
public function __call(string $name, array $args)
{
$class = method_exists($this, $name) ? 'parent' : get_class($this);
$class = method_exists($this, $name) ? 'parent' : static::class;
$items = (new ReflectionClass($this))->getMethods(ReflectionMethod::IS_PUBLIC);
$hint = ($t = Helpers::getSuggestion($items, $name)) ? ", did you mean $t()?" : '.';
$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");
}
@@ -42,9 +45,12 @@ trait Strict
*/
public static function __callStatic(string $name, array $args)
{
$rc = new ReflectionClass(get_called_class());
$items = array_intersect($rc->getMethods(ReflectionMethod::IS_PUBLIC), $rc->getMethods(ReflectionMethod::IS_STATIC));
$hint = ($t = Helpers::getSuggestion($items, $name)) ? ", did you mean $t()?" : '.';
$rc = new ReflectionClass(static::class);
$items = array_filter($rc->getMethods(\ReflectionMethod::IS_STATIC), function ($m) { return $m->isPublic(); });
$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");
}
@@ -61,9 +67,13 @@ trait Strict
$ret = $this->$m();
return $ret;
}
$rc = new ReflectionClass($this);
$items = array_diff($rc->getProperties(ReflectionProperty::IS_PUBLIC), $rc->getProperties(ReflectionProperty::IS_STATIC));
$hint = ($t = Helpers::getSuggestion($items, $name)) ? ", did you mean $$t?" : '.';
$items = array_filter($rc->getProperties(ReflectionProperty::IS_PUBLIC), function ($p) { return !$p->isStatic(); });
$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");
}
@@ -75,8 +85,11 @@ trait Strict
public function __set(string $name, $value)
{
$rc = new ReflectionClass($this);
$items = array_diff($rc->getProperties(ReflectionProperty::IS_PUBLIC), $rc->getProperties(ReflectionProperty::IS_STATIC));
$hint = ($t = Helpers::getSuggestion($items, $name)) ? ", did you mean $$t?" : '.';
$items = array_filter($rc->getProperties(ReflectionProperty::IS_PUBLIC), function ($p) { return !$p->isStatic(); });
$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");
}
@@ -93,7 +106,7 @@ trait Strict
*/
public function __unset(string $name)
{
$class = get_class($this);
$class = static::class;
throw new \LogicException("Attempt to unset undeclared property $class::$$name.");
}
}

View File

@@ -69,6 +69,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 = [];
@@ -92,28 +93,31 @@ final class Translator
$sql[] = $arg;
} else {
$sql[] = substr($arg, 0, $toSkip)
/*
. preg_replace_callback('/
(?=[`[\'":%?]) ## 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',
*/ // note: this can change $this->args & $this->cursor & ...
. preg_replace_callback('/(?=[`[\'":%?])(?:`(.+?)`|\[(.+?)\]|(\')((?:\'\'|[^\'])*)\'|(")((?:""|[^"])*)"|(\'|")|:(\S*?:)([a-zA-Z0-9._]?)|%([a-zA-Z~][a-zA-Z0-9~]{0,5})|(\?))/s',
// 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)
);
);
if (preg_last_error()) {
throw new PcreException;
}
}
continue;
}
@@ -136,8 +140,10 @@ final class Translator
if ($lastArr === $cursor - 1) {
$sql[] = ',';
}
$sql[] = $this->formatValue($arg, $commandIns ? 'l' : 'a');
}
$lastArr = $cursor;
continue;
}
@@ -146,7 +152,6 @@ final class Translator
$sql[] = $this->formatValue($arg, null);
} // while
if ($comment) {
$sql[] = '*/';
}
@@ -212,13 +217,14 @@ final class Translator
} else {
$op = '= ';
}
$vx[] = $k . $op . $v;
}
} else {
$vx[] = $this->formatValue($v, 'ex');
}
}
return '(' . implode(') ' . strtoupper($modifier) . ' (', $vx) . ')';
case 'n': // key, key, ... identifier names
@@ -230,6 +236,7 @@ final class Translator
$vx[] = $this->identifiers->{$pair[0]};
}
}
return implode(', ', $vx);
@@ -239,6 +246,7 @@ final class Translator
$vx[] = $this->identifiers->{$pair[0]} . '='
. $this->formatValue($v, $pair[1] ?? (is_array($v) ? 'ex!' : null));
}
return implode(', ', $vx);
@@ -248,6 +256,7 @@ final class Translator
$pair = explode('%', (string) $k, 2); // split into identifier & modifier
$vx[] = $this->formatValue($v, $pair[1] ?? (is_array($v) ? 'ex!' : null));
}
return '(' . (($vx || $modifier === 'l') ? implode(', ', $vx) : 'NULL') . ')';
@@ -257,6 +266,7 @@ final class Translator
$kx[] = $this->identifiers->{$pair[0]};
$vx[] = $this->formatValue($v, $pair[1] ?? (is_array($v) ? 'ex!' : null));
}
return '(' . implode(', ', $kx) . ') VALUES (' . implode(', ', $vx) . ')';
case 'm': // (key, key, ...) VALUES (val, val, ...), (val, val, ...), ...
@@ -279,9 +289,11 @@ final class Translator
$vx[$k2][] = $this->formatValue($v2, $pair[1] ?? (is_array($v2) ? 'ex!' : null));
}
}
foreach ($vx as $k => $v) {
$vx[$k] = '(' . implode(', ', $v) . ')';
}
return '(' . implode(', ', $kx) . ') VALUES ' . implode(', ', $vx);
case 'by': // key ASC, key DESC
@@ -289,12 +301,13 @@ final class Translator
if (is_array($v)) {
$vx[] = $this->formatValue($v, 'ex');
} elseif (is_string($k)) {
$v = (is_string($v) && strncasecmp($v, 'd', 1)) || $v > 0 ? 'ASC' : 'DESC';
$v = (is_string($v) ? strncasecmp($v, 'd', 1) : $v > 0) ? 'ASC' : 'DESC';
$vx[] = $this->identifiers->$k . ' ' . $v;
} else {
$vx[] = $this->identifiers->$v;
}
}
return implode(', ', $vx);
case 'ex!':
@@ -308,10 +321,15 @@ final class Translator
foreach ($value as $v) {
$vx[] = $this->formatValue($v, $modifier);
}
return implode(', ', $vx);
}
}
// object-to-scalar procession
if ($value instanceof \BackedEnum && is_scalar($value->value)) {
$value = $value->value;
}
// with modifier procession
if ($modifier) {
@@ -322,7 +340,13 @@ final class Translator
} elseif ($value instanceof Expression && $modifier === 'ex') {
return $this->connection->translate(...$value->getValues());
} elseif ($value instanceof \DateTimeInterface && ($modifier === 'd' || $modifier === 't' || $modifier === 'dt')) {
} elseif (
$value instanceof \DateTimeInterface
&& ($modifier === 'd'
|| $modifier === 't'
|| $modifier === 'dt'
)
) {
// continue
} else {
$type = is_object($value) ? get_class($value) : gettype($value);
@@ -332,21 +356,29 @@ final class Translator
switch ($modifier) {
case 's': // string
return $value === null ? 'NULL' : $this->driver->escapeText((string) $value);
return $value === null
? 'NULL'
: $this->driver->escapeText((string) $value);
case 'bin':// binary
return $value === null ? 'NULL' : $this->driver->escapeBinary($value);
return $value === null
? 'NULL'
: $this->driver->escapeBinary($value);
case 'b': // boolean
return $value === null ? 'NULL' : $this->driver->escapeBool((bool) $value);
return $value === null
? 'NULL'
: $this->driver->escapeBool((bool) $value);
case 'sN': // string or null
case 'sn':
return $value == '' ? 'NULL' : $this->driver->escapeText((string) $value); // notice two equal signs
return $value === '' || $value === 0 || $value === null
? 'NULL'
: $this->driver->escapeText((string) $value);
case 'iN': // signed int or null
if ($value == '') {
$value = null;
if ($value === '' || $value === 0 || $value === null) {
return 'NULL';
}
// break omitted
case 'i': // signed int
@@ -384,7 +416,10 @@ final class Translator
} elseif (!$value instanceof \DateTimeInterface) {
$value = new DateTime($value);
}
return $modifier === 'd' ? $this->driver->escapeDate($value) : $this->driver->escapeDateTime($value);
return $modifier === 'd'
? $this->driver->escapeDate($value)
: $this->driver->escapeDateTime($value);
case 'by':
case 'n': // composed identifier name
@@ -400,27 +435,43 @@ final class Translator
$toSkip = strcspn($value, '`[\'":');
if (strlen($value) !== $toSkip) {
$value = substr($value, 0, $toSkip)
. preg_replace_callback(
'/(?=[`[\'":])(?:`(.+?)`|\[(.+?)\]|(\')((?:\'\'|[^\'])*)\'|(")((?:""|[^"])*)"|(\'|")|:(\S*?:)([a-zA-Z0-9._]?))/s',
[$this, 'cb'],
substr($value, $toSkip)
);
. preg_replace_callback(
<<<'XX'
/
(?=[`['":])
(?:
`(.+?)`|
\[(.+?)\]|
(')((?:''|[^'])*)'|
(")((?:""|[^"])*)"|
('|")|
:(\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)
return (string) $value;
case 'like~': // LIKE string%
return $this->driver->escapeLike($value, 1);
return $this->driver->escapeLike($value, 2);
case '~like': // LIKE %string
return $this->driver->escapeLike($value, -1);
return $this->driver->escapeLike($value, 1);
case '~like~': // LIKE %string%
return $this->driver->escapeLike($value, 3);
case 'like': // LIKE string
return $this->driver->escapeLike($value, 0);
case 'and':
@@ -436,7 +487,6 @@ final class Translator
}
}
// without modifier procession
if (is_string($value)) {
return $this->driver->escapeText($value);
@@ -518,6 +568,7 @@ final class Translator
$this->comment = true;
return '/*';
}
return '';
} elseif ($mod === 'else') {
@@ -530,7 +581,6 @@ final class Translator
$this->comment = true;
return '/*';
}
} elseif ($mod === 'end') {
$this->ifLevel--;
if ($this->ifLevelStart === $this->ifLevel + 1) {
@@ -539,6 +589,7 @@ final class Translator
$this->comment = false;
return '*/';
}
return '';
} elseif ($mod === 'ex') { // array expansion
@@ -553,6 +604,7 @@ final class Translator
} else {
$this->limit = Helpers::intVal($arg);
}
return '';
} elseif ($mod === 'ofs') { // apply offset
@@ -563,6 +615,7 @@ final class Translator
} else {
$this->offset = Helpers::intVal($arg);
}
return '';
} else { // default processing
@@ -594,7 +647,9 @@ final class Translator
if ($matches[8]) { // SQL identifier substitution
$m = substr($matches[8], 0, -1);
$m = $this->connection->getSubstitutes()->$m;
return $matches[9] == '' ? $this->formatValue($m, null) : $m . $matches[9]; // value or identifier
return $matches[9] === ''
? $this->formatValue($m, null)
: $m . $matches[9]; // value or identifier
}
throw new \Exception('this should be never executed');
@@ -614,6 +669,7 @@ final class Translator
$v = $this->driver->escapeIdentifier($v);
}
}
return implode('.', $parts);
}
}

View File

@@ -30,6 +30,6 @@ class Type
final public function __construct()
{
throw new \LogicException('Cannot instantiate static class ' . __CLASS__);
throw new \LogicException('Cannot instantiate static class ' . self::class);
}
}

View File

@@ -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)
@@ -43,8 +44,7 @@ class dibi
IDENTIFIER = 'n';
/** version */
public const
VERSION = '4.1.0';
public const VERSION = '4.2.6';
/** sorting order */
public const
@@ -75,7 +75,7 @@ class dibi
*/
final public function __construct()
{
throw new LogicException('Cannot instantiate static class ' . get_class($this));
throw new LogicException('Cannot instantiate static class ' . static::class);
}
@@ -106,7 +106,7 @@ class dibi
* Retrieve active connection.
* @throws Dibi\Exception
*/
public static function getConnection(string $name = null): Dibi\Connection
public static function getConnection(?string $name = null): Dibi\Connection
{
if ($name === null) {
if (self::$connection === null) {
@@ -162,7 +162,7 @@ class dibi
/**
* Strips microseconds part.
*/
public static function stripMicroseconds(\DateTimeInterface $dt): \DateTimeInterface
public static function stripMicroseconds(DateTimeInterface $dt): DateTimeInterface
{
$class = get_class($dt);
return new $class($dt->format('Y-m-d H:i:s'), $dt->getTimezone());

View File

@@ -22,7 +22,7 @@ class Exception extends \Exception
/**
* @param int|string $code
*/
public function __construct(string $message = '', $code = 0, string $sql = null, \Throwable $previous = null)
public function __construct(string $message = '', $code = 0, ?string $sql = null, ?\Throwable $previous = null)
{
parent::__construct($message, 0, $previous);
$this->code = $code;
@@ -93,7 +93,7 @@ class ProcedureException extends Exception
/**
* Construct the exception.
*/
public function __construct(string $message = '', int $code = 0, string $severity = '', string $sql = null)
public function __construct(string $message = '', int $code = 0, string $severity = '', ?string $sql = null)
{
parent::__construct($message, $code, $sql);
$this->severity = $severity;

View File

@@ -51,19 +51,19 @@ 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.
@@ -224,20 +224,20 @@ interface IConnection
* Retrieves the ID generated for an AUTO_INCREMENT column by the previous INSERT query.
* @throws Exception
*/
function getInsertId(string $sequence = null): int;
function getInsertId(?string $sequence = null): int;
/**
* Begins a transaction (if supported).
*/
function begin(string $savepoint = null): void;
function begin(?string $savepoint = null): void;
/**
* Commits statements in a transaction.
*/
function commit(string $savepoint = null): void;
function commit(?string $savepoint = null): void;
/**
* Rollback changes in a transaction.
*/
function rollback(string $savepoint = null): void;
function rollback(?string $savepoint = null): void;
}

View File

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

View File

@@ -1,7 +0,0 @@
imports:
- { resource: '../temp/coding-standard/coding-standard-php71.yml' }
parameters:
skip:
PhpCsFixer\Fixer\Operator\TernaryToNullCoalescingFixer:
- src/Dibi/HashMap.php # issue #260

View 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

View File

@@ -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

View File

@@ -12,7 +12,7 @@ use Tester\Assert;
require __DIR__ . '/bootstrap.php';
test(function () use ($config) {
test('', function () use ($config) {
$conn = new Connection($config);
Assert::true($conn->isConnected());
@@ -21,7 +21,7 @@ test(function () use ($config) {
});
test(function () use ($config) { // lazy
test('lazy', function () use ($config) {
$conn = new Connection($config + ['lazy' => true]);
Assert::false($conn->isConnected());
@@ -30,7 +30,7 @@ test(function () use ($config) { // lazy
});
test(function () use ($config) {
test('', function () use ($config) {
$conn = new Connection($config);
Assert::true($conn->isConnected());
@@ -40,7 +40,7 @@ test(function () use ($config) {
});
test(function () use ($config) {
test('', function () use ($config) {
$conn = new Connection($config);
Assert::true($conn->isConnected());
@@ -52,7 +52,27 @@ test(function () use ($config) {
});
test(function () use ($config) {
test('', function () use ($config) {
$conn = new Connection($config);
Assert::equal('hello', $conn->query('SELECT %s', 'hello')->fetchSingle());
$conn->disconnect();
$conn->connect();
Assert::equal('hello', $conn->query('SELECT %s', 'hello')->fetchSingle());
});
test('', function () use ($config) {
$conn = new Connection($config);
Assert::true($conn->isConnected());
$conn->__destruct();
Assert::false($conn->isConnected());
});
test('', function () use ($config) {
Assert::exception(function () use ($config) {
new Connection($config + ['onConnect' => '']);
}, InvalidArgumentException::class, "Configuration option 'onConnect' must be array.");

View File

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

View File

@@ -30,21 +30,102 @@ Assert::exception(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() & 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(function () use ($conn) {
$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(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',
]);
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(function () use ($conn) {
$conn->transaction(function (Dibi\Connection $connection) {
$connection->begin();
});
}, LogicException::class, Dibi\Connection::class . '::begin() call is forbidden inside a transaction() callback');
Assert::exception(function () use ($conn) {
$conn->transaction(function (Dibi\Connection $connection) {
$connection->commit();
});
}, LogicException::class, Dibi\Connection::class . '::commit() call is forbidden inside a transaction() callback');
Assert::exception(function () use ($conn) {
$conn->transaction(function (Dibi\Connection $connection) {
$connection->rollback();
});
}, LogicException::class, Dibi\Connection::class . '::rollback() call is forbidden inside a transaction() callback');
});

View File

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

View File

@@ -14,8 +14,6 @@ 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('2050-08-13 11:40:00.000000', (string) new DateTime(2544000000));
Assert::same('2050-08-13 11:40:00.000000', (string) (new DateTime)->setTimestamp(2544000000));
Assert::same(is_int(2544000000) ? 2544000000 : '2544000000', (new DateTime(2544000000))->getTimestamp()); // 64 bit
Assert::same('1978-05-05 00:00:00.000000', (string) new DateTime('1978-05-05'));

View File

@@ -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]);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -32,7 +32,7 @@ Assert::same(
);
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)
@@ -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'));

View File

@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
use Dibi\Type;
@@ -24,7 +25,18 @@ class MockResult extends Dibi\Result
}
test(function () {
test('', function () {
$result = new MockResult;
$result->setType('col', Type::TEXT);
$result->setFormat(Type::TEXT, 'native');
Assert::same(['col' => null], $result->test(['col' => null]));
Assert::same(['col' => true], $result->test(['col' => true]));
Assert::same(['col' => false], $result->test(['col' => false]));
});
test('', function () {
$result = new MockResult;
$result->setType('col', Type::BOOL);
@@ -46,7 +58,7 @@ test(function () {
});
test(function () {
test('', function () {
$result = new MockResult;
$result->setType('col', Type::TEXT);
@@ -62,7 +74,7 @@ test(function () {
});
test(function () {
test('', function () {
$result = new MockResult;
$result->setType('col', Type::FLOAT);
@@ -139,7 +151,7 @@ test(function () {
});
test(function () {
test('', function () {
$result = new MockResult;
$result->setType('col', Type::INTEGER);
@@ -147,7 +159,14 @@ test(function () {
Assert::same(['col' => 1], $result->test(['col' => true]));
Assert::same(['col' => 0], $result->test(['col' => false]));
Assert::same(['col' => 0], @$result->test(['col' => ''])); // triggers warning in PHP 7.1
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::same(['col' => 0], $result->test(['col' => '0']));
Assert::same(['col' => 1], $result->test(['col' => '1']));
Assert::same(['col' => 10], $result->test(['col' => '10']));
@@ -165,7 +184,7 @@ test(function () {
});
test(function () {
test('', function () {
$result = new MockResult;
$result->setType('col', Type::DATETIME);
@@ -183,7 +202,7 @@ test(function () {
});
test(function () {
test('', function () {
$result = new MockResult;
$result->setType('col', Type::DATETIME);
$result->setFormat(Type::DATETIME, 'Y-m-d H:i:s');
@@ -202,7 +221,7 @@ test(function () {
});
test(function () {
test('', function () {
$result = new MockResult;
$result->setType('col', Type::DATE);
@@ -218,7 +237,7 @@ test(function () {
});
test(function () {
test('', function () {
$result = new MockResult;
$result->setType('col', Type::TIME);

View File

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

View File

@@ -35,6 +35,10 @@ Assert::error(function () use ($row) {
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) {

View File

@@ -23,7 +23,9 @@ Assert::equal(1, $conn->getInsertId());
$conn->query(
'CREATE TRIGGER %n ON %n AFTER INSERT AS INSERT INTO %n DEFAULT VALUES',
'UpdAAB', 'aab', 'aaa'
'UpdAAB',
'aab',
'aaa'
);
$conn->query('INSERT INTO %n DEFAULT VALUES', 'aab');

View File

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

View File

@@ -1,9 +1,5 @@
<?php
/**
* @phpversion 5.5
*/
declare(strict_types=1);
use Tester\Assert;

View File

@@ -19,12 +19,17 @@ Assert::same(
SELECT *
FROM [customers]
/* WHERE ... LIKE ... */'),
$conn->translate('
$conn->translate(
'
SELECT *
FROM [customers]
%if', isset($name), 'WHERE [name] LIKE %s', 'xxx', '%end'
));
%if',
isset($name),
'WHERE [name] LIKE %s',
'xxx',
'%end'
)
);
// if & else & end (last end is optional)
@@ -32,11 +37,14 @@ Assert::same(
reformat('
SELECT *
FROM [customers] /* ... */'),
$conn->translate('
$conn->translate(
'
SELECT *
FROM %if', true, '[customers] %else [products]'
));
FROM %if',
true,
'[customers] %else [products]'
)
);
// if & else & (optional) end
@@ -48,14 +56,14 @@ WHERE [id] > 0
/* AND ...=...
*/ AND [bar]=1
'),
$conn->translate('
SELECT *
FROM [people]
WHERE [id] > 0
%if', false, 'AND [foo]=%i', 1, '
%else %if', true, 'AND [bar]=%i', 1, '
'));
')
);
// nested condition
@@ -76,24 +84,35 @@ WHERE
/* AND ...=1 */
/* 1 LIMIT 10 */",
]),
$conn->translate('
$conn->translate(
'
SELECT *
FROM [customers]
WHERE
%if', true, '[name] LIKE %s', 'xxx', '
%if', false, 'AND [admin]=1 %end
%if',
true,
'[name] LIKE %s',
'xxx',
'
%if',
false,
'AND [admin]=1 %end
%else 1 LIMIT 10 %end'
));
)
);
// limit & offset
Assert::same(
'SELECT * FROM foo /* (limit 3) (offset 5) */',
$conn->translate(
'SELECT * FROM foo',
'%if', false,
'%lmt', 3,
'%ofs', 5,
'%end'
));
'SELECT * FROM foo',
'%if',
false,
'%lmt',
3,
'%ofs',
5,
'%end'
)
);

View File

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

View File

@@ -1,7 +1,7 @@
<?php
/**
* @dataProvider ../databases.ini
* @dataProvider ../databases.ini !=sqlsrv
*/
declare(strict_types=1);
@@ -71,3 +71,9 @@ Assert::truthy($conn->fetchSingle('SELECT ? LIKE %~like~', 'baa', 'aa'));
Assert::truthy($conn->fetchSingle('SELECT ? LIKE %~like~', 'aab', 'aa'));
Assert::falsey($conn->fetchSingle('SELECT ? LIKE %~like~', 'bba', '%a'));
Assert::truthy($conn->fetchSingle('SELECT ? LIKE %~like~', 'b%a', '%a'));
// matches
Assert::truthy($conn->fetchSingle('SELECT ? LIKE %like', 'a', 'a'));
Assert::falsey($conn->fetchSingle('SELECT ? LIKE %like', 'a', 'aa'));
Assert::falsey($conn->fetchSingle('SELECT ? LIKE %like', 'a', 'b'));

View File

@@ -261,7 +261,7 @@ if ($config['system'] === 'postgre') {
'sqlite' => "SELECT * FROM products WHERE (title LIKE 'C%' ESCAPE '\\' AND title LIKE '%r' ESCAPE '\\') OR title LIKE '%a\n\\%\\_\\\\''\"%' ESCAPE '\\'",
'odbc' => "SELECT * FROM products WHERE (title LIKE 'C%' AND title LIKE '%r') OR title LIKE '%a\n[%][_]\\''\"%'",
'sqlsrv' => "SELECT * FROM products WHERE (title LIKE 'C%' AND title LIKE '%r') OR title LIKE '%a\n[%][_]\\''\"%'",
"SELECT * FROM products WHERE (title LIKE 'C%' AND title LIKE '%r') OR title LIKE '%a\\n\\%\\_\\\\\\\\\'\"%'",
"SELECT * FROM products WHERE (title LIKE 'C%' AND title LIKE '%r') OR title LIKE '%a\\n\\%\\_\\\\\\\\\\'\"%'",
]),
$conn->translate($args[0], $args[1], $args[2], $args[3])
);
@@ -279,7 +279,7 @@ Assert::match(
CONCAT(last_name, ', ', first_name) AS full_name
GROUP BY `user`
HAVING MAX(salary) > %i 123
INTO OUTFILE '/tmp/result\'.txt'
INTO OUTFILE '/tmp/result\\'.txt'
FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '\\\"'
LINES TERMINATED BY '\\\\n'
",
@@ -344,7 +344,7 @@ WHERE (`test`.`a` LIKE '1995-03-01'
OR `b8` IN (RAND() `col1` > `col2` )
OR `b9` IN (RAND(), [col1] > [col2] )
OR `b10` IN ( )
AND `c` = 'embedded \' string'
AND `c` = 'embedded \\' string'
OR `d`=10
OR `e`=NULL
OR `true`= 1
@@ -437,7 +437,6 @@ WHERE ([test].[a] LIKE '1995-03-01'
OR [str_not_null]='hello'
LIMIT 10",
]),
$conn->translate('SELECT *
FROM [db.table]
WHERE ([test.a] LIKE %d', '1995-03-01', '
@@ -671,7 +670,6 @@ Assert::same(
'sqlsrv' => "UPDATE [colors] SET [color]=N'blue', [price]=-12.4, [spec]=-9E-005, [spec2]=1000, [spec3]=10000, [spec4]=10000 WHERE [price]=123.5",
"UPDATE [colors] SET [color]='blue', [price]=-12.4, [spec]=-9E-005, [spec2]=1000, [spec3]=10000, [spec4]=10000 WHERE [price]=123.5",
]),
$conn->translate('UPDATE [colors] SET', [
'color' => 'blue',
'price' => -12.4,

View File

@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
// The Nette Tester command-line runner can be
@@ -18,11 +19,15 @@ date_default_timezone_set('Europe/Prague');
// load connection
try {
$config = Tester\Environment::loadData();
} catch (Exception $e) {
} catch (Throwable $e) {
$config = parse_ini_file(__DIR__ . '/../databases.ini', true);
$config = reset($config);
}
if (isset($config['port'])) {
$config['port'] = (int) $config['port'];
}
// lock
define('TEMP_DIR', __DIR__ . '/../tmp');
@@ -37,7 +42,7 @@ if ($config['system'] === 'odbc') {
}
function test(Closure $function)
function test(string $title, Closure $function): void
{
$function();
}
@@ -46,7 +51,7 @@ function test(Closure $function)
/** Replaces [] with driver-specific quotes */
function reformat($s)
{
global $config;
$config = $GLOBALS['config'];
if (is_array($s)) {
if (isset($s[$config['system']])) {
return $s[$config['system']];
@@ -67,7 +72,7 @@ function reformat($s)
function num($n)
{
global $config;
$config = $GLOBALS['config'];
if (substr($config['dsn'] ?? '', 0, 5) === 'odbc:') {
$n = is_float($n) ? "$n.0" : (string) $n;
}

View File

@@ -36,7 +36,7 @@ Assert::same('SELECT', $e->getSql());
$e = Assert::exception(function () use ($conn) {
$conn->query('INSERT INTO products (product_id, title) VALUES (1, "New")');
}, Dibi\UniqueConstraintViolationException::class, "%a?%Duplicate entry '1' for key 'PRIMARY'", 1062);
}, Dibi\UniqueConstraintViolationException::class, "%a?%Duplicate entry '1' for key '%a?%PRIMARY'", 1062);
Assert::same("INSERT INTO products (product_id, title) VALUES (1, 'New')", $e->getSql());

View File

@@ -31,7 +31,7 @@ Assert::same("INSERT INTO products (product_id, title) VALUES (1, 'New')", $e->g
$e = Assert::exception(function () use ($conn) {
$conn->query('INSERT INTO products (title) VALUES (NULL)');
}, Dibi\NotNullConstraintViolationException::class, '%a?%null value in column "title" violates not-null constraint%A?%', '23502');
}, Dibi\NotNullConstraintViolationException::class, '%a?%null value in column "title"%a%violates not-null constraint%A?%', '23502');
Assert::same('INSERT INTO products (title) VALUES (NULL)', $e->getSql());

View File

@@ -12,5 +12,5 @@ extension=php_pdo_pgsql.dll
extension=php_pdo_sqlite.dll
extension=php_pgsql.dll
extension=php_sqlite3.dll
extension=php_sqlsrv_71_ts.dll
extension=php_sqlsrv_ts.dll
extension=php_odbc.dll