1
0
mirror of https://github.com/dg/dibi.git synced 2025-08-30 01:09:50 +02:00

Compare commits

..

5 Commits
v5.0 ... v3.0

Author SHA1 Message Date
David Grudl
da9c42d540 loader: added missing classes 2018-08-22 15:41:19 +02:00
David Grudl
e8559898f1 Released version 3.0.9 2018-03-09 12:56:49 +01:00
David Grudl
ece85e8b48 appveyor: PHP is downloaded using cURL
"03/mar/2018: We've upgraded the server bandwidth. This is however still not sufficient to handle all empty user agent connections. Please update the user agent in your scripts accordingly or contact us so we can discuss it."
2018-03-08 13:53:10 +01:00
David Grudl
acd5ac8616 loader: fixed missing class 'dibi.php' 2018-02-15 11:54:37 +01:00
David Grudl
44876973f5 Translator: fixed %dt with DateTimeInterface object [Closes #263] 2017-09-21 14:05:20 +02:00
151 changed files with 8244 additions and 7530 deletions

6
.gitattributes vendored
View File

@@ -1,10 +1,6 @@
.gitattributes export-ignore
.gitignore export-ignore
.github export-ignore
.travis.yml export-ignore
appveyor.yml export-ignore
ncs.* export-ignore
phpstan.neon export-ignore
tests/ export-ignore
*.sh eol=lf
*.php* diff=php linguist-language=PHP

View File

@@ -1,19 +0,0 @@
---
name: "\U0001F41B Bug Report"
about: "If something isn't working as expected \U0001F914"
---
Version: ?.?.?
### Bug Description
... A clear and concise description of what the bug is. A good bug report shouldn't leave others needing to chase you up for more information.
### Steps To Reproduce
... If possible a minimal demo of the problem ...
### Expected Behavior
... A clear and concise description of what you expected to happen.
### Possible Solution
... Only if you have suggestions on a fix for the bug

View File

@@ -1,9 +0,0 @@
---
name: "\U0001F680 Feature Request"
about: "I have a suggestion (and may want to implement it \U0001F642)"
---
- Is your feature request related to a problem? Please describe.
- Explain your intentions.
- It's up to you to make a strong case to convince the project's developers of the merits of this feature.

View File

@@ -1,17 +0,0 @@
---
name: "❤️ Support us"
about: "If you would like to support our efforts in maintaining this project 🙌"
---
--------------^ Click "Preview" for a nicer view!
Help support Dibi!
We develop Dibi for more than 14 years. In order to make your life more comfortable. Dibi cares about the safety of your sites. Dibi saves you time. Dibi earns you money. And is absolutely free.
To ensure future development and improving the documentation, we need your donation.
**[Please make a donation now](https://nette.org/make-donation?to=dibi)**.
Thank you!

1
.github/funding.yml vendored
View File

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

16
.github/issue_template.md vendored Normal file
View File

@@ -0,0 +1,16 @@
- bug report? yes/no
- feature request? yes/no
- version: ?.?.? <!-- exact release version, for bug reports -->
### Description
...
### Steps To Reproduce
... If possible a minimal demo of the problem ...
<!--
REMEMBER, AN ISSUE IS NOT THE PLACE TO ASK QUESTIONS. We will be happy to help you on Gitter https://gitter.im/nette/nette
A good bug report shouldn't leave others needing to chase you up for more information. Please try to be as detailed as possible in your report.
Feature requests are welcome. Explain your intentions. It's up to you to make a strong case to convince the project's developers of the merits of this feature.
-->

View File

@@ -1,4 +1,5 @@
- bug fix / new feature? <!-- #issue numbers, if any -->
- bug fix? yes/no <!-- #issue numbers, if any -->
- new feature? yes/no
- BC break? yes/no
<!--

View File

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

View File

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

View File

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

41
.travis.yml Normal file
View File

@@ -0,0 +1,41 @@
language: php
php:
- 5.4
- 5.5
- 5.6
- 7.0
- 7.1
script:
- vendor/bin/tester tests -s -p php -c tests/php-unix.ini $COVERAGE
- php temp/code-checker/src/code-checker.php --short-arrays
after_failure:
# Print *.actual content
- for i in $(find tests -name \*.actual); do echo "--- $i"; cat $i; echo; echo; done
before_script:
# Install Nette Tester & Code Checker
- travis_retry composer install --no-interaction
- travis_retry composer create-project nette/code-checker temp/code-checker ~2.5 --no-interaction
- if [ $TRAVIS_PHP_VERSION == "7.0" ]; then COVERAGE="-p phpdbg --coverage ./coverage.xml --coverage-src ./src"; fi
# Create databases.ini
- cp ./tests/databases.travis.ini ./tests/databases.ini
# Create Postgre database
- psql -c 'CREATE DATABASE dibi_test' -U postgres
after_script:
# Report Code Coverage
- >
if [ "$COVERAGE" != "" ]; then
wget https://github.com/satooshi/php-coveralls/releases/download/v1.0.1/coveralls.phar
&& php coveralls.phar --verbose --config tests/.coveralls.yml
|| true; fi
sudo: false
cache:
directories:
- $HOME/.composer/cache

View File

@@ -1,39 +1,46 @@
build: off
cache:
- c:\php -> appveyor.yml
- c:\php5 -> appveyor.yml
- c:\php7 -> appveyor.yml
- '%LOCALAPPDATA%\Composer\files -> appveyor.yml'
clone_folder: c:\projects\dibi
environment:
MYSQL_PWD: Password12!
services:
- mssql2012sp1
# - mssql2014
- mysql
init:
- SET PATH=c:\php;c:\Program Files\MySQL\MySQL Server 5.7\bin;%PATH%
- SET PATH=c:\php5;%PATH%
- SET ANSICON=121x90 (121x90)
install:
# Install PHP 8.0
- IF EXIST c:\php (SET PHP=0) ELSE (SET PHP=1)
- IF %PHP%==1 mkdir c:\php
- IF %PHP%==1 cd c:\php
- IF %PHP%==1 curl https://windows.php.net/downloads/releases/archives/php-8.0.1-Win32-vs16-x64.zip --output php.zip
# Install PHP 5
- IF EXIST c:\php5 (SET PHP=0) ELSE (SET PHP=1)
- IF %PHP%==1 mkdir c:\php5
- IF %PHP%==1 cd c:\php5
- IF %PHP%==1 curl https://windows.php.net/downloads/releases/archives/php-5.6.14-Win32-VC11-x86.zip --output php.zip
- IF %PHP%==1 7z x php.zip >nul
- IF %PHP%==1 echo extension_dir=ext >> php.ini
- IF %PHP%==1 echo extension=php_openssl.dll >> php.ini
- IF %PHP%==1 curl https://github.com/microsoft/msphpsql/releases/download/v5.9.0/Windows-8.0.zip -L --output sqlsrv.zip
- IF %PHP%==1 7z x sqlsrv.zip >nul
- IF %PHP%==1 copy Windows-8.0\x64\php_sqlsrv_80_ts.dll ext\php_sqlsrv_ts.dll
- IF %PHP%==1 appveyor DownloadFile https://files.nette.org/misc/php-sqlsrv.zip
- IF %PHP%==1 7z x php-sqlsrv.zip >nul
- IF %PHP%==1 copy SQLSRV\php_sqlsrv_56_ts.dll ext\php_sqlsrv_ts.dll
- IF %PHP%==1 copy SQLSRV\php_pdo_sqlsrv_56_ts.dll ext\php_pdo_sqlsrv_ts.dll
- IF %PHP%==1 del /Q *.zip
# Install Microsoft Access Database Engine x64
- IF %PHP%==1 curl https://download.microsoft.com/download/2/4/3/24375141-E08D-4803-AB0E-10F2E3A07AAA/AccessDatabaseEngine_X64.exe --output AccessDatabaseEngine_X64.exe
- cmd /c start /wait AccessDatabaseEngine_X64.exe /passive
# Install PHP 7
- 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.0.3-Win32-VC14-x86.zip --output php.zip
- IF %PHP%==1 7z x php.zip >nul
- IF %PHP%==1 echo extension_dir=ext >> 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_7_ts.dll ext\php_sqlsrv_ts.dll
- IF %PHP%==1 del /Q *.zip
# Install Nette Tester
- cd c:\projects\dibi
@@ -43,13 +50,10 @@ install:
# Create databases.ini
- copy tests\databases.appveyor.ini tests\databases.ini
before_test:
# Create MySQL database
- mysql --user=root -e "CREATE DATABASE dibi_test"
test_script:
- vendor\bin\tester tests -s -p c:\php\php -c tests\php-win.ini
- vendor\bin\tester tests -s -p c:\php5\php -c tests\php5-win.ini
- vendor\bin\tester tests -s -p c:\php7\php -c tests\php7-win.ini
on_failure:
# Print *.actual content
- for /r %%x in (*.actual) do ( type "%%x" )
- type tests\dibi\output\*.actual

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-only", "GPL-3.0-only"],
"license": ["BSD-3-Clause", "GPL-2.0", "GPL-3.0"],
"authors": [
{
"name": "David Grudl",
@@ -11,29 +11,22 @@
}
],
"require": {
"php": "8.0 - 8.4"
"php": ">=5.4.4"
},
"require-dev": {
"tracy/tracy": "^2.9",
"nette/tester": "^2.5",
"nette/di": "^3.1",
"phpstan/phpstan": "^1.0",
"jetbrains/phpstorm-attributes": "^1.0"
"tracy/tracy": "~2.2",
"nette/tester": "~1.7"
},
"replace": {
"dg/dibi": "*"
},
"autoload": {
"classmap": ["src/"]
},
"minimum-stability": "dev",
"scripts": {
"phpstan": "phpstan analyse",
"tester": "tester tests -s"
"classmap": ["src/"],
"files": ["src/loader.php"]
},
"extra": {
"branch-alias": {
"dev-master": "5.0-dev"
"dev-master": "3.0-dev"
}
}
}

3
dibi/dibi.php Normal file
View File

@@ -0,0 +1,3 @@
<?php
trigger_error('Dibi was moved to /src/loader.php', E_USER_WARNING);
require __DIR__ . '/../src/loader.php';

View File

@@ -1,22 +1,17 @@
<?php
declare(strict_types=1);
?>
<!DOCTYPE html><link rel="stylesheet" href="data/style.css">
<h1>Connecting to Databases | Dibi</h1>
<h1>Connecting to Databases | dibi</h1>
<?php
if (@!include __DIR__ . '/../vendor/autoload.php') {
die('Install packages using `composer install`');
}
require __DIR__ . '/../src/loader.php';
// connects to SQlite using Dibi class
// connects to SQlite using dibi class
echo '<p>Connecting to Sqlite: ';
try {
dibi::connect([
'driver' => 'sqlite',
'driver' => 'sqlite3',
'database' => 'data/sample.s3db',
]);
echo 'OK';
@@ -30,7 +25,7 @@ echo "</p>\n";
echo '<p>Connecting to Sqlite: ';
try {
$connection = new Dibi\Connection([
'driver' => 'sqlite',
'driver' => 'sqlite3',
'database' => 'data/sample.s3db',
]);
echo 'OK';
@@ -40,7 +35,18 @@ try {
echo "</p>\n";
// connects to MySQLi
// connects to MySQL using DSN
echo '<p>Connecting to MySQL: ';
try {
dibi::connect('driver=mysql&host=localhost&username=root&password=xxx&database=test&charset=cp1250');
echo 'OK';
} catch (Dibi\Exception $e) {
echo get_class($e), ': ', $e->getMessage(), "\n";
}
echo "</p>\n";
// connects to MySQLi using array
echo '<p>Connecting to MySQLi: ';
try {
dibi::connect([
@@ -68,7 +74,7 @@ try {
'driver' => 'odbc',
'username' => 'root',
'password' => '***',
'dsn' => 'Driver={Microsoft Access Driver (*.mdb, *.accdb)};Dbq=' . __DIR__ . '/data/sample.mdb',
'dsn' => 'Driver={Microsoft Access Driver (*.mdb)};Dbq='.__DIR__.'/data/sample.mdb',
]);
echo 'OK';
} catch (Dibi\Exception $e) {
@@ -83,7 +89,7 @@ try {
dibi::connect([
'driver' => 'postgre',
'string' => 'host=localhost port=5432 dbname=mary',
'persistent' => true,
'persistent' => TRUE,
]);
echo 'OK';
} catch (Dibi\Exception $e) {
@@ -106,6 +112,22 @@ try {
echo "</p>\n";
// connects to MS SQL
echo '<p>Connecting to MS SQL: ';
try {
dibi::connect([
'driver' => 'mssql',
'host' => 'localhost',
'username' => 'root',
'password' => 'xxx',
]);
echo 'OK';
} catch (Dibi\Exception $e) {
echo get_class($e), ': ', $e->getMessage(), "\n";
}
echo "</p>\n";
// connects to SQLSRV
echo '<p>Connecting to Microsoft SQL Server: ';
try {

Binary file not shown.

View File

@@ -1,25 +1,20 @@
<?php
declare(strict_types=1);
?>
<!DOCTYPE html><link rel="stylesheet" href="data/style.css">
<h1>Database Reflection | Dibi</h1>
<h1>Database Reflection | dibi</h1>
<?php
if (@!include __DIR__ . '/../vendor/autoload.php') {
die('Install packages using `composer install`');
}
require __DIR__ . '/../src/loader.php';
$dibi = new Dibi\Connection([
'driver' => 'sqlite',
dibi::connect([
'driver' => 'sqlite3',
'database' => 'data/sample.s3db',
]);
// retrieve database reflection
$database = $dibi->getDatabaseInfo();
$database = dibi::getDatabaseInfo();
echo "<h2>Database '{$database->getName()}'</h2>\n";
echo "<ul>\n";

View File

@@ -1,24 +1,19 @@
<?php
declare(strict_types=1);
?>
<!DOCTYPE html><link rel="stylesheet" href="data/style.css">
<h1>Dumping SQL and Result Set | Dibi</h1>
<h1>Dumping SQL and Result Set | dibi</h1>
<?php
if (@!include __DIR__ . '/../vendor/autoload.php') {
die('Install packages using `composer install`');
}
require __DIR__ . '/../src/loader.php';
$dibi = new Dibi\Connection([
'driver' => 'sqlite',
dibi::connect([
'driver' => 'sqlite3',
'database' => 'data/sample.s3db',
]);
$res = $dibi->query('
$res = dibi::query('
SELECT * FROM products
INNER JOIN orders USING (product_id)
INNER JOIN customers USING (customer_id)

View File

@@ -1,5 +1,4 @@
<?php
declare(strict_types=1);
if (@!include __DIR__ . '/../vendor/autoload.php') {
die('Install dependencies using `composer install --dev`');
@@ -10,12 +9,12 @@ Tracy\Debugger::enable();
?>
<!DOCTYPE html><link rel="stylesheet" href="data/style.css">
<h1>Fetching Examples | Dibi</h1>
<h1>Fetching Examples | dibi</h1>
<?php
$dibi = new Dibi\Connection([
'driver' => 'sqlite',
dibi::connect([
'driver' => 'sqlite3',
'database' => 'data/sample.s3db',
]);
@@ -34,46 +33,46 @@ product_id | title
// fetch a single row
echo "<h2>fetch()</h2>\n";
$row = $dibi->fetch('SELECT title FROM products');
$row = dibi::fetch('SELECT title FROM products');
Tracy\Dumper::dump($row); // Chair
// fetch a single value
echo "<h2>fetchSingle()</h2>\n";
$value = $dibi->fetchSingle('SELECT title FROM products');
$value = dibi::fetchSingle('SELECT title FROM products');
Tracy\Dumper::dump($value); // Chair
// fetch complete result set
echo "<h2>fetchAll()</h2>\n";
$all = $dibi->fetchAll('SELECT * FROM products');
$all = dibi::fetchAll('SELECT * FROM products');
Tracy\Dumper::dump($all);
// fetch complete result set like association array
echo "<h2>fetchAssoc('title')</h2>\n";
$res = $dibi->query('SELECT * FROM products');
$res = dibi::query('SELECT * FROM products');
$assoc = $res->fetchAssoc('title'); // key
Tracy\Dumper::dump($assoc);
// fetch complete result set like pairs key => value
echo "<h2>fetchPairs('product_id', 'title')</h2>\n";
$res = $dibi->query('SELECT * FROM products');
$res = dibi::query('SELECT * FROM products');
$pairs = $res->fetchPairs('product_id', 'title');
Tracy\Dumper::dump($pairs);
// fetch row by row
echo "<h2>using foreach</h2>\n";
$res = $dibi->query('SELECT * FROM products');
$res = dibi::query('SELECT * FROM products');
foreach ($res as $n => $row) {
Tracy\Dumper::dump($row);
}
// more complex association array
$res = $dibi->query('
$res = dibi::query('
SELECT *
FROM products
INNER JOIN orders USING (product_id)
@@ -85,11 +84,11 @@ $assoc = $res->fetchAssoc('name|title'); // key
Tracy\Dumper::dump($assoc);
echo "<h2>fetchAssoc('name[]title')</h2>\n";
$res = $dibi->query('SELECT * FROM products INNER JOIN orders USING (product_id) INNER JOIN customers USING (customer_id)');
$res = dibi::query('SELECT * FROM products INNER JOIN orders USING (product_id) INNER JOIN customers USING (customer_id)');
$assoc = $res->fetchAssoc('name[]title'); // key
Tracy\Dumper::dump($assoc);
echo "<h2>fetchAssoc('name->title')</h2>\n";
$res = $dibi->query('SELECT * FROM products INNER JOIN orders USING (product_id) INNER JOIN customers USING (customer_id)');
$res = dibi::query('SELECT * FROM products INNER JOIN orders USING (product_id) INNER JOIN customers USING (customer_id)');
$assoc = $res->fetchAssoc('name->title'); // key
Tracy\Dumper::dump($assoc);

View File

@@ -1,23 +1,18 @@
<?php
declare(strict_types=1);
?>
<!DOCTYPE html><link rel="stylesheet" href="data/style.css">
<h1>Importing SQL Dump from File | Dibi</h1>
<h1>Importing SQL Dump from File | dibi</h1>
<?php
if (@!include __DIR__ . '/../vendor/autoload.php') {
die('Install packages using `composer install`');
}
require __DIR__ . '/../src/loader.php';
$dibi = new Dibi\Connection([
'driver' => 'sqlite',
dibi::connect([
'driver' => 'sqlite3',
'database' => 'data/sample.s3db',
]);
$count = $dibi->loadFile('compress.zlib://data/sample.dump.sql.gz');
$count = dibi::loadFile('compress.zlib://data/sample.dump.sql.gz');
echo 'Number of SQL commands:', $count;

View File

@@ -1,43 +1,38 @@
<?php
declare(strict_types=1);
?>
<!DOCTYPE html><link rel="stylesheet" href="data/style.css">
<h1>Query Language & Conditions | Dibi</h1>
<h1>Query Language & Conditions | dibi</h1>
<?php
if (@!include __DIR__ . '/../vendor/autoload.php') {
die('Install packages using `composer install`');
}
require __DIR__ . '/../src/loader.php';
$dibi = new Dibi\Connection([
'driver' => 'sqlite',
dibi::connect([
'driver' => 'sqlite3',
'database' => 'data/sample.s3db',
]);
// some variables
$cond1 = true;
$cond2 = false;
$cond1 = TRUE;
$cond2 = FALSE;
$foo = -1;
$bar = 2;
// conditional variable
$name = $cond1 ? 'K%' : null;
$name = $cond1 ? 'K%' : NULL;
// if & end
$dibi->test('
dibi::test('
SELECT *
FROM customers
%if', isset($name), 'WHERE name LIKE ?', $name, '%end',
%if', isset($name), 'WHERE name LIKE ?', $name, '%end'
);
// -> SELECT * FROM customers WHERE name LIKE 'K%'
// if & else & (optional) end
$dibi->test('
dibi::test('
SELECT *
FROM people
WHERE id > 0
@@ -48,19 +43,19 @@ $dibi->test('
// nested condition
$dibi->test('
dibi::test('
SELECT *
FROM customers
WHERE
%if', isset($name), 'name LIKE ?', $name, '
%if', $cond2, 'AND admin=1 %end
%else 1 LIMIT 10 %end',
%else 1 LIMIT 10 %end'
);
// -> SELECT * FROM customers WHERE LIMIT 10
// IF()
$dibi->test('UPDATE products SET', [
'price' => $dibi->expression('IF(price_fixed, price, ?)', 123),
dibi::test('UPDATE products SET', [
'price' => ['IF(price_fixed, price, ?)', 123],
]);
// -> SELECT * FROM customers WHERE LIMIT 10

View File

@@ -1,21 +1,16 @@
<?php
declare(strict_types=1);
?>
<!DOCTYPE html><link rel="stylesheet" href="data/style.css">
<h1>Query Language Basic Examples | Dibi</h1>
<h1>Query Language Basic Examples | dibi</h1>
<?php
if (@!include __DIR__ . '/../vendor/autoload.php') {
die('Install packages using `composer install`');
}
require __DIR__ . '/../src/loader.php';
date_default_timezone_set('Europe/Prague');
$dibi = new Dibi\Connection([
'driver' => 'sqlite',
dibi::connect([
'driver' => 'sqlite3',
'database' => 'data/sample.s3db',
]);
@@ -24,21 +19,21 @@ $dibi = new Dibi\Connection([
$ipMask = '192.168.%';
$timestamp = mktime(0, 0, 0, 10, 13, 1997);
$dibi->test('
dibi::test('
SELECT COUNT(*) as [count]
FROM [comments]
WHERE [ip] LIKE ?', $ipMask, '
AND [date] > ', new Dibi\DateTime($timestamp),
AND [date] > ', new Dibi\DateTime($timestamp)
);
// -> SELECT COUNT(*) as [count] FROM [comments] WHERE [ip] LIKE '192.168.%' AND [date] > 876693600
// dibi detects INSERT or REPLACE command
$dibi->test('
dibi::test('
REPLACE INTO products', [
'title' => 'Super product',
'price' => 318,
'active' => true,
'active' => TRUE,
]);
// -> REPLACE INTO products ([title], [price], [active]) VALUES ('Super product', 318, 1)
@@ -47,15 +42,15 @@ $dibi->test('
$array = [
'title' => 'Super Product',
'price' => 12,
'brand' => null,
'brand' => NULL,
'created' => new DateTime,
];
$dibi->test('INSERT INTO products', $array, $array, $array);
dibi::test('INSERT INTO products', $array, $array, $array);
// -> INSERT INTO products ([title], [price], [brand], [created]) VALUES ('Super Product', ...) , (...) , (...)
// dibi detects UPDATE command
$dibi->test('
dibi::test('
UPDATE colors SET', [
'color' => 'blue',
'order' => 12,
@@ -66,10 +61,10 @@ $dibi->test('
// modifier applied to array
$array = [1, 2, 3];
$dibi->test('
dibi::test('
SELECT *
FROM people
WHERE id IN (?)', $array,
WHERE id IN (?)', $array
);
// -> SELECT * FROM people WHERE id IN ( 1, 2, 3 )
@@ -79,7 +74,7 @@ $order = [
'field1' => 'asc',
'field2' => 'desc',
];
$dibi->test('
dibi::test('
SELECT *
FROM people
ORDER BY %by', $order, '
@@ -88,5 +83,5 @@ $dibi->test('
// indentifiers and strings syntax mix
$dibi->test('UPDATE [table] SET `item` = "5 1/4"" diskette"');
dibi::test('UPDATE [table] SET `item` = "5 1/4"" diskette"');
// -> UPDATE [table] SET [item] = '5 1/4" diskette'

View File

@@ -1,5 +1,4 @@
<?php
declare(strict_types=1);
use Dibi\Type;
@@ -14,22 +13,22 @@ date_default_timezone_set('Europe/Prague');
?>
<!DOCTYPE html><link rel="stylesheet" href="data/style.css">
<h1>Result Set Data Types | Dibi</h1>
<h1>Result Set Data Types | dibi</h1>
<?php
$dibi = new Dibi\Connection([
'driver' => 'sqlite',
dibi::connect([
'driver' => 'sqlite3',
'database' => 'data/sample.s3db',
]);
// using manual hints
$res = $dibi->query('SELECT * FROM [customers]');
$res = dibi::query('SELECT * FROM [customers]');
$res->setType('customer_id', Type::Integer)
->setType('added', Type::DateTime)
->setFormat(Type::DateTime, 'Y-m-d H:i:s');
$res->setType('customer_id', Type::INTEGER)
->setType('added', Type::DATETIME)
->setFormat(Type::DATETIME, 'Y-m-d H:i:s');
Tracy\Dumper::dump($res->fetch());
@@ -41,7 +40,7 @@ Tracy\Dumper::dump($res->fetch());
// using auto-detection (works well with MySQL or other strictly typed databases)
$res = $dibi->query('SELECT * FROM [customers]');
$res = dibi::query('SELECT * FROM [customers]');
Tracy\Dumper::dump($res->fetch());
// outputs:

View File

@@ -1,5 +1,4 @@
<?php
declare(strict_types=1);
if (@!include __DIR__ . '/../vendor/autoload.php') {
die('Install dependencies using `composer install --dev`');
@@ -10,19 +9,22 @@ if (@!include __DIR__ . '/../vendor/autoload.php') {
Tracy\Debugger::enable();
$dibi = new Dibi\Connection([
'driver' => 'sqlite',
$connection = dibi::connect([
'driver' => 'sqlite3',
'database' => 'data/sample.s3db',
'profiler' => [
'run' => TRUE,
],
]);
// add panel to debug bar
$panel = new Dibi\Bridges\Tracy\Panel;
$panel->register($dibi);
$panel->register($connection);
// throws error because SQL is bad
$dibi->query('SELECT FROM customers WHERE customer_id < ?', 38);
dibi::query('SELECT FROM customers WHERE customer_id < ?', 38);
?><!DOCTYPE html><link rel="stylesheet" href="data/style.css">

View File

@@ -1,5 +1,4 @@
<?php
declare(strict_types=1);
if (@!include __DIR__ . '/../vendor/autoload.php') {
die('Install dependencies using `composer install --dev`');
@@ -10,22 +9,25 @@ if (@!include __DIR__ . '/../vendor/autoload.php') {
Tracy\Debugger::enable();
$dibi = new Dibi\Connection([
'driver' => 'sqlite',
$connection = dibi::connect([
'driver' => 'sqlite3',
'database' => 'data/sample.s3db',
'profiler' => [
'run' => TRUE,
],
]);
// add panel to debug bar
$panel = new Dibi\Bridges\Tracy\Panel;
$panel->register($dibi);
$panel->register($connection);
// query will be logged
$dibi->query('SELECT 123');
dibi::query('SELECT 123');
// result set will be dumped
Tracy\Debugger::barDump($dibi->fetchAll('SELECT * FROM customers WHERE customer_id < ?', 38), '[customers]');
Tracy\Debugger::barDump(dibi::fetchAll('SELECT * FROM customers WHERE customer_id < ?', 38), '[customers]');
?>

View File

@@ -1,22 +1,17 @@
<?php
declare(strict_types=1);
?>
<!DOCTYPE html><link rel="stylesheet" href="data/style.css">
<h1>Using DateTime | Dibi</h1>
<h1>Using DateTime | dibi</h1>
<?php
if (@!include __DIR__ . '/../vendor/autoload.php') {
die('Install packages using `composer install`');
}
require __DIR__ . '/../src/loader.php';
date_default_timezone_set('Europe/Prague');
// CHANGE TO REAL PARAMETERS!
$dibi = new Dibi\Connection([
'driver' => 'sqlite',
dibi::connect([
'driver' => 'sqlite3',
'database' => 'data/sample.s3db',
'formatDate' => "'Y-m-d'",
'formatDateTime' => "'Y-m-d H-i-s'",
@@ -24,11 +19,11 @@ $dibi = new Dibi\Connection([
// generate and dump SQL
$dibi->test('
dibi::test('
INSERT INTO [mytable]', [
'id' => 123,
'date' => new DateTime('12.3.2007'),
'stamp' => new DateTime('23.1.2007 10:23'),
],
]
);
// -> INSERT INTO [mytable] ([id], [date], [stamp]) VALUES (123, '2007-03-12', '2007-01-23 10-23-00')

View File

@@ -0,0 +1,33 @@
<?php
if (@!include __DIR__ . '/../vendor/autoload.php') {
die('Install dependencies using `composer install --dev`');
}
Tracy\Debugger::enable();
?>
<!DOCTYPE html><link rel="stylesheet" href="data/style.css">
<h1>Using Extension Methods | dibi</h1>
<?php
dibi::connect([
'driver' => 'sqlite3',
'database' => 'data/sample.s3db',
]);
// using the "prototype" to add custom method to class Dibi\Result
Dibi\Result::extensionMethod('fetchShuffle', function (Dibi\Result $obj) {
$all = $obj->fetchAll();
shuffle($all);
return $all;
});
// fetch complete result set shuffled
$res = dibi::query('SELECT * FROM [customers]');
$all = $res->fetchShuffle();
Tracy\Dumper::dump($all);

View File

@@ -1,21 +1,16 @@
<?php
declare(strict_types=1);
?>
<!DOCTYPE html><link rel="stylesheet" href="data/style.css">
<h1>Using Fluent Syntax | Dibi</h1>
<h1>Using Fluent Syntax | dibi</h1>
<?php
if (@!include __DIR__ . '/../vendor/autoload.php') {
die('Install packages using `composer install`');
}
require __DIR__ . '/../src/loader.php';
date_default_timezone_set('Europe/Prague');
$dibi = new Dibi\Connection([
'driver' => 'sqlite',
dibi::connect([
'driver' => 'sqlite3',
'database' => 'data/sample.s3db',
]);
@@ -24,11 +19,11 @@ $id = 10;
$record = [
'title' => 'Super product',
'price' => 318,
'active' => true,
'active' => TRUE,
];
// SELECT ...
$dibi->select('product_id')->as('id')
dibi::select('product_id')->as('id')
->select('title')
->from('products')
->innerJoin('orders')->using('(product_id)')
@@ -40,35 +35,35 @@ $dibi->select('product_id')->as('id')
// SELECT ...
echo $dibi->select('title')->as('id')
echo dibi::select('title')->as('id')
->from('products')
->fetchSingle();
// -> Chair (as result of query: SELECT [title] AS [id] FROM [products])
// INSERT ...
$dibi->insert('products', $record)
dibi::insert('products', $record)
->setFlag('IGNORE')
->test();
// -> INSERT IGNORE INTO [products] ([title], [price], [active]) VALUES ('Super product', 318, 1)
// UPDATE ...
$dibi->update('products', $record)
dibi::update('products', $record)
->where('product_id = ?', $id)
->test();
// -> UPDATE [products] SET [title]='Super product', [price]=318, [active]=1 WHERE product_id = 10
// DELETE ...
$dibi->delete('products')
dibi::delete('products')
->where('product_id = ?', $id)
->test();
// -> DELETE FROM [products] WHERE product_id = 10
// custom commands
$dibi->command()
dibi::command()
->update('products')
->where('product_id = ?', $id)
->set($record)
@@ -76,7 +71,7 @@ $dibi->command()
// -> UPDATE [products] SET [title]='Super product', [price]=318, [active]=1 WHERE product_id = 10
$dibi->command()
dibi::command()
->truncate('products')
->test();
// -> TRUNCATE [products]

View File

@@ -1,33 +1,28 @@
<?php
declare(strict_types=1);
?>
<!DOCTYPE html><link rel="stylesheet" href="data/style.css">
<h1>Using Limit & Offset | Dibi</h1>
<h1>Using Limit & Offset | dibi</h1>
<?php
if (@!include __DIR__ . '/../vendor/autoload.php') {
die('Install packages using `composer install`');
}
require __DIR__ . '/../src/loader.php';
$dibi = new Dibi\Connection([
'driver' => 'sqlite',
dibi::connect([
'driver' => 'sqlite3',
'database' => 'data/sample.s3db',
]);
// no limit
$dibi->test('SELECT * FROM [products]');
dibi::test('SELECT * FROM [products]');
// -> SELECT * FROM [products]
// with limit = 2
$dibi->test('SELECT * FROM [products] %lmt', 2);
dibi::test('SELECT * FROM [products] %lmt', 2);
// -> SELECT * FROM [products] LIMIT 2
// with limit = 2, offset = 1
$dibi->test('SELECT * FROM [products] %lmt %ofs', 2, 1);
dibi::test('SELECT * FROM [products] %lmt %ofs', 2, 1);
// -> SELECT * FROM [products] LIMIT 2 OFFSET 1

View File

@@ -1,42 +1,37 @@
<?php
declare(strict_types=1);
?>
<!DOCTYPE html><link rel="stylesheet" href="data/style.css">
<h1>Using Logger | Dibi</h1>
<h1>Using Logger | dibi</h1>
<?php
if (@!include __DIR__ . '/../vendor/autoload.php') {
die('Install packages using `composer install`');
}
require __DIR__ . '/../src/loader.php';
date_default_timezone_set('Europe/Prague');
$dibi = new Dibi\Connection([
'driver' => 'sqlite',
dibi::connect([
'driver' => 'sqlite3',
'database' => 'data/sample.s3db',
// enable query logging to this file
'profiler' => [
'file' => 'log/log.sql',
'errorsOnly' => false,
'run' => TRUE,
'file' => 'data/log.sql',
],
]);
try {
$res = $dibi->query('SELECT * FROM [customers] WHERE [customer_id] = ?', 1);
$res = dibi::query('SELECT * FROM [customers] WHERE [customer_id] = ?', 1);
$res = $dibi->query('SELECT * FROM [customers] WHERE [customer_id] < ?', 5);
$res = dibi::query('SELECT * FROM [customers] WHERE [customer_id] < ?', 5);
$res = $dibi->query('SELECT FROM [customers] WHERE [customer_id] < ?', 38);
$res = dibi::query('SELECT FROM [customers] WHERE [customer_id] < ?', 38);
} catch (Dibi\Exception $e) {
echo '<p>', get_class($e), ': ', $e->getMessage(), '</p>';
}
// outputs a log file
echo '<h2>File log/log.sql:</h2>';
echo '<h2>File data/log.sql:</h2>';
echo '<pre>', file_get_contents('log/log.sql'), '</pre>';
echo '<pre>', file_get_contents('data/log.sql'), '</pre>';

View File

@@ -0,0 +1,43 @@
<?php ob_start() // needed by FirePHP ?>
<!DOCTYPE html><link rel="stylesheet" href="data/style.css">
<h1>Using Profiler | dibi</h1>
<?php
require __DIR__ . '/../src/loader.php';
dibi::connect([
'driver' => 'sqlite3',
'database' => 'data/sample.s3db',
'profiler' => [
'run' => TRUE,
],
]);
// execute some queries...
for ($i = 0; $i < 20; $i++) {
$res = dibi::query('SELECT * FROM [customers] WHERE [customer_id] < ?', $i);
}
// display output
?>
<p>Last query: <strong><?php echo dibi::$sql; ?></strong></p>
<p>Number of queries: <strong><?php echo dibi::$numOfQueries; ?></strong></p>
<p>Elapsed time for last query: <strong><?php echo sprintf('%0.3f', dibi::$elapsedTime * 1000); ?> ms</strong></p>
<p>Total elapsed time: <strong><?php echo sprintf('%0.3f', dibi::$totalTime * 1000); ?> ms</strong></p>
<br>
<p>Dibi can log to your Firebug Console. You first need to install the Firefox, Firebug and FirePHP extensions. You can install them from here:</p>
<ul>
<li>Firebug: https://addons.mozilla.org/en-US/firefox/addon/1843
<li>FirePHP: http://www.firephp.org/
</ul>

View File

@@ -1,34 +1,29 @@
<?php
declare(strict_types=1);
?>
<!DOCTYPE html><link rel="stylesheet" href="data/style.css">
<h1>Using Substitutions | Dibi</h1>
<h1>Using Substitutions | dibi</h1>
<?php
if (@!include __DIR__ . '/../vendor/autoload.php') {
die('Install packages using `composer install`');
}
require __DIR__ . '/../src/loader.php';
$dibi = new Dibi\Connection([
'driver' => 'sqlite',
dibi::connect([
'driver' => 'sqlite3',
'database' => 'data/sample.s3db',
]);
// create new substitution :blog: ==> wp_
$dibi->getSubstitutes()->blog = 'wp_';
dibi::getSubstitutes()->blog = 'wp_';
$dibi->test('SELECT * FROM [:blog:items]');
dibi::test('SELECT * FROM [:blog:items]');
// -> SELECT * FROM [wp_items]
// create new substitution :: (empty) ==> my_
$dibi->getSubstitutes()->{''} = 'my_';
dibi::getSubstitutes()->{''} = 'my_';
$dibi->test("UPDATE ::table SET [text]='Hello World'");
dibi::test("UPDATE ::table SET [text]='Hello World'");
// -> UPDATE my_table SET [text]='Hello World'
@@ -43,17 +38,16 @@ function substFallBack($expr)
}
}
// define callback
$dibi->getSubstitutes()->setCallback('substFallBack');
dibi::getSubstitutes()->setCallback('substFallBack');
// define substitutes as constants
define('SUBST_ACCOUNT', 'eshop_');
define('SUBST_ACTIVE', 7);
$dibi->test("
dibi::test("
UPDATE :account:user
SET name='John Doe', status=:active:
WHERE id=", 7,
WHERE id=", 7
);
// -> UPDATE eshop_user SET name='John Doe', status=7 WHERE id= 7

View File

@@ -1,39 +1,34 @@
<?php
declare(strict_types=1);
?>
<!DOCTYPE html><link rel="stylesheet" href="data/style.css">
<h1>Using Transactions | Dibi</h1>
<h1>Using Transactions | dibi</h1>
<?php
if (@!include __DIR__ . '/../vendor/autoload.php') {
die('Install packages using `composer install`');
}
require __DIR__ . '/../src/loader.php';
$dibi = new Dibi\Connection([
'driver' => 'sqlite',
dibi::connect([
'driver' => 'sqlite3',
'database' => 'data/sample.s3db',
]);
echo "<h2>Before</h2>\n";
$dibi->query('SELECT * FROM [products]')->dump();
dibi::query('SELECT * FROM [products]')->dump();
// -> 3 rows
$dibi->begin();
$dibi->query('INSERT INTO [products]', [
dibi::begin();
dibi::query('INSERT INTO [products]', [
'title' => 'Test product',
]);
echo "<h2>After INSERT</h2>\n";
$dibi->query('SELECT * FROM [products]')->dump();
dibi::query('SELECT * FROM [products]')->dump();
$dibi->rollback(); // or $dibi->commit();
dibi::rollback(); // or dibi::commit();
echo "<h2>After rollback</h2>\n";
$dibi->query('SELECT * FROM [products]')->dump();
dibi::query('SELECT * FROM [products]')->dump();
// -> 3 rows again

View File

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

691
readme.md
View File

@@ -1,664 +1,133 @@
[Dibi](https://dibiphp.com) - smart database layer for PHP [![Buy me a coffee](https://files.nette.org/images/coffee1s.png)](https://nette.org/make-donation?to=dibi)
[Dibi](https://dibiphp.com) - smart database layer for PHP [![Buy me a coffee](https://files.nette.org/images/coffee1s.png)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=9XXL5ZJHAYQUN)
=========================================================
[![Downloads this Month](https://img.shields.io/packagist/dm/dibi/dibi.svg)](https://packagist.org/packages/dibi/dibi)
[![Tests](https://github.com/dg/dibi/workflows/Tests/badge.svg?branch=master)](https://github.com/dg/dibi/actions)
[![Build Status](https://travis-ci.org/dg/dibi.svg?branch=master)](https://travis-ci.org/dg/dibi)
[![Build Status Windows](https://ci.appveyor.com/api/projects/status/github/dg/dibi?branch=master&svg=true)](https://ci.appveyor.com/project/dg/dibi/branch/master)
[![Latest Stable Version](https://poser.pugx.org/dibi/dibi/v/stable)](https://github.com/dg/dibi/releases)
[![License](https://img.shields.io/badge/license-New%20BSD-blue.svg)](https://github.com/dg/dibi/blob/master/license.md)
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.
The best way to install Dibi is to use a [Composer](https://getcomposer.org/download):
Support Me
----------
php composer.phar require dibi/dibi
Do you like Dibi? Are you looking forward to the new features?
Or you can download the latest package from https://dibiphp.com. In this
package is also `Dibi.minified`, shrinked single-file version of whole Dibi,
useful when you don't want to modify the library, but just use it.
[![Buy me a coffee](https://files.nette.org/icons/donation-3.svg)](https://github.com/sponsors/dg)
Thank you!
Dibi requires PHP 5.4.4 or later. It has been tested with PHP 7 too.
Installation
------------
Install Dibi via Composer:
```bash
composer require dibi/dibi
```
The Dibi 5.0 requires PHP version 8.0 and supports PHP up to 8.4.
Usage
-----
Examples
--------
Refer to the `examples` directory for examples. Dibi documentation is
available on the [homepage](https://dibiphp.com).
### Connecting to database
The database connection is represented by the object `Dibi\Connection`:
```php
$database = new Dibi\Connection([
'driver' => 'mysqli',
'host' => 'localhost',
'username' => 'root',
'password' => '***',
'database' => 'table',
]);
$result = $database->query('SELECT * FROM users');
```
Alternatively, you can use the `dibi` static register, which maintains a connection object in a globally available storage and calls all the functions above it:
Connect to database:
```php
// connect to database (static way)
dibi::connect([
'driver' => 'mysqli',
'host' => 'localhost',
'username' => 'root',
'password' => '***',
'database' => 'test',
'charset' => 'utf8',
'driver' => 'mysql',
'host' => 'localhost',
'username' => 'root',
'password' => '***',
]);
$result = dibi::query('SELECT * FROM users');
// or object way; in all other examples use $connection-> instead of dibi::
$connection = new DibiConnection($options);
```
In the event of a connection error, it throws `Dibi\Exception`.
### Queries
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).
SELECT, INSERT, UPDATE
```php
$result = $database->query('SELECT * FROM users');
dibi::query('SELECT * FROM users WHERE id = ?', $id);
foreach ($result as $row) {
echo $row->id;
echo $row->name;
}
// array of all rows
$all = $result->fetchAll();
// array of all rows, key is 'id'
$all = $result->fetchAssoc('id');
// associative pairs id => name
$pairs = $result->fetchPairs('id', 'name');
// the number of rows of the result, if known, or number of affected rows
$count = $result->getRowCount();
```
Method fetchAssoc() can return a more complex associative array.
You can easily add parameters to the query, note the question mark:
```php
$result = $database->query('SELECT * FROM users WHERE name = ? AND active = ?', $name, $active);
// or
$result = $database->query('SELECT * FROM users WHERE name = ?', $name, 'AND active = ?', $active););
$ids = [10, 20, 30];
$result = $database->query('SELECT * FROM users WHERE id IN (?)', $ids);
```
**WARNING: Never concatenate parameters to SQL. It would create a [SQL injection](https://en.wikipedia.org/wiki/SQL_injection)** vulnerability.
```
$result = $database->query('SELECT * FROM users WHERE id = ' . $id); // BAD!!!
```
Instead of a question mark, so-called modifiers can be used.
```php
$result = $database->query('SELECT * FROM users WHERE name = %s', $name);
```
In case of failure `query()` throws `Dibi\Exception`, or one of the descendants:
- `ConstraintViolationException` - violation of a table constraint
- `ForeignKeyConstraintViolationException` - invalid foreign key
- `NotNullConstraintViolationException` - violation of the NOT NULL condition
- `UniqueConstraintViolationException` - collides unique index
You can use also shortcuts:
```php
// returns associative pairs id => name, shortcut for query(...)->fetchPairs()
$pairs = $database->fetchPairs('SELECT id, name FROM users');
// returns array of all rows, shortcut for query(...)->fetchAll()
$rows = $database->fetchAll('SELECT * FROM users');
// returns row, shortcut for query(...)->fetch()
$row = $database->fetch('SELECT * FROM users WHERE id = ?', $id);
// returns field, shortcut for query(...)->fetchSingle()
$name = $database->fetchSingle('SELECT name FROM users WHERE id = ?', $id);
```
### Modifiers
In addition to the `?` wildcard char, we can also use modifiers:
| modifier | description
|----------|-----
| %s | string
| %sN | string, but '' translates as NULL
| %bin | binary data
| %b | boolean
| %i | integer
| %iN | integer, but 0 is translates as NULL
| %f | float
| %d | date (accepts DateTime, string or UNIX timestamp)
| %dt | datetime (accepts DateTime, string or UNIX timestamp)
| %n | identifier, ie the name of the table or column
| %N | identifier, treats period as a common character, ie alias or a database name (`%n AS %N` or `DROP DATABASE %N`)
| %SQL | SQL - directly inserts into SQL (the alternative is Dibi\Literal)
| %ex | SQL expression or array of expressions
| %lmt | special - adds LIMIT to the query
| %ofs | special - adds OFFSET to the query
Example:
```php
$result = $database->query('SELECT * FROM users WHERE name = %s', $name);
```
If $name is null, the NULL is inserted into the SQL statement.
If the variable is an array, the modifier is applied to all of its elements and they are inserted into SQL separated by commas:
```php
$ids = [10, '20', 30];
$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):
```php
$table = 'blog.users';
$column = 'name';
$result = $database->query('SELECT * FROM %n WHERE %n = ?', $table, $column, $value);
// SELECT * FROM `blog`.`users` WHERE `name` = 'Jim'
```
Three special modifiers are available for LIKE:
| modifier | description
|----------|-----
| `%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:
```php
$result = $database->query('SELECT * FROM table WHERE name LIKE %like~', $query);
```
### Modifiers for arrays
The parameter entered in the SQL query can also be an array. These modifiers determine how to compile the SQL statement:
| modifier | | result
|----------|---|-----
| %and | | `key1 = value1 AND key2 = value2 AND ...`
| %or | | `key1 = value1 OR key2 = value2 OR ...`
| %a | assoc | `key1 = value1, key2 = value2, ...`
| %l %in | list | `(val1, val2, ...)`
| %v | values | `(key1, key2, ...) VALUES (value1, value2, ...)`
| %m | multi | `(key1, key2, ...) VALUES (value1, value2, ...), (value1, value2, ...), ...`
| %by | ordering | `key1 ASC, key2 DESC ...`
| %n | names | `key1, key2 AS alias, ...`
Example:
```php
$arr = [
'a' => 'hello',
'b' => true,
'name' => 'John',
'is_admin' => TRUE,
];
dibi::query('INSERT INTO users', $arr);
// INSERT INTO users (`name`, `is_admin`) VALUES ('John', 1)
$database->query('INSERT INTO table %v', $arr);
// INSERT INTO `table` (`a`, `b`) VALUES ('hello', 1)
dibi::query('UPDATE users SET', $arr, 'WHERE `id`=?', $x);
// UPDATE users SET `name`='John', `is_admin`=1 WHERE `id` = 123
$database->query('UPDATE `table` SET %a', $arr);
// UPDATE `table` SET `a`='hello', `b`=1
```
In the WHERE clause modifiers `%and` nebo `%or` can be used:
```php
$result = $database->query('SELECT * FROM users WHERE %and', [
'name' => $name,
'year' => $year,
dibi::query('UPDATE users SET', [
'title' => array('SHA1(?)', 'tajneheslo'),
]);
// SELECT * FROM users WHERE `name` = 'Jim' AND `year` = 1978
// UPDATE users SET 'title' = SHA1('tajneheslo')
```
The modifier `%by` is used to sort, the keys show the columns, and the boolean value will determine whether to sort in ascending order:
Getting results
```php
$result = $database->query('SELECT id FROM author ORDER BY %by', [
'id' => true, // ascending
'name' => false, // descending
]);
// SELECT id FROM author ORDER BY `id`, `name` DESC
```
$result = dibi::query('SELECT * FROM users');
$value = $result->fetchSingle(); // single value
$all = $result->fetchAll(); // all rows
$assoc = $result->fetchAssoc('id'); // all rows as associative array
$pairs = $result->fetchPairs('customerID', 'name'); // all rows as key => value pairs
### Insert, Update & Delete
We insert the data into an SQL query as an associative array. Modifiers and wildcards `?` are not required in these cases.
```php
$database->query('INSERT INTO users', [
'name' => $name,
'year' => $year,
]);
// INSERT INTO users (`name`, `year`) VALUES ('Jim', 1978)
$id = $database->getInsertId(); // returns the auto-increment of the inserted record
$id = $database->getInsertId($sequence); // or sequence value
```
Multiple INSERT:
```php
$database->query('INSERT INTO users', [
'name' => 'Jim',
'year' => 1978,
], [
'name' => 'Jack',
'year' => 1987,
]);
// INSERT INTO users (`name`, `year`) VALUES ('Jim', 1978), ('Jack', 1987)
```
Deleting:
```php
$database->query('DELETE FROM users WHERE id = ?', $id);
// returns the number of deleted rows
$affectedRows = $database->getAffectedRows();
```
Update:
```php
$database->query('UPDATE users SET', [
'name' => $name,
'year' => $year,
], 'WHERE id = ?', $id);
// UPDATE users SET `name` = 'Jim', `year` = 1978 WHERE id = 123
// returns the number of updated rows
$affectedRows = $database->getAffectedRows();
```
Insert an entry or update if it already exists:
```php
$database->query('INSERT INTO users', [
'id' => $id,
'name' => $name,
'year' => $year,
], 'ON DUPLICATE KEY UPDATE %a', [ // here the modifier %a must be used
'name' => $name,
'year' => $year,
]);
// INSERT INTO users (`id`, `name`, `year`) VALUES (123, 'Jim', 1978)
// ON DUPLICATE KEY UPDATE `name` = 'Jim', `year` = 1978
```
### Transaction
There are three methods for dealing with transactions:
```php
$database->begin();
$database->commit();
$database->rollback();
```
### Testing
In order to play with Dibi a little, there is a `test()` method that you pass parameters like to `query()`, but instead of executing the SQL statement, it is echoed on the screen.
The query results can be echoed as a table using `$result->dump()`.
These variables are also available:
```php
dibi::$sql; // the latest SQL query
dibi::$elapsedTime; // its duration in sec
dibi::$numOfQueries;
dibi::$totalTime;
```
### Complex queries
The parameter may also be an object `DateTime`.
```php
$result = $database->query('SELECT * FROM users WHERE created < ?', new DateTime);
$database->query('INSERT INTO users', [
'created' => new DateTime,
]);
```
Or SQL literal:
```php
$database->query('UPDATE table SET', [
'date' => $database->literal('NOW()'),
]);
// UPDATE table SET `date` = NOW()
```
Or an expression in which you can use `?` or modifiers:
```php
$database->query('UPDATE `table` SET', [
'title' => $database::expression('SHA1(?)', 'secret'),
]);
// UPDATE `table` SET `title` = SHA1('secret')
```
When updating, modifiers can be placed directly in the keys:
```php
$database->query('UPDATE table SET', [
'date%SQL' => 'NOW()', // %SQL means SQL ;)
]);
// UPDATE table SET `date` = NOW()
```
In conditions (ie, for `%and` and `%or` modifiers), it is not necessary to specify the keys:
```php
$result = $database->query('SELECT * FROM `table` WHERE %and', [
'number > 10',
'number < 100',
]);
// SELECT * FROM `table` WHERE (number > 10) AND (number < 100)
```
Modifiers or wildcards can also be used in expressions:
```php
$result = $database->query('SELECT * FROM `table` WHERE %and', [
['number > ?', 10], // or $database::expression('number > ?', 10)
['number < ?', 100],
['%or', [
'left' => 1,
'top' => 2,
]],
]);
// SELECT * FROM `table` WHERE (number > 10) AND (number < 100) AND (`left` = 1 OR `top` = 2)
```
The `%ex` modifier inserts all items of the array into SQL:
```php
$result = $database->query('SELECT * FROM `table` WHERE %ex', [
$database::expression('left = ?', 1),
'AND',
'top IS NULL',
]);
// SELECT * FROM `table` WHERE left = 1 AND top IS NULL
```
### Conditions in the SQL
Conditional SQL commands are controlled by three modifiers `%if`, `%else`, and `%end`. The `%if` must be at the end of the string representing SQL and is followed by the variable:
```php
$user = ???
$result = $database->query('
SELECT *
FROM table
%if', isset($user), 'WHERE user=%s', $user, '%end
ORDER BY name
');
```
The condition can be supplemented by the section `%else`:
```php
$result = $database->query('
SELECT *
FROM %if', $cond, 'one_table %else second_table
');
```
Conditions can nest together.
### Identifiers and strings in SQL
SQL itself goes through processing to meet the conventions of the database. The identifiers (names of tables and columns) can be entered into square brackets or backticks, strings are quoted with single or double quotation marks, but the server always sends what the database asks for. Example:
```php
$database->query("UPDATE `table` SET [status]='I''m fine'");
// MySQL: UPDATE `table` SET `status`='I\'m fine'
// ODBC: UPDATE [table] SET [status]='I''m fine'
```
The quotation marks are duplicated inside the string in SQL.
### Result as associative array
Example: returns results as an associative field, where the key will be the value of the `id` field:
```php
$assoc = $result->fetchAssoc('id');
```
The greatest power of `fetchAssoc()` is reflected in a SQL query joining several tables with different types of joins. The database will make a flat table, fetchAssoc returns the shape.
Example: Let's take a customer and order table (N:M binding) and query:
```php
$result = $database->query('
SELECT customer_id, customers.name, order_id, orders.number, ...
FROM customers
INNER JOIN orders USING (customer_id)
WHERE ...
');
```
And we'd like to get a nested associative array by Customer ID and then Order ID:
```php
$all = $result->fetchAssoc('customer_id|order_id');
// we will iterate like this:
foreach ($all as $customerId => $orders) {
foreach ($orders as $orderId => $order) {
...
}
// iterating
foreach ($result as $n => $row) {
print_r($row);
}
```
An associative descriptor has a similar syntax as when you type the array by assigning it to PHP. Thus `'customer_id|order_id'` represents the assignment series `$all[$customerId][$orderId] = $row;` sequentially for all rows.
Sometimes it would be useful to associate by the customer's name instead of his ID:
Modifiers for arrays:
```php
$all = $result->fetchAssoc('name|order_id');
// the elements then proceeds like this:
$order = $all['Arnold Rimmer'][$orderId];
```
But what if there are more customers with the same name? The table should be in the form of:
```php
$row = $all['Arnold Rimmer'][0][$orderId];
$row = $all['Arnold Rimmer'][1][$orderId];
...
```
So we can distinguish between multiple possible Rimmers using an array. The associative descriptor has a format similar to the assignment, with the sequence array representing `[]`:
```php
$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) {
...
}
}
```
Returning to the example with the `customer_id|order_id` descriptor, we will try to list the orders of each customer:
```php
$all = $result->fetchAssoc('customer_id|order_id');
foreach ($all as $customerId => $orders) {
echo "Customer $customerId":
foreach ($orders as $orderId => $order) {
echo "ID number: $order->number";
// customer name is in $order->name
}
}
```
It would be a nice to echo customer name too. But we would have to look for it in the `$orders` array. So let's adjust the results to such a shape:
```php
$all[$customerId]->name = 'John Doe';
$all[$customerId]->order_id[$orderId] = $row;
$all[$customerId]->order_id[$orderId2] = $row2;
```
So, between `$clientId` and `$orderId`, we will also insert an intermediate item. This time not the numbered indexes as we used to distinguish between individual Rimmers, but a database row. The solution is very similar, just remember that the row symbolizes the arrow:
```php
$all = $result->fetchAssoc('customer_id->order_id');
foreach ($all as $customerId => $row) {
echo "Customer $row->name":
foreach ($row->order_id as $orderId => $order) {
echo "ID number: $order->number";
}
}
```
### Prefixes & substitutions
Table and column names can contain variable parts. You will first define:
```php
// create new substitution :blog: ==> wp_
$database->substitute('blog', 'wp_');
```
and then use it in SQL. Note that in SQL they are quoted by the colon:
```php
$database->query("UPDATE [:blog:items] SET [text]='Hello World'");
// UPDATE `wp_items` SET `text`='Hello World'
```
### Field data types
Dibi automatically detects the types of query columns and converts fields them to native PHP types. We can also specify the type manually. You can find the possible types in the `Dibi\Type` class.
```php
$result->setType('id', Dibi\Type::Integer); // id will be integer
$row = $result->fetch();
is_int($row->id) // true
```
### Logger
Dibi has a built-in logger that lets you track all SQL statements executed and measure the length of their duration. Activating the logger:
```php
$database->connect([
'driver' => 'sqlite',
'database' => 'sample.sdb',
'profiler' => [
'file' => 'file.log',
],
dibi::query('SELECT * FROM users WHERE %and', [
array('number > ?', 10),
array('number < ?', 100),
]);
// SELECT * FROM users WHERE (number > 10) AND (number < 100)
```
A more versatile profiler is a Tracy panel that is activated when connected to Nette.
<table>
<tr><td> %and </td><td> </td><td> `[key]=val AND [key2]="val2" AND ...` </td></tr>
<tr><td> %or </td><td> </td><td> `[key]=val OR [key2]="val2" OR ...` </td></tr>
<tr><td> %a </td><td> assoc </td><td> `[key]=val, [key2]="val2", ...` </td></tr>
<tr><td> %l %in </td><td> list </td><td> `(val, "val2", ...)` </td></tr>
<tr><td> %v </td><td> values </td><td> `([key], [key2], ...) VALUES (val, "val2", ...)` </td></tr>
<tr><td> %m </td><td> multivalues </td><td> `([key], [key2], ...) VALUES (val, "val2", ...), (val, "val2", ...), ...` </td></tr>
<tr><td> %by </td><td> ordering </td><td> `[key] ASC, [key2] DESC ...` </td></tr>
<tr><td> %n </td><td> identifiers </td><td> `[key], [key2] AS alias, ...` </td></tr>
<tr><td> other </td><td> - </td><td> `val, val2, ...` </td></tr>
</table>
### Connect to [Nette](https://nette.org)
In the configuration file, we will register the DI extensions and add the `dibi` section to create the required objects and also the database panel in the [Tracy](https://tracy.nette.org) debugger bar.
```neon
extensions:
dibi: Dibi\Bridges\Nette\DibiExtension3
dibi:
host: localhost
username: root
password: ***
database: foo
lazy: true
```
Then the object of connection can be [obtained as a service from the container DI](https://doc.nette.org/di-usage), eg:
Modifiers for LIKE
```php
class Model
{
private $database;
public function __construct(Dibi\Connection $database)
{
$this->database = $database;
}
}
dibi::query("SELECT * FROM table WHERE name LIKE %like~", $query);
```
<table>
<tr><td> %like~ </td><td> begins with </td></tr>
<tr><td> %~like </td><td> ends with </td></tr>
<tr><td> %~like~ </td><td> contains </td></tr>
</table>
DateTime:
```php
dibi::query('UPDATE users SET', [
'time' => new DateTime,
]);
// UPDATE users SET ('2008-01-01 01:08:10')
```
Testing:
```php
echo dibi::$sql; // last SQL query
echo dibi::$elapsedTime;
echo dibi::$numOfQueries;
echo dibi::$totalTime;
```

View File

@@ -0,0 +1,52 @@
<?php
/**
* This file is part of the "dibi" - smart database abstraction layer.
* Copyright (c) 2005 David Grudl (https://davidgrudl.com)
*/
namespace Dibi\Bridges\Nette;
use Nette;
/**
* Dibi extension for Nette Framework 2.1. Creates 'connection' service.
*/
class DibiExtension21 extends Nette\DI\CompilerExtension
{
public function loadConfiguration()
{
$container = $this->getContainerBuilder();
$config = $this->getConfig();
$useProfiler = isset($config['profiler'])
? $config['profiler']
: $container->parameters['debugMode'];
unset($config['profiler']);
if (isset($config['flags'])) {
$flags = 0;
foreach ((array) $config['flags'] as $flag) {
$flags |= constant($flag);
}
$config['flags'] = $flags;
}
$connection = $container->addDefinition($this->prefix('connection'))
->setClass('Dibi\Connection', [$config])
->setAutowired(isset($config['autowired']) ? $config['autowired'] : TRUE);
if ($useProfiler) {
$panel = $container->addDefinition($this->prefix('panel'))
->setClass('Dibi\Bridges\Nette\Panel')
->addSetup('Nette\Diagnostics\Debugger::getBar()->addPanel(?)', ['@self'])
->addSetup('Nette\Diagnostics\Debugger::getBlueScreen()->addPanel(?)', ['Dibi\Bridges\Nette\Panel::renderException']);
$connection->addSetup('$service->onEvent[] = ?', [[$panel, 'logEvent']]);
}
}
}

View File

@@ -1,17 +1,14 @@
<?php
/**
* This file is part of the Dibi, smart database abstraction layer (https://dibiphp.com)
* This file is part of the "dibi" - smart database abstraction layer.
* Copyright (c) 2005 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Dibi\Bridges\Nette;
use Dibi;
use Nette;
use Tracy;
/**
@@ -19,27 +16,15 @@ use Tracy;
*/
class DibiExtension22 extends Nette\DI\CompilerExtension
{
public function __construct(
private ?bool $debugMode = null,
private ?bool $cliMode = null,
) {
}
public function loadConfiguration()
{
$container = $this->getContainerBuilder();
$config = $this->getConfig();
if ($this->debugMode === null) {
$this->debugMode = $container->parameters['debugMode'];
}
if ($this->cliMode === null) {
$this->cliMode = $container->parameters['consoleMode'];
}
$useProfiler = $config['profiler'] ?? (class_exists(Tracy\Debugger::class) && $this->debugMode && !$this->cliMode);
$useProfiler = isset($config['profiler'])
? $config['profiler']
: class_exists('Tracy\Debugger') && $container->parameters['debugMode'];
unset($config['profiler']);
@@ -48,28 +33,27 @@ class DibiExtension22 extends Nette\DI\CompilerExtension
foreach ((array) $config['flags'] as $flag) {
$flags |= constant($flag);
}
$config['flags'] = $flags;
}
$connection = $container->addDefinition($this->prefix('connection'))
->setFactory(Dibi\Connection::class, [$config])
->setAutowired($config['autowired'] ?? true);
->setClass('Dibi\Connection', [$config])
->setAutowired(isset($config['autowired']) ? $config['autowired'] : TRUE);
if (class_exists(Tracy\Debugger::class)) {
if (class_exists('Tracy\Debugger')) {
$connection->addSetup(
[new Nette\DI\Statement('Tracy\Debugger::getBlueScreen'), 'addPanel'],
[[Dibi\Bridges\Tracy\Panel::class, 'renderException']],
[['Dibi\Bridges\Tracy\Panel', 'renderException']]
);
}
if ($useProfiler) {
$panel = $container->addDefinition($this->prefix('panel'))
->setFactory(Dibi\Bridges\Tracy\Panel::class, [
$config['explain'] ?? true,
isset($config['filter']) && $config['filter'] === false ? Dibi\Event::ALL : Dibi\Event::QUERY,
->setClass('Dibi\Bridges\Tracy\Panel', [
isset($config['explain']) ? $config['explain'] : TRUE,
isset($config['filter']) && $config['filter'] === FALSE ? Dibi\Event::ALL : Dibi\Event::QUERY,
]);
$connection->addSetup([$panel, 'register'], [$connection]);
}
}
}

View File

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

View File

@@ -0,0 +1,153 @@
<?php
/**
* This file is part of the "dibi" - smart database abstraction layer.
* Copyright (c) 2005 David Grudl (https://davidgrudl.com)
*/
namespace Dibi\Bridges\Nette;
use Dibi;
use Dibi\Event;
use Dibi\Helpers;
use Nette;
use Nette\Diagnostics\Debugger;
/**
* Dibi panel for Nette\Diagnostics.
*/
class Panel implements Nette\Diagnostics\IBarPanel
{
use Dibi\Strict;
/** @var int maximum SQL length */
public static $maxLength = 1000;
/** @var bool explain queries? */
public $explain;
/** @var int */
public $filter;
/** @var array */
private $events = [];
public function __construct($explain = TRUE, $filter = NULL)
{
$this->filter = $filter ? (int) $filter : Event::QUERY;
$this->explain = $explain;
}
public function register(Dibi\Connection $connection)
{
Debugger::getBar()->addPanel($this);
Debugger::getBlueScreen()->addPanel([__CLASS__, 'renderException']);
$connection->onEvent[] = [$this, 'logEvent'];
}
/**
* After event notification.
* @return void
*/
public function logEvent(Event $event)
{
if (($event->type & $this->filter) === 0) {
return;
}
$this->events[] = $event;
}
/**
* Returns blue-screen custom tab.
* @return mixed
*/
public static function renderException($e)
{
if ($e instanceof Dibi\Exception && $e->getSql()) {
return [
'tab' => 'SQL',
'panel' => Helpers::dump($e->getSql(), TRUE),
];
}
}
/**
* Returns HTML code for custom tab. (Nette\Diagnostics\IBarPanel)
* @return mixed
*/
public function getTab()
{
$totalTime = 0;
foreach ($this->events as $event) {
$totalTime += $event->time;
}
return '<span title="dibi"><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAQAAAC1+jfqAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAEYSURBVBgZBcHPio5hGAfg6/2+R980k6wmJgsJ5U/ZOAqbSc2GnXOwUg7BESgLUeIQ1GSjLFnMwsKGGg1qxJRmPM97/1zXFAAAAEADdlfZzr26miup2svnelq7d2aYgt3rebl585wN6+K3I1/9fJe7O/uIePP2SypJkiRJ0vMhr55FLCA3zgIAOK9uQ4MS361ZOSX+OrTvkgINSjS/HIvhjxNNFGgQsbSmabohKDNoUGLohsls6BaiQIMSs2FYmnXdUsygQYmumy3Nhi6igwalDEOJEjPKP7CA2aFNK8Bkyy3fdNCg7r9/fW3jgpVJbDmy5+PB2IYp4MXFelQ7izPrhkPHB+P5/PjhD5gCgCenx+VR/dODEwD+A3T7nqbxwf1HAAAAAElFTkSuQmCC" />'
. count($this->events) . ' queries'
. ($totalTime ? sprintf(' / %0.1f ms', $totalTime * 1000) : '')
. '</span>';
}
/**
* Returns HTML code for custom panel. (Nette\Diagnostics\IBarPanel)
* @return mixed
*/
public function getPanel()
{
$totalTime = $s = NULL;
$h = 'htmlSpecialChars';
foreach ($this->events as $event) {
$totalTime += $event->time;
$explain = NULL; // EXPLAIN is called here to work SELECT FOUND_ROWS()
if ($this->explain && $event->type === Event::SELECT) {
try {
$backup = [$event->connection->onEvent, \dibi::$numOfQueries, \dibi::$totalTime];
$event->connection->onEvent = NULL;
$cmd = is_string($this->explain) ? $this->explain : ($event->connection->getConfig('driver') === 'oracle' ? 'EXPLAIN PLAN FOR' : 'EXPLAIN');
$explain = Helpers::dump($event->connection->nativeQuery("$cmd $event->sql"), TRUE);
} catch (Dibi\Exception $e) {
}
list($event->connection->onEvent, \dibi::$numOfQueries, \dibi::$totalTime) = $backup;
}
$s .= '<tr><td>' . sprintf('%0.3f', $event->time * 1000);
if ($explain) {
static $counter;
$counter++;
$s .= "<br /><a href='#nette-debug-DibiProfiler-row-$counter' class='nette-toggler nette-toggle-collapsed' rel='#nette-debug-DibiProfiler-row-$counter'>explain</a>";
}
$s .= '</td><td class="nette-DibiProfiler-sql">' . Helpers::dump(strlen($event->sql) > self::$maxLength ? substr($event->sql, 0, self::$maxLength) . '...' : $event->sql, TRUE);
if ($explain) {
$s .= "<div id='nette-debug-DibiProfiler-row-$counter' class='nette-collapsed'>{$explain}</div>";
}
if ($event->source) {
$helpers = 'Nette\Diagnostics\Helpers';
if (!class_exists($helpers)) {
$helpers = class_exists('NDebugHelpers') ? 'NDebugHelpers' : 'DebugHelpers';
}
$s .= call_user_func([$helpers, 'editorLink'], $event->source[0], $event->source[1])->class('nette-DibiProfiler-source');
}
$s .= "</td><td>{$event->count}</td><td>{$h($event->connection->getConfig('driver') . '/' . $event->connection->getConfig('name'))}</td></tr>";
}
return empty($this->events) ? '' :
'<style> #nette-debug td.nette-DibiProfiler-sql { background: white !important }
#nette-debug .nette-DibiProfiler-source { color: #999 !important }
#nette-debug nette-DibiProfiler tr table { margin: 8px 0; max-height: 150px; overflow:auto } </style>
<h1>Queries: ' . count($this->events) . ($totalTime === NULL ? '' : sprintf(', time: %0.3f ms', $totalTime * 1000)) . '</h1>
<div class="nette-inner nette-DibiProfiler">
<table>
<tr><th>Time&nbsp;ms</th><th>SQL Statement</th><th>Rows</th><th>Connection</th></tr>' . $s . '
</table>
</div>';
}
}

View File

@@ -1,12 +1,12 @@
# This will create service named 'dibi.connection'.
# Requires Nette Framework 3 or later
# Requires Nette Framework 2.2
extensions:
dibi: Dibi\Bridges\Nette\DibiExtension3
dibi: Dibi\Bridges\Nette\DibiExtension22
dibi:
host: localhost
username: root
password: ***
database: foo
lazy: true
lazy: TRUE

View File

@@ -1,12 +1,10 @@
<?php
/**
* This file is part of the Dibi, smart database abstraction layer (https://dibiphp.com)
* This file is part of the "dibi" - smart database abstraction layer.
* Copyright (c) 2005 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Dibi\Bridges\Tracy;
use Dibi;
@@ -20,152 +18,138 @@ use Tracy;
*/
class Panel implements Tracy\IBarPanel
{
public static int $maxLength = 1000;
use Dibi\Strict;
private array $events = [];
/** @var int maximum SQL length */
public static $maxLength = 1000;
/** @var bool explain queries? */
public $explain;
/** @var int */
public $filter;
/** @var array */
private $events = [];
public function __construct(
public bool|string $explain = true,
public int $filter = Event::QUERY,
) {
public function __construct($explain = TRUE, $filter = NULL)
{
$this->filter = $filter ? (int) $filter : Event::QUERY;
$this->explain = $explain;
}
public function register(Dibi\Connection $connection): void
public function register(Dibi\Connection $connection)
{
Tracy\Debugger::getBar()->addPanel($this);
Tracy\Debugger::getBlueScreen()->addPanel([self::class, 'renderException']);
Tracy\Debugger::getBlueScreen()->addPanel([__CLASS__, 'renderException']);
$connection->onEvent[] = [$this, 'logEvent'];
}
/**
* After event notification.
* @return void
*/
public function logEvent(Event $event): void
public function logEvent(Event $event)
{
if (($event->type & $this->filter) === 0) {
return;
}
$this->events[] = $event;
}
/**
* Returns blue-screen custom tab.
* @return array|NULL
*/
public static function renderException(?\Throwable $e): ?array
public static function renderException($e)
{
if ($e instanceof Dibi\Exception && $e->getSql()) {
return [
'tab' => 'SQL',
'panel' => Helpers::dump($e->getSql(), return: true),
'panel' => Helpers::dump($e->getSql(), TRUE),
];
}
return null;
}
/**
* Returns HTML code for custom tab. (Tracy\IBarPanel)
* @return string
*/
public function getTab(): string
public function getTab()
{
$totalTime = 0;
$count = count($this->events);
foreach ($this->events as $event) {
$totalTime += $event->time;
}
return '<span title="dibi"><svg viewBox="0 0 2048 2048" style="vertical-align: bottom; width:1.23em; height:1.55em"><path fill="' . ($count ? '#b079d6' : '#aaa') . '" d="M1024 896q237 0 443-43t325-127v170q0 69-103 128t-280 93.5-385 34.5-385-34.5-280-93.5-103-128v-170q119 84 325 127t443 43zm0 768q237 0 443-43t325-127v170q0 69-103 128t-280 93.5-385 34.5-385-34.5-280-93.5-103-128v-170q119 84 325 127t443 43zm0-384q237 0 443-43t325-127v170q0 69-103 128t-280 93.5-385 34.5-385-34.5-280-93.5-103-128v-170q119 84 325 127t443 43zm0-1152q208 0 385 34.5t280 93.5 103 128v128q0 69-103 128t-280 93.5-385 34.5-385-34.5-280-93.5-103-128v-128q0-69 103-128t280-93.5 385-34.5z"/></svg><span class="tracy-label">'
. $count . "\u{a0}queries"
. ($totalTime ? ' / ' . number_format($totalTime * 1000, 1, '.', "\u{202f}") . "\u{202f}ms" : '')
. $count . ' queries'
. ($totalTime ? ' / ' . number_format($totalTime * 1000, 1, '.', '') . 'ms' : '')
. '</span></span>';
}
/**
* Returns HTML code for custom panel. (Tracy\IBarPanel)
* @return string|NULL
*/
public function getPanel(): ?string
public function getPanel()
{
if (!$this->events) {
return null;
}
$totalTime = $s = null;
$singleConnection = reset($this->events)->connection;
foreach ($this->events as $event) {
if ($event->connection !== $singleConnection) {
$singleConnection = null;
break;
}
return NULL;
}
$totalTime = $s = NULL;
foreach ($this->events as $event) {
$totalTime += $event->time;
$connection = $event->connection;
$explain = null; // EXPLAIN is called here to work SELECT FOUND_ROWS()
$explain = NULL; // EXPLAIN is called here to work SELECT FOUND_ROWS()
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');
try {
$explain = @Helpers::dump($connection->nativeQuery("$cmd $event->sql"), return: true);
$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');
$explain = @Helpers::dump($connection->nativeQuery("$cmd $event->sql"), TRUE);
} catch (Dibi\Exception $e) {
}
[$connection->onEvent, \dibi::$numOfQueries, \dibi::$totalTime] = $backup;
list($connection->onEvent, \dibi::$numOfQueries, \dibi::$totalTime) = $backup;
}
$s .= '<tr><td data-order="' . $event->time . '">' . number_format($event->time * 1000, 3, '.', "\u{202f}");
$s .= '<tr><td>' . number_format($event->time * 1000, 3, '.', '');
if ($explain) {
static $counter;
$counter++;
$s .= "<br /><a href='#tracy-debug-DibiProfiler-row-$counter' class='tracy-toggle tracy-collapsed' rel='#tracy-debug-DibiProfiler-row-$counter'>explain</a>";
}
$s .= '</td><td class="tracy-DibiProfiler-sql">' . Helpers::dump(strlen($event->sql) > self::$maxLength ? substr($event->sql, 0, self::$maxLength) . '...' : $event->sql, return: true);
$s .= '</td><td class="tracy-DibiProfiler-sql">' . Helpers::dump(strlen($event->sql) > self::$maxLength ? substr($event->sql, 0, self::$maxLength) . '...' : $event->sql, TRUE);
if ($explain) {
$s .= "<div id='tracy-debug-DibiProfiler-row-$counter' class='tracy-collapsed'>{$explain}</div>";
}
if ($event->source) {
$s .= Tracy\Helpers::editorLink($event->source[0], $event->source[1]); //->class('tracy-DibiProfiler-source');
$s .= Tracy\Helpers::editorLink($event->source[0], $event->source[1]);//->class('tracy-DibiProfiler-source');
}
$s .= "</td><td>{$event->count}</td>";
if (!$singleConnection) {
$s .= '<td>' . htmlspecialchars($this->getConnectionName($connection)) . '</td></tr>';
}
$s .= "</td><td>{$event->count}</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:' . "\u{a0}" . count($this->events)
. ($totalTime === null ? '' : ", time:\u{a0}" . number_format($totalTime * 1000, 1, '.', "\u{202f}") . "\u{202f}ms")
. ($singleConnection === null ? '' : ', ' . htmlspecialchars($this->getConnectionName($singleConnection))) . '</h1>
<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>
<div class="tracy-inner tracy-DibiProfiler">
<table class="tracy-sortable">
<tr><th>Time&nbsp;ms</th><th>SQL Statement</th><th>Rows</th>' . (!$singleConnection ? '<th>Connection</th>' : '') . '</tr>
' . $s . '
<table>
<tr><th>Time&nbsp;ms</th><th>SQL Statement</th><th>Rows</th></tr>' . $s . '
</table>
</div>';
}
private function getConnectionName(Dibi\Connection $connection): string
{
$driver = $connection->getConfig('driver');
return get_debug_type($driver)
. ($connection->getConfig('name') ? '/' . $connection->getConfig('name') : '')
. ($connection->getConfig('host') ? "\u{202f}@\u{202f}" . $connection->getConfig('host') : '');
}
}

View File

@@ -1,103 +1,130 @@
<?php
/**
* This file is part of the Dibi, smart database abstraction layer (https://dibiphp.com)
* This file is part of the "dibi" - smart database abstraction layer.
* Copyright (c) 2005 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Dibi;
use JetBrains\PhpStorm\Language;
use Traversable;
/**
* Dibi connection.
* dibi connection.
*
* @property-read int $affectedRows
* @property-read int $insertId
*/
class Connection implements IConnection
class Connection
{
/** function (Event $event); Occurs after query is executed */
public ?array $onEvent = [];
private array $config;
use Strict;
/** @var string[] resultset formats */
private array $formats;
private ?Driver $driver = null;
private ?Translator $translator = null;
/** @var array of function (Event $event); Occurs after query is executed */
public $onEvent;
/** @var array<string, callable(object): Expression | null> */
private array $translators = [];
private bool $sortTranslators = false;
private HashMap $substitutes;
private int $transactionDepth = 0;
/** @var array Current connection configuration */
private $config;
/** @var Driver */
private $driver;
/** @var Translator */
private $translator;
/** @var bool Is connected? */
private $connected = FALSE;
/** @var HashMap Substitutes for identifiers */
private $substitutes;
/**
* Connection options: (see driver-specific options too)
* - lazy (bool) => if true, connection will be established only when required
* - lazy (bool) => if TRUE, connection will be established only when required
* - result (array) => result set options
* - normalize => normalizes result fields (default: true)
* - formatDateTime => date-time format
* empty for decoding as Dibi\DateTime (default)
* "..." formatted according to given format, see https://www.php.net/manual/en/datetime.format.php
* "native" for leaving value as is
* - formatTimeInterval => time-interval format
* empty for decoding as DateInterval (default)
* "..." formatted according to given format, see https://www.php.net/manual/en/dateinterval.format.php
* "native" for leaving value as is
* - formatJson => json format
* "array" for decoding json as an array (default)
* "object" for decoding json as \stdClass
* "native" for leaving value as is
* - profiler (array)
* - formatDateTime => date-time format (if empty, DateTime objects will be returned)
* - profiler (array or bool)
* - 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
* @param mixed connection parameters
* @param string connection name
* @throws Exception
*/
public function __construct(array $config, ?string $name = null)
public function __construct($config, $name = NULL)
{
if (is_string($config)) {
parse_str($config, $config);
} elseif ($config instanceof Traversable) {
$tmp = [];
foreach ($config as $key => $val) {
$tmp[$key] = $val instanceof Traversable ? iterator_to_array($val) : $val;
}
$config = $tmp;
} elseif (!is_array($config)) {
throw new \InvalidArgumentException('Configuration must be array, string or object.');
}
Helpers::alias($config, 'username', 'user');
Helpers::alias($config, 'password', 'pass');
Helpers::alias($config, 'host', 'hostname');
Helpers::alias($config, 'result|formatDate', 'resultDate');
Helpers::alias($config, 'result|formatDateTime', 'resultDateTime');
$config['driver'] ??= 'mysqli';
if (!isset($config['driver'])) {
$config['driver'] = \dibi::$defaultDriver;
}
if ($config['driver'] instanceof Driver) {
$this->driver = $config['driver'];
$config['driver'] = get_class($this->driver);
} elseif (is_subclass_of($config['driver'], 'Dibi\Driver')) {
$this->driver = new $config['driver'];
} else {
$class = preg_replace(['#\W#', '#sql#'], ['_', 'Sql'], ucfirst(strtolower($config['driver'])));
$class = "Dibi\\Drivers\\{$class}Driver";
if (!class_exists($class)) {
throw new Exception("Unable to create instance of dibi driver '$class'.");
}
$this->driver = new $class;
}
$config['name'] = $name;
$this->config = $config;
$this->formats = [
Type::Date => $this->config['result']['formatDate'],
Type::DateTime => $this->config['result']['formatDateTime'],
Type::JSON => $this->config['result']['formatJson'] ?? 'array',
Type::TimeInterval => $this->config['result']['formatTimeInterval'] ?? null,
];
// profiler
if (isset($config['profiler']['file']) && (!isset($config['profiler']['run']) || $config['profiler']['run'])) {
$filter = $config['profiler']['filter'] ?? Event::QUERY;
$errorsOnly = $config['profiler']['errorsOnly'] ?? false;
$this->onEvent[] = [new Loggers\FileLogger($config['profiler']['file'], $filter, $errorsOnly), 'logEvent'];
$profilerCfg = &$config['profiler'];
if (is_scalar($profilerCfg)) {
$profilerCfg = ['run' => (bool) $profilerCfg];
}
if (!empty($profilerCfg['run'])) {
$filter = isset($profilerCfg['filter']) ? $profilerCfg['filter'] : Event::QUERY;
if (isset($profilerCfg['file'])) {
$this->onEvent[] = [new Loggers\FileLogger($profilerCfg['file'], $filter), 'logEvent'];
}
if (Loggers\FirePhpLogger::isAvailable()) {
$this->onEvent[] = [new Loggers\FirePhpLogger($filter), 'logEvent'];
}
if (!interface_exists('Tracy\IBarPanel') && interface_exists('Nette\Diagnostics\IBarPanel') && class_exists('Dibi\Bridges\Nette\Panel')) {
$panel = new Bridges\Nette\Panel(isset($profilerCfg['explain']) ? $profilerCfg['explain'] : TRUE, $filter);
$panel->register($this);
}
}
$this->substitutes = new HashMap(fn(string $expr) => ":$expr:");
$this->substitutes = new HashMap(function ($expr) { return ":$expr:"; });
if (!empty($config['substitutes'])) {
foreach ($config['substitutes'] as $key => $value) {
$this->substitutes->$key = $value;
}
}
if (isset($config['onConnect']) && !is_array($config['onConnect'])) {
throw new \InvalidArgumentException("Configuration option 'onConnect' must be array.");
}
if (empty($config['lazy'])) {
$this->connect();
}
@@ -106,55 +133,29 @@ class Connection implements IConnection
/**
* Automatically frees the resources allocated for this result set.
* @return void
*/
public function __destruct()
{
if ($this->driver && $this->driver->getResource()) {
$this->disconnect();
}
// disconnects and rolls back transaction - do not rely on auto-disconnect and rollback!
$this->connected && $this->driver->getResource() && $this->disconnect();
}
/**
* Connects to a database.
* @return void
*/
final public function connect(): void
final public function connect()
{
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)) {
$class = $this->config['driver'];
} else {
$class = preg_replace(['#\W#', '#sql#'], ['_', 'Sql'], ucfirst(strtolower($this->config['driver'])));
$class = "Dibi\\Drivers\\{$class}Driver";
if (!class_exists($class)) {
throw new Exception("Unable to create instance of Dibi driver '$class'.");
}
}
$event = $this->onEvent ? new Event($this, Event::CONNECT) : null;
$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));
}
$this->driver->connect($this->config);
$this->connected = TRUE;
$event && $this->onEvent($event->done());
} catch (Exception $e) {
$event && $this->onEvent($event->done($e));
throw $e;
}
}
@@ -162,199 +163,245 @@ class Connection implements IConnection
/**
* Disconnects from a database.
* @return void
*/
final public function disconnect(): void
final public function disconnect()
{
if ($this->driver) {
$this->driver->disconnect();
$this->driver = $this->translator = null;
}
$this->driver->disconnect();
$this->connected = FALSE;
}
/**
* Returns true when connection was established.
* Returns TRUE when connection was established.
* @return bool
*/
final public function isConnected(): bool
final public function isConnected()
{
return (bool) $this->driver;
return $this->connected;
}
/**
* Returns configuration variable. If no $key is passed, returns the entire array.
* @see self::__construct
* @param string
* @param mixed default value to use if key not found
* @return mixed
*/
final public function getConfig(?string $key = null, $default = null): mixed
final public function getConfig($key = NULL, $default = NULL)
{
return $key === null
? $this->config
: ($this->config[$key] ?? $default);
if ($key === NULL) {
return $this->config;
} elseif (isset($this->config[$key])) {
return $this->config[$key];
} else {
return $default;
}
}
/** @deprecated */
public static function alias(&$config, $key, $alias)
{
trigger_error(__METHOD__ . '() is deprecated, use Helpers::alias().', E_USER_DEPRECATED);
Helpers::alias($config, $key, $alias);
}
/**
* Returns the driver and connects to a database in lazy mode.
* @return Driver
*/
final public function getDriver(): Driver
final public function getDriver()
{
if (!$this->driver) {
$this->connect();
}
$this->connected || $this->connect();
return $this->driver;
}
/**
* Generates (translates) and executes SQL query.
* @param array|mixed one or more arguments
* @return Result|int result set or number of affected rows
* @throws Exception
*/
final public function query(#[Language('GenericSQL')] mixed ...$args): Result
final public function query($args)
{
return $this->nativeQuery($this->translate(...$args));
$args = func_get_args();
return $this->nativeQuery($this->translateArgs($args));
}
/**
* Generates SQL query.
* @param array|mixed one or more arguments
* @return string
* @throws Exception
*/
final public function translate(#[Language('GenericSQL')] mixed ...$args): string
final public function translate($args)
{
if (!$this->driver) {
$this->connect();
}
return (clone $this->translator)->translate($args);
$args = func_get_args();
return $this->translateArgs($args);
}
/**
* Generates and prints SQL query.
* @param array|mixed one or more arguments
* @return bool
*/
final public function test(#[Language('GenericSQL')] mixed ...$args): bool
final public function test($args)
{
$args = func_get_args();
try {
Helpers::dump($this->translate(...$args));
return true;
Helpers::dump($this->translateArgs($args));
return TRUE;
} catch (Exception $e) {
if ($e->getSql()) {
Helpers::dump($e->getSql());
} else {
echo $e::class . ': ' . $e->getMessage() . (PHP_SAPI === 'cli' ? "\n" : '<br>');
echo get_class($e) . ': ' . $e->getMessage() . (PHP_SAPI === 'cli' ? "\n" : '<br>');
}
return false;
return FALSE;
}
}
/**
* Generates (translates) and returns SQL query as DataSource.
* @param array|mixed one or more arguments
* @return DataSource
* @throws Exception
*/
final public function dataSource(#[Language('GenericSQL')] mixed ...$args): DataSource
final public function dataSource($args)
{
return new DataSource($this->translate(...$args), $this);
$args = func_get_args();
return new DataSource($this->translateArgs($args), $this);
}
/**
* Generates SQL query.
* @param array
* @return string
*/
protected function translateArgs($args)
{
$this->connected || $this->connect();
if (!$this->translator) {
$this->translator = new Translator($this);
}
$translator = clone $this->translator;
return $translator->translate($args);
}
/**
* Executes the SQL query.
* @param string SQL statement.
* @return Result|int result set or number of affected rows
* @throws Exception
*/
final public function nativeQuery(#[Language('SQL')] string $sql): Result
final public function nativeQuery($sql)
{
if (!$this->driver) {
$this->connect();
}
$this->connected || $this->connect();
\dibi::$sql = $sql;
$event = $this->onEvent ? new Event($this, Event::QUERY, $sql) : null;
$event = $this->onEvent ? new Event($this, Event::QUERY, $sql) : NULL;
try {
$res = $this->driver->query($sql);
} catch (DriverException $e) {
if ($event) {
$this->onEvent($event->done($e));
}
} catch (Exception $e) {
$event && $this->onEvent($event->done($e));
throw $e;
}
$res = $this->createResultSet($res ?: new Drivers\NoDataResult(max(0, $this->driver->getAffectedRows())));
if ($event) {
$this->onEvent($event->done($res));
if ($res) {
$res = $this->createResultSet($res);
} else {
$res = $this->driver->getAffectedRows();
}
$event && $this->onEvent($event->done($res));
return $res;
}
/**
* Gets the number of affected rows by the last INSERT, UPDATE or DELETE query.
* @return int number of rows
* @throws Exception
*/
public function getAffectedRows(): int
public function getAffectedRows()
{
if (!$this->driver) {
$this->connect();
}
$this->connected || $this->connect();
$rows = $this->driver->getAffectedRows();
if ($rows === null || $rows < 0) {
if (!is_int($rows) || $rows < 0) {
throw new Exception('Cannot retrieve number of affected rows.');
}
return $rows;
}
/**
* Retrieves the ID generated for an AUTO_INCREMENT column by the previous INSERT query.
* Gets the number of affected rows. Alias for getAffectedRows().
* @return int number of rows
* @throws Exception
*/
public function getInsertId(?string $sequence = null): int
public function affectedRows()
{
if (!$this->driver) {
$this->connect();
}
return $this->getAffectedRows();
}
/**
* Retrieves the ID generated for an AUTO_INCREMENT column by the previous INSERT query.
* @param string optional sequence name
* @return int
* @throws Exception
*/
public function getInsertId($sequence = NULL)
{
$this->connected || $this->connect();
$id = $this->driver->getInsertId($sequence);
if ($id === null) {
if ($id < 1) {
throw new Exception('Cannot retrieve last generated ID.');
}
return (int) $id;
}
return $id;
/**
* Retrieves the ID generated for an AUTO_INCREMENT column. Alias for getInsertId().
* @param string optional sequence name
* @return int
* @throws Exception
*/
public function insertId($sequence = NULL)
{
return $this->getInsertId($sequence);
}
/**
* Begins a transaction (if supported).
* @param string optional savepoint name
* @return void
*/
public function begin(?string $savepoint = null): void
public function begin($savepoint = NULL)
{
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;
$this->connected || $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));
}
$event && $this->onEvent($event->done());
} catch (Exception $e) {
$event && $this->onEvent($event->done($e));
throw $e;
}
}
@@ -362,28 +409,19 @@ class Connection implements IConnection
/**
* Commits statements in a transaction.
* @param string optional savepoint name
* @return void
*/
public function commit(?string $savepoint = null): void
public function commit($savepoint = NULL)
{
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;
$this->connected || $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));
}
$event && $this->onEvent($event->done());
} catch (Exception $e) {
$event && $this->onEvent($event->done($e));
throw $e;
}
}
@@ -391,106 +429,96 @@ class Connection implements IConnection
/**
* Rollback changes in a transaction.
* @param string optional savepoint name
* @return void
*/
public function rollback(?string $savepoint = null): void
public function rollback($savepoint = NULL)
{
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;
$this->connected || $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));
}
$event && $this->onEvent($event->done());
} catch (Exception $e) {
$event && $this->onEvent($event->done($e));
throw $e;
}
}
public function transaction(callable $callback): mixed
{
if ($this->transactionDepth === 0) {
$this->begin();
}
$this->transactionDepth++;
try {
$res = $callback($this);
} catch (\Throwable $e) {
$this->transactionDepth--;
if ($this->transactionDepth === 0) {
$this->rollback();
}
throw $e;
}
$this->transactionDepth--;
if ($this->transactionDepth === 0) {
$this->commit();
}
return $res;
}
/**
* Result set factory.
* @param ResultDriver
* @return Result
*/
public function createResultSet(ResultDriver $resultDriver): Result
public function createResultSet(ResultDriver $resultDriver)
{
return (new Result($resultDriver, $this->config['result']['normalize'] ?? true))
->setFormats($this->formats);
$res = new Result($resultDriver);
return $res->setFormat(Type::DATE, $this->config['result']['formatDate'])
->setFormat(Type::DATETIME, $this->config['result']['formatDateTime']);
}
/********************* fluent SQL builders ****************d*g**/
public function command(): Fluent
/**
* @return Fluent
*/
public function command()
{
return new Fluent($this);
}
public function select(...$args): Fluent
/**
* @param mixed column name
* @return Fluent
*/
public function select($args)
{
return $this->command()->select(...$args);
$args = func_get_args();
return $this->command()->__call('select', $args);
}
/**
* @param string|string[] $table
* @param string table
* @param array
* @return Fluent
*/
public function update($table, iterable $args): Fluent
public function update($table, $args)
{
if (!(is_array($args) || $args instanceof Traversable)) {
throw new \InvalidArgumentException('Arguments must be array or Traversable.');
}
return $this->command()->update('%n', $table)->set($args);
}
public function insert(string $table, iterable $args): Fluent
/**
* @param string table
* @param array
* @return Fluent
*/
public function insert($table, $args)
{
if ($args instanceof Traversable) {
$args = iterator_to_array($args);
} elseif (!is_array($args)) {
throw new \InvalidArgumentException('Arguments must be array or Traversable.');
}
return $this->command()->insert()
->into('%n', $table, '(%n)', array_keys($args))->values('%l', $args);
}
public function delete(string $table): Fluent
/**
* @param string table
* @return Fluent
*/
public function delete($table)
{
return $this->command()->delete()->from('%n', $table);
}
@@ -501,8 +529,9 @@ class Connection implements IConnection
/**
* Returns substitution hashmap.
* @return HashMap
*/
public function getSubstitutes(): HashMap
public function getSubstitutes()
{
return $this->substitutes;
}
@@ -510,80 +539,13 @@ class Connection implements IConnection
/**
* Provides substitution.
* @return string
*/
public function substitute(string $value): string
public function substitute($value)
{
return str_contains($value, ':')
? preg_replace_callback('#:([^:\s]*):#', fn(array $m) => $this->substitutes->{$m[1]}, $value)
: $value;
}
/********************* value objects translation ****************d*g**/
/**
* @param callable(object): Expression $translator
*/
public function setObjectTranslator(callable $translator): void
{
if (!$translator instanceof \Closure) {
$translator = \Closure::fromCallable($translator);
}
$param = (new \ReflectionFunction($translator))->getParameters()[0] ?? null;
$type = $param?->getType();
$types = match (true) {
$type instanceof \ReflectionNamedType => [$type],
$type instanceof \ReflectionUnionType => $type->getTypes(),
default => throw new Exception('Object translator must have exactly one parameter with class typehint.'),
};
foreach ($types as $type) {
if ($type->isBuiltin() || $type->allowsNull()) {
throw new Exception("Object translator must have exactly one parameter with non-nullable class typehint, got '$type'.");
}
$this->translators[$type->getName()] = $translator;
}
$this->sortTranslators = true;
}
public function translateObject(object $object): ?Expression
{
if ($this->sortTranslators) {
$this->translators = array_filter($this->translators);
uksort($this->translators, fn($a, $b) => is_subclass_of($a, $b) ? -1 : 1);
$this->sortTranslators = false;
}
if (!array_key_exists($object::class, $this->translators)) {
$translator = null;
foreach ($this->translators as $class => $t) {
if ($object instanceof $class) {
$translator = $t;
break;
}
}
$this->translators[$object::class] = $translator;
}
$translator = $this->translators[$object::class];
if ($translator === null) {
return null;
}
$result = $translator($object);
if (!$result instanceof Expression) {
throw new Exception(sprintf(
"Object translator for class '%s' returned '%s' but %s expected.",
$object::class,
get_debug_type($result),
Expression::class,
));
}
return $result;
return strpos($value, ':') === FALSE
? $value
: preg_replace_callback('#:([^:\s]*):#', function ($m) { return $this->substitutes->{$m[1]}; }, $value);
}
@@ -592,81 +554,87 @@ class Connection implements IConnection
/**
* Executes SQL query and fetch result - shortcut for query() & fetch().
* @param array|mixed one or more arguments
* @return Row|FALSE
* @throws Exception
*/
public function fetch(#[Language('GenericSQL')] mixed ...$args): ?Row
public function fetch($args)
{
$args = func_get_args();
return $this->query($args)->fetch();
}
/**
* Executes SQL query and fetch results - shortcut for query() & fetchAll().
* @return Row[]|array[]
* @param array|mixed one or more arguments
* @return Row[]
* @throws Exception
*/
public function fetchAll(#[Language('GenericSQL')] mixed ...$args): array
public function fetchAll($args)
{
$args = func_get_args();
return $this->query($args)->fetchAll();
}
/**
* Executes SQL query and fetch first column - shortcut for query() & fetchSingle().
* @param array|mixed one or more arguments
* @return mixed
* @throws Exception
*/
public function fetchSingle(#[Language('GenericSQL')] mixed ...$args): mixed
public function fetchSingle($args)
{
$args = func_get_args();
return $this->query($args)->fetchSingle();
}
/**
* Executes SQL query and fetch pairs - shortcut for query() & fetchPairs().
* @param array|mixed one or more arguments
* @return array
* @throws Exception
*/
public function fetchPairs(#[Language('GenericSQL')] mixed ...$args): array
public function fetchPairs($args)
{
$args = func_get_args();
return $this->query($args)->fetchPairs();
}
public static function literal(string $value): Literal
/**
* @return Literal
*/
public static function literal($value)
{
return new Literal($value);
}
public static function expression(...$args): Expression
{
return new Expression(...$args);
}
/********************* misc ****************d*g**/
/**
* Import SQL dump from file.
* @param callable $onProgress function (int $count, ?float $percent): void
* @param string filename
* @return int count of sql commands
*/
public function loadFile(string $file, ?callable $onProgress = null): int
public function loadFile($file)
{
return Helpers::loadFromFile($this, $file, $onProgress);
return Helpers::loadFromFile($this, $file);
}
/**
* Gets a information about the current database.
* @return Reflection\Database
*/
public function getDatabaseInfo(): Reflection\Database
public function getDatabaseInfo()
{
if (!$this->driver) {
$this->connect();
}
return new Reflection\Database($this->driver->getReflector(), $this->config['database'] ?? null);
$this->connected || $this->connect();
return new Reflection\Database($this->driver->getReflector(), isset($this->config['database']) ? $this->config['database'] : NULL);
}
@@ -675,7 +643,7 @@ class Connection implements IConnection
*/
public function __wakeup()
{
throw new NotSupportedException('You cannot serialize or unserialize ' . static::class . ' instances.');
throw new NotSupportedException('You cannot serialize or unserialize ' . get_class($this) . ' instances.');
}
@@ -684,14 +652,15 @@ class Connection implements IConnection
*/
public function __sleep()
{
throw new NotSupportedException('You cannot serialize or unserialize ' . static::class . ' instances.');
throw new NotSupportedException('You cannot serialize or unserialize ' . get_class($this) . ' instances.');
}
protected function onEvent($arg): void
protected function onEvent($arg)
{
foreach ($this->onEvent as $handler) {
$handler($arg);
foreach ($this->onEvent ?: [] as $handler) {
call_user_func($handler, $arg);
}
}
}

View File

@@ -1,105 +1,141 @@
<?php
/**
* This file is part of the Dibi, smart database abstraction layer (https://dibiphp.com)
* This file is part of the "dibi" - smart database abstraction layer.
* Copyright (c) 2005 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Dibi;
/**
* Default implementation of IDataSource.
* Default implementation of IDataSource for dibi.
*
*/
class DataSource implements IDataSource
{
private Connection $connection;
private string $sql;
private ?Result $result = null;
private ?int $count = null;
private ?int $totalCount = null;
private array $cols = [];
private array $sorting = [];
private array $conds = [];
private ?int $offset = null;
private ?int $limit = null;
use Strict;
/** @var Connection */
private $connection;
/** @var string */
private $sql;
/** @var Result */
private $result;
/** @var int */
private $count;
/** @var int */
private $totalCount;
/** @var array */
private $cols = [];
/** @var array */
private $sorting = [];
/** @var array */
private $conds = [];
/** @var int|NULL */
private $offset;
/** @var int|NULL */
private $limit;
/**
* @param string $sql command or table or view name, as data source
* @param string SQL command or table or view name, as data source
* @param Connection connection
*/
public function __construct(string $sql, Connection $connection)
public function __construct($sql, Connection $connection)
{
$this->sql = strpbrk($sql, " \t\r\n") === false
? $connection->getDriver()->escapeIdentifier($sql) // table name
: '(' . $sql . ') t'; // SQL command
if (strpbrk($sql, " \t\r\n") === FALSE) {
$this->sql = $connection->getDriver()->escapeIdentifier($sql); // table name
} else {
$this->sql = '(' . $sql . ') t'; // SQL command
}
$this->connection = $connection;
}
/**
* Selects columns to query.
* @param string|array $col column name or array of column names
* @param string $as column alias
* @param string|array column name or array of column names
* @param string column alias
* @return self
*/
public function select(string|array $col, ?string $as = null): static
public function select($col, $as = NULL)
{
if (is_array($col)) {
$this->cols = $col;
} else {
$this->cols[$col] = $as;
}
$this->result = null;
$this->result = NULL;
return $this;
}
/**
* Adds conditions to query.
* @param mixed conditions
* @return self
*/
public function where($cond): static
public function where($cond)
{
$this->conds[] = is_array($cond)
? $cond // TODO: not consistent with select and orderBy
: func_get_args();
$this->result = $this->count = null;
if (is_array($cond)) {
// TODO: not consistent with select and orderBy
$this->conds[] = $cond;
} else {
$this->conds[] = func_get_args();
}
$this->result = $this->count = NULL;
return $this;
}
/**
* Selects columns to order by.
* @param string|array $row column name or array of column names
* @param string|array column name or array of column names
* @param string sorting direction
* @return self
*/
public function orderBy(string|array $row, string $direction = 'ASC'): static
public function orderBy($row, $sorting = 'ASC')
{
if (is_array($row)) {
$this->sorting = $row;
} else {
$this->sorting[$row] = $direction;
$this->sorting[$row] = $sorting;
}
$this->result = null;
$this->result = NULL;
return $this;
}
/**
* Limits number of rows.
* @param int|NULL limit
* @param int offset
* @return self
*/
public function applyLimit(int $limit, ?int $offset = null): static
public function applyLimit($limit, $offset = NULL)
{
$this->limit = $limit;
$this->offset = $offset;
$this->result = $this->count = null;
$this->result = $this->count = NULL;
return $this;
}
final public function getConnection(): Connection
/**
* Returns the dibi connection.
* @return Connection
*/
final public function getConnection()
{
return $this->connection;
}
@@ -110,18 +146,21 @@ class DataSource implements IDataSource
/**
* Returns (and queries) Result.
* @return Result
*/
public function getResult(): Result
public function getResult()
{
if ($this->result === null) {
if ($this->result === NULL) {
$this->result = $this->connection->nativeQuery($this->__toString());
}
return $this->result;
}
public function getIterator(): ResultIterator
/**
* @return ResultIterator
*/
public function getIterator()
{
return $this->getResult()->getIterator();
}
@@ -129,8 +168,9 @@ class DataSource implements IDataSource
/**
* Generates, executes SQL query and fetches the single row.
* @return Row|FALSE
*/
public function fetch(): ?Row
public function fetch()
{
return $this->getResult()->fetch();
}
@@ -138,9 +178,9 @@ class DataSource implements IDataSource
/**
* Like fetch(), but returns only first field.
* @return mixed value on success, null if no next record
* @return mixed value on success, FALSE if no next record
*/
public function fetchSingle(): mixed
public function fetchSingle()
{
return $this->getResult()->fetchSingle();
}
@@ -148,8 +188,9 @@ class DataSource implements IDataSource
/**
* Fetches all records from table.
* @return array
*/
public function fetchAll(): array
public function fetchAll()
{
return $this->getResult()->fetchAll();
}
@@ -157,8 +198,10 @@ class DataSource implements IDataSource
/**
* Fetches all records from table and returns associative tree.
* @param string associative descriptor
* @return array
*/
public function fetchAssoc(string $assoc): array
public function fetchAssoc($assoc)
{
return $this->getResult()->fetchAssoc($assoc);
}
@@ -166,8 +209,11 @@ class DataSource implements IDataSource
/**
* Fetches all records from table like $key => $value pairs.
* @param string associative key
* @param string value
* @return array
*/
public function fetchPairs(?string $key = null, ?string $value = null): array
public function fetchPairs($key = NULL, $value = NULL)
{
return $this->getResult()->fetchPairs($key, $value);
}
@@ -175,10 +221,11 @@ class DataSource implements IDataSource
/**
* Discards the internal cache.
* @return void
*/
public function release(): void
public function release()
{
$this->result = $this->count = $this->totalCount = null;
$this->result = $this->count = $this->totalCount = NULL;
}
@@ -187,8 +234,9 @@ class DataSource implements IDataSource
/**
* Returns this data source wrapped in Fluent object.
* @return Fluent
*/
public function toFluent(): Fluent
public function toFluent()
{
return $this->connection->select('*')->from('(%SQL) t', $this->__toString());
}
@@ -196,8 +244,9 @@ class DataSource implements IDataSource
/**
* Returns this data source wrapped in DataSource object.
* @return DataSource
*/
public function toDataSource(): self
public function toDataSource()
{
return new self($this->__toString(), $this->connection);
}
@@ -205,22 +254,21 @@ class DataSource implements IDataSource
/**
* Returns SQL query.
* @return string
*/
public function __toString(): string
public function __toString()
{
return $this->connection->translate(
"\nSELECT %n",
(empty($this->cols) ? '*' : $this->cols),
"\nFROM %SQL",
$this->sql,
"\n%ex",
$this->conds ? ['WHERE %and', $this->conds] : null,
"\n%ex",
$this->sorting ? ['ORDER BY %by', $this->sorting] : null,
"\n%ofs %lmt",
$this->offset,
$this->limit,
);
try {
return $this->connection->translate('
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
);
} catch (\Exception $e) {
trigger_error($e->getMessage(), E_USER_ERROR);
}
}
@@ -229,32 +277,33 @@ class DataSource implements IDataSource
/**
* Returns the number of rows in a given data source.
* @return int
*/
public function count(): int
public function count()
{
if ($this->count === null) {
if ($this->count === NULL) {
$this->count = $this->conds || $this->offset || $this->limit
? Helpers::intVal($this->connection->nativeQuery(
'SELECT COUNT(*) FROM (' . $this->__toString() . ') t',
)->fetchSingle())
? (int) $this->connection->nativeQuery(
'SELECT COUNT(*) FROM (' . $this->__toString() . ') t'
)->fetchSingle()
: $this->getTotalCount();
}
return $this->count;
}
/**
* Returns the number of rows in a given data source.
* @return int
*/
public function getTotalCount(): int
public function getTotalCount()
{
if ($this->totalCount === null) {
$this->totalCount = Helpers::intVal($this->connection->nativeQuery(
'SELECT COUNT(*) FROM ' . $this->sql,
)->fetchSingle());
if ($this->totalCount === NULL) {
$this->totalCount = (int) $this->connection->nativeQuery(
'SELECT COUNT(*) FROM ' . $this->sql
)->fetchSingle();
}
return $this->totalCount;
}
}

View File

@@ -1,34 +1,75 @@
<?php
/**
* This file is part of the Dibi, smart database abstraction layer (https://dibiphp.com)
* This file is part of the "dibi" - smart database abstraction layer.
* Copyright (c) 2005 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Dibi;
/**
* DateTime.
*/
class DateTime extends \DateTimeImmutable
class DateTime extends \DateTime
{
public function __construct(string|int $time = 'now', ?\DateTimeZone $timezone = null)
use Strict;
/**
* @param string|int
*/
public function __construct($time = 'now', \DateTimeZone $timezone = NULL)
{
$timezone = $timezone ?: new \DateTimeZone(date_default_timezone_get());
if (is_numeric($time)) {
$tmp = (new self('@' . $time))->setTimezone($timezone);
parent::__construct($tmp->format('Y-m-d H:i:s.u'), $tmp->getTimezone());
parent::__construct('@' . $time);
$this->setTimeZone($timezone ? $timezone : new \DateTimeZone(date_default_timezone_get()));
} elseif ($timezone === NULL) {
parent::__construct($time);
} else {
parent::__construct($time, $timezone);
}
}
public function __toString(): string
public function modifyClone($modify = '')
{
return $this->format('Y-m-d H:i:s.u');
$dolly = clone($this);
return $modify ? $dolly->modify($modify) : $dolly;
}
public function setTimestamp($timestamp)
{
$zone = $this->getTimezone();
$this->__construct('@' . $timestamp);
return $this->setTimeZone($zone);
}
public function getTimestamp()
{
$ts = $this->format('U');
return is_float($tmp = $ts * 1) ? $ts : $tmp;
}
public function __toString()
{
return $this->format('Y-m-d H:i:s');
}
public function __wakeup()
{
if (isset($this->fix, $this->fix[1])) {
$this->__construct($this->fix[0], new \DateTimeZone($this->fix[1]));
unset($this->fix);
} elseif (isset($this->fix)) {
$this->__construct($this->fix[0]);
unset($this->fix);
} else {
parent::__wakeup();
}
}
}

View File

@@ -1,218 +0,0 @@
<?php
/**
* This file is part of the Dibi, smart database abstraction layer (https://dibiphp.com)
* Copyright (c) 2005 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Dibi\Drivers;
use Dibi;
/**
* The dummy driver for testing purposes.
*/
class DummyDriver implements Dibi\Driver, Dibi\ResultDriver, Dibi\Reflector
{
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(): mixed
{
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(): mixed
{
return null;
}
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

@@ -1,20 +1,17 @@
<?php
/**
* This file is part of the Dibi, smart database abstraction layer (https://dibiphp.com)
* This file is part of the "dibi" - smart database abstraction layer.
* Copyright (c) 2005 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Dibi\Drivers;
use Dibi;
use Dibi\Helpers;
/**
* The driver for Firebird/InterBase database.
* The dibi driver for Firebird/InterBase database.
*
* Driver options:
* - database => the path to database file (server:/path/database.fdb)
@@ -23,30 +20,49 @@ use Dibi\Helpers;
* - charset => character encoding to set
* - buffers (int) => buffers is the number of database buffers to allocate for the server-side cache. If 0 or omitted, server chooses its own default.
* - resource (resource) => existing connection resource
* - lazy, profiler, result, substitutes, ... => see Dibi\Connection options
*/
class FirebirdDriver implements Dibi\Driver
class FirebirdDriver implements Dibi\Driver, Dibi\ResultDriver, Dibi\Reflector
{
public const ErrorExceptionThrown = -836;
use Dibi\Strict;
/** @deprecated use FirebirdDriver::ErrorExceptionThrown */
public const ERROR_EXCEPTION_THROWN = self::ErrorExceptionThrown;
const ERROR_EXCEPTION_THROWN = -836;
/** @var resource */
/** @var resource|NULL */
private $connection;
/** @var ?resource */
/** @var resource|NULL */
private $resultSet;
/** @var bool */
private $autoFree = TRUE;
/** @var resource|NULL */
private $transaction;
private bool $inTransaction = false;
/** @var bool */
private $inTransaction = FALSE;
/** @throws Dibi\NotSupportedException */
public function __construct(array $config)
/**
* @throws Dibi\NotSupportedException
*/
public function __construct()
{
if (!extension_loaded('interbase')) {
throw new Dibi\NotSupportedException("PHP extension 'interbase' is not loaded.");
}
}
Helpers::alias($config, 'database', 'db');
/**
* Connects to a database.
* @return void
* @throws Dibi\Exception
*/
public function connect(array &$config)
{
Dibi\Helpers::alias($config, 'database', 'db');
if (isset($config['resource'])) {
$this->connection = $config['resource'];
@@ -61,9 +77,11 @@ class FirebirdDriver implements Dibi\Driver
'buffers' => 0,
];
$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 (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 @
}
if (!is_resource($this->connection)) {
throw new Dibi\DriverException(ibase_errmsg(), ibase_errcode());
@@ -74,8 +92,9 @@ class FirebirdDriver implements Dibi\Driver
/**
* Disconnects from a database.
* @return void
*/
public function disconnect(): void
public function disconnect()
{
@ibase_close($this->connection); // @ - connection can be already disconnected
}
@@ -83,71 +102,77 @@ class FirebirdDriver implements Dibi\Driver
/**
* Executes the SQL query.
* @param string SQL statement.
* @return Dibi\ResultDriver|NULL
* @throws Dibi\DriverException|Dibi\Exception
*/
public function query(string $sql): ?Dibi\ResultDriver
public function query($sql)
{
$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 ($res === FALSE) {
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;
return NULL;
}
/**
* Gets the number of affected rows by the last INSERT, UPDATE or DELETE query.
* @return int|FALSE number of rows or FALSE on error
*/
public function getAffectedRows(): ?int
public function getAffectedRows()
{
return Helpers::false2Null(ibase_affected_rows($this->connection));
return ibase_affected_rows($this->connection);
}
/**
* Retrieves the ID generated for an AUTO_INCREMENT column by the previous INSERT query.
* @param string generator name
* @return int|FALSE int on success or FALSE on failure
*/
public function getInsertId(?string $sequence): ?int
public function getInsertId($sequence)
{
return Helpers::false2Null(ibase_gen_id($sequence, 0, $this->connection));
return ibase_gen_id($sequence, 0, $this->connection);
}
/**
* Begins a transaction (if supported).
* @param string optional savepoint name
* @return void
* @throws Dibi\DriverException
*/
public function begin(?string $savepoint = null): void
public function begin($savepoint = NULL)
{
if ($savepoint !== null) {
if ($savepoint !== NULL) {
throw new Dibi\NotSupportedException('Savepoints are not supported in Firebird/Interbase.');
}
$this->transaction = ibase_trans($this->getResource());
$this->inTransaction = true;
$this->inTransaction = TRUE;
}
/**
* Commits statements in a transaction.
* @param string optional savepoint name
* @return void
* @throws Dibi\DriverException
*/
public function commit(?string $savepoint = null): void
public function commit($savepoint = NULL)
{
if ($savepoint !== null) {
if ($savepoint !== NULL) {
throw new Dibi\NotSupportedException('Savepoints are not supported in Firebird/Interbase.');
}
@@ -155,17 +180,19 @@ class FirebirdDriver implements Dibi\Driver
throw new Dibi\DriverException('Unable to handle operation - failure when commiting transaction.');
}
$this->inTransaction = false;
$this->inTransaction = FALSE;
}
/**
* Rollback changes in a transaction.
* @param string optional savepoint name
* @return void
* @throws Dibi\DriverException
*/
public function rollback(?string $savepoint = null): void
public function rollback($savepoint = NULL)
{
if ($savepoint !== null) {
if ($savepoint !== NULL) {
throw new Dibi\NotSupportedException('Savepoints are not supported in Firebird/Interbase.');
}
@@ -173,14 +200,15 @@ class FirebirdDriver implements Dibi\Driver
throw new Dibi\DriverException('Unable to handle operation - failure when rolbacking transaction.');
}
$this->inTransaction = false;
$this->inTransaction = FALSE;
}
/**
* Is in transaction?
* @return bool
*/
public function inTransaction(): bool
public function inTransaction()
{
return $this->inTransaction;
}
@@ -188,30 +216,34 @@ class FirebirdDriver implements Dibi\Driver
/**
* Returns the connection resource.
* @return resource|null
* @return resource|NULL
*/
public function getResource(): mixed
public function getResource()
{
return is_resource($this->connection) ? $this->connection : null;
return is_resource($this->connection) ? $this->connection : NULL;
}
/**
* Returns the connection reflector.
* @return Dibi\Reflector
*/
public function getReflector(): Dibi\Reflector
public function getReflector()
{
return new FirebirdReflector($this);
return $this;
}
/**
* Result set driver factory.
* @param resource $resource
* @param resource
* @return Dibi\ResultDriver
*/
public function createResultDriver($resource): FirebirdResult
public function createResultDriver($resource)
{
return new FirebirdResult($resource);
$res = clone $this;
$res->resultSet = $resource;
return $res;
}
@@ -220,67 +252,587 @@ class FirebirdDriver implements Dibi\Driver
/**
* Encodes data for use in a SQL statement.
* @param string
* @return string
*/
public function escapeText(string $value): string
public function escapeText($value)
{
return "'" . str_replace("'", "''", $value) . "'";
}
public function escapeBinary(string $value): string
/**
* @param string
* @return string
*/
public function escapeBinary($value)
{
return "'" . str_replace("'", "''", $value) . "'";
}
public function escapeIdentifier(string $value): string
/**
* @param string
* @return string
*/
public function escapeIdentifier($value)
{
return '"' . str_replace('"', '""', $value) . '"';
return '"' . str_replace('"', '""', $value). '"';
}
public function escapeBool(bool $value): string
/**
* @param bool
* @return string
*/
public function escapeBool($value)
{
return $value ? '1' : '0';
}
public function escapeDate(\DateTimeInterface $value): string
/**
* @param \DateTime|\DateTimeInterface|string|int
* @return string
*/
public function escapeDate($value)
{
if (!$value instanceof \DateTime && !$value instanceof \DateTimeInterface) {
$value = new Dibi\DateTime($value);
}
return $value->format("'Y-m-d'");
}
public function escapeDateTime(\DateTimeInterface $value): string
/**
* @param \DateTime|\DateTimeInterface|string|int
* @return string
*/
public function escapeDateTime($value)
{
return "'" . substr($value->format('Y-m-d H:i:s.u'), 0, -2) . "'";
if (!$value instanceof \DateTime && !$value instanceof \DateTimeInterface) {
$value = new Dibi\DateTime($value);
}
return $value->format("'Y-m-d H:i:s'");
}
public function escapeDateInterval(\DateInterval $value): string
/**
* Encodes string for use in a LIKE statement.
* @param string
* @param int
* @return string
*/
public function escapeLike($value, $pos)
{
throw new Dibi\NotImplementedException;
}
/**
* Encodes string for use in a LIKE statement.
* Decodes data from result set.
* @param string
* @return string
*/
public function escapeLike(string $value, int $pos): string
public function unescapeBinary($value)
{
$value = addcslashes($this->escapeText($value), '%_\\');
return ($pos & 1 ? "'%" : "'") . $value . ($pos & 2 ? "%'" : "'") . " ESCAPE '\\'";
return $value;
}
/** @deprecated */
public function escape($value, $type)
{
trigger_error(__METHOD__ . '() is deprecated.', E_USER_DEPRECATED);
return Dibi\Helpers::escape($this, $value, $type);
}
/**
* Injects LIMIT/OFFSET to the SQL query.
* @param string
* @param int|NULL
* @param int|NULL
* @return void
*/
public function applyLimit(string &$sql, ?int $limit, ?int $offset): void
public function applyLimit(&$sql, $limit, $offset)
{
if ($limit > 0 || $offset > 0) {
// http://www.firebirdsql.org/refdocs/langrefupd20-select.html
$sql = 'SELECT ' . ($limit > 0 ? 'FIRST ' . $limit : '') . ($offset > 0 ? ' SKIP ' . $offset : '') . ' * FROM (' . $sql . ')';
$sql = 'SELECT ' . ($limit > 0 ? 'FIRST ' . (int) $limit : '') . ($offset > 0 ? ' SKIP ' . (int) $offset : '') . ' * FROM (' . $sql . ')';
}
}
/********************* result set ********************/
/**
* Automatically frees the resources allocated for this result set.
* @return void
*/
public function __destruct()
{
$this->autoFree && $this->getResultResource() && $this->free();
}
/**
* Returns the number of rows in a result set.
* @return int
*/
public function getRowCount()
{
throw new Dibi\NotSupportedException('Firebird/Interbase do not support returning number of rows in result set.');
}
/**
* Fetches the row at current position and moves the internal cursor to the next position.
* @param bool TRUE for associative array, FALSE for numeric
* @return array array on success, nonarray if no next record
*/
public function fetch($assoc)
{
$result = $assoc ? @ibase_fetch_assoc($this->resultSet, IBASE_TEXT) : @ibase_fetch_row($this->resultSet, IBASE_TEXT); // intentionally @
if (ibase_errcode()) {
if (ibase_errcode() == self::ERROR_EXCEPTION_THROWN) {
preg_match('/exception (\d+) (\w+) (.*)/is', ibase_errmsg(), $match);
throw new Dibi\ProcedureException($match[3], $match[1], $match[2]);
} else {
throw new Dibi\DriverException(ibase_errmsg(), ibase_errcode());
}
}
return $result;
}
/**
* Moves cursor position without fetching row.
* @param int the 0-based cursor pos to seek to
* @return bool TRUE on success, FALSE if unable to seek to specified record
* @throws Dibi\Exception
*/
public function seek($row)
{
throw new Dibi\NotSupportedException('Firebird/Interbase do not support seek in result set.');
}
/**
* Frees the resources allocated for this result set.
* @return void
*/
public function free()
{
ibase_free_result($this->resultSet);
$this->resultSet = NULL;
}
/**
* Returns the result set resource.
* @return resource|NULL
*/
public function getResultResource()
{
$this->autoFree = FALSE;
return is_resource($this->resultSet) ? $this->resultSet : NULL;
}
/**
* Returns metadata for all columns in a result set.
* @return array
*/
public function getResultColumns()
{
$count = ibase_num_fields($this->resultSet);
$columns = [];
for ($i = 0; $i < $count; $i++) {
$row = (array) ibase_field_info($this->resultSet, $i);
$columns[] = [
'name' => $row['name'],
'fullname' => $row['name'],
'table' => $row['relation'],
'nativetype' => $row['type'],
];
}
return $columns;
}
/********************* Dibi\Reflector ********************/
/**
* Returns list of tables.
* @return array
*/
public function getTables()
{
$res = $this->query("
SELECT TRIM(RDB\$RELATION_NAME),
CASE RDB\$VIEW_BLR WHEN NULL THEN 'TRUE' ELSE 'FALSE' END
FROM RDB\$RELATIONS
WHERE RDB\$SYSTEM_FLAG = 0;"
);
$tables = [];
while ($row = $res->fetch(FALSE)) {
$tables[] = [
'name' => $row[0],
'view' => $row[1] === 'TRUE',
];
}
return $tables;
}
/**
* Returns metadata for all columns in a table.
* @param string
* @return array
*/
public function getColumns($table)
{
$table = strtoupper($table);
$res = $this->query("
SELECT TRIM(r.RDB\$FIELD_NAME) AS FIELD_NAME,
CASE f.RDB\$FIELD_TYPE
WHEN 261 THEN 'BLOB'
WHEN 14 THEN 'CHAR'
WHEN 40 THEN 'CSTRING'
WHEN 11 THEN 'D_FLOAT'
WHEN 27 THEN 'DOUBLE'
WHEN 10 THEN 'FLOAT'
WHEN 16 THEN 'INT64'
WHEN 8 THEN 'INTEGER'
WHEN 9 THEN 'QUAD'
WHEN 7 THEN 'SMALLINT'
WHEN 12 THEN 'DATE'
WHEN 13 THEN 'TIME'
WHEN 35 THEN 'TIMESTAMP'
WHEN 37 THEN 'VARCHAR'
ELSE 'UNKNOWN'
END AS FIELD_TYPE,
f.RDB\$FIELD_LENGTH AS FIELD_LENGTH,
r.RDB\$DEFAULT_VALUE AS DEFAULT_VALUE,
CASE r.RDB\$NULL_FLAG
WHEN 1 THEN 'FALSE' ELSE 'TRUE'
END AS NULLABLE
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;"
);
$columns = [];
while ($row = $res->fetch(TRUE)) {
$key = $row['FIELD_NAME'];
$columns[$key] = [
'name' => $key,
'table' => $table,
'nativetype' => trim($row['FIELD_TYPE']),
'size' => $row['FIELD_LENGTH'],
'nullable' => $row['NULLABLE'] === 'TRUE',
'default' => $row['DEFAULT_VALUE'],
'autoincrement' => FALSE,
];
}
return $columns;
}
/**
* Returns metadata for all indexes in a table (the constraints are included).
* @param string
* @return array
*/
public function getIndexes($table)
{
$table = strtoupper($table);
$res = $this->query("
SELECT TRIM(s.RDB\$INDEX_NAME) AS INDEX_NAME,
TRIM(s.RDB\$FIELD_NAME) AS FIELD_NAME,
i.RDB\$UNIQUE_FLAG AS UNIQUE_FLAG,
i.RDB\$FOREIGN_KEY AS FOREIGN_KEY,
TRIM(r.RDB\$CONSTRAINT_TYPE) AS CONSTRAINT_TYPE,
s.RDB\$FIELD_POSITION AS FIELD_POSITION
FROM RDB\$INDEX_SEGMENTS s
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"
);
$indexes = [];
while ($row = $res->fetch(TRUE)) {
$key = $row['INDEX_NAME'];
$indexes[$key]['name'] = $key;
$indexes[$key]['unique'] = $row['UNIQUE_FLAG'] === 1;
$indexes[$key]['primary'] = $row['CONSTRAINT_TYPE'] === 'PRIMARY KEY';
$indexes[$key]['table'] = $table;
$indexes[$key]['columns'][$row['FIELD_POSITION']] = $row['FIELD_NAME'];
}
return $indexes;
}
/**
* Returns metadata for all foreign keys in a table.
* @param string
* @return array
*/
public function getForeignKeys($table)
{
$table = strtoupper($table);
$res = $this->query("
SELECT TRIM(s.RDB\$INDEX_NAME) AS INDEX_NAME,
TRIM(s.RDB\$FIELD_NAME) AS FIELD_NAME,
FROM RDB\$INDEX_SEGMENTS s
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"
);
$keys = [];
while ($row = $res->fetch(TRUE)) {
$key = $row['INDEX_NAME'];
$keys[$key] = [
'name' => $key,
'column' => $row['FIELD_NAME'],
'table' => $table,
];
}
return $keys;
}
/**
* Returns list of indices in given table (the constraints are not listed).
* @param string
* @return array
*/
public function getIndices($table)
{
$res = $this->query("
SELECT TRIM(RDB\$INDEX_NAME)
FROM RDB\$INDICES
WHERE RDB\$RELATION_NAME = UPPER('$table')
AND RDB\$UNIQUE_FLAG IS NULL
AND RDB\$FOREIGN_KEY IS NULL;"
);
$indices = [];
while ($row = $res->fetch(FALSE)) {
$indices[] = $row[0];
}
return $indices;
}
/**
* Returns list of constraints in given table.
* @param string
* @return array
*/
public function getConstraints($table)
{
$res = $this->query("
SELECT TRIM(RDB\$INDEX_NAME)
FROM RDB\$INDICES
WHERE RDB\$RELATION_NAME = UPPER('$table')
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;
}
/**
* Returns metadata for all triggers in a table or database.
* (Only if user has permissions on ALTER TABLE, INSERT/UPDATE/DELETE record in table)
* @param string
* @param string
* @return array
*/
public function getTriggersMeta($table = NULL)
{
$res = $this->query("
SELECT TRIM(RDB\$TRIGGER_NAME) AS TRIGGER_NAME,
TRIM(RDB\$RELATION_NAME) AS TABLE_NAME,
CASE RDB\$TRIGGER_TYPE
WHEN 1 THEN 'BEFORE'
WHEN 2 THEN 'AFTER'
WHEN 3 THEN 'BEFORE'
WHEN 4 THEN 'AFTER'
WHEN 5 THEN 'BEFORE'
WHEN 6 THEN 'AFTER'
END AS TRIGGER_TYPE,
CASE RDB\$TRIGGER_TYPE
WHEN 1 THEN 'INSERT'
WHEN 2 THEN 'INSERT'
WHEN 3 THEN 'UPDATE'
WHEN 4 THEN 'UPDATE'
WHEN 5 THEN 'DELETE'
WHEN 6 THEN 'DELETE'
END AS TRIGGER_EVENT,
CASE RDB\$TRIGGER_INACTIVE
WHEN 1 THEN 'FALSE' ELSE 'TRUE'
END AS TRIGGER_ENABLED
FROM RDB\$TRIGGERS
WHERE RDB\$SYSTEM_FLAG = 0"
. ($table === NULL ? ';' : " AND RDB\$RELATION_NAME = UPPER('$table');")
);
$triggers = [];
while ($row = $res->fetch(TRUE)) {
$triggers[$row['TRIGGER_NAME']] = [
'name' => $row['TRIGGER_NAME'],
'table' => $row['TABLE_NAME'],
'type' => trim($row['TRIGGER_TYPE']),
'event' => trim($row['TRIGGER_EVENT']),
'enabled' => trim($row['TRIGGER_ENABLED']) === 'TRUE',
];
}
return $triggers;
}
/**
* Returns list of triggers for given table.
* (Only if user has permissions on ALTER TABLE, INSERT/UPDATE/DELETE record in table)
* @param string
* @return array
*/
public function getTriggers($table = NULL)
{
$q = "SELECT TRIM(RDB\$TRIGGER_NAME)
FROM RDB\$TRIGGERS
WHERE RDB\$SYSTEM_FLAG = 0";
$q .= $table === NULL ? ';' : " AND RDB\$RELATION_NAME = UPPER('$table')";
$res = $this->query($q);
$triggers = [];
while ($row = $res->fetch(FALSE)) {
$triggers[] = $row[0];
}
return $triggers;
}
/**
* Returns metadata from stored procedures and their input and output parameters.
* @param string
* @return array
*/
public function getProceduresMeta()
{
$res = $this->query("
SELECT
TRIM(p.RDB\$PARAMETER_NAME) AS PARAMETER_NAME,
TRIM(p.RDB\$PROCEDURE_NAME) AS PROCEDURE_NAME,
CASE p.RDB\$PARAMETER_TYPE
WHEN 0 THEN 'INPUT'
WHEN 1 THEN 'OUTPUT'
ELSE 'UNKNOWN'
END AS PARAMETER_TYPE,
CASE f.RDB\$FIELD_TYPE
WHEN 261 THEN 'BLOB'
WHEN 14 THEN 'CHAR'
WHEN 40 THEN 'CSTRING'
WHEN 11 THEN 'D_FLOAT'
WHEN 27 THEN 'DOUBLE'
WHEN 10 THEN 'FLOAT'
WHEN 16 THEN 'INT64'
WHEN 8 THEN 'INTEGER'
WHEN 9 THEN 'QUAD'
WHEN 7 THEN 'SMALLINT'
WHEN 12 THEN 'DATE'
WHEN 13 THEN 'TIME'
WHEN 35 THEN 'TIMESTAMP'
WHEN 37 THEN 'VARCHAR'
ELSE 'UNKNOWN'
END AS FIELD_TYPE,
f.RDB\$FIELD_LENGTH AS FIELD_LENGTH,
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;"
);
$procedures = [];
while ($row = $res->fetch(TRUE)) {
$key = $row['PROCEDURE_NAME'];
$io = trim($row['PARAMETER_TYPE']);
$num = $row['PARAMETER_NUMBER'];
$procedures[$key]['name'] = $row['PROCEDURE_NAME'];
$procedures[$key]['params'][$io][$num]['name'] = $row['PARAMETER_NAME'];
$procedures[$key]['params'][$io][$num]['type'] = trim($row['FIELD_TYPE']);
$procedures[$key]['params'][$io][$num]['size'] = $row['FIELD_LENGTH'];
}
return $procedures;
}
/**
* Returns list of stored procedures.
* @return array
*/
public function getProcedures()
{
$res = $this->query("
SELECT TRIM(RDB\$PROCEDURE_NAME)
FROM RDB\$PROCEDURES;"
);
$procedures = [];
while ($row = $res->fetch(FALSE)) {
$procedures[] = $row[0];
}
return $procedures;
}
/**
* Returns list of generators.
* @return array
*/
public function getGenerators()
{
$res = $this->query("
SELECT TRIM(RDB\$GENERATOR_NAME)
FROM RDB\$GENERATORS
WHERE RDB\$SYSTEM_FLAG = 0;"
);
$generators = [];
while ($row = $res->fetch(FALSE)) {
$generators[] = $row[0];
}
return $generators;
}
/**
* Returns list of user defined functions (UDF).
* @return array
*/
public function getFunctions()
{
$res = $this->query("
SELECT TRIM(RDB\$FUNCTION_NAME)
FROM RDB\$FUNCTIONS
WHERE RDB\$SYSTEM_FLAG = 0;"
);
$functions = [];
while ($row = $res->fetch(FALSE)) {
$functions[] = $row[0];
}
return $functions;
}
}

View File

@@ -1,385 +0,0 @@
<?php
/**
* This file is part of the Dibi, smart database abstraction layer (https://dibiphp.com)
* Copyright (c) 2005 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Dibi\Drivers;
use Dibi;
/**
* The reflector for Firebird/InterBase database.
*/
class FirebirdReflector implements Dibi\Reflector
{
public function __construct(
private Dibi\Driver $driver,
) {
}
/**
* Returns list of tables.
*/
public function getTables(): array
{
$res = $this->driver->query("
SELECT TRIM(RDB\$RELATION_NAME),
CASE RDB\$VIEW_BLR WHEN NULL THEN 'TRUE' ELSE 'FALSE' END
FROM RDB\$RELATIONS
WHERE RDB\$SYSTEM_FLAG = 0;
");
$tables = [];
while ($row = $res->fetch(false)) {
$tables[] = [
'name' => $row[0],
'view' => $row[1] === 'TRUE',
];
}
return $tables;
}
/**
* Returns metadata for all columns in a table.
*/
public function getColumns(string $table): array
{
$table = strtoupper($table);
$res = $this->driver->query("
SELECT TRIM(r.RDB\$FIELD_NAME) AS FIELD_NAME,
CASE f.RDB\$FIELD_TYPE
WHEN 261 THEN 'BLOB'
WHEN 14 THEN 'CHAR'
WHEN 40 THEN 'CSTRING'
WHEN 11 THEN 'D_FLOAT'
WHEN 27 THEN 'DOUBLE'
WHEN 10 THEN 'FLOAT'
WHEN 16 THEN 'INT64'
WHEN 8 THEN 'INTEGER'
WHEN 9 THEN 'QUAD'
WHEN 7 THEN 'SMALLINT'
WHEN 12 THEN 'DATE'
WHEN 13 THEN 'TIME'
WHEN 35 THEN 'TIMESTAMP'
WHEN 37 THEN 'VARCHAR'
ELSE 'UNKNOWN'
END AS FIELD_TYPE,
f.RDB\$FIELD_LENGTH AS FIELD_LENGTH,
r.RDB\$DEFAULT_VALUE AS DEFAULT_VALUE,
CASE r.RDB\$NULL_FLAG
WHEN 1 THEN 'FALSE' ELSE 'TRUE'
END AS NULLABLE
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;
");
$columns = [];
while ($row = $res->fetch(true)) {
$key = $row['FIELD_NAME'];
$columns[$key] = [
'name' => $key,
'table' => $table,
'nativetype' => trim($row['FIELD_TYPE']),
'size' => $row['FIELD_LENGTH'],
'nullable' => $row['NULLABLE'] === 'TRUE',
'default' => $row['DEFAULT_VALUE'],
'autoincrement' => false,
];
}
return $columns;
}
/**
* Returns metadata for all indexes in a table (the constraints are included).
*/
public function getIndexes(string $table): array
{
$table = strtoupper($table);
$res = $this->driver->query("
SELECT TRIM(s.RDB\$INDEX_NAME) AS INDEX_NAME,
TRIM(s.RDB\$FIELD_NAME) AS FIELD_NAME,
i.RDB\$UNIQUE_FLAG AS UNIQUE_FLAG,
i.RDB\$FOREIGN_KEY AS FOREIGN_KEY,
TRIM(r.RDB\$CONSTRAINT_TYPE) AS CONSTRAINT_TYPE,
s.RDB\$FIELD_POSITION AS FIELD_POSITION
FROM RDB\$INDEX_SEGMENTS s
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
");
$indexes = [];
while ($row = $res->fetch(true)) {
$key = $row['INDEX_NAME'];
$indexes[$key]['name'] = $key;
$indexes[$key]['unique'] = $row['UNIQUE_FLAG'] === 1;
$indexes[$key]['primary'] = $row['CONSTRAINT_TYPE'] === 'PRIMARY KEY';
$indexes[$key]['table'] = $table;
$indexes[$key]['columns'][$row['FIELD_POSITION']] = $row['FIELD_NAME'];
}
return $indexes;
}
/**
* Returns metadata for all foreign keys in a table.
*/
public function getForeignKeys(string $table): array
{
$table = strtoupper($table);
$res = $this->driver->query("
SELECT TRIM(s.RDB\$INDEX_NAME) AS INDEX_NAME,
TRIM(s.RDB\$FIELD_NAME) AS FIELD_NAME,
FROM RDB\$INDEX_SEGMENTS s
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
");
$keys = [];
while ($row = $res->fetch(true)) {
$key = $row['INDEX_NAME'];
$keys[$key] = [
'name' => $key,
'column' => $row['FIELD_NAME'],
'table' => $table,
];
}
return $keys;
}
/**
* Returns list of indices in given table (the constraints are not listed).
*/
public function getIndices(string $table): array
{
$res = $this->driver->query("
SELECT TRIM(RDB\$INDEX_NAME)
FROM RDB\$INDICES
WHERE RDB\$RELATION_NAME = UPPER('$table')
AND RDB\$UNIQUE_FLAG IS NULL
AND RDB\$FOREIGN_KEY IS NULL;
");
$indices = [];
while ($row = $res->fetch(false)) {
$indices[] = $row[0];
}
return $indices;
}
/**
* Returns list of constraints in given table.
*/
public function getConstraints(string $table): array
{
$res = $this->driver->query("
SELECT TRIM(RDB\$INDEX_NAME)
FROM RDB\$INDICES
WHERE RDB\$RELATION_NAME = UPPER('$table')
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;
}
/**
* 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
{
$res = $this->driver->query(
"
SELECT TRIM(RDB\$TRIGGER_NAME) AS TRIGGER_NAME,
TRIM(RDB\$RELATION_NAME) AS TABLE_NAME,
CASE RDB\$TRIGGER_TYPE
WHEN 1 THEN 'BEFORE'
WHEN 2 THEN 'AFTER'
WHEN 3 THEN 'BEFORE'
WHEN 4 THEN 'AFTER'
WHEN 5 THEN 'BEFORE'
WHEN 6 THEN 'AFTER'
END AS TRIGGER_TYPE,
CASE RDB\$TRIGGER_TYPE
WHEN 1 THEN 'INSERT'
WHEN 2 THEN 'INSERT'
WHEN 3 THEN 'UPDATE'
WHEN 4 THEN 'UPDATE'
WHEN 5 THEN 'DELETE'
WHEN 6 THEN 'DELETE'
END AS TRIGGER_EVENT,
CASE RDB\$TRIGGER_INACTIVE
WHEN 1 THEN 'FALSE' ELSE 'TRUE'
END AS TRIGGER_ENABLED
FROM RDB\$TRIGGERS
WHERE RDB\$SYSTEM_FLAG = 0"
. ($table === null ? ';' : " AND RDB\$RELATION_NAME = UPPER('$table');"),
);
$triggers = [];
while ($row = $res->fetch(true)) {
$triggers[$row['TRIGGER_NAME']] = [
'name' => $row['TRIGGER_NAME'],
'table' => $row['TABLE_NAME'],
'type' => trim($row['TRIGGER_TYPE']),
'event' => trim($row['TRIGGER_EVENT']),
'enabled' => trim($row['TRIGGER_ENABLED']) === 'TRUE',
];
}
return $triggers;
}
/**
* 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
{
$q = 'SELECT TRIM(RDB$TRIGGER_NAME)
FROM RDB$TRIGGERS
WHERE RDB$SYSTEM_FLAG = 0';
$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;
}
/**
* Returns metadata from stored procedures and their input and output parameters.
*/
public function getProceduresMeta(): array
{
$res = $this->driver->query("
SELECT
TRIM(p.RDB\$PARAMETER_NAME) AS PARAMETER_NAME,
TRIM(p.RDB\$PROCEDURE_NAME) AS PROCEDURE_NAME,
CASE p.RDB\$PARAMETER_TYPE
WHEN 0 THEN 'INPUT'
WHEN 1 THEN 'OUTPUT'
ELSE 'UNKNOWN'
END AS PARAMETER_TYPE,
CASE f.RDB\$FIELD_TYPE
WHEN 261 THEN 'BLOB'
WHEN 14 THEN 'CHAR'
WHEN 40 THEN 'CSTRING'
WHEN 11 THEN 'D_FLOAT'
WHEN 27 THEN 'DOUBLE'
WHEN 10 THEN 'FLOAT'
WHEN 16 THEN 'INT64'
WHEN 8 THEN 'INTEGER'
WHEN 9 THEN 'QUAD'
WHEN 7 THEN 'SMALLINT'
WHEN 12 THEN 'DATE'
WHEN 13 THEN 'TIME'
WHEN 35 THEN 'TIMESTAMP'
WHEN 37 THEN 'VARCHAR'
ELSE 'UNKNOWN'
END AS FIELD_TYPE,
f.RDB\$FIELD_LENGTH AS FIELD_LENGTH,
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;
");
$procedures = [];
while ($row = $res->fetch(true)) {
$key = $row['PROCEDURE_NAME'];
$io = trim($row['PARAMETER_TYPE']);
$num = $row['PARAMETER_NUMBER'];
$procedures[$key]['name'] = $row['PROCEDURE_NAME'];
$procedures[$key]['params'][$io][$num]['name'] = $row['PARAMETER_NAME'];
$procedures[$key]['params'][$io][$num]['type'] = trim($row['FIELD_TYPE']);
$procedures[$key]['params'][$io][$num]['size'] = $row['FIELD_LENGTH'];
}
return $procedures;
}
/**
* Returns list of stored procedures.
*/
public function getProcedures(): array
{
$res = $this->driver->query('
SELECT TRIM(RDB$PROCEDURE_NAME)
FROM RDB$PROCEDURES;
');
$procedures = [];
while ($row = $res->fetch(false)) {
$procedures[] = $row[0];
}
return $procedures;
}
/**
* Returns list of generators.
*/
public function getGenerators(): array
{
$res = $this->driver->query('
SELECT TRIM(RDB$GENERATOR_NAME)
FROM RDB$GENERATORS
WHERE RDB$SYSTEM_FLAG = 0;
');
$generators = [];
while ($row = $res->fetch(false)) {
$generators[] = $row[0];
}
return $generators;
}
/**
* Returns list of user defined functions (UDF).
*/
public function getFunctions(): array
{
$res = $this->driver->query('
SELECT TRIM(RDB$FUNCTION_NAME)
FROM RDB$FUNCTIONS
WHERE RDB$SYSTEM_FLAG = 0;
');
$functions = [];
while ($row = $res->fetch(false)) {
$functions[] = $row[0];
}
return $functions;
}
}

View File

@@ -1,118 +0,0 @@
<?php
/**
* This file is part of the Dibi, smart database abstraction layer (https://dibiphp.com)
* Copyright (c) 2005 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Dibi\Drivers;
use Dibi;
use Dibi\Helpers;
/**
* The driver for Firebird/InterBase result set.
*/
class FirebirdResult implements Dibi\ResultDriver
{
public function __construct(
/** @var resource */
private $resultSet,
) {
}
/**
* Returns the number of rows in a result set.
*/
public function getRowCount(): int
{
throw new Dibi\NotSupportedException('Firebird/Interbase do not support returning number of rows in result set.');
}
/**
* Fetches the row at current position and moves the internal cursor to the next position.
* @param bool $assoc true for associative array, false for numeric
*/
public function fetch(bool $assoc): ?array
{
$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) {
preg_match('/exception (\d+) (\w+) (.*)/is', ibase_errmsg(), $match);
throw new Dibi\ProcedureException($match[3], $match[1], $match[2]);
} else {
throw new Dibi\DriverException(ibase_errmsg(), ibase_errcode());
}
}
return Helpers::false2Null($result);
}
/**
* Moves cursor position without fetching row.
* @throws Dibi\Exception
*/
public function seek(int $row): bool
{
throw new Dibi\NotSupportedException('Firebird/Interbase do not support seek in result set.');
}
/**
* Frees the resources allocated for this result set.
*/
public function free(): void
{
ibase_free_result($this->resultSet);
}
/**
* Returns the result set resource.
* @return resource|null
*/
public function getResultResource(): mixed
{
return is_resource($this->resultSet) ? $this->resultSet : null;
}
/**
* Returns metadata for all columns in a result set.
*/
public function getResultColumns(): array
{
$count = ibase_num_fields($this->resultSet);
$columns = [];
for ($i = 0; $i < $count; $i++) {
$row = (array) ibase_field_info($this->resultSet, $i);
$columns[] = [
'name' => $row['name'],
'fullname' => $row['name'],
'table' => $row['relation'],
'nativetype' => $row['type'],
];
}
return $columns;
}
/**
* Decodes data from result set.
*/
public function unescapeBinary(string $value): string
{
return $value;
}
}

View File

@@ -0,0 +1,410 @@
<?php
/**
* This file is part of the "dibi" - smart database abstraction layer.
* Copyright (c) 2005 David Grudl (https://davidgrudl.com)
*/
namespace Dibi\Drivers;
use Dibi;
/**
* The dibi driver for MS SQL database.
*
* Driver options:
* - host => the MS SQL server host name. It can also include a port number (hostname:port)
* - username (or user)
* - password (or pass)
* - database => the database name to select
* - persistent (bool) => try to find a persistent link?
* - resource (resource) => existing connection resource
* - lazy, profiler, result, substitutes, ... => see Dibi\Connection options
*/
class MsSqlDriver implements Dibi\Driver, Dibi\ResultDriver
{
use Dibi\Strict;
/** @var resource|NULL */
private $connection;
/** @var resource|NULL */
private $resultSet;
/** @var bool */
private $autoFree = TRUE;
/**
* @throws Dibi\NotSupportedException
*/
public function __construct()
{
if (!extension_loaded('mssql')) {
throw new Dibi\NotSupportedException("PHP extension 'mssql' is not loaded.");
}
}
/**
* Connects to a database.
* @return void
* @throws Dibi\Exception
*/
public function connect(array &$config)
{
if (isset($config['resource'])) {
$this->connection = $config['resource'];
} elseif (empty($config['persistent'])) {
$this->connection = @mssql_connect($config['host'], $config['username'], $config['password'], TRUE); // intentionally @
} else {
$this->connection = @mssql_pconnect($config['host'], $config['username'], $config['password']); // intentionally @
}
if (!is_resource($this->connection)) {
throw new Dibi\DriverException("Can't connect to DB.");
}
if (isset($config['database']) && !@mssql_select_db($this->escapeIdentifier($config['database']), $this->connection)) { // intentionally @
throw new Dibi\DriverException("Can't select DB '$config[database]'.");
}
}
/**
* Disconnects from a database.
* @return void
*/
public function disconnect()
{
@mssql_close($this->connection); // @ - connection can be already disconnected
}
/**
* Executes the SQL query.
* @param string SQL statement.
* @return Dibi\ResultDriver|NULL
* @throws Dibi\DriverException
*/
public function query($sql)
{
$res = @mssql_query($sql, $this->connection); // intentionally @
if ($res === FALSE) {
throw new Dibi\DriverException(mssql_get_last_message(), 0, $sql);
} elseif (is_resource($res)) {
return $this->createResultDriver($res);
}
return NULL;
}
/**
* Gets the number of affected rows by the last INSERT, UPDATE or DELETE query.
* @return int|FALSE number of rows or FALSE on error
*/
public function getAffectedRows()
{
return mssql_rows_affected($this->connection);
}
/**
* Retrieves the ID generated for an AUTO_INCREMENT column by the previous INSERT query.
* @return int|FALSE int on success or FALSE on failure
*/
public function getInsertId($sequence)
{
$res = mssql_query('SELECT @@IDENTITY', $this->connection);
if (is_resource($res)) {
$row = mssql_fetch_row($res);
return $row[0];
}
return FALSE;
}
/**
* Begins a transaction (if supported).
* @param string optional savepoint name
* @return void
* @throws Dibi\DriverException
*/
public function begin($savepoint = NULL)
{
$this->query('BEGIN TRANSACTION');
}
/**
* Commits statements in a transaction.
* @param string optional savepoint name
* @return void
* @throws Dibi\DriverException
*/
public function commit($savepoint = NULL)
{
$this->query('COMMIT');
}
/**
* Rollback changes in a transaction.
* @param string optional savepoint name
* @return void
* @throws Dibi\DriverException
*/
public function rollback($savepoint = NULL)
{
$this->query('ROLLBACK');
}
/**
* Returns the connection resource.
* @return resource|NULL
*/
public function getResource()
{
return is_resource($this->connection) ? $this->connection : NULL;
}
/**
* Returns the connection reflector.
* @return Dibi\Reflector
*/
public function getReflector()
{
return new MsSqlReflector($this);
}
/**
* Result set driver factory.
* @param resource
* @return Dibi\ResultDriver
*/
public function createResultDriver($resource)
{
$res = clone $this;
$res->resultSet = $resource;
return $res;
}
/********************* SQL ****************d*g**/
/**
* Encodes data for use in a SQL statement.
* @param string value
* @return string encoded value
*/
public function escapeText($value)
{
return "'" . str_replace("'", "''", $value) . "'";
}
/**
* @param string
* @return string
*/
public function escapeBinary($value)
{
return "'" . str_replace("'", "''", $value) . "'";
}
/**
* @param string
* @return string
*/
public function escapeIdentifier($value)
{
// @see https://msdn.microsoft.com/en-us/library/ms176027.aspx
return '[' . str_replace(['[', ']'], ['[[', ']]'], $value) . ']';
}
/**
* @param bool
* @return string
*/
public function escapeBool($value)
{
return $value ? '1' : '0';
}
/**
* @param \DateTime|\DateTimeInterface|string|int
* @return string
*/
public function escapeDate($value)
{
if (!$value instanceof \DateTime && !$value instanceof \DateTimeInterface) {
$value = new Dibi\DateTime($value);
}
return $value->format("'Y-m-d'");
}
/**
* @param \DateTime|\DateTimeInterface|string|int
* @return string
*/
public function escapeDateTime($value)
{
if (!$value instanceof \DateTime && !$value instanceof \DateTimeInterface) {
$value = new Dibi\DateTime($value);
}
return $value->format("'Y-m-d H:i:s'");
}
/**
* Encodes string for use in a LIKE statement.
* @param string
* @param int
* @return string
*/
public function escapeLike($value, $pos)
{
$value = strtr($value, ["'" => "''", '%' => '[%]', '_' => '[_]', '[' => '[[]']);
return ($pos <= 0 ? "'%" : "'") . $value . ($pos >= 0 ? "%'" : "'");
}
/**
* Decodes data from result set.
* @param string
* @return string
*/
public function unescapeBinary($value)
{
return $value;
}
/** @deprecated */
public function escape($value, $type)
{
trigger_error(__METHOD__ . '() is deprecated.', E_USER_DEPRECATED);
return Dibi\Helpers::escape($this, $value, $type);
}
/**
* Injects LIMIT/OFFSET to the SQL query.
* @param string
* @param int|NULL
* @param int|NULL
* @return void
*/
public function applyLimit(&$sql, $limit, $offset)
{
if ($offset) {
throw new Dibi\NotSupportedException('Offset is not supported by this database.');
} elseif ($limit < 0) {
throw new Dibi\NotSupportedException('Negative offset or limit.');
} elseif ($limit !== NULL) {
$sql = 'SELECT TOP ' . (int) $limit . ' * FROM (' . $sql . ') t';
}
}
/********************* result set ****************d*g**/
/**
* Automatically frees the resources allocated for this result set.
* @return void
*/
public function __destruct()
{
$this->autoFree && $this->getResultResource() && $this->free();
}
/**
* Returns the number of rows in a result set.
* @return int
*/
public function getRowCount()
{
return mssql_num_rows($this->resultSet);
}
/**
* Fetches the row at current position and moves the internal cursor to the next position.
* @param bool TRUE for associative array, FALSE for numeric
* @return array array on success, nonarray if no next record
*/
public function fetch($assoc)
{
return mssql_fetch_array($this->resultSet, $assoc ? MSSQL_ASSOC : MSSQL_NUM);
}
/**
* Moves cursor position without fetching row.
* @param int the 0-based cursor pos to seek to
* @return boolean TRUE on success, FALSE if unable to seek to specified record
*/
public function seek($row)
{
return mssql_data_seek($this->resultSet, $row);
}
/**
* Frees the resources allocated for this result set.
* @return void
*/
public function free()
{
mssql_free_result($this->resultSet);
$this->resultSet = NULL;
}
/**
* Returns metadata for all columns in a result set.
* @return array
*/
public function getResultColumns()
{
$count = mssql_num_fields($this->resultSet);
$columns = [];
for ($i = 0; $i < $count; $i++) {
$row = (array) mssql_fetch_field($this->resultSet, $i);
$columns[] = [
'name' => $row['name'],
'fullname' => $row['column_source'] ? $row['column_source'] . '.' . $row['name'] : $row['name'],
'table' => $row['column_source'],
'nativetype' => $row['type'],
];
}
return $columns;
}
/**
* Returns the result set resource.
* @return resource|NULL
*/
public function getResultResource()
{
$this->autoFree = FALSE;
return is_resource($this->resultSet) ? $this->resultSet : NULL;
}
}

View File

@@ -0,0 +1,216 @@
<?php
/**
* This file is part of the "dibi" - smart database abstraction layer.
* Copyright (c) 2005 David Grudl (https://davidgrudl.com)
*/
namespace Dibi\Drivers;
use Dibi;
/**
* The dibi reflector for MS SQL databases.
* @internal
*/
class MsSqlReflector implements Dibi\Reflector
{
use Dibi\Strict;
/** @var Dibi\Driver */
private $driver;
public function __construct(Dibi\Driver $driver)
{
$this->driver = $driver;
}
/**
* Returns list of tables.
* @return array
*/
public function getTables()
{
$res = $this->driver->query('
SELECT TABLE_NAME, TABLE_TYPE
FROM INFORMATION_SCHEMA.TABLES
');
$tables = [];
while ($row = $res->fetch(FALSE)) {
$tables[] = [
'name' => $row[0],
'view' => isset($row[1]) && $row[1] === 'VIEW',
];
}
return $tables;
}
/**
* Returns count of rows in a table
* @param string
* @return int
*/
public function getTableCount($table, $fallback = TRUE)
{
if (empty($table)) {
return FALSE;
}
$result = $this->driver->query("
SELECT MAX(rowcnt)
FROM sys.sysindexes
WHERE id=OBJECT_ID({$this->driver->escapeIdentifier($table)})
");
$row = $result->fetch(FALSE);
if (!is_array($row) || count($row) < 1) {
if ($fallback) {
$row = $this->driver->query("SELECT COUNT(*) FROM {$this->driver->escapeIdentifier($table)}")->fetch(FALSE);
$count = intval($row[0]);
} else {
$count = FALSE;
}
} else {
$count = intval($row[0]);
}
return $count;
}
/**
* Returns metadata for all columns in a table.
* @param string
* @return array
*/
public function getColumns($table)
{
$res = $this->driver->query("
SELECT * FROM
INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = {$this->driver->escapeText($table)}
ORDER BY TABLE_NAME, ORDINAL_POSITION
");
$columns = [];
while ($row = $res->fetch(TRUE)) {
$size = FALSE;
$type = strtoupper($row['DATA_TYPE']);
$size_cols = [
'DATETIME' => 'DATETIME_PRECISION',
'DECIMAL' => 'NUMERIC_PRECISION',
'CHAR' => 'CHARACTER_MAXIMUM_LENGTH',
'NCHAR' => 'CHARACTER_OCTET_LENGTH',
'NVARCHAR' => 'CHARACTER_OCTET_LENGTH',
'VARCHAR' => 'CHARACTER_OCTET_LENGTH',
];
if (isset($size_cols[$type])) {
if ($size_cols[$type]) {
$size = $row[$size_cols[$type]];
}
}
$columns[] = [
'name' => $row['COLUMN_NAME'],
'table' => $table,
'nativetype' => $type,
'size' => $size,
'unsigned' => NULL,
'nullable' => $row['IS_NULLABLE'] === 'YES',
'default' => $row['COLUMN_DEFAULT'],
'autoincrement' => FALSE,
'vendor' => $row,
];
}
return $columns;
}
/**
* Returns metadata for all indexes in a table.
* @param string
* @return array
*/
public function getIndexes($table)
{
$res = $this->driver->query(
"SELECT ind.name index_name, ind.index_id, ic.index_column_id,
col.name column_name, ind.is_unique, ind.is_primary_key
FROM sys.indexes ind
INNER JOIN sys.index_columns ic ON
(ind.object_id = ic.object_id AND ind.index_id = ic.index_id)
INNER JOIN sys.columns col ON
(ic.object_id = col.object_id and ic.column_id = col.column_id)
INNER JOIN sys.tables t ON
(ind.object_id = t.object_id)
WHERE t.name = {$this->driver->escapeText($table)}
AND t.is_ms_shipped = 0
ORDER BY
t.name, ind.name, ind.index_id, ic.index_column_id
");
$indexes = [];
while ($row = $res->fetch(TRUE)) {
$index_name = $row['index_name'];
if (!isset($indexes[$index_name])) {
$indexes[$index_name] = [];
$indexes[$index_name]['name'] = $index_name;
$indexes[$index_name]['unique'] = (bool) $row['is_unique'];
$indexes[$index_name]['primary'] = (bool) $row['is_primary_key'];
$indexes[$index_name]['columns'] = [];
}
$indexes[$index_name]['columns'][] = $row['column_name'];
}
return array_values($indexes);
}
/**
* Returns metadata for all foreign keys in a table.
* @param string
* @return array
*/
public function getForeignKeys($table)
{
$res = $this->driver->query("
SELECT f.name AS foreign_key,
OBJECT_NAME(f.parent_object_id) AS table_name,
COL_NAME(fc.parent_object_id,
fc.parent_column_id) AS column_name,
OBJECT_NAME (f.referenced_object_id) AS reference_table_name,
COL_NAME(fc.referenced_object_id,
fc.referenced_column_id) AS reference_column_name,
fc.*
FROM sys.foreign_keys AS f
INNER JOIN sys.foreign_key_columns AS fc
ON f.OBJECT_ID = fc.constraint_object_id
WHERE OBJECT_NAME(f.parent_object_id) = {$this->driver->escapeText($table)}
");
$keys = [];
while ($row = $res->fetch(TRUE)) {
$key_name = $row['foreign_key'];
if (!isset($keys[$key_name])) {
$keys[$key_name]['name'] = $row['foreign_key']; // foreign key name
$keys[$key_name]['local'] = [$row['column_name']]; // local columns
$keys[$key_name]['table'] = $row['reference_table_name']; // referenced table
$keys[$key_name]['foreign'] = [$row['reference_column_name']]; // referenced columns
$keys[$key_name]['onDelete'] = FALSE;
$keys[$key_name]['onUpdate'] = FALSE;
} else {
$keys[$key_name]['local'][] = $row['column_name']; // local columns
$keys[$key_name]['foreign'][] = $row['reference_column_name']; // referenced columns
}
}
return array_values($keys);
}
}

View File

@@ -0,0 +1,503 @@
<?php
/**
* This file is part of the "dibi" - smart database abstraction layer.
* Copyright (c) 2005 David Grudl (https://davidgrudl.com)
*/
namespace Dibi\Drivers;
use Dibi;
/**
* The dibi driver for MySQL database.
*
* Driver options:
* - host => the MySQL server host name
* - port (int) => the port number to attempt to connect to the MySQL server
* - socket => the socket or named pipe
* - username (or user)
* - password (or pass)
* - database => the database name to select
* - flags (int) => driver specific constants (MYSQL_CLIENT_*)
* - charset => character encoding to set (default is utf8)
* - persistent (bool) => try to find a persistent link?
* - unbuffered (bool) => sends query without fetching and buffering the result rows automatically?
* - sqlmode => see http://dev.mysql.com/doc/refman/5.0/en/server-sql-mode.html
* - resource (resource) => existing connection resource
* - lazy, profiler, result, substitutes, ... => see Dibi\Connection options
*/
class MySqlDriver implements Dibi\Driver, Dibi\ResultDriver
{
use Dibi\Strict;
const ERROR_ACCESS_DENIED = 1045;
const ERROR_DUPLICATE_ENTRY = 1062;
const ERROR_DATA_TRUNCATED = 1265;
/** @var resource|NULL */
private $connection;
/** @var resource|NULL */
private $resultSet;
/** @var bool */
private $autoFree = TRUE;
/** @var bool Is buffered (seekable and countable)? */
private $buffered;
/**
* @throws Dibi\NotSupportedException
*/
public function __construct()
{
if (!extension_loaded('mysql')) {
throw new Dibi\NotSupportedException("PHP extension 'mysql' is not loaded.");
}
}
/**
* Connects to a database.
* @return void
* @throws Dibi\Exception
*/
public function connect(array &$config)
{
if (isset($config['resource'])) {
$this->connection = $config['resource'];
} else {
// default values
Dibi\Helpers::alias($config, 'flags', 'options');
$config += [
'charset' => 'utf8',
'timezone' => date('P'),
'username' => ini_get('mysql.default_user'),
'password' => ini_get('mysql.default_password'),
];
if (!isset($config['host'])) {
$host = ini_get('mysql.default_host');
if ($host) {
$config['host'] = $host;
$config['port'] = ini_get('mysql.default_port');
} else {
if (!isset($config['socket'])) {
$config['socket'] = ini_get('mysql.default_socket');
}
$config['host'] = NULL;
}
}
if (empty($config['socket'])) {
$host = $config['host'] . (empty($config['port']) ? '' : ':' . $config['port']);
} else {
$host = ':' . $config['socket'];
}
if (empty($config['persistent'])) {
$this->connection = @mysql_connect($host, $config['username'], $config['password'], TRUE, $config['flags']); // intentionally @
} else {
$this->connection = @mysql_pconnect($host, $config['username'], $config['password'], $config['flags']); // intentionally @
}
}
if (!is_resource($this->connection)) {
throw new Dibi\DriverException(mysql_error(), mysql_errno());
}
if (isset($config['charset'])) {
if (!@mysql_set_charset($config['charset'], $this->connection)) { // intentionally @
$this->query("SET NAMES '$config[charset]'");
}
}
if (isset($config['database'])) {
if (!@mysql_select_db($config['database'], $this->connection)) { // intentionally @
throw new Dibi\DriverException(mysql_error($this->connection), mysql_errno($this->connection));
}
}
if (isset($config['sqlmode'])) {
$this->query("SET sql_mode='$config[sqlmode]'");
}
if (isset($config['timezone'])) {
$this->query("SET time_zone='$config[timezone]'");
}
$this->buffered = empty($config['unbuffered']);
}
/**
* Disconnects from a database.
* @return void
*/
public function disconnect()
{
@mysql_close($this->connection); // @ - connection can be already disconnected
}
/**
* Executes the SQL query.
* @param string SQL statement.
* @return Dibi\ResultDriver|NULL
* @throws Dibi\DriverException
*/
public function query($sql)
{
if ($this->buffered) {
$res = @mysql_query($sql, $this->connection); // intentionally @
} else {
$res = @mysql_unbuffered_query($sql, $this->connection); // intentionally @
}
if ($code = mysql_errno($this->connection)) {
throw MySqliDriver::createException(mysql_error($this->connection), $code, $sql);
} elseif (is_resource($res)) {
return $this->createResultDriver($res);
}
}
/**
* Retrieves information about the most recently executed query.
* @return array
*/
public function getInfo()
{
$res = [];
preg_match_all('#(.+?): +(\d+) *#', mysql_info($this->connection), $matches, PREG_SET_ORDER);
if (preg_last_error()) {
throw new Dibi\PcreException;
}
foreach ($matches as $m) {
$res[$m[1]] = (int) $m[2];
}
return $res;
}
/**
* Gets the number of affected rows by the last INSERT, UPDATE or DELETE query.
* @return int|FALSE number of rows or FALSE on error
*/
public function getAffectedRows()
{
return mysql_affected_rows($this->connection);
}
/**
* Retrieves the ID generated for an AUTO_INCREMENT column by the previous INSERT query.
* @return int|FALSE int on success or FALSE on failure
*/
public function getInsertId($sequence)
{
return mysql_insert_id($this->connection);
}
/**
* Begins a transaction (if supported).
* @param string optional savepoint name
* @return void
* @throws Dibi\DriverException
*/
public function begin($savepoint = NULL)
{
$this->query($savepoint ? "SAVEPOINT $savepoint" : 'START TRANSACTION');
}
/**
* Commits statements in a transaction.
* @param string optional savepoint name
* @return void
* @throws Dibi\DriverException
*/
public function commit($savepoint = NULL)
{
$this->query($savepoint ? "RELEASE SAVEPOINT $savepoint" : 'COMMIT');
}
/**
* Rollback changes in a transaction.
* @param string optional savepoint name
* @return void
* @throws Dibi\DriverException
*/
public function rollback($savepoint = NULL)
{
$this->query($savepoint ? "ROLLBACK TO SAVEPOINT $savepoint" : 'ROLLBACK');
}
/**
* Returns the connection resource.
* @return resource|NULL
*/
public function getResource()
{
return is_resource($this->connection) ? $this->connection : NULL;
}
/**
* Returns the connection reflector.
* @return Dibi\Reflector
*/
public function getReflector()
{
return new MySqlReflector($this);
}
/**
* Result set driver factory.
* @param resource
* @return Dibi\ResultDriver
*/
public function createResultDriver($resource)
{
$res = clone $this;
$res->resultSet = $resource;
return $res;
}
/********************* SQL ****************d*g**/
/**
* Encodes data for use in a SQL statement.
* @param string value
* @return string encoded value
*/
public function escapeText($value)
{
if (!is_resource($this->connection)) {
throw new Dibi\Exception('Lost connection to server.');
}
return "'" . mysql_real_escape_string($value, $this->connection) . "'";
}
/**
* @param string
* @return string
*/
public function escapeBinary($value)
{
if (!is_resource($this->connection)) {
throw new Dibi\Exception('Lost connection to server.');
}
return "_binary'" . mysql_real_escape_string($value, $this->connection) . "'";
}
/**
* @param string
* @return string
*/
public function escapeIdentifier($value)
{
// @see http://dev.mysql.com/doc/refman/5.0/en/identifiers.html
return '`' . str_replace('`', '``', $value) . '`';
}
/**
* @param bool
* @return string
*/
public function escapeBool($value)
{
return $value ? 1 : 0;
}
/**
* @param \DateTime|\DateTimeInterface|string|int
* @return string
*/
public function escapeDate($value)
{
if (!$value instanceof \DateTime && !$value instanceof \DateTimeInterface) {
$value = new Dibi\DateTime($value);
}
return $value->format("'Y-m-d'");
}
/**
* @param \DateTime|\DateTimeInterface|string|int
* @return string
*/
public function escapeDateTime($value)
{
if (!$value instanceof \DateTime && !$value instanceof \DateTimeInterface) {
$value = new Dibi\DateTime($value);
}
return $value->format("'Y-m-d H:i:s'");
}
/**
* Encodes string for use in a LIKE statement.
* @param string
* @param int
* @return string
*/
public function escapeLike($value, $pos)
{
$value = addcslashes(str_replace('\\', '\\\\', $value), "\x00\n\r\\'%_");
return ($pos <= 0 ? "'%" : "'") . $value . ($pos >= 0 ? "%'" : "'");
}
/**
* Decodes data from result set.
* @param string
* @return string
*/
public function unescapeBinary($value)
{
return $value;
}
/** @deprecated */
public function escape($value, $type)
{
trigger_error(__METHOD__ . '() is deprecated.', E_USER_DEPRECATED);
return Dibi\Helpers::escape($this, $value, $type);
}
/**
* Injects LIMIT/OFFSET to the SQL query.
* @param string
* @param int|NULL
* @param int|NULL
* @return void
*/
public function applyLimit(&$sql, $limit, $offset)
{
if ($limit < 0 || $offset < 0) {
throw new Dibi\NotSupportedException('Negative offset or limit.');
} elseif ($limit !== NULL || $offset) {
// see http://dev.mysql.com/doc/refman/5.0/en/select.html
$sql .= ' LIMIT ' . ($limit === NULL ? '18446744073709551615' : (int) $limit)
. ($offset ? ' OFFSET ' . (int) $offset : '');
}
}
/********************* result set ****************d*g**/
/**
* Automatically frees the resources allocated for this result set.
* @return void
*/
public function __destruct()
{
$this->autoFree && $this->getResultResource() && $this->free();
}
/**
* Returns the number of rows in a result set.
* @return int
*/
public function getRowCount()
{
if (!$this->buffered) {
throw new Dibi\NotSupportedException('Row count is not available for unbuffered queries.');
}
return mysql_num_rows($this->resultSet);
}
/**
* Fetches the row at current position and moves the internal cursor to the next position.
* @param bool TRUE for associative array, FALSE for numeric
* @return array array on success, nonarray if no next record
*/
public function fetch($assoc)
{
return mysql_fetch_array($this->resultSet, $assoc ? MYSQL_ASSOC : MYSQL_NUM);
}
/**
* Moves cursor position without fetching row.
* @param int the 0-based cursor pos to seek to
* @return bool TRUE on success, FALSE if unable to seek to specified record
* @throws Dibi\Exception
*/
public function seek($row)
{
if (!$this->buffered) {
throw new Dibi\NotSupportedException('Cannot seek an unbuffered result set.');
}
return mysql_data_seek($this->resultSet, $row);
}
/**
* Frees the resources allocated for this result set.
* @return void
*/
public function free()
{
mysql_free_result($this->resultSet);
$this->resultSet = NULL;
}
/**
* Returns metadata for all columns in a result set.
* @return array
*/
public function getResultColumns()
{
$count = mysql_num_fields($this->resultSet);
$columns = [];
for ($i = 0; $i < $count; $i++) {
$row = (array) mysql_fetch_field($this->resultSet, $i);
$columns[] = [
'name' => $row['name'],
'table' => $row['table'],
'fullname' => $row['table'] ? $row['table'] . '.' . $row['name'] : $row['name'],
'nativetype' => strtoupper($row['type']),
'type' => $row['type'] === 'time' ? Dibi\Type::TIME_INTERVAL : NULL,
'vendor' => $row,
];
}
return $columns;
}
/**
* Returns the result set resource.
* @return resource|NULL
*/
public function getResultResource()
{
$this->autoFree = FALSE;
return is_resource($this->resultSet) ? $this->resultSet : NULL;
}
}

View File

@@ -1,97 +1,106 @@
<?php
/**
* This file is part of the Dibi, smart database abstraction layer (https://dibiphp.com)
* This file is part of the "dibi" - smart database abstraction layer.
* Copyright (c) 2005 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Dibi\Drivers;
use Dibi;
/**
* The reflector for MySQL databases.
* The dibi reflector for MySQL databases.
* @internal
*/
class MySqlReflector implements Dibi\Reflector
{
public function __construct(
private Dibi\Driver $driver,
) {
use Dibi\Strict;
/** @var Dibi\Driver */
private $driver;
public function __construct(Dibi\Driver $driver)
{
$this->driver = $driver;
}
/**
* Returns list of tables.
* @return array
*/
public function getTables(): array
public function getTables()
{
$res = $this->driver->query('SHOW FULL TABLES');
$tables = [];
while ($row = $res->fetch(false)) {
while ($row = $res->fetch(FALSE)) {
$tables[] = [
'name' => $row[0],
'view' => isset($row[1]) && $row[1] === 'VIEW',
];
}
return $tables;
}
/**
* Returns metadata for all columns in a table.
* @param string
* @return array
*/
public function getColumns(string $table): array
public function getColumns($table)
{
$res = $this->driver->query("SHOW FULL COLUMNS FROM {$this->driver->escapeIdentifier($table)}");
$columns = [];
while ($row = $res->fetch(true)) {
while ($row = $res->fetch(TRUE)) {
$type = explode('(', $row['Type']);
$columns[] = [
'name' => $row['Field'],
'table' => $table,
'nativetype' => strtoupper($type[0]),
'size' => isset($type[1]) ? (int) $type[1] : null,
'size' => isset($type[1]) ? (int) $type[1] : NULL,
'unsigned' => (bool) strstr($row['Type'], 'unsigned'),
'nullable' => $row['Null'] === 'YES',
'default' => $row['Default'],
'autoincrement' => $row['Extra'] === 'auto_increment',
'vendor' => $row,
];
}
return $columns;
}
/**
* Returns metadata for all indexes in a table.
* @param string
* @return array
*/
public function getIndexes(string $table): array
public function getIndexes($table)
{
$res = $this->driver->query("SHOW INDEX FROM {$this->driver->escapeIdentifier($table)}");
$indexes = [];
while ($row = $res->fetch(true)) {
while ($row = $res->fetch(TRUE)) {
$indexes[$row['Key_name']]['name'] = $row['Key_name'];
$indexes[$row['Key_name']]['unique'] = !$row['Non_unique'];
$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);
}
/**
* Returns metadata for all foreign keys in a table.
* @param string
* @return array
* @throws Dibi\NotSupportedException
*/
public function getForeignKeys(string $table): array
public function getForeignKeys($table)
{
$data = $this->driver->query("SELECT `ENGINE` FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = {$this->driver->escapeText($table)}")->fetch(true);
$data = $this->driver->query("SELECT `ENGINE` FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = {$this->driver->escapeText($table)}")->fetch(TRUE);
if ($data['ENGINE'] !== 'InnoDB') {
throw new Dibi\NotSupportedException("Foreign keys are not supported in {$data['ENGINE']} tables.");
}
@@ -110,7 +119,7 @@ class MySqlReflector implements Dibi\Reflector
");
$foreignKeys = [];
while ($row = $res->fetch(true)) {
while ($row = $res->fetch(TRUE)) {
$keyName = $row['CONSTRAINT_NAME'];
$foreignKeys[$keyName]['name'] = $keyName;
@@ -120,7 +129,7 @@ class MySqlReflector implements Dibi\Reflector
$foreignKeys[$keyName]['onDelete'] = $row['DELETE_RULE'];
$foreignKeys[$keyName]['onUpdate'] = $row['UPDATE_RULE'];
}
return array_values($foreignKeys);
}
}

View File

@@ -1,19 +1,17 @@
<?php
/**
* This file is part of the Dibi, smart database abstraction layer (https://dibiphp.com)
* This file is part of the "dibi" - smart database abstraction layer.
* Copyright (c) 2005 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Dibi\Drivers;
use Dibi;
/**
* The driver for MySQL database.
* The dibi driver for MySQL database via improved extension.
*
* Driver options:
* - host => the MySQL server host name
@@ -29,35 +27,49 @@ use Dibi;
* - unbuffered (bool) => sends query without fetching and buffering the result rows automatically?
* - sqlmode => see http://dev.mysql.com/doc/refman/5.0/en/server-sql-mode.html
* - resource (mysqli) => existing connection resource
* - lazy, profiler, result, substitutes, ... => see Dibi\Connection options
*/
class MySqliDriver implements Dibi\Driver
class MySqliDriver implements Dibi\Driver, Dibi\ResultDriver
{
public const ErrorAccessDenied = 1045;
public const ErrorDuplicateEntry = 1062;
public const ErrorDataTruncated = 1265;
use Dibi\Strict;
/** @deprecated use MySqliDriver::ErrorAccessDenied */
public const ERROR_ACCESS_DENIED = self::ErrorAccessDenied;
const ERROR_ACCESS_DENIED = 1045;
const ERROR_DUPLICATE_ENTRY = 1062;
const ERROR_DATA_TRUNCATED = 1265;
/** @deprecated use MySqliDriver::ErrorDuplicateEntry */
public const ERROR_DUPLICATE_ENTRY = self::ErrorDuplicateEntry;
/** @var \mysqli|NULL */
private $connection;
/** @deprecated use MySqliDriver::ErrorDataTruncated */
public const ERROR_DATA_TRUNCATED = self::ErrorDataTruncated;
/** @var \mysqli_result|NULL */
private $resultSet;
private \mysqli $connection;
private bool $buffered = false;
/** @var bool */
private $autoFree = TRUE;
/** @var bool Is buffered (seekable and countable)? */
private $buffered;
/** @throws Dibi\NotSupportedException */
public function __construct(array $config)
/**
* @throws Dibi\NotSupportedException
*/
public function __construct()
{
if (!extension_loaded('mysqli')) {
throw new Dibi\NotSupportedException("PHP extension 'mysqli' is not loaded.");
}
}
/**
* Connects to a database.
* @return void
* @throws Dibi\Exception
*/
public function connect(array &$config)
{
mysqli_report(MYSQLI_REPORT_OFF);
if (isset($config['resource']) && $config['resource'] instanceof \mysqli) {
if (isset($config['resource'])) {
$this->connection = $config['resource'];
} else {
@@ -68,16 +80,16 @@ class MySqliDriver implements Dibi\Driver
'username' => ini_get('mysqli.default_user'),
'password' => ini_get('mysqli.default_pw'),
'socket' => (string) ini_get('mysqli.default_socket'),
'port' => null,
'port' => NULL,
];
if (!isset($config['host'])) {
$host = ini_get('mysqli.default_host');
if ($host) {
$config['host'] = $host;
$config['port'] = (int) ini_get('mysqli.default_port');
$config['port'] = ini_get('mysqli.default_port');
} else {
$config['host'] = null;
$config['port'] = null;
$config['host'] = NULL;
$config['port'] = NULL;
}
}
@@ -86,28 +98,24 @@ class MySqliDriver implements Dibi\Driver
$this->connection = mysqli_init();
if (isset($config['options'])) {
foreach ($config['options'] as $key => $value) {
$this->connection->options($key, $value);
if (is_scalar($config['options'])) {
$config['flags'] = $config['options']; // back compatibility
trigger_error(__CLASS__ . ": configuration item 'options' must be array; for constants MYSQLI_CLIENT_* use 'flags'.", E_USER_NOTICE);
} else {
foreach ((array) $config['options'] as $key => $value) {
mysqli_options($this->connection, $key, $value);
}
}
}
@mysqli_real_connect($this->connection, (empty($config['persistent']) ? '' : 'p:') . $config['host'], $config['username'], $config['password'], $config['database'], $config['port'], $config['socket'], $config['flags']); // intentionally @
@$this->connection->real_connect( // intentionally @
(empty($config['persistent']) ? '' : 'p:') . $config['host'],
$config['username'],
$config['password'] ?? '',
$config['database'] ?? '',
$config['port'] ?? 0,
$config['socket'],
$config['flags'] ?? 0,
);
if ($this->connection->connect_errno) {
throw new Dibi\DriverException($this->connection->connect_error, $this->connection->connect_errno);
if ($errno = mysqli_connect_errno()) {
throw new Dibi\DriverException(mysqli_connect_error(), $errno);
}
}
if (isset($config['charset'])) {
if (!@$this->connection->set_charset($config['charset'])) {
if (!@mysqli_set_charset($this->connection, $config['charset'])) {
$this->query("SET NAMES '$config[charset]'");
}
}
@@ -126,50 +134,46 @@ class MySqliDriver implements Dibi\Driver
/**
* Disconnects from a database.
* @return void
*/
public function disconnect(): void
public function disconnect()
{
@$this->connection->close(); // @ - connection can be already disconnected
}
/**
* Pings a server connection, or tries to reconnect if the connection has gone down.
*/
public function ping(): bool
{
return $this->connection->ping();
@mysqli_close($this->connection); // @ - connection can be already disconnected
}
/**
* Executes the SQL query.
* @param string SQL statement.
* @return Dibi\ResultDriver|NULL
* @throws Dibi\DriverException
*/
public function query(string $sql): ?Dibi\ResultDriver
public function query($sql)
{
$res = @$this->connection->query($sql, $this->buffered ? MYSQLI_STORE_RESULT : MYSQLI_USE_RESULT); // intentionally @
$res = @mysqli_query($this->connection, $sql, $this->buffered ? MYSQLI_STORE_RESULT : MYSQLI_USE_RESULT); // intentionally @
if ($code = mysqli_errno($this->connection)) {
throw static::createException(mysqli_error($this->connection), $code, $sql);
throw self::createException(mysqli_error($this->connection), $code, $sql);
} elseif ($res instanceof \mysqli_result) {
} elseif (is_object($res)) {
return $this->createResultDriver($res);
}
return null;
return NULL;
}
public static function createException(string $message, int|string $code, string $sql): Dibi\DriverException
/**
* @return Dibi\DriverException
*/
public static function createException($message, $code, $sql)
{
if (in_array($code, [1216, 1217, 1451, 1452, 1701], true)) {
if (in_array($code, [1216, 1217, 1451, 1452, 1701], TRUE)) {
return new Dibi\ForeignKeyConstraintViolationException($message, $code, $sql);
} elseif (in_array($code, [1062, 1557, 1569, 1586], true)) {
} elseif (in_array($code, [1062, 1557, 1569, 1586], TRUE)) {
return new Dibi\UniqueConstraintViolationException($message, $code, $sql);
} elseif (in_array($code, [1048, 1121, 1138, 1171, 1252, 1263, 1566], true)) {
} elseif (in_array($code, [1048, 1121, 1138, 1171, 1252, 1263, 1566], TRUE)) {
return new Dibi\NotNullConstraintViolationException($message, $code, $sql);
} else {
@@ -180,11 +184,12 @@ class MySqliDriver implements Dibi\Driver
/**
* Retrieves information about the most recently executed query.
* @return array
*/
public function getInfo(): array
public function getInfo()
{
$res = [];
preg_match_all('#(.+?): +(\d+) *#', $this->connection->info, $matches, PREG_SET_ORDER);
preg_match_all('#(.+?): +(\d+) *#', mysqli_info($this->connection), $matches, PREG_SET_ORDER);
if (preg_last_error()) {
throw new Dibi\PcreException;
}
@@ -192,36 +197,37 @@ class MySqliDriver implements Dibi\Driver
foreach ($matches as $m) {
$res[$m[1]] = (int) $m[2];
}
return $res;
}
/**
* Gets the number of affected rows by the last INSERT, UPDATE or DELETE query.
* @return int|FALSE number of rows or FALSE on error
*/
public function getAffectedRows(): ?int
public function getAffectedRows()
{
return $this->connection->affected_rows === -1
? null
: $this->connection->affected_rows;
return mysqli_affected_rows($this->connection) === -1 ? FALSE : mysqli_affected_rows($this->connection);
}
/**
* Retrieves the ID generated for an AUTO_INCREMENT column by the previous INSERT query.
* @return int|FALSE int on success or FALSE on failure
*/
public function getInsertId(?string $sequence): ?int
public function getInsertId($sequence)
{
return $this->connection->insert_id ?: null;
return mysqli_insert_id($this->connection);
}
/**
* Begins a transaction (if supported).
* @param string optional savepoint name
* @return void
* @throws Dibi\DriverException
*/
public function begin(?string $savepoint = null): void
public function begin($savepoint = NULL)
{
$this->query($savepoint ? "SAVEPOINT $savepoint" : 'START TRANSACTION');
}
@@ -229,9 +235,11 @@ class MySqliDriver implements Dibi\Driver
/**
* Commits statements in a transaction.
* @param string optional savepoint name
* @return void
* @throws Dibi\DriverException
*/
public function commit(?string $savepoint = null): void
public function commit($savepoint = NULL)
{
$this->query($savepoint ? "RELEASE SAVEPOINT $savepoint" : 'COMMIT');
}
@@ -239,9 +247,11 @@ class MySqliDriver implements Dibi\Driver
/**
* Rollback changes in a transaction.
* @param string optional savepoint name
* @return void
* @throws Dibi\DriverException
*/
public function rollback(?string $savepoint = null): void
public function rollback($savepoint = NULL)
{
$this->query($savepoint ? "ROLLBACK TO SAVEPOINT $savepoint" : 'ROLLBACK');
}
@@ -249,21 +259,19 @@ class MySqliDriver implements Dibi\Driver
/**
* Returns the connection resource.
* @return \mysqli
*/
public function getResource(): ?\mysqli
public function getResource()
{
try {
return @$this->connection->thread_id ? $this->connection : null;
} catch (\Throwable $e) {
return null;
}
return @$this->connection->thread_id ? $this->connection : NULL;
}
/**
* Returns the connection reflector.
* @return Dibi\Reflector
*/
public function getReflector(): Dibi\Reflector
public function getReflector()
{
return new MySqlReflector($this);
}
@@ -271,10 +279,13 @@ class MySqliDriver implements Dibi\Driver
/**
* Result set driver factory.
* @return Dibi\ResultDriver
*/
public function createResultDriver(\mysqli_result $result): MySqliResult
public function createResultDriver(\mysqli_result $resource)
{
return new MySqliResult($result, $this->buffered);
$res = clone $this;
$res->resultSet = $resource;
return $res;
}
@@ -283,75 +294,229 @@ class MySqliDriver implements Dibi\Driver
/**
* Encodes data for use in a SQL statement.
* @param string value
* @return string encoded value
*/
public function escapeText(string $value): string
public function escapeText($value)
{
return "'" . $this->connection->escape_string($value) . "'";
return "'" . mysqli_real_escape_string($this->connection, $value) . "'";
}
public function escapeBinary(string $value): string
/**
* @param string
* @return string
*/
public function escapeBinary($value)
{
return "_binary'" . $this->connection->escape_string($value) . "'";
return "_binary'" . mysqli_real_escape_string($this->connection, $value) . "'";
}
public function escapeIdentifier(string $value): string
/**
* @param string
* @return string
*/
public function escapeIdentifier($value)
{
return '`' . str_replace('`', '``', $value) . '`';
}
public function escapeBool(bool $value): string
/**
* @param bool
* @return string
*/
public function escapeBool($value)
{
return $value ? '1' : '0';
}
public function escapeDate(\DateTimeInterface $value): string
/**
* @param \DateTime|\DateTimeInterface|string|int
* @return string
*/
public function escapeDate($value)
{
if (!$value instanceof \DateTime && !$value instanceof \DateTimeInterface) {
$value = new Dibi\DateTime($value);
}
return $value->format("'Y-m-d'");
}
public function escapeDateTime(\DateTimeInterface $value): string
/**
* @param \DateTime|\DateTimeInterface|string|int
* @return string
*/
public function escapeDateTime($value)
{
return $value->format("'Y-m-d H:i:s.u'");
}
public function escapeDateInterval(\DateInterval $value): string
{
if ($value->y || $value->m || $value->d) {
throw new Dibi\NotSupportedException('Only time interval is supported.');
if (!$value instanceof \DateTime && !$value instanceof \DateTimeInterface) {
$value = new Dibi\DateTime($value);
}
return $value->format("'%r%H:%I:%S.%f'");
return $value->format("'Y-m-d H:i:s'");
}
/**
* Encodes string for use in a LIKE statement.
* @param string
* @param int
* @return string
*/
public function escapeLike(string $value, int $pos): string
public function escapeLike($value, $pos)
{
$value = addcslashes(str_replace('\\', '\\\\', $value), "\x00\n\r\\'%_");
return ($pos & 1 ? "'%" : "'") . $value . ($pos & 2 ? "%'" : "'");
return ($pos <= 0 ? "'%" : "'") . $value . ($pos >= 0 ? "%'" : "'");
}
/**
* Decodes data from result set.
* @param string
* @return string
*/
public function unescapeBinary($value)
{
return $value;
}
/** @deprecated */
public function escape($value, $type)
{
trigger_error(__METHOD__ . '() is deprecated.', E_USER_DEPRECATED);
return Dibi\Helpers::escape($this, $value, $type);
}
/**
* Injects LIMIT/OFFSET to the SQL query.
* @param string
* @param int|NULL
* @param int|NULL
* @return void
*/
public function applyLimit(string &$sql, ?int $limit, ?int $offset): void
public function applyLimit(&$sql, $limit, $offset)
{
if ($limit < 0 || $offset < 0) {
throw new Dibi\NotSupportedException('Negative offset or limit.');
} elseif ($limit !== null || $offset) {
} elseif ($limit !== NULL || $offset) {
// see http://dev.mysql.com/doc/refman/5.0/en/select.html
$sql .= ' LIMIT ' . ($limit ?? '18446744073709551615')
. ($offset ? ' OFFSET ' . $offset : '');
$sql .= ' LIMIT ' . ($limit === NULL ? '18446744073709551615' : (int) $limit)
. ($offset ? ' OFFSET ' . (int) $offset : '');
}
}
/********************* result set ****************d*g**/
/**
* Automatically frees the resources allocated for this result set.
* @return void
*/
public function __destruct()
{
$this->autoFree && $this->getResultResource() && @$this->free();
}
/**
* Returns the number of rows in a result set.
* @return int
*/
public function getRowCount()
{
if (!$this->buffered) {
throw new Dibi\NotSupportedException('Row count is not available for unbuffered queries.');
}
return mysqli_num_rows($this->resultSet);
}
/**
* Fetches the row at current position and moves the internal cursor to the next position.
* @param bool TRUE for associative array, FALSE for numeric
* @return array array on success, nonarray if no next record
*/
public function fetch($assoc)
{
return mysqli_fetch_array($this->resultSet, $assoc ? MYSQLI_ASSOC : MYSQLI_NUM);
}
/**
* Moves cursor position without fetching row.
* @param int the 0-based cursor pos to seek to
* @return bool TRUE on success, FALSE if unable to seek to specified record
* @throws Dibi\Exception
*/
public function seek($row)
{
if (!$this->buffered) {
throw new Dibi\NotSupportedException('Cannot seek an unbuffered result set.');
}
return mysqli_data_seek($this->resultSet, $row);
}
/**
* Frees the resources allocated for this result set.
* @return void
*/
public function free()
{
mysqli_free_result($this->resultSet);
$this->resultSet = NULL;
}
/**
* Returns metadata for all columns in a result set.
* @return array
*/
public function getResultColumns()
{
static $types;
if ($types === NULL) {
$consts = get_defined_constants(TRUE);
$types = [];
foreach (isset($consts['mysqli']) ? $consts['mysqli'] : [] as $key => $value) {
if (strncmp($key, 'MYSQLI_TYPE_', 12) === 0) {
$types[$value] = substr($key, 12);
}
}
$types[MYSQLI_TYPE_TINY] = $types[MYSQLI_TYPE_SHORT] = $types[MYSQLI_TYPE_LONG] = 'INT';
}
$count = mysqli_num_fields($this->resultSet);
$columns = [];
for ($i = 0; $i < $count; $i++) {
$row = (array) mysqli_fetch_field_direct($this->resultSet, $i);
$columns[] = [
'name' => $row['name'],
'table' => $row['orgtable'],
'fullname' => $row['table'] ? $row['table'] . '.' . $row['name'] : $row['name'],
'nativetype' => isset($types[$row['type']]) ? $types[$row['type']] : $row['type'],
'type' => $row['type'] === MYSQLI_TYPE_TIME ? Dibi\Type::TIME_INTERVAL : NULL,
'vendor' => $row,
];
}
return $columns;
}
/**
* Returns the result set resource.
* @return \mysqli_result|NULL
*/
public function getResultResource()
{
$this->autoFree = FALSE;
return $this->resultSet;
}
}

View File

@@ -1,127 +0,0 @@
<?php
/**
* This file is part of the Dibi, smart database abstraction layer (https://dibiphp.com)
* Copyright (c) 2005 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Dibi\Drivers;
use Dibi;
/**
* The driver for MySQL result set.
*/
class MySqliResult implements Dibi\ResultDriver
{
public function __construct(
private \mysqli_result $resultSet,
private bool $buffered,
) {
}
/**
* Returns the number of rows in a result set.
*/
public function getRowCount(): int
{
if (!$this->buffered) {
throw new Dibi\NotSupportedException('Row count is not available for unbuffered queries.');
}
return $this->resultSet->num_rows;
}
/**
* Fetches the row at current position and moves the internal cursor to the next position.
* @param bool $assoc true for associative array, false for numeric
*/
public function fetch(bool $assoc): ?array
{
return $assoc
? $this->resultSet->fetch_assoc()
: $this->resultSet->fetch_row();
}
/**
* Moves cursor position without fetching row.
* @throws Dibi\Exception
*/
public function seek(int $row): bool
{
if (!$this->buffered) {
throw new Dibi\NotSupportedException('Cannot seek an unbuffered result set.');
}
return $this->resultSet->data_seek($row);
}
/**
* Frees the resources allocated for this result set.
*/
public function free(): void
{
$this->resultSet->free();
}
/**
* Returns metadata for all columns in a result set.
*/
public function getResultColumns(): array
{
static $types;
if ($types === null) {
$consts = get_defined_constants(true);
$types = [];
foreach ($consts['mysqli'] ?? [] as $key => $value) {
if (strncmp($key, 'MYSQLI_TYPE_', 12) === 0) {
$types[$value] = substr($key, 12);
}
}
$types[MYSQLI_TYPE_TINY] = $types[MYSQLI_TYPE_SHORT] = $types[MYSQLI_TYPE_LONG] = 'INT';
}
$count = $this->resultSet->field_count;
$columns = [];
for ($i = 0; $i < $count; $i++) {
$row = (array) $this->resultSet->fetch_field_direct($i);
$columns[] = [
'name' => $row['name'],
'table' => $row['orgtable'],
'fullname' => $row['table'] ? $row['table'] . '.' . $row['name'] : $row['name'],
'nativetype' => $types[$row['type']] ?? $row['type'],
'type' => $row['type'] === MYSQLI_TYPE_TIME ? Dibi\Type::TimeInterval : null,
'vendor' => $row,
];
}
return $columns;
}
/**
* Returns the result set resource.
*/
public function getResultResource(): \mysqli_result
{
return $this->resultSet;
}
/**
* Decodes data from result set.
*/
public function unescapeBinary(string $value): string
{
return $value;
}
}

View File

@@ -1,68 +0,0 @@
<?php
/**
* This file is part of the Dibi, smart database abstraction layer (https://dibiphp.com)
* Copyright (c) 2005 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Dibi\Drivers;
use Dibi;
/**
* The driver for no result set.
*/
class NoDataResult implements Dibi\ResultDriver
{
public function __construct(
private int $rows,
) {
}
/**
* Returns the number of affected rows.
*/
public function getRowCount(): int
{
return $this->rows;
}
public function fetch(bool $assoc): ?array
{
return null;
}
public function seek(int $row): bool
{
return false;
}
public function free(): void
{
}
public function getResultColumns(): array
{
return [];
}
public function getResultResource(): mixed
{
return null;
}
public function unescapeBinary(string $value): string
{
return $value;
}
}

View File

@@ -1,19 +1,17 @@
<?php
/**
* This file is part of the Dibi, smart database abstraction layer (https://dibiphp.com)
* This file is part of the "dibi" - smart database abstraction layer.
* Copyright (c) 2005 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Dibi\Drivers;
use Dibi;
/**
* The driver interacting with databases via ODBC connections.
* The dibi driver interacting with databases via ODBC connections.
*
* Driver options:
* - dsn => driver specific DSN
@@ -21,23 +19,46 @@ use Dibi;
* - password (or pass)
* - persistent (bool) => try to find a persistent link?
* - resource (resource) => existing connection resource
* - microseconds (bool) => use microseconds in datetime format?
* - lazy, profiler, result, substitutes, ... => see Dibi\Connection options
*/
class OdbcDriver implements Dibi\Driver
class OdbcDriver implements Dibi\Driver, Dibi\ResultDriver, Dibi\Reflector
{
/** @var resource */
use Dibi\Strict;
/** @var resource|NULL */
private $connection;
private ?int $affectedRows;
private bool $microseconds = true;
/** @var resource|NULL */
private $resultSet;
/** @var bool */
private $autoFree = TRUE;
/** @var int|FALSE Affected rows */
private $affectedRows = FALSE;
/** @var int Cursor */
private $row = 0;
/** @throws Dibi\NotSupportedException */
public function __construct(array $config)
/**
* @throws Dibi\NotSupportedException
*/
public function __construct()
{
if (!extension_loaded('odbc')) {
throw new Dibi\NotSupportedException("PHP extension 'odbc' is not loaded.");
}
}
/**
* Connects to a database.
* @return void
* @throws Dibi\Exception
*/
public function connect(array &$config)
{
if (isset($config['resource'])) {
$this->connection = $config['resource'];
} else {
@@ -48,25 +69,24 @@ class OdbcDriver implements Dibi\Driver
'dsn' => ini_get('odbc.default_db'),
];
$this->connection = empty($config['persistent'])
? @odbc_connect($config['dsn'], $config['username'] ?? '', $config['password'] ?? '') // intentionally @
: @odbc_pconnect($config['dsn'], $config['username'] ?? '', $config['password'] ?? ''); // intentionally @
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 @
}
}
if (!is_resource($this->connection)) {
throw new Dibi\DriverException(odbc_errormsg() . ' ' . odbc_error());
}
if (isset($config['microseconds'])) {
$this->microseconds = (bool) $config['microseconds'];
}
}
/**
* Disconnects from a database.
* @return void
*/
public function disconnect(): void
public function disconnect()
{
@odbc_close($this->connection); // @ - connection can be already disconnected
}
@@ -74,31 +94,31 @@ class OdbcDriver implements Dibi\Driver
/**
* Executes the SQL query.
* @param string SQL statement.
* @return Dibi\ResultDriver|NULL
* @throws Dibi\DriverException
*/
public function query(string $sql): ?Dibi\ResultDriver
public function query($sql)
{
$this->affectedRows = null;
$this->affectedRows = FALSE;
$res = @odbc_exec($this->connection, $sql); // intentionally @
if ($res === false) {
if ($res === FALSE) {
throw new Dibi\DriverException(odbc_errormsg($this->connection) . ' ' . odbc_error($this->connection), 0, $sql);
} elseif (is_resource($res)) {
$this->affectedRows = Dibi\Helpers::false2Null(odbc_num_rows($res));
return odbc_num_fields($res)
? $this->createResultDriver($res)
: null;
$this->affectedRows = odbc_num_rows($res);
return $this->createResultDriver($res);
}
return null;
return NULL;
}
/**
* Gets the number of affected rows by the last INSERT, UPDATE or DELETE query.
* @return int|FALSE number of rows or FALSE on error
*/
public function getAffectedRows(): ?int
public function getAffectedRows()
{
return $this->affectedRows;
}
@@ -106,8 +126,9 @@ class OdbcDriver implements Dibi\Driver
/**
* Retrieves the ID generated for an AUTO_INCREMENT column by the previous INSERT query.
* @return int|FALSE int on success or FALSE on failure
*/
public function getInsertId(?string $sequence): ?int
public function getInsertId($sequence)
{
throw new Dibi\NotSupportedException('ODBC does not support autoincrementing.');
}
@@ -115,11 +136,13 @@ class OdbcDriver implements Dibi\Driver
/**
* Begins a transaction (if supported).
* @param string optional savepoint name
* @return void
* @throws Dibi\DriverException
*/
public function begin(?string $savepoint = null): void
public function begin($savepoint = NULL)
{
if (!odbc_autocommit($this->connection)) {
if (!odbc_autocommit($this->connection, FALSE)) {
throw new Dibi\DriverException(odbc_errormsg($this->connection) . ' ' . odbc_error($this->connection));
}
}
@@ -127,36 +150,39 @@ class OdbcDriver implements Dibi\Driver
/**
* Commits statements in a transaction.
* @param string optional savepoint name
* @return void
* @throws Dibi\DriverException
*/
public function commit(?string $savepoint = null): void
public function commit($savepoint = NULL)
{
if (!odbc_commit($this->connection)) {
throw new Dibi\DriverException(odbc_errormsg($this->connection) . ' ' . odbc_error($this->connection));
}
odbc_autocommit($this->connection, true);
odbc_autocommit($this->connection, TRUE);
}
/**
* Rollback changes in a transaction.
* @param string optional savepoint name
* @return void
* @throws Dibi\DriverException
*/
public function rollback(?string $savepoint = null): void
public function rollback($savepoint = NULL)
{
if (!odbc_rollback($this->connection)) {
throw new Dibi\DriverException(odbc_errormsg($this->connection) . ' ' . odbc_error($this->connection));
}
odbc_autocommit($this->connection, true);
odbc_autocommit($this->connection, TRUE);
}
/**
* Is in transaction?
* @return bool
*/
public function inTransaction(): bool
public function inTransaction()
{
return !odbc_autocommit($this->connection);
}
@@ -164,30 +190,34 @@ class OdbcDriver implements Dibi\Driver
/**
* Returns the connection resource.
* @return resource|null
* @return resource|NULL
*/
public function getResource(): mixed
public function getResource()
{
return is_resource($this->connection) ? $this->connection : null;
return is_resource($this->connection) ? $this->connection : NULL;
}
/**
* Returns the connection reflector.
* @return Dibi\Reflector
*/
public function getReflector(): Dibi\Reflector
public function getReflector()
{
return new OdbcReflector($this);
return $this;
}
/**
* Result set driver factory.
* @param resource $resource
* @param resource
* @return Dibi\ResultDriver
*/
public function createResultDriver($resource): OdbcResult
public function createResultDriver($resource)
{
return new OdbcResult($resource);
$res = clone $this;
$res->resultSet = $resource;
return $res;
}
@@ -196,63 +226,111 @@ class OdbcDriver implements Dibi\Driver
/**
* Encodes data for use in a SQL statement.
* @param string value
* @return string encoded value
*/
public function escapeText(string $value): string
public function escapeText($value)
{
return "'" . str_replace("'", "''", $value) . "'";
}
public function escapeBinary(string $value): string
/**
* @param string
* @return string
*/
public function escapeBinary($value)
{
return "'" . str_replace("'", "''", $value) . "'";
}
public function escapeIdentifier(string $value): string
/**
* @param string
* @return string
*/
public function escapeIdentifier($value)
{
return '[' . str_replace(['[', ']'], ['[[', ']]'], $value) . ']';
}
public function escapeBool(bool $value): string
/**
* @param bool
* @return string
*/
public function escapeBool($value)
{
return $value ? '1' : '0';
}
public function escapeDate(\DateTimeInterface $value): string
/**
* @param \DateTime|\DateTimeInterface|string|int
* @return string
*/
public function escapeDate($value)
{
return $value->format('#m/d/Y#');
if (!$value instanceof \DateTime && !$value instanceof \DateTimeInterface) {
$value = new Dibi\DateTime($value);
}
return $value->format("#m/d/Y#");
}
public function escapeDateTime(\DateTimeInterface $value): string
/**
* @param \DateTime|\DateTimeInterface|string|int
* @return string
*/
public function escapeDateTime($value)
{
return $value->format($this->microseconds ? '#m/d/Y H:i:s.u#' : '#m/d/Y H:i:s#');
}
public function escapeDateInterval(\DateInterval $value): string
{
throw new Dibi\NotImplementedException;
if (!$value instanceof \DateTime && !$value instanceof \DateTimeInterface) {
$value = new Dibi\DateTime($value);
}
return $value->format("#m/d/Y H:i:s#");
}
/**
* Encodes string for use in a LIKE statement.
* @param string
* @param int
* @return string
*/
public function escapeLike(string $value, int $pos): string
public function escapeLike($value, $pos)
{
$value = strtr($value, ["'" => "''", '%' => '[%]', '_' => '[_]', '[' => '[[]']);
return ($pos & 1 ? "'%" : "'") . $value . ($pos & 2 ? "%'" : "'");
return ($pos <= 0 ? "'%" : "'") . $value . ($pos >= 0 ? "%'" : "'");
}
/**
* Decodes data from result set.
* @param string
* @return string
*/
public function unescapeBinary($value)
{
return $value;
}
/** @deprecated */
public function escape($value, $type)
{
trigger_error(__METHOD__ . '() is deprecated.', E_USER_DEPRECATED);
return Dibi\Helpers::escape($this, $value, $type);
}
/**
* Injects LIMIT/OFFSET to the SQL query.
* @param string
* @param int|NULL
* @param int|NULL
* @return void
*/
public function applyLimit(string &$sql, ?int $limit, ?int $offset): void
public function applyLimit(&$sql, $limit, $offset)
{
if ($offset) {
throw new Dibi\NotSupportedException('Offset is not supported by this database.');
@@ -260,8 +338,183 @@ class OdbcDriver implements Dibi\Driver
} elseif ($limit < 0) {
throw new Dibi\NotSupportedException('Negative offset or limit.');
} elseif ($limit !== null) {
$sql = 'SELECT TOP ' . $limit . ' * FROM (' . $sql . ') t';
} elseif ($limit !== NULL) {
$sql = 'SELECT TOP ' . (int) $limit . ' * FROM (' . $sql . ') t';
}
}
/********************* result set ****************d*g**/
/**
* Automatically frees the resources allocated for this result set.
* @return void
*/
public function __destruct()
{
$this->autoFree && $this->getResultResource() && $this->free();
}
/**
* Returns the number of rows in a result set.
* @return int
*/
public function getRowCount()
{
// will return -1 with many drivers :-(
return odbc_num_rows($this->resultSet);
}
/**
* Fetches the row at current position and moves the internal cursor to the next position.
* @param bool TRUE for associative array, FALSE for numeric
* @return array array on success, nonarray if no next record
*/
public function fetch($assoc)
{
if ($assoc) {
return odbc_fetch_array($this->resultSet, ++$this->row);
} else {
$set = $this->resultSet;
if (!odbc_fetch_row($set, ++$this->row)) {
return FALSE;
}
$count = odbc_num_fields($set);
$cols = [];
for ($i = 1; $i <= $count; $i++) {
$cols[] = odbc_result($set, $i);
}
return $cols;
}
}
/**
* Moves cursor position without fetching row.
* @param int the 0-based cursor pos to seek to
* @return bool TRUE on success, FALSE if unable to seek to specified record
*/
public function seek($row)
{
$this->row = $row;
return TRUE;
}
/**
* Frees the resources allocated for this result set.
* @return void
*/
public function free()
{
odbc_free_result($this->resultSet);
$this->resultSet = NULL;
}
/**
* Returns metadata for all columns in a result set.
* @return array
*/
public function getResultColumns()
{
$count = odbc_num_fields($this->resultSet);
$columns = [];
for ($i = 1; $i <= $count; $i++) {
$columns[] = [
'name' => odbc_field_name($this->resultSet, $i),
'table' => NULL,
'fullname' => odbc_field_name($this->resultSet, $i),
'nativetype' => odbc_field_type($this->resultSet, $i),
];
}
return $columns;
}
/**
* Returns the result set resource.
* @return resource|NULL
*/
public function getResultResource()
{
$this->autoFree = FALSE;
return is_resource($this->resultSet) ? $this->resultSet : NULL;
}
/********************* Dibi\Reflector ****************d*g**/
/**
* Returns list of tables.
* @return array
*/
public function getTables()
{
$res = odbc_tables($this->connection);
$tables = [];
while ($row = odbc_fetch_array($res)) {
if ($row['TABLE_TYPE'] === 'TABLE' || $row['TABLE_TYPE'] === 'VIEW') {
$tables[] = [
'name' => $row['TABLE_NAME'],
'view' => $row['TABLE_TYPE'] === 'VIEW',
];
}
}
odbc_free_result($res);
return $tables;
}
/**
* Returns metadata for all columns in a table.
* @param string
* @return array
*/
public function getColumns($table)
{
$res = odbc_columns($this->connection);
$columns = [];
while ($row = odbc_fetch_array($res)) {
if ($row['TABLE_NAME'] === $table) {
$columns[] = [
'name' => $row['COLUMN_NAME'],
'table' => $table,
'nativetype' => $row['TYPE_NAME'],
'size' => $row['COLUMN_SIZE'],
'nullable' => (bool) $row['NULLABLE'],
'default' => $row['COLUMN_DEF'],
];
}
}
odbc_free_result($res);
return $columns;
}
/**
* Returns metadata for all indexes in a table.
* @param string
* @return array
*/
public function getIndexes($table)
{
throw new Dibi\NotImplementedException;
}
/**
* Returns metadata for all foreign keys in a table.
* @param string
* @return array
*/
public function getForeignKeys($table)
{
throw new Dibi\NotImplementedException;
}
}

View File

@@ -1,88 +0,0 @@
<?php
/**
* This file is part of the Dibi, smart database abstraction layer (https://dibiphp.com)
* Copyright (c) 2005 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Dibi\Drivers;
use Dibi;
/**
* The reflector for ODBC connections.
*/
class OdbcReflector implements Dibi\Reflector
{
public function __construct(
private Dibi\Driver $driver,
) {
}
/**
* Returns list of tables.
*/
public function getTables(): array
{
$res = odbc_tables($this->driver->getResource());
$tables = [];
while ($row = odbc_fetch_array($res)) {
if ($row['TABLE_TYPE'] === 'TABLE' || $row['TABLE_TYPE'] === 'VIEW') {
$tables[] = [
'name' => $row['TABLE_NAME'],
'view' => $row['TABLE_TYPE'] === 'VIEW',
];
}
}
odbc_free_result($res);
return $tables;
}
/**
* Returns metadata for all columns in a table.
*/
public function getColumns(string $table): array
{
$res = odbc_columns($this->driver->getResource());
$columns = [];
while ($row = odbc_fetch_array($res)) {
if ($row['TABLE_NAME'] === $table) {
$columns[] = [
'name' => $row['COLUMN_NAME'],
'table' => $table,
'nativetype' => $row['TYPE_NAME'],
'size' => $row['COLUMN_SIZE'],
'nullable' => (bool) $row['NULLABLE'],
'default' => $row['COLUMN_DEF'],
];
}
}
odbc_free_result($res);
return $columns;
}
/**
* Returns metadata for all indexes in a table.
*/
public function getIndexes(string $table): array
{
throw new Dibi\NotImplementedException;
}
/**
* Returns metadata for all foreign keys in a table.
*/
public function getForeignKeys(string $table): array
{
throw new Dibi\NotImplementedException;
}
}

View File

@@ -1,121 +0,0 @@
<?php
/**
* This file is part of the Dibi, smart database abstraction layer (https://dibiphp.com)
* Copyright (c) 2005 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Dibi\Drivers;
use Dibi;
/**
* The driver interacting with result set via ODBC connections.
*/
class OdbcResult implements Dibi\ResultDriver
{
private int $row = 0;
public function __construct(
/** @var resource */
private $resultSet,
) {
}
/**
* Returns the number of rows in a result set.
*/
public function getRowCount(): int
{
// will return -1 with many drivers :-(
return odbc_num_rows($this->resultSet);
}
/**
* Fetches the row at current position and moves the internal cursor to the next position.
* @param bool $assoc true for associative array, false for numeric
*/
public function fetch(bool $assoc): ?array
{
if ($assoc) {
return Dibi\Helpers::false2Null(odbc_fetch_array($this->resultSet, ++$this->row));
} else {
$set = $this->resultSet;
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;
}
}
/**
* Moves cursor position without fetching row.
*/
public function seek(int $row): bool
{
$this->row = $row;
return true;
}
/**
* Frees the resources allocated for this result set.
*/
public function free(): void
{
odbc_free_result($this->resultSet);
}
/**
* Returns metadata for all columns in a result set.
*/
public function getResultColumns(): array
{
$count = odbc_num_fields($this->resultSet);
$columns = [];
for ($i = 1; $i <= $count; $i++) {
$columns[] = [
'name' => odbc_field_name($this->resultSet, $i),
'table' => null,
'fullname' => odbc_field_name($this->resultSet, $i),
'nativetype' => odbc_field_type($this->resultSet, $i),
];
}
return $columns;
}
/**
* Returns the result set resource.
* @return resource|null
*/
public function getResultResource(): mixed
{
return is_resource($this->resultSet) ? $this->resultSet : null;
}
/**
* Decodes data from result set.
*/
public function unescapeBinary(string $value): string
{
return $value;
}
}

View File

@@ -1,19 +1,17 @@
<?php
/**
* This file is part of the Dibi, smart database abstraction layer (https://dibiphp.com)
* This file is part of the "dibi" - smart database abstraction layer.
* Copyright (c) 2005 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Dibi\Drivers;
use Dibi;
/**
* The driver for Oracle database.
* The dibi driver for Oracle database.
*
* Driver options:
* - database => the name of the local Oracle instance or the name of the entry in tnsnames.ora
@@ -21,28 +19,56 @@ use Dibi;
* - password (or pass)
* - charset => character encoding to set
* - schema => alters session schema
* - nativeDate => use native date format (defaults to true)
* - formatDate => how to format date in SQL (@see date)
* - formatDateTime => how to format datetime in SQL (@see date)
* - resource (resource) => existing connection resource
* - persistent => Creates persistent connections with oci_pconnect instead of oci_new_connect
* - lazy, profiler, result, substitutes, ... => see Dibi\Connection options
*/
class OracleDriver implements Dibi\Driver
class OracleDriver implements Dibi\Driver, Dibi\ResultDriver, Dibi\Reflector
{
/** @var resource */
use Dibi\Strict;
/** @var resource|NULL */
private $connection;
private bool $autocommit = true;
private bool $nativeDate;
private ?int $affectedRows;
/** @var resource|NULL */
private $resultSet;
/** @var bool */
private $autoFree = TRUE;
/** @var bool */
private $autocommit = TRUE;
/** @var string Date and datetime format */
private $fmtDate, $fmtDateTime;
/** @var int|FALSE Number of affected rows */
private $affectedRows = FALSE;
/** @throws Dibi\NotSupportedException */
public function __construct(array $config)
/**
* @throws Dibi\NotSupportedException
*/
public function __construct()
{
if (!extension_loaded('oci8')) {
throw new Dibi\NotSupportedException("PHP extension 'oci8' is not loaded.");
}
}
/**
* Connects to a database.
* @return void
* @throws Dibi\Exception
*/
public function connect(array &$config)
{
$foo = &$config['charset'];
$this->nativeDate = $config['nativeDate'] ?? true;
$this->fmtDate = isset($config['formatDate']) ? $config['formatDate'] : 'U';
$this->fmtDateTime = isset($config['formatDateTime']) ? $config['formatDateTime'] : 'U';
if (isset($config['resource'])) {
$this->connection = $config['resource'];
@@ -65,8 +91,9 @@ class OracleDriver implements Dibi\Driver
/**
* Disconnects from a database.
* @return void
*/
public function disconnect(): void
public function disconnect()
{
@oci_close($this->connection); // @ - connection can be already disconnected
}
@@ -74,42 +101,44 @@ class OracleDriver implements Dibi\Driver
/**
* Executes the SQL query.
* @param string SQL statement.
* @return Dibi\ResultDriver|NULL
* @throws Dibi\DriverException
*/
public function query(string $sql): ?Dibi\ResultDriver
public function query($sql)
{
$this->affectedRows = null;
$this->affectedRows = FALSE;
$res = oci_parse($this->connection, $sql);
if ($res) {
@oci_execute($res, $this->autocommit ? OCI_COMMIT_ON_SUCCESS : OCI_DEFAULT);
$err = oci_error($res);
if ($err) {
throw static::createException($err['message'], $err['code'], $sql);
throw self::createException($err['message'], $err['code'], $sql);
} elseif (is_resource($res)) {
$this->affectedRows = Dibi\Helpers::false2Null(oci_num_rows($res));
return oci_num_fields($res)
? $this->createResultDriver($res)
: null;
$this->affectedRows = oci_num_rows($res);
return $this->createResultDriver($res);
}
} else {
$err = oci_error($this->connection);
throw new Dibi\DriverException($err['message'], $err['code'], $sql);
}
return null;
return NULL;
}
public static function createException(string $message, $code, string $sql): Dibi\DriverException
/**
* @return Dibi\DriverException
*/
public static function createException($message, $code, $sql)
{
if (in_array($code, [1, 2299, 38911], true)) {
if (in_array($code, [1, 2299, 38911], TRUE)) {
return new Dibi\UniqueConstraintViolationException($message, $code, $sql);
} elseif (in_array($code, [1400], true)) {
} elseif (in_array($code, [1400], TRUE)) {
return new Dibi\NotNullConstraintViolationException($message, $code, $sql);
} elseif (in_array($code, [2266, 2291, 2292], true)) {
} elseif (in_array($code, [2266, 2291, 2292], TRUE)) {
return new Dibi\ForeignKeyConstraintViolationException($message, $code, $sql);
} else {
@@ -120,8 +149,9 @@ class OracleDriver implements Dibi\Driver
/**
* Gets the number of affected rows by the last INSERT, UPDATE or DELETE query.
* @return int|FALSE number of rows or FALSE on error
*/
public function getAffectedRows(): ?int
public function getAffectedRows()
{
return $this->affectedRows;
}
@@ -129,79 +159,88 @@ class OracleDriver implements Dibi\Driver
/**
* Retrieves the ID generated for an AUTO_INCREMENT column by the previous INSERT query.
* @return int|FALSE int on success or FALSE on failure
*/
public function getInsertId(?string $sequence): ?int
public function getInsertId($sequence)
{
$row = $this->query("SELECT $sequence.CURRVAL AS ID FROM DUAL")->fetch(true);
return isset($row['ID']) ? Dibi\Helpers::intVal($row['ID']) : null;
$row = $this->query("SELECT $sequence.CURRVAL AS ID FROM DUAL")->fetch(TRUE);
return isset($row['ID']) ? (int) $row['ID'] : FALSE;
}
/**
* Begins a transaction (if supported).
* @param string optional savepoint name
* @return void
*/
public function begin(?string $savepoint = null): void
public function begin($savepoint = NULL)
{
$this->autocommit = false;
$this->autocommit = FALSE;
}
/**
* Commits statements in a transaction.
* @param string optional savepoint name
* @return void
* @throws Dibi\DriverException
*/
public function commit(?string $savepoint = null): void
public function commit($savepoint = NULL)
{
if (!oci_commit($this->connection)) {
$err = oci_error($this->connection);
throw new Dibi\DriverException($err['message'], $err['code']);
}
$this->autocommit = true;
$this->autocommit = TRUE;
}
/**
* Rollback changes in a transaction.
* @param string optional savepoint name
* @return void
* @throws Dibi\DriverException
*/
public function rollback(?string $savepoint = null): void
public function rollback($savepoint = NULL)
{
if (!oci_rollback($this->connection)) {
$err = oci_error($this->connection);
throw new Dibi\DriverException($err['message'], $err['code']);
}
$this->autocommit = true;
$this->autocommit = TRUE;
}
/**
* Returns the connection resource.
* @return resource|null
* @return resource|NULL
*/
public function getResource(): mixed
public function getResource()
{
return is_resource($this->connection) ? $this->connection : null;
return is_resource($this->connection) ? $this->connection : NULL;
}
/**
* Returns the connection reflector.
* @return Dibi\Reflector
*/
public function getReflector(): Dibi\Reflector
public function getReflector()
{
return new OracleReflector($this);
return $this;
}
/**
* Result set driver factory.
* @param resource $resource
* @param resource
* @return Dibi\ResultDriver
*/
public function createResultDriver($resource): OracleResult
public function createResultDriver($resource)
{
return new OracleResult($resource);
$res = clone $this;
$res->resultSet = $resource;
return $res;
}
@@ -210,69 +249,113 @@ class OracleDriver implements Dibi\Driver
/**
* Encodes data for use in a SQL statement.
* @param string value
* @return string encoded value
*/
public function escapeText(string $value): string
public function escapeText($value)
{
return "'" . str_replace("'", "''", $value) . "'"; // TODO: not tested
}
public function escapeBinary(string $value): string
/**
* @param string
* @return string
*/
public function escapeBinary($value)
{
return "'" . str_replace("'", "''", $value) . "'"; // TODO: not tested
}
public function escapeIdentifier(string $value): string
/**
* @param string
* @return string
*/
public function escapeIdentifier($value)
{
// @see http://download.oracle.com/docs/cd/B10500_01/server.920/a96540/sql_elements9a.htm
return '"' . str_replace('"', '""', $value) . '"';
}
public function escapeBool(bool $value): string
/**
* @param bool
* @return string
*/
public function escapeBool($value)
{
return $value ? '1' : '0';
}
public function escapeDate(\DateTimeInterface $value): string
/**
* @param \DateTime|\DateTimeInterface|string|int
* @return string
*/
public function escapeDate($value)
{
return $this->nativeDate
? "to_date('" . $value->format('Y-m-d') . "', 'YYYY-mm-dd')"
: $value->format('U');
if (!$value instanceof \DateTime && !$value instanceof \DateTimeInterface) {
$value = new Dibi\DateTime($value);
}
return $value->format($this->fmtDate);
}
public function escapeDateTime(\DateTimeInterface $value): string
/**
* @param \DateTime|\DateTimeInterface|string|int
* @return string
*/
public function escapeDateTime($value)
{
return $this->nativeDate
? "to_date('" . $value->format('Y-m-d G:i:s') . "', 'YYYY-mm-dd hh24:mi:ss')"
: $value->format('U');
}
public function escapeDateInterval(\DateInterval $value): string
{
throw new Dibi\NotImplementedException;
if (!$value instanceof \DateTime && !$value instanceof \DateTimeInterface) {
$value = new Dibi\DateTime($value);
}
return $value->format($this->fmtDateTime);
}
/**
* Encodes string for use in a LIKE statement.
* @param string
* @param int
* @return string
*/
public function escapeLike(string $value, int $pos): string
public function escapeLike($value, $pos)
{
$value = addcslashes(str_replace('\\', '\\\\', $value), "\x00\\%_");
$value = str_replace("'", "''", $value);
return ($pos & 1 ? "'%" : "'") . $value . ($pos & 2 ? "%'" : "'");
return ($pos <= 0 ? "'%" : "'") . $value . ($pos >= 0 ? "%'" : "'");
}
/**
* Decodes data from result set.
* @param string
* @return string
*/
public function unescapeBinary($value)
{
return $value;
}
/** @deprecated */
public function escape($value, $type)
{
trigger_error(__METHOD__ . '() is deprecated.', E_USER_DEPRECATED);
return Dibi\Helpers::escape($this, $value, $type);
}
/**
* Injects LIMIT/OFFSET to the SQL query.
* @param string
* @param int|NULL
* @param int|NULL
* @return void
*/
public function applyLimit(string &$sql, ?int $limit, ?int $offset): void
public function applyLimit(&$sql, $limit, $offset)
{
if ($limit < 0 || $offset < 0) {
throw new Dibi\NotSupportedException('Negative offset or limit.');
@@ -280,11 +363,169 @@ class OracleDriver implements Dibi\Driver
} elseif ($offset) {
// see http://www.oracle.com/technology/oramag/oracle/06-sep/o56asktom.html
$sql = 'SELECT * FROM (SELECT t.*, ROWNUM AS "__rnum" FROM (' . $sql . ') t '
. ($limit !== null ? 'WHERE ROWNUM <= ' . ($offset + $limit) : '')
. ') WHERE "__rnum" > ' . $offset;
. ($limit !== NULL ? 'WHERE ROWNUM <= ' . ((int) $offset + (int) $limit) : '')
. ') WHERE "__rnum" > '. (int) $offset;
} elseif ($limit !== null) {
$sql = 'SELECT * FROM (' . $sql . ') WHERE ROWNUM <= ' . $limit;
} elseif ($limit !== NULL) {
$sql = 'SELECT * FROM (' . $sql . ') WHERE ROWNUM <= ' . (int) $limit;
}
}
/********************* result set ****************d*g**/
/**
* Automatically frees the resources allocated for this result set.
* @return void
*/
public function __destruct()
{
$this->autoFree && $this->getResultResource() && $this->free();
}
/**
* Returns the number of rows in a result set.
* @return int
*/
public function getRowCount()
{
throw new Dibi\NotSupportedException('Row count is not available for unbuffered queries.');
}
/**
* Fetches the row at current position and moves the internal cursor to the next position.
* @param bool TRUE for associative array, FALSE for numeric
* @return array array on success, nonarray if no next record
*/
public function fetch($assoc)
{
return oci_fetch_array($this->resultSet, ($assoc ? OCI_ASSOC : OCI_NUM) | OCI_RETURN_NULLS);
}
/**
* Moves cursor position without fetching row.
* @param int the 0-based cursor pos to seek to
* @return bool TRUE on success, FALSE if unable to seek to specified record
*/
public function seek($row)
{
throw new Dibi\NotImplementedException;
}
/**
* Frees the resources allocated for this result set.
* @return void
*/
public function free()
{
oci_free_statement($this->resultSet);
$this->resultSet = NULL;
}
/**
* Returns metadata for all columns in a result set.
* @return array
*/
public function getResultColumns()
{
$count = oci_num_fields($this->resultSet);
$columns = [];
for ($i = 1; $i <= $count; $i++) {
$type = oci_field_type($this->resultSet, $i);
$columns[] = [
'name' => oci_field_name($this->resultSet, $i),
'table' => NULL,
'fullname' => oci_field_name($this->resultSet, $i),
'nativetype' => $type === 'NUMBER' && oci_field_scale($this->resultSet, $i) === 0 ? 'INTEGER' : $type,
];
}
return $columns;
}
/**
* Returns the result set resource.
* @return resource|NULL
*/
public function getResultResource()
{
$this->autoFree = FALSE;
return is_resource($this->resultSet) ? $this->resultSet : NULL;
}
/********************* Dibi\Reflector ****************d*g**/
/**
* Returns list of tables.
* @return array
*/
public function getTables()
{
$res = $this->query('SELECT * FROM cat');
$tables = [];
while ($row = $res->fetch(FALSE)) {
if ($row[1] === 'TABLE' || $row[1] === 'VIEW') {
$tables[] = [
'name' => $row[0],
'view' => $row[1] === 'VIEW',
];
}
}
return $tables;
}
/**
* Returns metadata for all columns in a table.
* @param string
* @return array
*/
public function getColumns($table)
{
$res = $this->query('SELECT * FROM "ALL_TAB_COLUMNS" WHERE "TABLE_NAME" = ' . $this->escapeText($table));
$columns = [];
while ($row = $res->fetch(TRUE)) {
$columns[] = [
'table' => $row['TABLE_NAME'],
'name' => $row['COLUMN_NAME'],
'nativetype' => $row['DATA_TYPE'],
'size' => isset($row['DATA_LENGTH']) ? $row['DATA_LENGTH'] : NULL,
'nullable' => $row['NULLABLE'] === 'Y',
'default' => $row['DATA_DEFAULT'],
'vendor' => $row,
];
}
return $columns;
}
/**
* Returns metadata for all indexes in a table.
* @param string
* @return array
*/
public function getIndexes($table)
{
throw new Dibi\NotImplementedException;
}
/**
* Returns metadata for all foreign keys in a table.
* @param string
* @return array
*/
public function getForeignKeys($table)
{
throw new Dibi\NotImplementedException;
}
}

View File

@@ -1,85 +0,0 @@
<?php
/**
* This file is part of the Dibi, smart database abstraction layer (https://dibiphp.com)
* Copyright (c) 2005 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Dibi\Drivers;
use Dibi;
/**
* The reflector for Oracle database.
*/
class OracleReflector implements Dibi\Reflector
{
public function __construct(
private Dibi\Driver $driver,
) {
}
/**
* Returns list of tables.
*/
public function getTables(): array
{
$res = $this->driver->query('SELECT * FROM cat');
$tables = [];
while ($row = $res->fetch(false)) {
if ($row[1] === 'TABLE' || $row[1] === 'VIEW') {
$tables[] = [
'name' => $row[0],
'view' => $row[1] === 'VIEW',
];
}
}
return $tables;
}
/**
* Returns metadata for all columns in a table.
*/
public function getColumns(string $table): array
{
$res = $this->driver->query('SELECT * FROM "ALL_TAB_COLUMNS" WHERE "TABLE_NAME" = ' . $this->driver->escapeText($table));
$columns = [];
while ($row = $res->fetch(true)) {
$columns[] = [
'table' => $row['TABLE_NAME'],
'name' => $row['COLUMN_NAME'],
'nativetype' => $row['DATA_TYPE'],
'size' => $row['DATA_LENGTH'] ?? null,
'nullable' => $row['NULLABLE'] === 'Y',
'default' => $row['DATA_DEFAULT'],
'vendor' => $row,
];
}
return $columns;
}
/**
* Returns metadata for all indexes in a table.
*/
public function getIndexes(string $table): array
{
throw new Dibi\NotImplementedException;
}
/**
* Returns metadata for all foreign keys in a table.
*/
public function getForeignKeys(string $table): array
{
throw new Dibi\NotImplementedException;
}
}

View File

@@ -1,103 +0,0 @@
<?php
/**
* This file is part of the Dibi, smart database abstraction layer (https://dibiphp.com)
* Copyright (c) 2005 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Dibi\Drivers;
use Dibi;
/**
* The driver for Oracle result set.
*/
class OracleResult implements Dibi\ResultDriver
{
public function __construct(
/** @var resource */
private $resultSet,
) {
}
/**
* Returns the number of rows in a result set.
*/
public function getRowCount(): int
{
throw new Dibi\NotSupportedException('Row count is not available for unbuffered queries.');
}
/**
* Fetches the row at current position and moves the internal cursor to the next position.
* @param bool $assoc true for associative array, false for numeric
*/
public function fetch(bool $assoc): ?array
{
return Dibi\Helpers::false2Null(oci_fetch_array($this->resultSet, ($assoc ? OCI_ASSOC : OCI_NUM) | OCI_RETURN_NULLS));
}
/**
* Moves cursor position without fetching row.
*/
public function seek(int $row): bool
{
throw new Dibi\NotImplementedException;
}
/**
* Frees the resources allocated for this result set.
*/
public function free(): void
{
oci_free_statement($this->resultSet);
}
/**
* Returns metadata for all columns in a result set.
*/
public function getResultColumns(): array
{
$count = oci_num_fields($this->resultSet);
$columns = [];
for ($i = 1; $i <= $count; $i++) {
$type = oci_field_type($this->resultSet, $i);
$columns[] = [
'name' => oci_field_name($this->resultSet, $i),
'table' => null,
'fullname' => oci_field_name($this->resultSet, $i),
'type' => $type === 'LONG' ? Dibi\Type::Text : null,
'nativetype' => $type === 'NUMBER' && oci_field_scale($this->resultSet, $i) === 0 ? 'INTEGER' : $type,
];
}
return $columns;
}
/**
* Returns the result set resource.
* @return resource|null
*/
public function getResultResource(): mixed
{
return is_resource($this->resultSet) ? $this->resultSet : null;
}
/**
* Decodes data from result set.
*/
public function unescapeBinary(string $value): string
{
return $value;
}
}

View File

@@ -1,21 +1,18 @@
<?php
/**
* This file is part of the Dibi, smart database abstraction layer (https://dibiphp.com)
* This file is part of the "dibi" - smart database abstraction layer.
* Copyright (c) 2005 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Dibi\Drivers;
use Dibi;
use Dibi\Helpers;
use PDO;
/**
* The driver for PDO.
* The dibi driver for PDO.
*
* Driver options:
* - dsn => driver specific DSN
@@ -24,91 +21,132 @@ use PDO;
* - options (array) => driver specific options {@see PDO::__construct}
* - resource (PDO) => existing connection
* - version
* - lazy, profiler, result, substitutes, ... => see Dibi\Connection options
*/
class PdoDriver implements Dibi\Driver
class PdoDriver implements Dibi\Driver, Dibi\ResultDriver
{
private ?PDO $connection;
private ?int $affectedRows;
private string $driverName;
private string $serverVersion = '';
use Dibi\Strict;
/** @var PDO Connection resource */
private $connection;
/** @var \PDOStatement|NULL Resultset resource */
private $resultSet;
/** @var int|FALSE Affected rows */
private $affectedRows = FALSE;
/** @var string */
private $driverName;
/** @var string */
private $serverVersion = '';
/** @throws Dibi\NotSupportedException */
public function __construct(array $config)
/**
* @throws Dibi\NotSupportedException
*/
public function __construct()
{
if (!extension_loaded('pdo')) {
throw new Dibi\NotSupportedException("PHP extension 'pdo' is not loaded.");
}
}
/**
* Connects to a database.
* @return void
* @throws Dibi\Exception
*/
public function connect(array &$config)
{
$foo = &$config['dsn'];
$foo = &$config['options'];
Helpers::alias($config, 'resource', 'pdo');
Dibi\Helpers::alias($config, 'resource', 'pdo');
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());
}
}
$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
$this->serverVersion = isset($config['version'])
? $config['version']
: @$this->connection->getAttribute(PDO::ATTR_SERVER_VERSION); // @ - may be not supported
}
/**
* Disconnects from a database.
* @return void
*/
public function disconnect(): void
public function disconnect()
{
$this->connection = null;
$this->connection = NULL;
}
/**
* Executes the SQL query.
* @param string SQL statement.
* @return Dibi\ResultDriver|NULL
* @throws Dibi\DriverException
*/
public function query(string $sql): ?Dibi\ResultDriver
public function query($sql)
{
$res = $this->connection->query($sql);
if ($res) {
$this->affectedRows = $res->rowCount();
return $res->columnCount() ? $this->createResultDriver($res) : null;
// must detect if SQL returns result set or num of affected rows
$cmd = strtoupper(substr(ltrim($sql), 0, 6));
static $list = ['UPDATE' => 1, 'DELETE' => 1, 'INSERT' => 1, 'REPLAC' => 1];
$this->affectedRows = FALSE;
if (isset($list[$cmd])) {
$this->affectedRows = $this->connection->exec($sql);
if ($this->affectedRows !== FALSE) {
return NULL;
}
} else {
$res = $this->connection->query($sql);
if ($res) {
return $this->createResultDriver($res);
}
}
$this->affectedRows = null;
[$sqlState, $code, $message] = $this->connection->errorInfo();
$code ??= 0;
list($sqlState, $code, $message) = $this->connection->errorInfo();
$message = "SQLSTATE[$sqlState]: $message";
throw match ($this->driverName) {
'mysql' => MySqliDriver::createException($message, $code, $sql),
'oci' => OracleDriver::createException($message, $code, $sql),
'pgsql' => PostgreDriver::createException($message, $sqlState, $sql),
'sqlite' => SqliteDriver::createException($message, $code, $sql),
default => new Dibi\DriverException($message, $code, $sql),
};
switch ($this->driverName) {
case 'mysql':
throw MySqliDriver::createException($message, $code, $sql);
case 'oci':
throw OracleDriver::createException($message, $code, $sql);
case 'pgsql':
throw PostgreDriver::createException($message, $sqlState, $sql);
case 'sqlite':
throw Sqlite3Driver::createException($message, $code, $sql);
default:
throw new Dibi\DriverException($message, $code, $sql);
}
}
/**
* Gets the number of affected rows by the last INSERT, UPDATE or DELETE query.
* @return int|FALSE number of rows or FALSE on error
*/
public function getAffectedRows(): ?int
public function getAffectedRows()
{
return $this->affectedRows;
}
@@ -116,56 +154,64 @@ class PdoDriver implements Dibi\Driver
/**
* Retrieves the ID generated for an AUTO_INCREMENT column by the previous INSERT query.
* @return int|FALSE int on success or FALSE on failure
*/
public function getInsertId(?string $sequence): ?int
public function getInsertId($sequence)
{
return Helpers::intVal($this->connection->lastInsertId($sequence));
return $this->connection->lastInsertId();
}
/**
* Begins a transaction (if supported).
* @param string optional savepoint name
* @return void
* @throws Dibi\DriverException
*/
public function begin(?string $savepoint = null): void
public function begin($savepoint = NULL)
{
if (!$this->connection->beginTransaction()) {
$err = $this->connection->errorInfo();
throw new Dibi\DriverException("SQLSTATE[$err[0]]: $err[2]", $err[1] ?? 0);
throw new Dibi\DriverException("SQLSTATE[$err[0]]: $err[2]", $err[1]);
}
}
/**
* Commits statements in a transaction.
* @param string optional savepoint name
* @return void
* @throws Dibi\DriverException
*/
public function commit(?string $savepoint = null): void
public function commit($savepoint = NULL)
{
if (!$this->connection->commit()) {
$err = $this->connection->errorInfo();
throw new Dibi\DriverException("SQLSTATE[$err[0]]: $err[2]", $err[1] ?? 0);
throw new Dibi\DriverException("SQLSTATE[$err[0]]: $err[2]", $err[1]);
}
}
/**
* Rollback changes in a transaction.
* @param string optional savepoint name
* @return void
* @throws Dibi\DriverException
*/
public function rollback(?string $savepoint = null): void
public function rollback($savepoint = NULL)
{
if (!$this->connection->rollBack()) {
$err = $this->connection->errorInfo();
throw new Dibi\DriverException("SQLSTATE[$err[0]]: $err[2]", $err[1] ?? 0);
throw new Dibi\DriverException("SQLSTATE[$err[0]]: $err[2]", $err[1]);
}
}
/**
* Returns the connection resource.
* @return PDO
*/
public function getResource(): ?PDO
public function getResource()
{
return $this->connection;
}
@@ -173,26 +219,33 @@ class PdoDriver implements Dibi\Driver
/**
* Returns the connection reflector.
* @return Dibi\Reflector
*/
public function getReflector(): Dibi\Reflector
public function getReflector()
{
return match ($this->driverName) {
'mysql' => new MySqlReflector($this),
'oci' => new OracleReflector($this),
'pgsql' => new PostgreReflector($this, $this->connection->getAttribute(PDO::ATTR_SERVER_VERSION)),
'sqlite' => new SqliteReflector($this),
'mssql', 'dblib', 'sqlsrv' => new SqlsrvReflector($this),
default => throw new Dibi\NotSupportedException,
};
switch ($this->driverName) {
case 'mysql':
return new MySqlReflector($this);
case 'sqlite':
return new SqliteReflector($this);
default:
throw new Dibi\NotSupportedException;
}
}
/**
* Result set driver factory.
* @param \PDOStatement
* @return Dibi\ResultDriver
*/
public function createResultDriver(\PDOStatement $result): PdoResult
public function createResultDriver(\PDOStatement $resource)
{
return new PdoResult($result, $this->driverName);
$res = clone $this;
$res->resultSet = $resource;
return $res;
}
@@ -201,41 +254,69 @@ class PdoDriver implements Dibi\Driver
/**
* Encodes data for use in a SQL statement.
* @param string value
* @return string encoded value
*/
public function escapeText(string $value): string
public function escapeText($value)
{
return match ($this->driverName) {
'odbc' => "'" . str_replace("'", "''", $value) . "'",
'sqlsrv' => "N'" . str_replace("'", "''", $value) . "'",
default => $this->connection->quote($value, PDO::PARAM_STR),
};
if ($this->driverName === 'odbc') {
return "'" . str_replace("'", "''", $value) . "'";
} else {
return $this->connection->quote($value, PDO::PARAM_STR);
}
}
public function escapeBinary(string $value): string
/**
* @param string
* @return string
*/
public function escapeBinary($value)
{
return match ($this->driverName) {
'odbc' => "'" . str_replace("'", "''", $value) . "'",
'sqlsrv' => '0x' . bin2hex($value),
default => $this->connection->quote($value, PDO::PARAM_LOB),
};
if ($this->driverName === 'odbc') {
return "'" . str_replace("'", "''", $value) . "'";
} else {
return $this->connection->quote($value, PDO::PARAM_LOB);
}
}
public function escapeIdentifier(string $value): string
/**
* @param string
* @return string
*/
public function escapeIdentifier($value)
{
return match ($this->driverName) {
'mysql' => '`' . str_replace('`', '``', $value) . '`',
'oci', 'pgsql' => '"' . str_replace('"', '""', $value) . '"',
'sqlite' => '[' . strtr($value, '[]', ' ') . ']',
'odbc', 'mssql' => '[' . str_replace(['[', ']'], ['[[', ']]'], $value) . ']',
'dblib', 'sqlsrv' => '[' . str_replace(']', ']]', $value) . ']',
default => $value,
};
switch ($this->driverName) {
case 'mysql':
return '`' . str_replace('`', '``', $value) . '`';
case 'oci':
case 'pgsql':
return '"' . str_replace('"', '""', $value) . '"';
case 'sqlite':
return '[' . strtr($value, '[]', ' ') . ']';
case 'odbc':
case 'mssql':
return '[' . str_replace(['[', ']'], ['[[', ']]'], $value) . ']';
case 'dblib':
case 'sqlsrv':
return '[' . str_replace(']', ']]', $value) . ']';
default:
return $value;
}
}
public function escapeBool(bool $value): string
/**
* @param bool
* @return string
*/
public function escapeBool($value)
{
if ($this->driverName === 'pgsql') {
return $value ? 'TRUE' : 'FALSE';
@@ -245,59 +326,66 @@ class PdoDriver implements Dibi\Driver
}
public function escapeDate(\DateTimeInterface $value): string
/**
* @param \DateTime|\DateTimeInterface|string|int
* @return string
*/
public function escapeDate($value)
{
if (!$value instanceof \DateTime && !$value instanceof \DateTimeInterface) {
$value = new Dibi\DateTime($value);
}
return $value->format($this->driverName === 'odbc' ? '#m/d/Y#' : "'Y-m-d'");
}
public function escapeDateTime(\DateTimeInterface $value): string
/**
* @param \DateTime|\DateTimeInterface|string|int
* @return string
*/
public function escapeDateTime($value)
{
return match ($this->driverName) {
'odbc' => $value->format('#m/d/Y H:i:s.u#'),
'mssql', 'dblib', 'sqlsrv' => 'CONVERT(DATETIME2(7), ' . $value->format("'Y-m-d H:i:s.u'") . ')',
default => $value->format("'Y-m-d H:i:s.u'"),
};
}
public function escapeDateInterval(\DateInterval $value): string
{
throw new Dibi\NotImplementedException;
if (!$value instanceof \DateTime && !$value instanceof \DateTimeInterface) {
$value = new Dibi\DateTime($value);
}
return $value->format($this->driverName === 'odbc' ? "#m/d/Y H:i:s#" : "'Y-m-d H:i:s'");
}
/**
* Encodes string for use in a LIKE statement.
* @param string
* @param int
* @return string
*/
public function escapeLike(string $value, int $pos): string
public function escapeLike($value, $pos)
{
switch ($this->driverName) {
case 'mysql':
$value = addcslashes(str_replace('\\', '\\\\', $value), "\x00\n\r\\'%_");
return ($pos & 1 ? "'%" : "'") . $value . ($pos & 2 ? "%'" : "'");
return ($pos <= 0 ? "'%" : "'") . $value . ($pos >= 0 ? "%'" : "'");
case 'oci':
$value = addcslashes(str_replace('\\', '\\\\', $value), "\x00\\%_");
$value = str_replace("'", "''", $value);
return ($pos & 1 ? "'%" : "'") . $value . ($pos & 2 ? "%'" : "'");
return ($pos <= 0 ? "'%" : "'") . $value . ($pos >= 0 ? "%'" : "'");
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 & 1 ? "'%" : "'") . $value . ($pos & 2 ? "%'" : "'");
return ($pos <= 0 ? "'%" : "'") . $value . ($pos >= 0 ? "%'" : "'");
case 'sqlite':
$value = addcslashes(substr($this->connection->quote($value, PDO::PARAM_STR), 1, -1), '%_\\');
return ($pos & 1 ? "'%" : "'") . $value . ($pos & 2 ? "%'" : "'") . " ESCAPE '\\'";
return ($pos <= 0 ? "'%" : "'") . $value . ($pos >= 0 ? "%'" : "'") . " ESCAPE '\\'";
case 'odbc':
case 'mssql':
case 'dblib':
case 'sqlsrv':
$value = strtr($value, ["'" => "''", '%' => '[%]', '_' => '[_]', '[' => '[[]']);
return ($pos & 1 ? "'%" : "'") . $value . ($pos & 2 ? "%'" : "'");
return ($pos <= 0 ? "'%" : "'") . $value . ($pos >= 0 ? "%'" : "'");
default:
throw new Dibi\NotImplementedException;
@@ -306,9 +394,32 @@ class PdoDriver implements Dibi\Driver
/**
* Injects LIMIT/OFFSET to the SQL query.
* Decodes data from result set.
* @param string
* @return string
*/
public function applyLimit(string &$sql, ?int $limit, ?int $offset): void
public function unescapeBinary($value)
{
return $value;
}
/** @deprecated */
public function escape($value, $type)
{
trigger_error(__METHOD__ . '() is deprecated.', E_USER_DEPRECATED);
return Dibi\Helpers::escape($this, $value, $type);
}
/**
* Injects LIMIT/OFFSET to the SQL query.
* @param string
* @param int|NULL
* @param int|NULL
* @return void
*/
public function applyLimit(&$sql, $limit, $offset)
{
if ($limit < 0 || $offset < 0) {
throw new Dibi\NotSupportedException('Negative offset or limit.');
@@ -316,44 +427,39 @@ class PdoDriver implements Dibi\Driver
switch ($this->driverName) {
case 'mysql':
if ($limit !== null || $offset) {
if ($limit !== NULL || $offset) {
// see http://dev.mysql.com/doc/refman/5.0/en/select.html
$sql .= ' LIMIT ' . ($limit ?? '18446744073709551615')
. ($offset ? ' OFFSET ' . $offset : '');
$sql .= ' LIMIT ' . ($limit === NULL ? '18446744073709551615' : (int) $limit)
. ($offset ? ' OFFSET ' . (int) $offset : '');
}
break;
case 'pgsql':
if ($limit !== null) {
$sql .= ' LIMIT ' . $limit;
if ($limit !== NULL) {
$sql .= ' LIMIT ' . (int) $limit;
}
if ($offset) {
$sql .= ' OFFSET ' . $offset;
$sql .= ' OFFSET ' . (int) $offset;
}
break;
case 'sqlite':
if ($limit !== null || $offset) {
$sql .= ' LIMIT ' . ($limit ?? '-1')
. ($offset ? ' OFFSET ' . $offset : '');
if ($limit !== NULL || $offset) {
$sql .= ' LIMIT ' . ($limit === NULL ? '-1' : (int) $limit)
. ($offset ? ' OFFSET ' . (int) $offset : '');
}
break;
case 'oci':
if ($offset) {
// see http://www.oracle.com/technology/oramag/oracle/06-sep/o56asktom.html
$sql = 'SELECT * FROM (SELECT t.*, ROWNUM AS "__rnum" FROM (' . $sql . ') t '
. ($limit !== null ? 'WHERE ROWNUM <= ' . ($offset + $limit) : '')
. ') WHERE "__rnum" > ' . $offset;
. ($limit !== NULL ? 'WHERE ROWNUM <= ' . ((int) $offset + (int) $limit) : '')
. ') WHERE "__rnum" > '. (int) $offset;
} elseif ($limit !== null) {
$sql = 'SELECT * FROM (' . $sql . ') WHERE ROWNUM <= ' . $limit;
} elseif ($limit !== NULL) {
$sql = 'SELECT * FROM (' . $sql . ') WHERE ROWNUM <= ' . (int) $limit;
}
break;
case 'mssql':
@@ -361,26 +467,115 @@ class PdoDriver implements Dibi\Driver
case 'dblib':
if (version_compare($this->serverVersion, '11.0') >= 0) { // 11 == SQL Server 2012
// requires ORDER BY, see https://technet.microsoft.com/en-us/library/gg699618(v=sql.110).aspx
if ($limit !== null) {
if ($limit !== NULL) {
$sql = sprintf('%s OFFSET %d ROWS FETCH NEXT %d ROWS ONLY', rtrim($sql), $offset, $limit);
} elseif ($offset) {
$sql = sprintf('%s OFFSET %d ROWS', rtrim($sql), $offset);
}
break;
}
// break omitted
// intentionally break omitted
case 'odbc':
if ($offset) {
throw new Dibi\NotSupportedException('Offset is not supported by this database.');
} elseif ($limit !== null) {
$sql = 'SELECT TOP ' . $limit . ' * FROM (' . $sql . ') t';
} elseif ($limit !== NULL) {
$sql = 'SELECT TOP ' . (int) $limit . ' * FROM (' . $sql . ') t';
break;
}
// break omitted
// intentionally break omitted
default:
throw new Dibi\NotSupportedException('PDO or driver does not support applying limit or offset.');
}
}
/********************* result set ****************d*g**/
/**
* Returns the number of rows in a result set.
* @return int
*/
public function getRowCount()
{
return $this->resultSet->rowCount();
}
/**
* Fetches the row at current position and moves the internal cursor to the next position.
* @param bool TRUE for associative array, FALSE for numeric
* @return array array on success, nonarray if no next record
*/
public function fetch($assoc)
{
return $this->resultSet->fetch($assoc ? PDO::FETCH_ASSOC : PDO::FETCH_NUM);
}
/**
* Moves cursor position without fetching row.
* @param int the 0-based cursor pos to seek to
* @return bool TRUE on success, FALSE if unable to seek to specified record
*/
public function seek($row)
{
throw new Dibi\NotSupportedException('Cannot seek an unbuffered result set.');
}
/**
* Frees the resources allocated for this result set.
* @return void
*/
public function free()
{
$this->resultSet = NULL;
}
/**
* Returns metadata for all columns in a result set.
* @return array
* @throws Dibi\Exception
*/
public function getResultColumns()
{
$count = $this->resultSet->columnCount();
$columns = [];
for ($i = 0; $i < $count; $i++) {
$row = @$this->resultSet->getColumnMeta($i); // intentionally @
if ($row === FALSE) {
throw new Dibi\NotSupportedException('Driver does not support meta data.');
}
$row = $row + [
'table' => NULL,
'native_type' => 'VAR_STRING',
];
$columns[] = [
'name' => $row['name'],
'table' => $row['table'],
'nativetype' => $row['native_type'],
'type' => $row['native_type'] === 'TIME' && $this->driverName === 'mysql' ? Dibi\Type::TIME_INTERVAL : NULL,
'fullname' => $row['table'] ? $row['table'] . '.' . $row['name'] : $row['name'],
'vendor' => $row,
];
}
return $columns;
}
/**
* Returns the result set resource.
* @return \PDOStatement|NULL
*/
public function getResultResource()
{
return $this->resultSet;
}
}

View File

@@ -1,115 +0,0 @@
<?php
/**
* This file is part of the Dibi, smart database abstraction layer (https://dibiphp.com)
* Copyright (c) 2005 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Dibi\Drivers;
use Dibi;
use Dibi\Helpers;
use PDO;
/**
* The driver for PDO result set.
*/
class PdoResult implements Dibi\ResultDriver
{
public function __construct(
private ?\PDOStatement $resultSet,
private string $driverName,
) {
}
/**
* Returns the number of rows in a result set.
*/
public function getRowCount(): int
{
return $this->resultSet->rowCount();
}
/**
* Fetches the row at current position and moves the internal cursor to the next position.
* @param bool $assoc true for associative array, false for numeric
*/
public function fetch(bool $assoc): ?array
{
return Helpers::false2Null($this->resultSet->fetch($assoc ? PDO::FETCH_ASSOC : PDO::FETCH_NUM));
}
/**
* Moves cursor position without fetching row.
*/
public function seek(int $row): bool
{
throw new Dibi\NotSupportedException('Cannot seek an unbuffered result set.');
}
/**
* Frees the resources allocated for this result set.
*/
public function free(): void
{
$this->resultSet = null;
}
/**
* Returns metadata for all columns in a result set.
* @throws Dibi\Exception
*/
public function getResultColumns(): array
{
$count = $this->resultSet->columnCount();
$columns = [];
for ($i = 0; $i < $count; $i++) {
$row = @$this->resultSet->getColumnMeta($i); // intentionally @
if ($row === false) {
throw new Dibi\NotSupportedException('Driver does not support meta data.');
}
$row += [
'table' => null,
'native_type' => 'VAR_STRING',
];
$columns[] = [
'name' => $row['name'],
'table' => $row['table'],
'nativetype' => $row['native_type'],
'type' => $row['native_type'] === 'TIME' && $this->driverName === 'mysql' ? Dibi\Type::TimeInterval : null,
'fullname' => $row['table'] ? $row['table'] . '.' . $row['name'] : $row['name'],
'vendor' => $row,
];
}
return $columns;
}
/**
* Returns the result set resource.
*/
public function getResultResource(): ?\PDOStatement
{
return $this->resultSet;
}
/**
* Decodes data from result set.
*/
public function unescapeBinary(string $value): string
{
return $value;
}
}

View File

@@ -1,21 +1,17 @@
<?php
/**
* This file is part of the Dibi, smart database abstraction layer (https://dibiphp.com)
* This file is part of the "dibi" - smart database abstraction layer.
* Copyright (c) 2005 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Dibi\Drivers;
use Dibi;
use Dibi\Helpers;
use PgSql;
/**
* The driver for PostgreSQL database.
* The dibi driver for PostgreSQL database.
*
* Driver options:
* - host, hostaddr, port, dbname, user, password, connect_timeout, options, sslmode, service => see PostgreSQL API
@@ -24,23 +20,44 @@ use PgSql;
* - 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()
* - lazy, profiler, result, substitutes, ... => see Dibi\Connection options
*/
class PostgreDriver implements Dibi\Driver
class PostgreDriver implements Dibi\Driver, Dibi\ResultDriver, Dibi\Reflector
{
/** @var resource|PgSql\Connection */
use Dibi\Strict;
/** @var resource|NULL */
private $connection;
private ?int $affectedRows;
/** @var resource|NULL */
private $resultSet;
/** @var bool */
private $autoFree = TRUE;
/** @var int|FALSE Affected rows */
private $affectedRows = FALSE;
/** @throws Dibi\NotSupportedException */
public function __construct(array $config)
/**
* @throws Dibi\NotSupportedException
*/
public function __construct()
{
if (!extension_loaded('pgsql')) {
throw new Dibi\NotSupportedException("PHP extension 'pgsql' is not loaded.");
}
}
$error = null;
/**
* Connects to a database.
* @return void
* @throws Dibi\Exception
*/
public function connect(array &$config)
{
$error = NULL;
if (isset($config['resource'])) {
$this->connection = $config['resource'];
@@ -52,8 +69,8 @@ class PostgreDriver implements Dibi\Driver
$string = $config['string'];
} else {
$string = '';
Helpers::alias($config, 'user', 'username');
Helpers::alias($config, 'dbname', 'database');
Dibi\Helpers::alias($config, 'user', 'username');
Dibi\Helpers::alias($config, 'dbname', 'database');
foreach (['host', 'hostaddr', 'port', 'dbname', 'user', 'password', 'connect_timeout', 'options', 'sslmode', 'service'] as $key) {
if (isset($config[$key])) {
$string .= $key . '=' . $config[$key] . ' ';
@@ -61,25 +78,25 @@ class PostgreDriver implements Dibi\Driver
}
}
$connectType = $config['connect_type'] ?? PGSQL_CONNECT_FORCE_NEW;
set_error_handler(function (int $severity, string $message) use (&$error) {
set_error_handler(function($severity, $message) use (&$error) {
$error = $message;
});
$this->connection = empty($config['persistent'])
? pg_connect($string, $connectType)
: pg_pconnect($string, $connectType);
if (empty($config['persistent'])) {
$this->connection = pg_connect($string, PGSQL_CONNECT_FORCE_NEW);
} else {
$this->connection = pg_pconnect($string, PGSQL_CONNECT_FORCE_NEW);
}
restore_error_handler();
}
if (!is_resource($this->connection) && !$this->connection instanceof PgSql\Connection) {
if (!is_resource($this->connection)) {
throw new Dibi\DriverException($error ?: 'Connecting error.');
}
pg_set_error_verbosity($this->connection, PGSQL_ERRORS_VERBOSE);
if (isset($config['charset']) && pg_set_client_encoding($this->connection, $config['charset'])) {
throw static::createException(pg_last_error($this->connection));
throw self::createException(pg_last_error($this->connection));
}
if (isset($config['schema'])) {
@@ -90,8 +107,9 @@ class PostgreDriver implements Dibi\Driver
/**
* Disconnects from a database.
* @return void
*/
public function disconnect(): void
public function disconnect()
{
@pg_close($this->connection); // @ - connection can be already disconnected
}
@@ -99,8 +117,9 @@ class PostgreDriver implements Dibi\Driver
/**
* Pings database.
* @return bool
*/
public function ping(): bool
public function ping()
{
return pg_ping($this->connection);
}
@@ -108,35 +127,39 @@ class PostgreDriver implements Dibi\Driver
/**
* Executes the SQL query.
* @param string SQL statement.
* @return Dibi\ResultDriver|NULL
* @throws Dibi\DriverException
*/
public function query(string $sql): ?Dibi\ResultDriver
public function query($sql)
{
$this->affectedRows = null;
$this->affectedRows = FALSE;
$res = @pg_query($this->connection, $sql); // intentionally @
if ($res === false) {
throw static::createException(pg_last_error($this->connection), null, $sql);
if ($res === FALSE) {
throw self::createException(pg_last_error($this->connection), NULL, $sql);
} elseif (is_resource($res) || $res instanceof PgSql\Result) {
$this->affectedRows = Helpers::false2Null(pg_affected_rows($res));
} elseif (is_resource($res)) {
$this->affectedRows = pg_affected_rows($res);
if (pg_num_fields($res)) {
return $this->createResultDriver($res);
}
}
return null;
return NULL;
}
public static function createException(string $message, $code = null, ?string $sql = null): Dibi\DriverException
/**
* @return Dibi\DriverException
*/
public static function createException($message, $code = NULL, $sql = NULL)
{
if ($code === null && preg_match('#^ERROR:\s+(\S+):\s*#', $message, $m)) {
if ($code === NULL && preg_match('#^ERROR:\s+(\S+):\s*#', $message, $m)) {
$code = $m[1];
$message = substr($message, strlen($m[0]));
}
if ($code === '0A000' && str_contains($message, 'truncate')) {
if ($code === '0A000' && strpos($message, 'truncate') !== FALSE) {
return new Dibi\ForeignKeyConstraintViolationException($message, $code, $sql);
} elseif ($code === '23502') {
@@ -156,8 +179,9 @@ class PostgreDriver implements Dibi\Driver
/**
* Gets the number of affected rows by the last INSERT, UPDATE or DELETE query.
* @return int|FALSE number of rows or FALSE on error
*/
public function getAffectedRows(): ?int
public function getAffectedRows()
{
return $this->affectedRows;
}
@@ -165,89 +189,102 @@ class PostgreDriver implements Dibi\Driver
/**
* Retrieves the ID generated for an AUTO_INCREMENT column by the previous INSERT query.
* @return int|FALSE int on success or FALSE on failure
*/
public function getInsertId(?string $sequence): ?int
public function getInsertId($sequence)
{
$res = $sequence === null
? $this->query('SELECT LASTVAL()') // PostgreSQL 8.1 is needed
: $this->query("SELECT CURRVAL('$sequence')");
if (!$res) {
return null;
if ($sequence === NULL) {
// PostgreSQL 8.1 is needed
$res = $this->query('SELECT LASTVAL()');
} else {
$res = $this->query("SELECT CURRVAL('$sequence')");
}
$row = $res->fetch(false);
return is_array($row) ? (int) $row[0] : null;
if (!$res) {
return FALSE;
}
$row = $res->fetch(FALSE);
return is_array($row) ? $row[0] : FALSE;
}
/**
* Begins a transaction (if supported).
* @param string optional savepoint name
* @return void
* @throws Dibi\DriverException
*/
public function begin(?string $savepoint = null): void
public function begin($savepoint = NULL)
{
$this->query($savepoint ? "SAVEPOINT {$this->escapeIdentifier($savepoint)}" : 'START TRANSACTION');
$this->query($savepoint ? "SAVEPOINT $savepoint" : 'START TRANSACTION');
}
/**
* Commits statements in a transaction.
* @param string optional savepoint name
* @return void
* @throws Dibi\DriverException
*/
public function commit(?string $savepoint = null): void
public function commit($savepoint = NULL)
{
$this->query($savepoint ? "RELEASE SAVEPOINT {$this->escapeIdentifier($savepoint)}" : 'COMMIT');
$this->query($savepoint ? "RELEASE SAVEPOINT $savepoint" : 'COMMIT');
}
/**
* Rollback changes in a transaction.
* @param string optional savepoint name
* @return void
* @throws Dibi\DriverException
*/
public function rollback(?string $savepoint = null): void
public function rollback($savepoint = NULL)
{
$this->query($savepoint ? "ROLLBACK TO SAVEPOINT {$this->escapeIdentifier($savepoint)}" : 'ROLLBACK');
$this->query($savepoint ? "ROLLBACK TO SAVEPOINT $savepoint" : 'ROLLBACK');
}
/**
* Is in transaction?
* @return bool
*/
public function inTransaction(): bool
public function inTransaction()
{
return !in_array(pg_transaction_status($this->connection), [PGSQL_TRANSACTION_UNKNOWN, PGSQL_TRANSACTION_IDLE], true);
return !in_array(pg_transaction_status($this->connection), [PGSQL_TRANSACTION_UNKNOWN, PGSQL_TRANSACTION_IDLE], TRUE);
}
/**
* Returns the connection resource.
* @return resource|null
* @return resource|NULL
*/
public function getResource(): mixed
public function getResource()
{
return is_resource($this->connection) || $this->connection instanceof PgSql\Connection
? $this->connection
: null;
return is_resource($this->connection) ? $this->connection : NULL;
}
/**
* Returns the connection reflector.
* @return Dibi\Reflector
*/
public function getReflector(): Dibi\Reflector
public function getReflector()
{
return new PostgreReflector($this, pg_parameter_status($this->connection, 'server_version'));
return $this;
}
/**
* Result set driver factory.
* @param resource $resource
* @param resource
* @return Dibi\ResultDriver
*/
public function createResultDriver($resource): PostgreResult
public function createResultDriver($resource)
{
return new PostgreResult($resource);
$res = clone $this;
$res->resultSet = $resource;
return $res;
}
@@ -256,85 +293,452 @@ class PostgreDriver implements Dibi\Driver
/**
* Encodes data for use in a SQL statement.
* @param string value
* @return string encoded value
*/
public function escapeText(string $value): string
public function escapeText($value)
{
if (!$this->getResource()) {
if (!is_resource($this->connection)) {
throw new Dibi\Exception('Lost connection to server.');
}
return "'" . pg_escape_string($this->connection, $value) . "'";
}
public function escapeBinary(string $value): string
/**
* @param string
* @return string
*/
public function escapeBinary($value)
{
if (!$this->getResource()) {
if (!is_resource($this->connection)) {
throw new Dibi\Exception('Lost connection to server.');
}
return "'" . pg_escape_bytea($this->connection, $value) . "'";
}
public function escapeIdentifier(string $value): string
/**
* @param string
* @return string
*/
public function escapeIdentifier($value)
{
// @see http://www.postgresql.org/docs/8.2/static/sql-syntax-lexical.html#SQL-SYNTAX-IDENTIFIERS
return '"' . str_replace('"', '""', $value) . '"';
}
public function escapeBool(bool $value): string
/**
* @param bool
* @return string
*/
public function escapeBool($value)
{
return $value ? 'TRUE' : 'FALSE';
}
public function escapeDate(\DateTimeInterface $value): string
/**
* @param \DateTime|\DateTimeInterface|string|int
* @return string
*/
public function escapeDate($value)
{
if (!$value instanceof \DateTime && !$value instanceof \DateTimeInterface) {
$value = new Dibi\DateTime($value);
}
return $value->format("'Y-m-d'");
}
public function escapeDateTime(\DateTimeInterface $value): string
/**
* @param \DateTime|\DateTimeInterface|string|int
* @return string
*/
public function escapeDateTime($value)
{
return $value->format("'Y-m-d H:i:s.u'");
}
public function escapeDateInterval(\DateInterval $value): string
{
throw new Dibi\NotImplementedException;
if (!$value instanceof \DateTime && !$value instanceof \DateTimeInterface) {
$value = new Dibi\DateTime($value);
}
return $value->format("'Y-m-d H:i:s'");
}
/**
* Encodes string for use in a LIKE statement.
* @param string
* @param int
* @return string
*/
public function escapeLike(string $value, int $pos): string
public function escapeLike($value, $pos)
{
$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 & 1 ? "'%" : "'") . $value . ($pos & 2 ? "%'" : "'");
return ($pos <= 0 ? "'%" : "'") . $value . ($pos >= 0 ? "%'" : "'");
}
/**
* Decodes data from result set.
* @param string
* @return string
*/
public function unescapeBinary($value)
{
return pg_unescape_bytea($value);
}
/** @deprecated */
public function escape($value, $type)
{
trigger_error(__METHOD__ . '() is deprecated.', E_USER_DEPRECATED);
return Dibi\Helpers::escape($this, $value, $type);
}
/**
* Injects LIMIT/OFFSET to the SQL query.
* @param string
* @param int|NULL
* @param int|NULL
* @return void
*/
public function applyLimit(string &$sql, ?int $limit, ?int $offset): void
public function applyLimit(&$sql, $limit, $offset)
{
if ($limit < 0 || $offset < 0) {
throw new Dibi\NotSupportedException('Negative offset or limit.');
}
if ($limit !== null) {
$sql .= ' LIMIT ' . $limit;
if ($limit !== NULL) {
$sql .= ' LIMIT ' . (int) $limit;
}
if ($offset) {
$sql .= ' OFFSET ' . $offset;
$sql .= ' OFFSET ' . (int) $offset;
}
}
/********************* result set ****************d*g**/
/**
* Automatically frees the resources allocated for this result set.
* @return void
*/
public function __destruct()
{
$this->autoFree && $this->getResultResource() && $this->free();
}
/**
* Returns the number of rows in a result set.
* @return int
*/
public function getRowCount()
{
return pg_num_rows($this->resultSet);
}
/**
* Fetches the row at current position and moves the internal cursor to the next position.
* @param bool TRUE for associative array, FALSE for numeric
* @return array array on success, nonarray if no next record
*/
public function fetch($assoc)
{
return pg_fetch_array($this->resultSet, NULL, $assoc ? PGSQL_ASSOC : PGSQL_NUM);
}
/**
* Moves cursor position without fetching row.
* @param int the 0-based cursor pos to seek to
* @return bool TRUE on success, FALSE if unable to seek to specified record
*/
public function seek($row)
{
return pg_result_seek($this->resultSet, $row);
}
/**
* Frees the resources allocated for this result set.
* @return void
*/
public function free()
{
pg_free_result($this->resultSet);
$this->resultSet = NULL;
}
/**
* Returns metadata for all columns in a result set.
* @return array
*/
public function getResultColumns()
{
$count = pg_num_fields($this->resultSet);
$columns = [];
for ($i = 0; $i < $count; $i++) {
$row = [
'name' => pg_field_name($this->resultSet, $i),
'table' => pg_field_table($this->resultSet, $i),
'nativetype' => pg_field_type($this->resultSet, $i),
];
$row['fullname'] = $row['table'] ? $row['table'] . '.' . $row['name'] : $row['name'];
$columns[] = $row;
}
return $columns;
}
/**
* Returns the result set resource.
* @return resource|NULL
*/
public function getResultResource()
{
$this->autoFree = FALSE;
return is_resource($this->resultSet) ? $this->resultSet : NULL;
}
/********************* Dibi\Reflector ****************d*g**/
/**
* Returns list of tables.
* @return array
*/
public function getTables()
{
$version = pg_parameter_status($this->getResource(), 'server_version');
if ($version < 7.4) {
throw new Dibi\DriverException('Reflection requires PostgreSQL 7.4 and newer.');
}
$query = "
SELECT
table_name AS name,
CASE table_type
WHEN 'VIEW' THEN 1
ELSE 0
END AS view
FROM
information_schema.tables
WHERE
table_schema = ANY (current_schemas(false))";
if ($version >= 9.3) {
$query .= '
UNION ALL
SELECT
matviewname, 1
FROM
pg_matviews
WHERE
schemaname = ANY (current_schemas(false))';
}
$res = $this->query($query);
$tables = pg_fetch_all($res->resultSet);
return $tables ? $tables : [];
}
/**
* Returns metadata for all columns in a table.
* @param string
* @return array
*/
public function getColumns($table)
{
$_table = $this->escapeText($this->escapeIdentifier($table));
$res = $this->query("
SELECT indkey
FROM pg_class
LEFT JOIN pg_index on pg_class.oid = pg_index.indrelid AND pg_index.indisprimary
WHERE pg_class.oid = $_table::regclass
");
$primary = (int) pg_fetch_object($res->resultSet)->indkey;
$res = $this->query("
SELECT *
FROM information_schema.columns c
JOIN pg_class ON pg_class.relname = c.table_name
JOIN pg_namespace nsp ON nsp.oid = pg_class.relnamespace AND nsp.nspname = c.table_schema
WHERE pg_class.oid = $_table::regclass
ORDER BY c.ordinal_position
");
if (!$res->getRowCount()) {
$res = $this->query("
SELECT
a.attname AS column_name,
pg_type.typname AS udt_name,
a.attlen AS numeric_precision,
a.atttypmod-4 AS character_maximum_length,
NOT a.attnotnull AS is_nullable,
a.attnum AS ordinal_position,
adef.adsrc AS column_default
FROM
pg_attribute a
JOIN pg_type ON a.atttypid = pg_type.oid
JOIN pg_class cls ON a.attrelid = cls.oid
LEFT JOIN pg_attrdef adef ON adef.adnum = a.attnum AND adef.adrelid = a.attrelid
WHERE
cls.relkind IN ('r', 'v', 'mv')
AND a.attrelid = $_table::regclass
AND a.attnum > 0
AND NOT a.attisdropped
ORDER BY ordinal_position
");
}
$columns = [];
while ($row = $res->fetch(TRUE)) {
$size = (int) max($row['character_maximum_length'], $row['numeric_precision']);
$columns[] = [
'name' => $row['column_name'],
'table' => $table,
'nativetype' => strtoupper($row['udt_name']),
'size' => $size > 0 ? $size : NULL,
'nullable' => $row['is_nullable'] === 'YES' || $row['is_nullable'] === 't',
'default' => $row['column_default'],
'autoincrement' => (int) $row['ordinal_position'] === $primary && substr($row['column_default'], 0, 7) === 'nextval',
'vendor' => $row,
];
}
return $columns;
}
/**
* Returns metadata for all indexes in a table.
* @param string
* @return array
*/
public function getIndexes($table)
{
$_table = $this->escapeText($this->escapeIdentifier($table));
$res = $this->query("
SELECT
a.attnum AS ordinal_position,
a.attname AS column_name
FROM
pg_attribute a
JOIN pg_class cls ON a.attrelid = cls.oid
WHERE
a.attrelid = $_table::regclass
AND a.attnum > 0
AND NOT a.attisdropped
ORDER BY ordinal_position
");
$columns = [];
while ($row = $res->fetch(TRUE)) {
$columns[$row['ordinal_position']] = $row['column_name'];
}
$res = $this->query("
SELECT pg_class2.relname, indisunique, indisprimary, indkey
FROM pg_class
LEFT JOIN pg_index on pg_class.oid = pg_index.indrelid
INNER JOIN pg_class as pg_class2 on pg_class2.oid = pg_index.indexrelid
WHERE pg_class.oid = $_table::regclass
");
$indexes = [];
while ($row = $res->fetch(TRUE)) {
$indexes[$row['relname']]['name'] = $row['relname'];
$indexes[$row['relname']]['unique'] = $row['indisunique'] === 't';
$indexes[$row['relname']]['primary'] = $row['indisprimary'] === 't';
foreach (explode(' ', $row['indkey']) as $index) {
$indexes[$row['relname']]['columns'][] = $columns[$index];
}
}
return array_values($indexes);
}
/**
* Returns metadata for all foreign keys in a table.
* @param string
* @return array
*/
public function getForeignKeys($table)
{
$_table = $this->escapeText($this->escapeIdentifier($table));
$res = $this->query("
SELECT
c.conname AS name,
lt.attname AS local,
c.confrelid::regclass AS table,
ft.attname AS foreign,
CASE c.confupdtype
WHEN 'a' THEN 'NO ACTION'
WHEN 'r' THEN 'RESTRICT'
WHEN 'c' THEN 'CASCADE'
WHEN 'n' THEN 'SET NULL'
WHEN 'd' THEN 'SET DEFAULT'
ELSE 'UNKNOWN'
END AS \"onUpdate\",
CASE c.confdeltype
WHEN 'a' THEN 'NO ACTION'
WHEN 'r' THEN 'RESTRICT'
WHEN 'c' THEN 'CASCADE'
WHEN 'n' THEN 'SET NULL'
WHEN 'd' THEN 'SET DEFAULT'
ELSE 'UNKNOWN'
END AS \"onDelete\",
c.conkey,
lt.attnum AS lnum,
c.confkey,
ft.attnum AS fnum
FROM
pg_constraint c
JOIN pg_attribute lt ON c.conrelid = lt.attrelid AND lt.attnum = ANY (c.conkey)
JOIN pg_attribute ft ON c.confrelid = ft.attrelid AND ft.attnum = ANY (c.confkey)
WHERE
c.contype = 'f'
AND
c.conrelid = $_table::regclass
");
$fKeys = $references = [];
while ($row = $res->fetch(TRUE)) {
if (!isset($fKeys[$row['name']])) {
$fKeys[$row['name']] = [
'name' => $row['name'],
'table' => $row['table'],
'local' => [],
'foreign' => [],
'onUpdate' => $row['onUpdate'],
'onDelete' => $row['onDelete'],
];
$l = explode(',', trim($row['conkey'], '{}'));
$f = explode(',', trim($row['confkey'], '{}'));
$references[$row['name']] = array_combine($l, $f);
}
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'];
}
}
return $fKeys;
}
}

View File

@@ -1,250 +0,0 @@
<?php
/**
* This file is part of the Dibi, smart database abstraction layer (https://dibiphp.com)
* Copyright (c) 2005 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Dibi\Drivers;
use Dibi;
/**
* The reflector for PostgreSQL database.
*/
class PostgreReflector implements Dibi\Reflector
{
public function __construct(
private Dibi\Driver $driver,
private string $version,
) {
}
/**
* Returns list of tables.
*/
public function getTables(): array
{
$query = "
SELECT
table_name AS name,
CASE table_type
WHEN 'VIEW' THEN 1
ELSE 0
END AS view
FROM
information_schema.tables
WHERE
table_schema = ANY (current_schemas(false))";
if ($this->version >= 9.3) {
$query .= '
UNION ALL
SELECT
matviewname, 1
FROM
pg_matviews
WHERE
schemaname = ANY (current_schemas(false))';
}
$res = $this->driver->query($query);
$tables = [];
while ($row = $res->fetch(true)) {
$tables[] = $row;
}
return $tables;
}
/**
* Returns metadata for all columns in a table.
*/
public function getColumns(string $table): array
{
$_table = $this->driver->escapeText($this->driver->escapeIdentifier($table));
$res = $this->driver->query("
SELECT *
FROM information_schema.columns c
JOIN pg_class ON pg_class.relname = c.table_name
JOIN pg_namespace nsp ON nsp.oid = pg_class.relnamespace AND nsp.nspname = c.table_schema
WHERE pg_class.oid = $_table::regclass
ORDER BY c.ordinal_position
");
if (!$res->getRowCount()) {
$res = $this->driver->query("
SELECT
a.attname AS column_name,
pg_type.typname AS udt_name,
a.attlen AS numeric_precision,
a.atttypmod-4 AS character_maximum_length,
NOT a.attnotnull AS is_nullable,
a.attnum AS ordinal_position,
pg_get_expr(adef.adbin, adef.adrelid) AS column_default,
CASE WHEN a.attidentity IN ('a', 'd') THEN 'YES' ELSE 'NO' END AS is_identity
FROM
pg_attribute a
JOIN pg_type ON a.atttypid = pg_type.oid
JOIN pg_class cls ON a.attrelid = cls.oid
LEFT JOIN pg_attrdef adef ON adef.adnum = a.attnum AND adef.adrelid = a.attrelid
WHERE
cls.relkind IN ('r', 'v', 'mv')
AND a.attrelid = $_table::regclass
AND a.attnum > 0
AND NOT a.attisdropped
ORDER BY ordinal_position
");
}
$columns = [];
while ($row = $res->fetch(true)) {
$size = (int) max($row['character_maximum_length'], $row['numeric_precision']);
$columns[] = [
'name' => $row['column_name'],
'table' => $table,
'nativetype' => strtoupper($row['udt_name']),
'size' => $size > 0 ? $size : null,
'nullable' => $row['is_nullable'] === 'YES' || $row['is_nullable'] === 't' || $row['is_nullable'] === true,
'default' => $row['column_default'],
'autoincrement' => $row['is_identity'] === 'YES' || str_starts_with($row['column_default'] ?? '', 'nextval('),
'vendor' => $row,
];
}
return $columns;
}
/**
* Returns metadata for all indexes in a table.
*/
public function getIndexes(string $table): array
{
$_table = $this->driver->escapeText($this->driver->escapeIdentifier($table));
$res = $this->driver->query("
SELECT
a.attnum AS ordinal_position,
a.attname AS column_name
FROM
pg_attribute a
JOIN pg_class cls ON a.attrelid = cls.oid
WHERE
a.attrelid = $_table::regclass
AND a.attnum > 0
AND NOT a.attisdropped
ORDER BY ordinal_position
");
$columns = [];
while ($row = $res->fetch(true)) {
$columns[$row['ordinal_position']] = $row['column_name'];
}
$res = $this->driver->query("
SELECT pg_class2.relname, indisunique, indisprimary, indkey
FROM pg_class
LEFT JOIN pg_index on pg_class.oid = pg_index.indrelid
INNER JOIN pg_class as pg_class2 on pg_class2.oid = pg_index.indexrelid
WHERE pg_class.oid = $_table::regclass
");
$indexes = [];
while ($row = $res->fetch(true)) {
$indexes[$row['relname']]['name'] = $row['relname'];
$indexes[$row['relname']]['unique'] = $row['indisunique'] === 't' || $row['indisunique'] === true;
$indexes[$row['relname']]['primary'] = $row['indisprimary'] === 't' || $row['indisprimary'] === true;
$indexes[$row['relname']]['columns'] = [];
foreach (explode(' ', $row['indkey']) as $index) {
if (isset($columns[$index])) {
$indexes[$row['relname']]['columns'][] = $columns[$index];
}
}
}
return array_values($indexes);
}
/**
* Returns metadata for all foreign keys in a table.
*/
public function getForeignKeys(string $table): array
{
$_table = $this->driver->escapeText($this->driver->escapeIdentifier($table));
$res = $this->driver->query("
SELECT
c.conname AS name,
lt.attname AS local,
c.confrelid::regclass AS table,
ft.attname AS foreign,
CASE c.confupdtype
WHEN 'a' THEN 'NO ACTION'
WHEN 'r' THEN 'RESTRICT'
WHEN 'c' THEN 'CASCADE'
WHEN 'n' THEN 'SET NULL'
WHEN 'd' THEN 'SET DEFAULT'
ELSE 'UNKNOWN'
END AS \"onUpdate\",
CASE c.confdeltype
WHEN 'a' THEN 'NO ACTION'
WHEN 'r' THEN 'RESTRICT'
WHEN 'c' THEN 'CASCADE'
WHEN 'n' THEN 'SET NULL'
WHEN 'd' THEN 'SET DEFAULT'
ELSE 'UNKNOWN'
END AS \"onDelete\",
c.conkey,
lt.attnum AS lnum,
c.confkey,
ft.attnum AS fnum
FROM
pg_constraint c
JOIN pg_attribute lt ON c.conrelid = lt.attrelid AND lt.attnum = ANY (c.conkey)
JOIN pg_attribute ft ON c.confrelid = ft.attrelid AND ft.attnum = ANY (c.confkey)
WHERE
c.contype = 'f'
AND
c.conrelid = $_table::regclass
");
$fKeys = $references = [];
while ($row = $res->fetch(true)) {
if (!isset($fKeys[$row['name']])) {
$fKeys[$row['name']] = [
'name' => $row['name'],
'table' => $row['table'],
'local' => [],
'foreign' => [],
'onUpdate' => $row['onUpdate'],
'onDelete' => $row['onDelete'],
];
$l = explode(',', trim($row['conkey'], '{}'));
$f = explode(',', trim($row['confkey'], '{}'));
$references[$row['name']] = array_combine($l, $f);
}
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'];
}
}
return $fKeys;
}
}

View File

@@ -1,108 +0,0 @@
<?php
/**
* This file is part of the Dibi, smart database abstraction layer (https://dibiphp.com)
* Copyright (c) 2005 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Dibi\Drivers;
use Dibi;
use Dibi\Helpers;
use PgSql;
/**
* The driver for PostgreSQL result set.
*/
class PostgreResult implements Dibi\ResultDriver
{
public function __construct(
/** @var resource|PgSql\Result */
private $resultSet,
) {
}
/**
* Returns the number of rows in a result set.
*/
public function getRowCount(): int
{
return pg_num_rows($this->resultSet);
}
/**
* Fetches the row at current position and moves the internal cursor to the next position.
* @param bool $assoc true for associative array, false for numeric
*/
public function fetch(bool $assoc): ?array
{
return Helpers::false2Null(pg_fetch_array($this->resultSet, null, $assoc ? PGSQL_ASSOC : PGSQL_NUM));
}
/**
* Moves cursor position without fetching row.
*/
public function seek(int $row): bool
{
return pg_result_seek($this->resultSet, $row);
}
/**
* Frees the resources allocated for this result set.
*/
public function free(): void
{
pg_free_result($this->resultSet);
}
/**
* Returns metadata for all columns in a result set.
*/
public function getResultColumns(): array
{
$count = pg_num_fields($this->resultSet);
$columns = [];
for ($i = 0; $i < $count; $i++) {
$row = [
'name' => pg_field_name($this->resultSet, $i),
'table' => pg_field_table($this->resultSet, $i),
'nativetype' => pg_field_type($this->resultSet, $i),
];
$row['fullname'] = $row['table']
? $row['table'] . '.' . $row['name']
: $row['name'];
$columns[] = $row;
}
return $columns;
}
/**
* Returns the result set resource.
* @return resource|PgSql\Result|null
*/
public function getResultResource(): mixed
{
return is_resource($this->resultSet) || $this->resultSet instanceof PgSql\Result
? $this->resultSet
: null;
}
/**
* Decodes data from result set.
*/
public function unescapeBinary(string $value): string
{
return pg_unescape_bytea($value);
}
}

View File

@@ -1,18 +1,496 @@
<?php
/**
* This file is part of the Dibi, smart database abstraction layer (https://dibiphp.com)
* This file is part of the "dibi" - smart database abstraction layer.
* Copyright (c) 2005 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Dibi\Drivers;
use Dibi;
use SQLite3;
/**
* Alias for SqliteDriver driver.
* The dibi driver for SQLite3 database.
*
* Driver options:
* - database (or file) => the filename of the SQLite3 database
* - formatDate => how to format date in SQL (@see date)
* - formatDateTime => how to format datetime in SQL (@see date)
* - dbcharset => database character encoding (will be converted to 'charset')
* - charset => character encoding to set (default is UTF-8)
* - resource (SQLite3) => existing connection resource
* - lazy, profiler, result, substitutes, ... => see Dibi\Connection options
*/
class Sqlite3Driver extends SqliteDriver
class Sqlite3Driver implements Dibi\Driver, Dibi\ResultDriver
{
use Dibi\Strict;
/** @var SQLite3|NULL */
private $connection;
/** @var \SQLite3Result|NULL */
private $resultSet;
/** @var bool */
private $autoFree = TRUE;
/** @var string Date and datetime format */
private $fmtDate, $fmtDateTime;
/** @var string character encoding */
private $dbcharset, $charset;
/**
* @throws Dibi\NotSupportedException
*/
public function __construct()
{
if (!extension_loaded('sqlite3')) {
throw new Dibi\NotSupportedException("PHP extension 'sqlite3' is not loaded.");
}
}
/**
* Connects to a database.
* @return void
* @throws Dibi\Exception
*/
public function connect(array &$config)
{
Dibi\Helpers::alias($config, 'database', 'file');
$this->fmtDate = isset($config['formatDate']) ? $config['formatDate'] : 'U';
$this->fmtDateTime = isset($config['formatDateTime']) ? $config['formatDateTime'] : 'U';
if (isset($config['resource']) && $config['resource'] instanceof SQLite3) {
$this->connection = $config['resource'];
} else {
try {
$this->connection = new SQLite3($config['database']);
} catch (\Exception $e) {
throw new Dibi\DriverException($e->getMessage(), $e->getCode());
}
}
$this->dbcharset = empty($config['dbcharset']) ? 'UTF-8' : $config['dbcharset'];
$this->charset = empty($config['charset']) ? 'UTF-8' : $config['charset'];
if (strcasecmp($this->dbcharset, $this->charset) === 0) {
$this->dbcharset = $this->charset = NULL;
}
// enable foreign keys support (defaultly disabled; if disabled then foreign key constraints are not enforced)
$version = SQLite3::version();
if ($version['versionNumber'] >= '3006019') {
$this->query('PRAGMA foreign_keys = ON');
}
}
/**
* Disconnects from a database.
* @return void
*/
public function disconnect()
{
$this->connection->close();
}
/**
* Executes the SQL query.
* @param string SQL statement.
* @return Dibi\ResultDriver|NULL
* @throws Dibi\DriverException
*/
public function query($sql)
{
if ($this->dbcharset !== NULL) {
$sql = iconv($this->charset, $this->dbcharset . '//IGNORE', $sql);
}
$res = @$this->connection->query($sql); // intentionally @
if ($code = $this->connection->lastErrorCode()) {
throw self::createException($this->connection->lastErrorMsg(), $code, $sql);
} elseif ($res instanceof \SQLite3Result) {
return $this->createResultDriver($res);
}
return NULL;
}
/**
* @return Dibi\DriverException
*/
public static function createException($message, $code, $sql)
{
if ($code !== 19) {
return new Dibi\DriverException($message, $code, $sql);
} elseif (strpos($message, 'must be unique') !== FALSE
|| strpos($message, 'is not unique') !== FALSE
|| strpos($message, 'UNIQUE constraint failed') !== FALSE
) {
return new Dibi\UniqueConstraintViolationException($message, $code, $sql);
} elseif (strpos($message, 'may not be NULL') !== FALSE
|| strpos($message, 'NOT NULL constraint failed') !== FALSE
) {
return new Dibi\NotNullConstraintViolationException($message, $code, $sql);
} elseif (strpos($message, 'foreign key constraint failed') !== FALSE
|| strpos($message, 'FOREIGN KEY constraint failed') !== FALSE
) {
return new Dibi\ForeignKeyConstraintViolationException($message, $code, $sql);
} else {
return new Dibi\ConstraintViolationException($message, $code, $sql);
}
}
/**
* Gets the number of affected rows by the last INSERT, UPDATE or DELETE query.
* @return int|FALSE number of rows or FALSE on error
*/
public function getAffectedRows()
{
return $this->connection->changes();
}
/**
* Retrieves the ID generated for an AUTO_INCREMENT column by the previous INSERT query.
* @return int|FALSE int on success or FALSE on failure
*/
public function getInsertId($sequence)
{
return $this->connection->lastInsertRowID();
}
/**
* Begins a transaction (if supported).
* @param string optional savepoint name
* @return void
* @throws Dibi\DriverException
*/
public function begin($savepoint = NULL)
{
$this->query($savepoint ? "SAVEPOINT $savepoint" : 'BEGIN');
}
/**
* Commits statements in a transaction.
* @param string optional savepoint name
* @return void
* @throws Dibi\DriverException
*/
public function commit($savepoint = NULL)
{
$this->query($savepoint ? "RELEASE SAVEPOINT $savepoint" : 'COMMIT');
}
/**
* Rollback changes in a transaction.
* @param string optional savepoint name
* @return void
* @throws Dibi\DriverException
*/
public function rollback($savepoint = NULL)
{
$this->query($savepoint ? "ROLLBACK TO SAVEPOINT $savepoint" : 'ROLLBACK');
}
/**
* Returns the connection resource.
* @return SQLite3
*/
public function getResource()
{
return $this->connection;
}
/**
* Returns the connection reflector.
* @return Dibi\Reflector
*/
public function getReflector()
{
return new SqliteReflector($this);
}
/**
* Result set driver factory.
* @param \SQLite3Result
* @return Dibi\ResultDriver
*/
public function createResultDriver(\SQLite3Result $resource)
{
$res = clone $this;
$res->resultSet = $resource;
return $res;
}
/********************* SQL ****************d*g**/
/**
* Encodes data for use in a SQL statement.
* @param string value
* @return string encoded value
*/
public function escapeText($value)
{
return "'" . $this->connection->escapeString($value) . "'";
}
/**
* @param string
* @return string
*/
public function escapeBinary($value)
{
return "X'" . bin2hex((string) $value) . "'";
}
/**
* @param string
* @return string
*/
public function escapeIdentifier($value)
{
return '[' . strtr($value, '[]', ' ') . ']';
}
/**
* @param bool
* @return string
*/
public function escapeBool($value)
{
return $value ? '1' : '0';
}
/**
* @param \DateTime|\DateTimeInterface|string|int
* @return string
*/
public function escapeDate($value)
{
if (!$value instanceof \DateTime && !$value instanceof \DateTimeInterface) {
$value = new Dibi\DateTime($value);
}
return $value->format($this->fmtDate);
}
/**
* @param \DateTime|\DateTimeInterface|string|int
* @return string
*/
public function escapeDateTime($value)
{
if (!$value instanceof \DateTime && !$value instanceof \DateTimeInterface) {
$value = new Dibi\DateTime($value);
}
return $value->format($this->fmtDateTime);
}
/**
* Encodes string for use in a LIKE statement.
* @param string
* @param int
* @return string
*/
public function escapeLike($value, $pos)
{
$value = addcslashes($this->connection->escapeString($value), '%_\\');
return ($pos <= 0 ? "'%" : "'") . $value . ($pos >= 0 ? "%'" : "'") . " ESCAPE '\\'";
}
/**
* Decodes data from result set.
* @param string
* @return string
*/
public function unescapeBinary($value)
{
return $value;
}
/** @deprecated */
public function escape($value, $type)
{
trigger_error(__METHOD__ . '() is deprecated.', E_USER_DEPRECATED);
return Dibi\Helpers::escape($this, $value, $type);
}
/**
* Injects LIMIT/OFFSET to the SQL query.
* @param string
* @param int|NULL
* @param int|NULL
* @return void
*/
public function applyLimit(&$sql, $limit, $offset)
{
if ($limit < 0 || $offset < 0) {
throw new Dibi\NotSupportedException('Negative offset or limit.');
} elseif ($limit !== NULL || $offset) {
$sql .= ' LIMIT ' . ($limit === NULL ? '-1' : (int) $limit)
. ($offset ? ' OFFSET ' . (int) $offset : '');
}
}
/********************* result set ****************d*g**/
/**
* Automatically frees the resources allocated for this result set.
* @return void
*/
public function __destruct()
{
$this->autoFree && $this->resultSet && @$this->free();
}
/**
* Returns the number of rows in a result set.
* @return int
* @throws Dibi\NotSupportedException
*/
public function getRowCount()
{
throw new Dibi\NotSupportedException('Row count is not available for unbuffered queries.');
}
/**
* Fetches the row at current position and moves the internal cursor to the next position.
* @param bool TRUE for associative array, FALSE for numeric
* @return array array on success, nonarray if no next record
*/
public function fetch($assoc)
{
$row = $this->resultSet->fetchArray($assoc ? SQLITE3_ASSOC : SQLITE3_NUM);
$charset = $this->charset === NULL ? NULL : $this->charset . '//TRANSLIT';
if ($row && ($assoc || $charset)) {
$tmp = [];
foreach ($row as $k => $v) {
if ($charset !== NULL && is_string($v)) {
$v = iconv($this->dbcharset, $charset, $v);
}
$tmp[str_replace(['[', ']'], '', $k)] = $v;
}
return $tmp;
}
return $row;
}
/**
* Moves cursor position without fetching row.
* @param int the 0-based cursor pos to seek to
* @return bool TRUE on success, FALSE if unable to seek to specified record
* @throws Dibi\NotSupportedException
*/
public function seek($row)
{
throw new Dibi\NotSupportedException('Cannot seek an unbuffered result set.');
}
/**
* Frees the resources allocated for this result set.
* @return void
*/
public function free()
{
$this->resultSet->finalize();
$this->resultSet = NULL;
}
/**
* Returns metadata for all columns in a result set.
* @return array
*/
public function getResultColumns()
{
$count = $this->resultSet->numColumns();
$columns = [];
static $types = [SQLITE3_INTEGER => 'int', SQLITE3_FLOAT => 'float', SQLITE3_TEXT => 'text', SQLITE3_BLOB => 'blob', SQLITE3_NULL => 'null'];
for ($i = 0; $i < $count; $i++) {
$columns[] = [
'name' => $this->resultSet->columnName($i),
'table' => NULL,
'fullname' => $this->resultSet->columnName($i),
'nativetype' => $types[$this->resultSet->columnType($i)],
];
}
return $columns;
}
/**
* Returns the result set resource.
* @return \SQLite3Result|NULL
*/
public function getResultResource()
{
$this->autoFree = FALSE;
return $this->resultSet;
}
/********************* user defined functions ****************d*g**/
/**
* Registers an user defined function for use in SQL statements.
* @param string function name
* @param mixed callback
* @param int num of arguments
* @return void
*/
public function registerFunction($name, callable $callback, $numArgs = -1)
{
$this->connection->createFunction($name, $callback, $numArgs);
}
/**
* Registers an aggregating user defined function for use in SQL statements.
* @param string function name
* @param mixed callback called for each row of the result set
* @param mixed callback called to aggregate the "stepped" data from each row
* @param int num of arguments
* @return void
*/
public function registerAggregateFunction($name, callable $rowCallback, callable $agrCallback, $numArgs = -1)
{
$this->connection->createAggregate($name, $rowCallback, $agrCallback, $numArgs);
}
}

View File

@@ -1,18 +0,0 @@
<?php
/**
* This file is part of the Dibi, smart database abstraction layer (https://dibiphp.com)
* Copyright (c) 2005 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Dibi\Drivers;
/**
* Alias for SqliteResult driver.
*/
class Sqlite3Result extends SqliteResult
{
}

View File

@@ -1,292 +0,0 @@
<?php
/**
* This file is part of the Dibi, smart database abstraction layer (https://dibiphp.com)
* Copyright (c) 2005 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Dibi\Drivers;
use Dibi;
use Dibi\Helpers;
use SQLite3;
/**
* The driver for SQLite v3 database.
*
* Driver options:
* - database (or file) => the filename of the SQLite3 database
* - formatDate => how to format date in SQL (@see date)
* - formatDateTime => how to format datetime in SQL (@see date)
* - resource (SQLite3) => existing connection resource
*/
class SqliteDriver implements Dibi\Driver
{
private SQLite3 $connection;
private string $fmtDate;
private string $fmtDateTime;
/** @throws Dibi\NotSupportedException */
public function __construct(array $config)
{
if (!extension_loaded('sqlite3')) {
throw new Dibi\NotSupportedException("PHP extension 'sqlite3' is not loaded.");
}
if (isset($config['dbcharset']) || isset($config['charset'])) {
throw new Dibi\NotSupportedException('Options dbcharset and charset are not longer supported.');
}
Helpers::alias($config, 'database', 'file');
$this->fmtDate = $config['formatDate'] ?? 'U';
$this->fmtDateTime = $config['formatDateTime'] ?? 'U';
if (isset($config['resource']) && $config['resource'] instanceof SQLite3) {
$this->connection = $config['resource'];
} else {
try {
$this->connection = new SQLite3($config['database']);
} catch (\Throwable $e) {
throw new Dibi\DriverException($e->getMessage(), $e->getCode());
}
}
// enable foreign keys support (defaultly disabled; if disabled then foreign key constraints are not enforced)
$version = SQLite3::version();
if ($version['versionNumber'] >= '3006019') {
$this->query('PRAGMA foreign_keys = ON');
}
}
/**
* Disconnects from a database.
*/
public function disconnect(): void
{
$this->connection->close();
}
/**
* Executes the SQL query.
* @throws Dibi\DriverException
*/
public function query(string $sql): ?Dibi\ResultDriver
{
$res = @$this->connection->query($sql); // intentionally @
if ($code = $this->connection->lastErrorCode()) {
throw static::createException($this->connection->lastErrorMsg(), $code, $sql);
} elseif ($res instanceof \SQLite3Result && $res->numColumns()) {
return $this->createResultDriver($res);
}
return null;
}
public static function createException(string $message, $code, string $sql): Dibi\DriverException
{
if ($code !== 19) {
return new Dibi\DriverException($message, $code, $sql);
} elseif (str_contains($message, 'must be unique')
|| str_contains($message, 'is not unique')
|| str_contains($message, 'UNIQUE constraint failed')
) {
return new Dibi\UniqueConstraintViolationException($message, $code, $sql);
} elseif (str_contains($message, 'may not be null')
|| str_contains($message, 'NOT NULL constraint failed')
) {
return new Dibi\NotNullConstraintViolationException($message, $code, $sql);
} elseif (str_contains($message, 'foreign key constraint failed')
|| str_contains($message, 'FOREIGN KEY constraint failed')
) {
return new Dibi\ForeignKeyConstraintViolationException($message, $code, $sql);
} else {
return new Dibi\ConstraintViolationException($message, $code, $sql);
}
}
/**
* Gets the number of affected rows by the last INSERT, UPDATE or DELETE query.
*/
public function getAffectedRows(): ?int
{
return $this->connection->changes();
}
/**
* Retrieves the ID generated for an AUTO_INCREMENT column by the previous INSERT query.
*/
public function getInsertId(?string $sequence): ?int
{
return $this->connection->lastInsertRowID() ?: null;
}
/**
* Begins a transaction (if supported).
* @throws Dibi\DriverException
*/
public function begin(?string $savepoint = null): void
{
$this->query($savepoint ? "SAVEPOINT $savepoint" : 'BEGIN');
}
/**
* Commits statements in a transaction.
* @throws Dibi\DriverException
*/
public function commit(?string $savepoint = null): void
{
$this->query($savepoint ? "RELEASE SAVEPOINT $savepoint" : 'COMMIT');
}
/**
* Rollback changes in a transaction.
* @throws Dibi\DriverException
*/
public function rollback(?string $savepoint = null): void
{
$this->query($savepoint ? "ROLLBACK TO SAVEPOINT $savepoint" : 'ROLLBACK');
}
/**
* Returns the connection resource.
*/
public function getResource(): ?SQLite3
{
return $this->connection;
}
/**
* Returns the connection reflector.
*/
public function getReflector(): Dibi\Reflector
{
return new SqliteReflector($this);
}
/**
* Result set driver factory.
*/
public function createResultDriver(\SQLite3Result $result): SqliteResult
{
return new SqliteResult($result);
}
/********************* SQL ****************d*g**/
/**
* Encodes data for use in a SQL statement.
*/
public function escapeText(string $value): string
{
return "'" . $this->connection->escapeString($value) . "'";
}
public function escapeBinary(string $value): string
{
return "X'" . bin2hex($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($this->fmtDate);
}
public function escapeDateTime(\DateTimeInterface $value): string
{
return $value->format($this->fmtDateTime);
}
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 = addcslashes($this->connection->escapeString($value), '%_\\');
return ($pos & 1 ? "'%" : "'") . $value . ($pos & 2 ? "%'" : "'") . " ESCAPE '\\'";
}
/**
* 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 : '');
}
}
/********************* user defined functions ****************d*g**/
/**
* Registers an user defined function for use in SQL statements.
*/
public function registerFunction(string $name, callable $callback, int $numArgs = -1): void
{
$this->connection->createFunction($name, $callback, $numArgs);
}
/**
* Registers an aggregating user defined function for use in SQL statements.
*/
public function registerAggregateFunction(
string $name,
callable $rowCallback,
callable $agrCallback,
int $numArgs = -1,
): void
{
$this->connection->createAggregate($name, $rowCallback, $agrCallback, $numArgs);
}
}

View File

@@ -1,32 +1,38 @@
<?php
/**
* This file is part of the Dibi, smart database abstraction layer (https://dibiphp.com)
* This file is part of the "dibi" - smart database abstraction layer.
* Copyright (c) 2005 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Dibi\Drivers;
use Dibi;
/**
* The reflector for SQLite database.
* The dibi reflector for SQLite database.
* @internal
*/
class SqliteReflector implements Dibi\Reflector
{
public function __construct(
private Dibi\Driver $driver,
) {
use Dibi\Strict;
/** @var Dibi\Driver */
private $driver;
public function __construct(Dibi\Driver $driver)
{
$this->driver = $driver;
}
/**
* Returns list of tables.
* @return array
*/
public function getTables(): array
public function getTables()
{
$res = $this->driver->query("
SELECT name, type = 'view' as view FROM sqlite_master WHERE type IN ('table', 'view')
@@ -35,22 +41,23 @@ class SqliteReflector implements Dibi\Reflector
ORDER BY name
");
$tables = [];
while ($row = $res->fetch(true)) {
while ($row = $res->fetch(TRUE)) {
$tables[] = $row;
}
return $tables;
}
/**
* Returns metadata for all columns in a table.
* @param string
* @return array
*/
public function getColumns(string $table): array
public function getColumns($table)
{
$res = $this->driver->query("PRAGMA table_info({$this->driver->escapeIdentifier($table)})");
$columns = [];
while ($row = $res->fetch(true)) {
while ($row = $res->fetch(TRUE)) {
$column = $row['name'];
$type = explode('(', $row['type']);
$columns[] = [
@@ -58,33 +65,34 @@ class SqliteReflector implements Dibi\Reflector
'table' => $table,
'fullname' => "$table.$column",
'nativetype' => strtoupper($type[0]),
'size' => isset($type[1]) ? (int) $type[1] : null,
'nullable' => $row['notnull'] === 0,
'size' => isset($type[1]) ? (int) $type[1] : NULL,
'nullable' => $row['notnull'] == '0',
'default' => $row['dflt_value'],
'autoincrement' => $row['pk'] && $type[0] === 'INTEGER',
'vendor' => $row,
];
}
return $columns;
}
/**
* Returns metadata for all indexes in a table.
* @param string
* @return array
*/
public function getIndexes(string $table): array
public function getIndexes($table)
{
$res = $this->driver->query("PRAGMA index_list({$this->driver->escapeIdentifier($table)})");
$indexes = [];
while ($row = $res->fetch(true)) {
while ($row = $res->fetch(TRUE)) {
$indexes[$row['name']]['name'] = $row['name'];
$indexes[$row['name']]['unique'] = (bool) $row['unique'];
}
foreach ($indexes as $index => $values) {
$res = $this->driver->query("PRAGMA index_info({$this->driver->escapeIdentifier($index)})");
while ($row = $res->fetch(true)) {
while ($row = $res->fetch(TRUE)) {
$indexes[$index]['columns'][$row['seqno']] = $row['name'];
}
}
@@ -92,24 +100,22 @@ class SqliteReflector implements Dibi\Reflector
$columns = $this->getColumns($table);
foreach ($indexes as $index => $values) {
$column = $indexes[$index]['columns'][0];
$primary = false;
$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']) {
$indexes[] = [
'name' => 'ROWID',
'unique' => true,
'primary' => true,
'unique' => TRUE,
'primary' => TRUE,
'columns' => [$column['name']],
];
break;
@@ -123,12 +129,14 @@ class SqliteReflector implements Dibi\Reflector
/**
* Returns metadata for all foreign keys in a table.
* @param string
* @return array
*/
public function getForeignKeys(string $table): array
public function getForeignKeys($table)
{
$res = $this->driver->query("PRAGMA foreign_key_list({$this->driver->escapeIdentifier($table)})");
$keys = [];
while ($row = $res->fetch(true)) {
while ($row = $res->fetch(TRUE)) {
$keys[$row['id']]['name'] = $row['id']; // foreign key name
$keys[$row['id']]['local'][$row['seq']] = $row['from']; // local columns
$keys[$row['id']]['table'] = $row['table']; // referenced table
@@ -136,11 +144,11 @@ class SqliteReflector implements Dibi\Reflector
$keys[$row['id']]['onDelete'] = $row['on_delete'];
$keys[$row['id']]['onUpdate'] = $row['on_update'];
if ($keys[$row['id']]['foreign'][0] == null) {
$keys[$row['id']]['foreign'] = null;
if ($keys[$row['id']]['foreign'][0] == NULL) {
$keys[$row['id']]['foreign'] = NULL;
}
}
return array_values($keys);
}
}

View File

@@ -1,103 +0,0 @@
<?php
/**
* This file is part of the Dibi, smart database abstraction layer (https://dibiphp.com)
* Copyright (c) 2005 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Dibi\Drivers;
use Dibi;
use Dibi\Helpers;
/**
* The driver for SQLite result set.
*/
class SqliteResult implements Dibi\ResultDriver
{
public function __construct(
private \SQLite3Result $resultSet,
) {
}
/**
* Returns the number of rows in a result set.
* @throws Dibi\NotSupportedException
*/
public function getRowCount(): int
{
throw new Dibi\NotSupportedException('Row count is not available for unbuffered queries.');
}
/**
* Fetches the row at current position and moves the internal cursor to the next position.
* @param bool $assoc true for associative array, false for numeric
*/
public function fetch(bool $assoc): ?array
{
return Helpers::false2Null($this->resultSet->fetchArray($assoc ? SQLITE3_ASSOC : SQLITE3_NUM));
}
/**
* Moves cursor position without fetching row.
* @throws Dibi\NotSupportedException
*/
public function seek(int $row): bool
{
throw new Dibi\NotSupportedException('Cannot seek an unbuffered result set.');
}
/**
* Frees the resources allocated for this result set.
*/
public function free(): void
{
$this->resultSet->finalize();
}
/**
* Returns metadata for all columns in a result set.
*/
public function getResultColumns(): array
{
$count = $this->resultSet->numColumns();
$columns = [];
$types = [SQLITE3_INTEGER => 'int', SQLITE3_FLOAT => 'float', SQLITE3_TEXT => 'text', SQLITE3_BLOB => 'blob', SQLITE3_NULL => 'null'];
for ($i = 0; $i < $count; $i++) {
$columns[] = [
'name' => $this->resultSet->columnName($i),
'table' => null,
'fullname' => $this->resultSet->columnName($i),
'nativetype' => $types[$this->resultSet->columnType($i)] ?? null, // buggy in PHP 7.4.4 & 7.3.16, bug 79414
];
}
return $columns;
}
/**
* Returns the result set resource.
*/
public function getResultResource(): \SQLite3Result
{
return $this->resultSet;
}
/**
* Decodes data from result set.
*/
public function unescapeBinary(string $value): string
{
return $value;
}
}

View File

@@ -1,20 +1,19 @@
<?php
/**
* This file is part of the Dibi, smart database abstraction layer (https://dibiphp.com)
* This file is part of the "dibi" - smart database abstraction layer.
* Copyright (c) 2005 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Dibi\Drivers;
use Dibi;
use Dibi\Connection;
use Dibi\Helpers;
/**
* The driver for Microsoft SQL Server and SQL Azure databases.
* The dibi driver for Microsoft SQL Server and SQL Azure databases.
*
* Driver options:
* - host => the MS SQL server host name. It can also include a port number (hostname:port)
@@ -24,22 +23,46 @@ use Dibi\Helpers;
* - options (array) => connection options {@link https://msdn.microsoft.com/en-us/library/cc296161(SQL.90).aspx}
* - charset => character encoding to set (default is UTF-8)
* - resource (resource) => existing connection resource
* - lazy, profiler, result, substitutes, ... => see Dibi\Connection options
*/
class SqlsrvDriver implements Dibi\Driver
class SqlsrvDriver implements Dibi\Driver, Dibi\ResultDriver
{
/** @var resource */
use Dibi\Strict;
/** @var resource|NULL */
private $connection;
private ?int $affectedRows;
private string $version = '';
/** @var resource|NULL */
private $resultSet;
/** @var bool */
private $autoFree = TRUE;
/** @var int|FALSE Affected rows */
private $affectedRows = FALSE;
/** @var string */
private $version = '';
/** @throws Dibi\NotSupportedException */
public function __construct(array $config)
/**
* @throws Dibi\NotSupportedException
*/
public function __construct()
{
if (!extension_loaded('sqlsrv')) {
throw new Dibi\NotSupportedException("PHP extension 'sqlsrv' is not loaded.");
}
}
/**
* Connects to a database.
* @return void
* @throws Dibi\Exception
*/
public function connect(array &$config)
{
Helpers::alias($config, 'options|UID', 'username');
Helpers::alias($config, 'options|PWD', 'password');
Helpers::alias($config, 'options|Database', 'database');
@@ -47,36 +70,34 @@ class SqlsrvDriver implements Dibi\Driver
if (isset($config['resource'])) {
$this->connection = $config['resource'];
if (!is_resource($this->connection)) {
throw new \InvalidArgumentException("Configuration option 'resource' is not resource.");
}
} else {
$options = $config['options'];
// Default values
$options['CharacterSet'] ??= 'UTF-8';
if (!isset($options['CharacterSet'])) {
$options['CharacterSet'] = 'UTF-8';
}
$options['PWD'] = (string) $options['PWD'];
$options['UID'] = (string) $options['UID'];
$options['Database'] = (string) $options['Database'];
sqlsrv_configure('WarningsReturnAsErrors', 0);
$this->connection = sqlsrv_connect($config['host'], $options);
if (!is_resource($this->connection)) {
$info = sqlsrv_errors(SQLSRV_ERR_ERRORS);
throw new Dibi\DriverException($info[0]['message'], $info[0]['code']);
}
sqlsrv_configure('WarningsReturnAsErrors', 1);
}
if (!is_resource($this->connection)) {
$info = sqlsrv_errors();
throw new Dibi\DriverException($info[0]['message'], $info[0]['code']);
}
$this->version = sqlsrv_server_info($this->connection)['SQLServerVersion'];
}
/**
* Disconnects from a database.
* @return void
*/
public function disconnect(): void
public function disconnect()
{
@sqlsrv_close($this->connection); // @ - connection can be already disconnected
}
@@ -84,32 +105,32 @@ class SqlsrvDriver implements Dibi\Driver
/**
* Executes the SQL query.
* @param string SQL statement.
* @return Dibi\ResultDriver|NULL
* @throws Dibi\DriverException
*/
public function query(string $sql): ?Dibi\ResultDriver
public function query($sql)
{
$this->affectedRows = null;
$this->affectedRows = FALSE;
$res = sqlsrv_query($this->connection, $sql);
if ($res === false) {
if ($res === FALSE) {
$info = sqlsrv_errors();
throw new Dibi\DriverException($info[0]['message'], $info[0]['code'], $sql);
} elseif (is_resource($res)) {
$this->affectedRows = Helpers::false2Null(sqlsrv_rows_affected($res));
return sqlsrv_num_fields($res)
? $this->createResultDriver($res)
: null;
$this->affectedRows = sqlsrv_rows_affected($res);
return $this->createResultDriver($res);
}
return null;
return NULL;
}
/**
* Gets the number of affected rows by the last INSERT, UPDATE or DELETE query.
* @return int|FALSE number of rows or FALSE on error
*/
public function getAffectedRows(): ?int
public function getAffectedRows()
{
return $this->affectedRows;
}
@@ -117,24 +138,26 @@ class SqlsrvDriver implements Dibi\Driver
/**
* Retrieves the ID generated for an AUTO_INCREMENT column by the previous INSERT query.
* @return int|FALSE int on success or FALSE on failure
*/
public function getInsertId(?string $sequence): ?int
public function getInsertId($sequence)
{
$res = sqlsrv_query($this->connection, 'SELECT SCOPE_IDENTITY()');
if (is_resource($res)) {
$row = sqlsrv_fetch_array($res, SQLSRV_FETCH_NUMERIC);
return Dibi\Helpers::intVal($row[0]);
return $row[0];
}
return null;
return FALSE;
}
/**
* Begins a transaction (if supported).
* @param string optional savepoint name
* @return void
* @throws Dibi\DriverException
*/
public function begin(?string $savepoint = null): void
public function begin($savepoint = NULL)
{
sqlsrv_begin_transaction($this->connection);
}
@@ -142,9 +165,11 @@ class SqlsrvDriver implements Dibi\Driver
/**
* Commits statements in a transaction.
* @param string optional savepoint name
* @return void
* @throws Dibi\DriverException
*/
public function commit(?string $savepoint = null): void
public function commit($savepoint = NULL)
{
sqlsrv_commit($this->connection);
}
@@ -152,9 +177,11 @@ class SqlsrvDriver implements Dibi\Driver
/**
* Rollback changes in a transaction.
* @param string optional savepoint name
* @return void
* @throws Dibi\DriverException
*/
public function rollback(?string $savepoint = null): void
public function rollback($savepoint = NULL)
{
sqlsrv_rollback($this->connection);
}
@@ -162,18 +189,19 @@ class SqlsrvDriver implements Dibi\Driver
/**
* Returns the connection resource.
* @return resource|null
* @return resource|NULL
*/
public function getResource(): mixed
public function getResource()
{
return is_resource($this->connection) ? $this->connection : null;
return is_resource($this->connection) ? $this->connection : NULL;
}
/**
* Returns the connection reflector.
* @return Dibi\Reflector
*/
public function getReflector(): Dibi\Reflector
public function getReflector()
{
return new SqlsrvReflector($this);
}
@@ -181,11 +209,14 @@ class SqlsrvDriver implements Dibi\Driver
/**
* Result set driver factory.
* @param resource $resource
* @param resource
* @return Dibi\ResultDriver
*/
public function createResultDriver($resource): SqlsrvResult
public function createResultDriver($resource)
{
return new SqlsrvResult($resource);
$res = clone $this;
$res->resultSet = $resource;
return $res;
}
@@ -194,64 +225,112 @@ class SqlsrvDriver implements Dibi\Driver
/**
* Encodes data for use in a SQL statement.
* @param string value
* @return string encoded value
*/
public function escapeText(string $value): string
public function escapeText($value)
{
return "N'" . str_replace("'", "''", $value) . "'";
return "'" . str_replace("'", "''", $value) . "'";
}
public function escapeBinary(string $value): string
/**
* @param string
* @return string
*/
public function escapeBinary($value)
{
return '0x' . bin2hex($value);
return "'" . str_replace("'", "''", $value) . "'";
}
public function escapeIdentifier(string $value): string
/**
* @param string
* @return string
*/
public function escapeIdentifier($value)
{
// @see https://msdn.microsoft.com/en-us/library/ms176027.aspx
return '[' . str_replace(']', ']]', $value) . ']';
}
public function escapeBool(bool $value): string
/**
* @param bool
* @return string
*/
public function escapeBool($value)
{
return $value ? '1' : '0';
}
public function escapeDate(\DateTimeInterface $value): string
/**
* @param \DateTime|\DateTimeInterface|string|int
* @return string
*/
public function escapeDate($value)
{
if (!$value instanceof \DateTime && !$value instanceof \DateTimeInterface) {
$value = new Dibi\DateTime($value);
}
return $value->format("'Y-m-d'");
}
public function escapeDateTime(\DateTimeInterface $value): string
/**
* @param \DateTime|\DateTimeInterface|string|int
* @return string
*/
public function escapeDateTime($value)
{
return 'CONVERT(DATETIME2(7), ' . $value->format("'Y-m-d H:i:s.u'") . ')';
}
public function escapeDateInterval(\DateInterval $value): string
{
throw new Dibi\NotImplementedException;
if (!$value instanceof \DateTime && !$value instanceof \DateTimeInterface) {
$value = new Dibi\DateTime($value);
}
return $value->format("'Y-m-d H:i:s'");
}
/**
* Encodes string for use in a LIKE statement.
* @param string
* @param int
* @return string
*/
public function escapeLike(string $value, int $pos): string
public function escapeLike($value, $pos)
{
$value = strtr($value, ["'" => "''", '%' => '[%]', '_' => '[_]', '[' => '[[]']);
return ($pos & 1 ? "'%" : "'") . $value . ($pos & 2 ? "%'" : "'");
return ($pos <= 0 ? "'%" : "'") . $value . ($pos >= 0 ? "%'" : "'");
}
/**
* Decodes data from result set.
* @param string
* @return string
*/
public function unescapeBinary($value)
{
return $value;
}
/** @deprecated */
public function escape($value, $type)
{
trigger_error(__METHOD__ . '() is deprecated.', E_USER_DEPRECATED);
return Helpers::escape($this, $value, $type);
}
/**
* Injects LIMIT/OFFSET to the SQL query.
* @param string
* @param int|NULL
* @param int|NULL
* @return void
*/
public function applyLimit(string &$sql, ?int $limit, ?int $offset): void
public function applyLimit(&$sql, $limit, $offset)
{
if ($limit < 0 || $offset < 0) {
throw new Dibi\NotSupportedException('Negative offset or limit.');
@@ -260,10 +339,11 @@ class SqlsrvDriver implements Dibi\Driver
if ($offset) {
throw new Dibi\NotSupportedException('Offset is not supported by this database.');
} elseif ($limit !== null) {
} elseif ($limit !== NULL) {
$sql = sprintf('SELECT TOP (%d) * FROM (%s) t', $limit, $sql);
}
} elseif ($limit !== null) {
} 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);
} elseif ($offset) {
@@ -271,4 +351,90 @@ class SqlsrvDriver implements Dibi\Driver
$sql = sprintf('%s OFFSET %d ROWS', rtrim($sql), $offset);
}
}
/********************* result set ****************d*g**/
/**
* Automatically frees the resources allocated for this result set.
* @return void
*/
public function __destruct()
{
$this->autoFree && $this->getResultResource() && $this->free();
}
/**
* Returns the number of rows in a result set.
* @return int
*/
public function getRowCount()
{
throw new Dibi\NotSupportedException('Row count is not available for unbuffered queries.');
}
/**
* Fetches the row at current position and moves the internal cursor to the next position.
* @param bool TRUE for associative array, FALSE for numeric
* @return array array on success, nonarray if no next record
*/
public function fetch($assoc)
{
return sqlsrv_fetch_array($this->resultSet, $assoc ? SQLSRV_FETCH_ASSOC : SQLSRV_FETCH_NUMERIC);
}
/**
* Moves cursor position without fetching row.
* @param int the 0-based cursor pos to seek to
* @return bool TRUE on success, FALSE if unable to seek to specified record
*/
public function seek($row)
{
throw new Dibi\NotSupportedException('Cannot seek an unbuffered result set.');
}
/**
* Frees the resources allocated for this result set.
* @return void
*/
public function free()
{
sqlsrv_free_stmt($this->resultSet);
$this->resultSet = NULL;
}
/**
* Returns metadata for all columns in a result set.
* @return array
*/
public function getResultColumns()
{
$columns = [];
foreach ((array) sqlsrv_field_metadata($this->resultSet) as $fieldMetadata) {
$columns[] = [
'name' => $fieldMetadata['Name'],
'fullname' => $fieldMetadata['Name'],
'nativetype' => $fieldMetadata['Type'],
];
}
return $columns;
}
/**
* Returns the result set resource.
* @return resource|NULL
*/
public function getResultResource()
{
$this->autoFree = FALSE;
return is_resource($this->resultSet) ? $this->resultSet : NULL;
}
}

View File

@@ -1,50 +1,57 @@
<?php
/**
* This file is part of the Dibi, smart database abstraction layer (https://dibiphp.com)
* This file is part of the "dibi" - smart database abstraction layer.
* Copyright (c) 2005 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Dibi\Drivers;
use Dibi;
/**
* The reflector for Microsoft SQL Server and SQL Azure databases.
* The dibi reflector for Microsoft SQL Server and SQL Azure databases.
* @internal
*/
class SqlsrvReflector implements Dibi\Reflector
{
public function __construct(
private Dibi\Driver $driver,
) {
use Dibi\Strict;
/** @var Dibi\Driver */
private $driver;
public function __construct(Dibi\Driver $driver)
{
$this->driver = $driver;
}
/**
* Returns list of tables.
* @return array
*/
public function getTables(): array
public function getTables()
{
$res = $this->driver->query("SELECT TABLE_NAME, TABLE_TYPE FROM INFORMATION_SCHEMA.TABLES WHERE [TABLE_SCHEMA] = 'dbo'");
$tables = [];
while ($row = $res->fetch(false)) {
while ($row = $res->fetch(FALSE)) {
$tables[] = [
'name' => $row[0],
'view' => isset($row[1]) && $row[1] === 'VIEW',
];
}
return $tables;
}
/**
* Returns metadata for all columns in a table.
* @param string
* @return array
*/
public function getColumns(string $table): array
public function getColumns($table)
{
$res = $this->driver->query("
SELECT c.name as COLUMN_NAME, c.is_identity AS AUTO_INCREMENT
@@ -54,7 +61,7 @@ class SqlsrvReflector implements Dibi\Reflector
");
$autoIncrements = [];
while ($row = $res->fetch(true)) {
while ($row = $res->fetch(TRUE)) {
$autoIncrements[$row['COLUMN_NAME']] = (bool) $row['AUTO_INCREMENT'];
}
@@ -74,52 +81,56 @@ class SqlsrvReflector implements Dibi\Reflector
WHERE C.TABLE_NAME = {$this->driver->escapeText($table)}
");
$columns = [];
while ($row = $res->fetch(true)) {
while ($row = $res->fetch(TRUE)) {
$columns[] = [
'name' => $row['COLUMN_NAME'],
'table' => $table,
'nativetype' => strtoupper($row['DATA_TYPE']),
'size' => $row['CHARACTER_MAXIMUM_LENGTH'],
'unsigned' => TRUE,
'nullable' => $row['IS_NULLABLE'] === 'YES',
'default' => $row['COLUMN_DEFAULT'],
'autoincrement' => $autoIncrements[$row['COLUMN_NAME']],
'vendor' => $row,
];
}
return $columns;
}
/**
* Returns metadata for all indexes in a table.
* @param string
* @return array
*/
public function getIndexes(string $table): array
public function getIndexes($table)
{
$keyUsagesRes = $this->driver->query(sprintf('EXEC [sys].[sp_helpindex] @objname = %s', $this->driver->escapeText($table)));
$keyUsagesRes = $this->driver->query(sprintf("EXEC [sys].[sp_helpindex] @objname = N%s", $this->driver->escapeText($table)));
$keyUsages = [];
while ($row = $keyUsagesRes->fetch(true)) {
while ($row = $keyUsagesRes->fetch(TRUE)) {
$keyUsages[$row['index_name']] = explode(',', $row['index_keys']);
}
$res = $this->driver->query("SELECT [i].* FROM [sys].[indexes] [i] INNER JOIN [sys].[tables] [t] ON [i].[object_id] = [t].[object_id] WHERE [t].[name] = {$this->driver->escapeText($table)}");
$indexes = [];
while ($row = $res->fetch(true)) {
while ($row = $res->fetch(TRUE)) {
$indexes[$row['name']]['name'] = $row['name'];
$indexes[$row['name']]['unique'] = $row['is_unique'] === 1;
$indexes[$row['name']]['primary'] = $row['is_primary_key'] === 1;
$indexes[$row['name']]['columns'] = $keyUsages[$row['name']] ?? [];
$indexes[$row['name']]['columns'] = isset($keyUsages[$row['name']]) ? $keyUsages[$row['name']] : [];
}
return array_values($indexes);
}
/**
* Returns metadata for all foreign keys in a table.
* @param string
* @return array
*/
public function getForeignKeys(string $table): array
public function getForeignKeys($table)
{
throw new Dibi\NotImplementedException;
}
}

View File

@@ -1,99 +0,0 @@
<?php
/**
* This file is part of the Dibi, smart database abstraction layer (https://dibiphp.com)
* Copyright (c) 2005 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Dibi\Drivers;
use Dibi;
/**
* The driver for Microsoft SQL Server and SQL Azure result set.
*/
class SqlsrvResult implements Dibi\ResultDriver
{
public function __construct(
/** @var resource */
private $resultSet,
) {
}
/**
* Returns the number of rows in a result set.
*/
public function getRowCount(): int
{
throw new Dibi\NotSupportedException('Row count is not available for unbuffered queries.');
}
/**
* Fetches the row at current position and moves the internal cursor to the next position.
* @param bool $assoc true for associative array, false for numeric
*/
public function fetch(bool $assoc): ?array
{
return Dibi\Helpers::false2Null(sqlsrv_fetch_array($this->resultSet, $assoc ? SQLSRV_FETCH_ASSOC : SQLSRV_FETCH_NUMERIC));
}
/**
* Moves cursor position without fetching row.
*/
public function seek(int $row): bool
{
throw new Dibi\NotSupportedException('Cannot seek an unbuffered result set.');
}
/**
* Frees the resources allocated for this result set.
*/
public function free(): void
{
sqlsrv_free_stmt($this->resultSet);
}
/**
* Returns metadata for all columns in a result set.
*/
public function getResultColumns(): array
{
$columns = [];
foreach ((array) sqlsrv_field_metadata($this->resultSet) as $fieldMetadata) {
$columns[] = [
'name' => $fieldMetadata['Name'],
'fullname' => $fieldMetadata['Name'],
'nativetype' => $fieldMetadata['Type'],
];
}
return $columns;
}
/**
* Returns the result set resource.
* @return resource|null
*/
public function getResultResource(): mixed
{
return is_resource($this->resultSet) ? $this->resultSet : null;
}
/**
* Decodes data from result set.
*/
public function unescapeBinary(string $value): string
{
return $value;
}
}

View File

@@ -1,12 +1,10 @@
<?php
/**
* This file is part of the Dibi, smart database abstraction layer (https://dibiphp.com)
* This file is part of the "dibi" - smart database abstraction layer.
* Copyright (c) 2005 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Dibi;
@@ -15,9 +13,10 @@ namespace Dibi;
*/
class Event
{
use Strict;
/** event type */
public const
CONNECT = 1,
const CONNECT = 1,
SELECT = 4,
INSERT = 8,
DELETE = 16,
@@ -29,60 +28,71 @@ class Event
TRANSACTION = 448, // BEGIN | COMMIT | ROLLBACK
ALL = 1023;
public Connection $connection;
public int $type;
public string $sql;
public Result|DriverException|null $result;
public float $time;
public ?int $count = null;
public ?array $source = null;
/** @var Connection */
public $connection;
/** @var int */
public $type;
/** @var string */
public $sql;
/** @var Result|DriverException|NULL */
public $result;
/** @var float */
public $time;
/** @var int */
public $count;
/** @var array */
public $source;
public function __construct(Connection $connection, int $type, ?string $sql = null)
public function __construct(Connection $connection, $type, $sql = NULL)
{
$this->connection = $connection;
$this->type = $type;
$this->sql = trim((string) $sql);
$this->time = -microtime(true);
$this->sql = trim($sql);
$this->time = -microtime(TRUE);
if ($type === self::QUERY && preg_match('#\(?\s*(SELECT|UPDATE|INSERT|DELETE)#iA', $this->sql, $matches)) {
$types = [
static $types = [
'SELECT' => self::SELECT, 'UPDATE' => self::UPDATE,
'INSERT' => self::INSERT, 'DELETE' => self::DELETE,
];
$this->type = $types[strtoupper($matches[1])];
}
$dibiDir = dirname((new \ReflectionClass('dibi'))->getFileName()) . DIRECTORY_SEPARATOR;
foreach (debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS) as $row) {
if (
isset($row['file'])
&& preg_match('~\.(php.?|phtml)$~', $row['file'])
&& !str_starts_with($row['file'], $dibiDir)
) {
$rc = new \ReflectionClass('dibi');
$dibiDir = dirname($rc->getFileName()) . DIRECTORY_SEPARATOR;
foreach (debug_backtrace(FALSE) as $row) {
if (isset($row['file']) && is_file($row['file']) && strpos($row['file'], $dibiDir) !== 0) {
$this->source = [$row['file'], (int) $row['line']];
break;
}
}
\dibi::$elapsedTime = null;
\dibi::$elapsedTime = FALSE;
\dibi::$numOfQueries++;
\dibi::$sql = $sql;
}
public function done(Result|DriverException|null $result = null): static
public function done($result = NULL)
{
$this->result = $result;
try {
$this->count = $result instanceof Result ? count($result) : null;
$this->count = $result instanceof Result ? count($result) : NULL;
} catch (Exception $e) {
$this->count = null;
$this->count = NULL;
}
$this->time += microtime(true);
$this->time += microtime(TRUE);
\dibi::$elapsedTime = $this->time;
\dibi::$totalTime += $this->time;
return $this;
}
}

View File

@@ -1,31 +0,0 @@
<?php
/**
* This file is part of the Dibi, smart database abstraction layer (https://dibiphp.com)
* Copyright (c) 2005 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Dibi;
/**
* SQL expression.
*/
class Expression
{
private array $values;
public function __construct(...$values)
{
$this->values = $values;
}
public function getValues(): array
{
return $this->values;
}
}

View File

@@ -1,21 +1,19 @@
<?php
/**
* This file is part of the Dibi, smart database abstraction layer (https://dibiphp.com)
* This file is part of the "dibi" - smart database abstraction layer.
* Copyright (c) 2005 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Dibi;
/**
* SQL builder via fluent interfaces.
* dibi SQL builder via fluent interfaces. EXPERIMENTAL!
*
* @method Fluent select(...$field)
* @method Fluent distinct()
* @method Fluent from($table, ...$args = null)
* @method Fluent from($table)
* @method Fluent where(...$cond)
* @method Fluent groupBy(...$field)
* @method Fluent having(...$cond)
@@ -27,42 +25,27 @@ namespace Dibi;
* @method Fluent innerJoin(...$table)
* @method Fluent rightJoin(...$table)
* @method Fluent outerJoin(...$table)
* @method Fluent union(Fluent $fluent)
* @method Fluent unionAll(Fluent $fluent)
* @method Fluent as(...$field)
* @method Fluent on(...$cond)
* @method Fluent and(...$cond)
* @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()
*/
class Fluent implements IDataSource
{
public const
AffectedRows = 'a',
Identifier = 'n',
Remove = false;
use Strict;
/** @deprecated use Fluent::Remove */
public const REMOVE = self::Remove;
const REMOVE = FALSE;
public static array $masks = [
/** @var array */
public static $masks = [
'SELECT' => ['SELECT', 'DISTINCT', 'FROM', 'WHERE', 'GROUP BY',
'HAVING', 'ORDER BY', 'LIMIT', 'OFFSET', ],
'HAVING', 'ORDER BY', 'LIMIT', 'OFFSET'],
'UPDATE' => ['UPDATE', 'SET', 'WHERE', 'ORDER BY', 'LIMIT'],
'INSERT' => ['INSERT', 'INTO', 'VALUES', 'SELECT'],
'DELETE' => ['DELETE', 'FROM', 'USING', 'WHERE', 'ORDER BY', 'LIMIT'],
];
/** default modifiers for arrays */
public static array $modifiers = [
/** @var array default modifiers for arrays */
public static $modifiers = [
'SELECT' => '%n',
'FROM' => '%n',
'IN' => '%in',
@@ -74,63 +57,79 @@ class Fluent implements IDataSource
'GROUP BY' => '%by',
];
/** clauses separators */
public static array $separators = [
/** @var array clauses separators */
public static $separators = [
'SELECT' => ',',
'FROM' => ',',
'WHERE' => 'AND',
'GROUP BY' => ',',
'HAVING' => 'AND',
'ORDER BY' => ',',
'LIMIT' => false,
'OFFSET' => false,
'LIMIT' => FALSE,
'OFFSET' => FALSE,
'SET' => ',',
'VALUES' => ',',
'INTO' => false,
'INTO' => FALSE,
];
/** clauses */
public static array $clauseSwitches = [
/** @var array clauses */
public static $clauseSwitches = [
'JOIN' => 'FROM',
'INNER JOIN' => 'FROM',
'LEFT JOIN' => 'FROM',
'RIGHT JOIN' => 'FROM',
];
private Connection $connection;
private array $setups = [];
private ?string $command = null;
private array $clauses = [];
private array $flags = [];
/** @var Connection */
private $connection;
/** @var array */
private $setups = [];
/** @var string */
private $command;
/** @var array */
private $clauses = [];
/** @var array */
private $flags = [];
/** @var array */
private $cursor;
/** normalized clauses */
private static HashMap $normalizer;
/** @var HashMap normalized clauses */
private static $normalizer;
/**
* @param Connection
*/
public function __construct(Connection $connection)
{
$this->connection = $connection;
if (!isset(self::$normalizer)) {
self::$normalizer = new HashMap([self::class, '_formatClause']);
if (self::$normalizer === NULL) {
self::$normalizer = new HashMap([__CLASS__, '_formatClause']);
}
}
/**
* Appends new argument to the clause.
* @param string clause name
* @param array arguments
* @return self
*/
public function __call(string $clause, array $args): static
public function __call($clause, $args)
{
$clause = self::$normalizer->$clause;
// lazy initialization
if ($this->command === null) {
if ($this->command === NULL) {
if (isset(self::$masks[$clause])) {
$this->clauses = array_fill_keys(self::$masks[$clause], null);
$this->clauses = array_fill_keys(self::$masks[$clause], NULL);
}
$this->cursor = &$this->clauses[$clause];
$this->cursor = [];
$this->command = $clause;
@@ -146,38 +145,39 @@ class Fluent implements IDataSource
$this->cursor = &$this->clauses[$clause];
// TODO: really delete?
if ($args === [self::Remove]) {
$this->cursor = null;
if ($args === [self::REMOVE]) {
$this->cursor = NULL;
return $this;
}
if (isset(self::$separators[$clause])) {
$sep = self::$separators[$clause];
if ($sep === false) { // means: replace
if ($sep === FALSE) { // means: replace
$this->cursor = [];
} elseif (!empty($this->cursor)) {
$this->cursor[] = $sep;
}
}
} else {
// append to currect flow
if ($args === [self::Remove]) {
if ($args === [self::REMOVE]) {
return $this;
}
$this->cursor[] = $clause;
}
if ($this->cursor === null) {
if ($this->cursor === NULL) {
$this->cursor = [];
}
// special types or argument
if (count($args) === 1) {
$arg = $args[0];
// TODO: really ignore true?
if ($arg === true) { // flag
// TODO: really ignore TRUE?
if ($arg === TRUE) { // flag
return $this;
} elseif (is_string($arg) && preg_match('#^[a-z:_][a-z0-9_.:]*\z#i', $arg)) { // identifier
@@ -190,14 +190,13 @@ class Fluent implements IDataSource
} elseif (is_string(key($arg))) { // associative array
$args = ['%a', $arg];
}
} // case $arg === false is handled above
} // case $arg === FALSE is handled above
}
foreach ($args as $arg) {
if ($arg instanceof self) {
$arg = new Literal("($arg)");
}
$this->cursor[] = $arg;
}
@@ -207,11 +206,13 @@ class Fluent implements IDataSource
/**
* Switch to a clause.
* @param string clause name
* @return self
*/
public function clause(string $clause): static
public function clause($clause)
{
$this->cursor = &$this->clauses[self::$normalizer->$clause];
if ($this->cursor === null) {
if ($this->cursor === NULL) {
$this->cursor = [];
}
@@ -221,34 +222,40 @@ class Fluent implements IDataSource
/**
* Removes a clause.
* @param string clause name
* @return self
*/
public function removeClause(string $clause): static
public function removeClause($clause)
{
$this->clauses[self::$normalizer->$clause] = null;
$this->clauses[self::$normalizer->$clause] = NULL;
return $this;
}
/**
* Change a SQL flag.
* @param string flag name
* @param bool value
* @return self
*/
public function setFlag(string $flag, bool $value = true): static
public function setFlag($flag, $value = TRUE)
{
$flag = strtoupper($flag);
if ($value) {
$this->flags[$flag] = true;
$this->flags[$flag] = TRUE;
} else {
unset($this->flags[$flag]);
}
return $this;
}
/**
* Is a flag set?
* @param string flag name
* @return bool
*/
final public function getFlag(string $flag): bool
final public function getFlag($flag)
{
return isset($this->flags[strtoupper($flag)]);
}
@@ -256,14 +263,19 @@ class Fluent implements IDataSource
/**
* Returns SQL command.
* @return string
*/
final public function getCommand(): ?string
final public function getCommand()
{
return $this->command;
}
final public function getConnection(): Connection
/**
* Returns the dibi connection.
* @return Connection
*/
final public function getConnection()
{
return $this->connection;
}
@@ -271,8 +283,11 @@ class Fluent implements IDataSource
/**
* Adds Result setup.
* @param string method
* @param mixed args
* @return self
*/
public function setupResult(string $method): static
public function setupResult($method)
{
$this->setups[] = func_get_args();
return $this;
@@ -284,58 +299,70 @@ class Fluent implements IDataSource
/**
* Generates and executes SQL query.
* Returns result set or number of affected rows
* @return ($return is self::Identifier|self::AffectedRows ? int : Result)
* @param mixed what to return?
* @return Result|int result set or number of affected rows
* @throws Exception
*/
public function execute(?string $return = null): Result|int|null
public function execute($return = NULL)
{
$res = $this->query($this->_export());
return match ($return) {
self::Identifier => $this->connection->getInsertId(),
self::AffectedRows => $this->connection->getAffectedRows(),
default => $res,
};
switch ($return) {
case \dibi::IDENTIFIER:
return $this->connection->getInsertId();
case \dibi::AFFECTED_ROWS:
return $this->connection->getAffectedRows();
default:
return $res;
}
}
/**
* Generates, executes SQL query and fetches the single row.
* @return Row|FALSE
*/
public function fetch(): Row|array|null
public function fetch()
{
return $this->command === 'SELECT' && !$this->clauses['LIMIT']
? $this->query($this->_export(null, ['%lmt', 1]))->fetch()
: $this->query($this->_export())->fetch();
if ($this->command === 'SELECT' && !$this->clauses['LIMIT']) {
return $this->query($this->_export(NULL, ['%lmt', 1]))->fetch();
} else {
return $this->query($this->_export())->fetch();
}
}
/**
* Like fetch(), but returns only first field.
* Returns value on success, null if no next record
* @return mixed value on success, FALSE if no next record
*/
public function fetchSingle(): mixed
public function fetchSingle()
{
return $this->command === 'SELECT' && !$this->clauses['LIMIT']
? $this->query($this->_export(null, ['%lmt', 1]))->fetchSingle()
: $this->query($this->_export())->fetchSingle();
if ($this->command === 'SELECT' && !$this->clauses['LIMIT']) {
return $this->query($this->_export(NULL, ['%lmt', 1]))->fetchSingle();
} else {
return $this->query($this->_export())->fetchSingle();
}
}
/**
* Fetches all records from table.
* @param int offset
* @param int limit
* @return array
*/
public function fetchAll(?int $offset = null, ?int $limit = null): array
public function fetchAll($offset = NULL, $limit = NULL)
{
return $this->query($this->_export(null, ['%ofs %lmt', $offset, $limit]))->fetchAll();
return $this->query($this->_export(NULL, ['%ofs %lmt', $offset, $limit]))->fetchAll();
}
/**
* Fetches all records from table and returns associative tree.
* @param string $assoc associative descriptor
* @param string associative descriptor
* @return array
*/
public function fetchAssoc(string $assoc): array
public function fetchAssoc($assoc)
{
return $this->query($this->_export())->fetchAssoc($assoc);
}
@@ -343,8 +370,11 @@ class Fluent implements IDataSource
/**
* Fetches all records from table like $key => $value pairs.
* @param string associative key
* @param string value
* @return array
*/
public function fetchPairs(?string $key = null, ?string $value = null): array
public function fetchPairs($key = NULL, $value = NULL)
{
return $this->query($this->_export())->fetchPairs($key, $value);
}
@@ -352,38 +382,47 @@ class Fluent implements IDataSource
/**
* Required by the IteratorAggregate interface.
* @param int offset
* @param int limit
* @return ResultIterator
*/
public function getIterator(?int $offset = null, ?int $limit = null): ResultIterator
public function getIterator($offset = NULL, $limit = NULL)
{
return $this->query($this->_export(null, ['%ofs %lmt', $offset, $limit]))->getIterator();
return $this->query($this->_export(NULL, ['%ofs %lmt', $offset, $limit]))->getIterator();
}
/**
* Generates and prints SQL query or it's part.
* @param string clause name
* @return bool
*/
public function test(?string $clause = null): bool
public function test($clause = NULL)
{
return $this->connection->test($this->_export($clause));
}
public function count(): int
/**
* @return int
*/
public function count()
{
return Helpers::intVal($this->query([
return (int) $this->query([
'SELECT COUNT(*) FROM (%ex', $this->_export(), ') [data]',
])->fetchSingle());
])->fetchSingle();
}
private function query($args): Result
/**
* @return Result
*/
private function query($args)
{
$res = $this->connection->query($args);
foreach ($this->setups as $setup) {
$method = array_shift($setup);
$res->$method(...$setup);
call_user_func_array([$res, array_shift($setup)], $setup);
}
return $res;
}
@@ -391,7 +430,10 @@ class Fluent implements IDataSource
/********************* exporting ****************d*g**/
public function toDataSource(): DataSource
/**
* @return DataSource
*/
public function toDataSource()
{
return new DataSource($this->connection->translate($this->_export()), $this->connection);
}
@@ -399,24 +441,32 @@ class Fluent implements IDataSource
/**
* Returns SQL query.
* @return string
*/
final public function __toString(): string
final public function __toString()
{
return $this->connection->translate($this->_export());
try {
return $this->connection->translate($this->_export());
} catch (\Exception $e) {
trigger_error($e->getMessage(), E_USER_ERROR);
}
}
/**
* Generates parameters for Translator.
* @param string clause name
* @return array
*/
protected function _export(?string $clause = null, array $args = []): array
protected function _export($clause = NULL, $args = [])
{
if ($clause === null) {
if ($clause === NULL) {
$data = $this->clauses;
if ($this->command === 'SELECT' && ($data['LIMIT'] || $data['OFFSET'])) {
$args = array_merge(['%lmt %ofs', $data['LIMIT'][0] ?? null, $data['OFFSET'][0] ?? null], $args);
$args = array_merge(['%lmt %ofs', $data['LIMIT'][0], $data['OFFSET'][0]], $args);
unset($data['LIMIT'], $data['OFFSET']);
}
} else {
$clause = self::$normalizer->$clause;
if (array_key_exists($clause, $this->clauses)) {
@@ -427,12 +477,11 @@ class Fluent implements IDataSource
}
foreach ($data as $clause => $statement) {
if ($statement !== null) {
if ($statement !== NULL) {
$args[] = $clause;
if ($clause === $this->command && $this->flags) {
$args[] = implode(' ', array_keys($this->flags));
}
foreach ($statement as $arg) {
$args[] = $arg;
}
@@ -445,15 +494,16 @@ class Fluent implements IDataSource
/**
* Format camelCase clause name to UPPER CASE.
* @param string
* @return string
* @internal
*/
public static function _formatClause(string $s): string
public static function _formatClause($s)
{
if ($s === 'order' || $s === 'group') {
$s .= 'By';
trigger_error("Did you mean '$s'?", E_USER_NOTICE);
}
return strtoupper(preg_replace('#[a-z](?=[A-Z])#', '$0 ', $s));
}
@@ -465,7 +515,7 @@ class Fluent implements IDataSource
$this->clauses[$clause] = &$val;
unset($val);
}
$this->cursor = &$foo;
}
}

View File

@@ -1,12 +1,10 @@
<?php
/**
* This file is part of the Dibi, smart database abstraction layer (https://dibiphp.com)
* This file is part of the "dibi" - smart database abstraction layer.
* Copyright (c) 2005 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Dibi;
@@ -14,10 +12,8 @@ namespace Dibi;
* Lazy cached storage.
* @internal
*/
#[\AllowDynamicProperties]
abstract class HashMapBase
{
/** @var callable */
private $callback;
@@ -27,42 +23,45 @@ abstract class HashMapBase
}
public function setCallback(callable $callback): void
public function setCallback(callable $callback)
{
$this->callback = $callback;
}
public function getCallback(): callable
public function getCallback()
{
return $this->callback;
}
}
/**
* Lazy cached storage.
*
* @internal
*/
final class HashMap extends HashMapBase
{
public function __set(string $nm, mixed $val): void
public function __set($nm, $val)
{
if ($nm === '') {
if ($nm == '') {
$nm = "\xFF";
}
$this->$nm = $val;
}
public function __get(string $nm): mixed
public function __get($nm)
{
if ($nm === '') {
if ($nm == '') {
$nm = "\xFF";
return isset($this->$nm) && true ? $this->$nm : $this->$nm = $this->getCallback()('');
return isset($this->$nm) ? $this->$nm : $this->$nm = call_user_func($this->getCallback(), '');
} else {
return $this->$nm = $this->getCallback()($nm);
return $this->$nm = call_user_func($this->getCallback(), $nm);
}
}
}

View File

@@ -1,28 +1,31 @@
<?php
/**
* This file is part of the Dibi, smart database abstraction layer (https://dibiphp.com)
* This file is part of the "dibi" - smart database abstraction layer.
* Copyright (c) 2005 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Dibi;
class Helpers
{
private static HashMap $types;
use Strict;
/** @var array */
private static $types;
/**
* Prints out a syntax highlighted version of the SQL command or Result.
* @param string|Result
* @param bool return output instead of printing it?
* @return string
*/
public static function dump(string|Result|null $sql = null, bool $return = false): ?string
public static function dump($sql = NULL, $return = FALSE)
{
ob_start();
if ($sql instanceof Result && PHP_SAPI === 'cli') {
$hasColors = (str_starts_with((string) getenv('TERM'), 'xterm'));
$hasColors = (substr(getenv('TERM'), 0, 5) === 'xterm');
$maxLen = 0;
foreach ($sql as $i => $row) {
if ($i === 0) {
@@ -35,9 +38,8 @@ class Helpers
echo $hasColors ? "\033[1;37m#row: $i\033[0m\n" : "#row: $i\n";
foreach ($row as $col => $val) {
$spaces = $maxLen - mb_strlen($col) + 2;
echo "$col" . str_repeat(' ', $spaces) . "$val\n";
echo "$col" . str_repeat(' ', $spaces) . "$val\n";
}
echo "\n";
}
@@ -48,17 +50,15 @@ class Helpers
if ($i === 0) {
echo "\n<table class=\"dump\">\n<thead>\n\t<tr>\n\t\t<th>#row</th>\n";
foreach ($row as $col => $foo) {
echo "\t\t<th>" . htmlspecialchars((string) $col) . "</th>\n";
echo "\t\t<th>" . htmlSpecialChars((string) $col) . "</th>\n";
}
echo "\t</tr>\n</thead>\n<tbody>\n";
}
echo "\t<tr>\n\t\t<th>", $i, "</th>\n";
foreach ($row as $col) {
echo "\t\t<td>", htmlspecialchars((string) $col), "</td>\n";
echo "\t\t<td>", htmlSpecialChars((string) $col), "</td>\n";
}
echo "\t</tr>\n";
}
@@ -67,12 +67,12 @@ class Helpers
: "</tbody>\n</table>\n";
} else {
if ($sql === null) {
if ($sql === NULL) {
$sql = \dibi::$sql;
}
$keywords1 = 'SELECT|(?:ON\s+DUPLICATE\s+KEY)?UPDATE|INSERT(?:\s+INTO)?|REPLACE(?:\s+INTO)?|DELETE|CALL|UNION|FROM|WHERE|HAVING|GROUP\s+BY|ORDER\s+BY|LIMIT|OFFSET|FETCH\s+NEXT|SET|VALUES|LEFT\s+JOIN|INNER\s+JOIN|TRUNCATE|START\s+TRANSACTION|BEGIN|COMMIT|ROLLBACK(?:\s+TO\s+SAVEPOINT)?|(?:RELEASE\s+)?SAVEPOINT';
$keywords2 = 'ALL|DISTINCT|DISTINCTROW|IGNORE|AS|USING|ON|AND|OR|IN|IS|NOT|NULL|LIKE|RLIKE|REGEXP|TRUE|FALSE';
static $keywords1 = 'SELECT|(?:ON\s+DUPLICATE\s+KEY)?UPDATE|INSERT(?:\s+INTO)?|REPLACE(?:\s+INTO)?|DELETE|CALL|UNION|FROM|WHERE|HAVING|GROUP\s+BY|ORDER\s+BY|LIMIT|OFFSET|FETCH\s+NEXT|SET|VALUES|LEFT\s+JOIN|INNER\s+JOIN|TRUNCATE|START\s+TRANSACTION|BEGIN|COMMIT|ROLLBACK(?:\s+TO\s+SAVEPOINT)?|(?:RELEASE\s+)?SAVEPOINT';
static $keywords2 = 'ALL|DISTINCT|DISTINCTROW|IGNORE|AS|USING|ON|AND|OR|IN|IS|NOT|NULL|LIKE|RLIKE|REGEXP|TRUE|FALSE';
// insert new lines
$sql = " $sql ";
@@ -87,8 +87,8 @@ class Helpers
// syntax highlight
$highlighter = "#(/\\*.+?\\*/)|(\\*\\*.+?\\*\\*)|(?<=[\\s,(])($keywords1)(?=[\\s,)])|(?<=[\\s,(=])($keywords2)(?=[\\s,)=])#is";
if (PHP_SAPI === 'cli') {
if (str_starts_with((string) getenv('TERM'), 'xterm')) {
$sql = preg_replace_callback($highlighter, function (array $m) {
if (substr(getenv('TERM'), 0, 5) === 'xterm') {
$sql = preg_replace_callback($highlighter, function ($m) {
if (!empty($m[1])) { // comment
return "\033[1;30m" . $m[1] . "\033[0m";
@@ -103,12 +103,11 @@ class Helpers
}
}, $sql);
}
echo trim($sql) . "\n\n";
} else {
$sql = htmlspecialchars($sql);
$sql = preg_replace_callback($highlighter, function (array $m) {
$sql = htmlSpecialChars($sql);
$sql = preg_replace_callback($highlighter, function ($m) {
if (!empty($m[1])) { // comment
return '<em style="color:gray">' . $m[1] . '</em>';
@@ -130,41 +129,40 @@ class Helpers
return ob_get_clean();
} else {
ob_end_flush();
return null;
}
}
/**
* Finds the best suggestion.
* @return string|NULL
* @internal
*/
public static function getSuggestion(array $items, string $value): ?string
public static function getSuggestion(array $items, $value)
{
$best = null;
$best = NULL;
$min = (strlen($value) / 4 + 1) * 10 + .1;
$items = array_map('strval', $items);
foreach (array_unique($items) as $item) {
foreach (array_unique($items, SORT_REGULAR) as $item) {
$item = is_object($item) ? $item->getName() : $item;
if (($len = levenshtein($item, $value, 10, 11, 10)) > 0 && $len < $min) {
$min = $len;
$best = $item;
}
}
return $best;
}
/** @internal */
public static function escape(Driver $driver, $value, string $type): string
public static function escape(Driver $driver, $value, $type)
{
$types = [
Type::Text => 'text',
Type::Binary => 'binary',
Type::Bool => 'bool',
Type::Date => 'date',
Type::DateTime => 'datetime',
Fluent::Identifier => 'identifier',
static $types = [
Type::TEXT => 'text',
Type::BINARY => 'binary',
Type::BOOL => 'bool',
Type::DATE => 'date',
Type::DATETIME => 'datetime',
\dibi::IDENTIFIER => 'identifier',
];
if (isset($types[$type])) {
return $driver->{'escape' . $types[$type]}($value);
@@ -176,22 +174,22 @@ class Helpers
/**
* Heuristic type detection.
* @param string
* @return string|NULL
* @internal
*/
public static function detectType(string $type): ?string
public static function detectType($type)
{
$patterns = [
'^_' => Type::Text, // PostgreSQL arrays
'RANGE$' => Type::Text, // PostgreSQL range types
'BYTEA|BLOB|BIN' => Type::Binary,
'TEXT|CHAR|POINT|INTERVAL|STRING' => Type::Text,
'YEAR|BYTE|COUNTER|SERIAL|INT|LONG|SHORT|^TINY$' => Type::Integer,
'CURRENCY|REAL|MONEY|FLOAT|DOUBLE|DECIMAL|NUMERIC|NUMBER' => Type::Float,
'^TIME$' => Type::Time,
'TIME' => Type::DateTime, // DATETIME, TIMESTAMP
'DATE' => Type::Date,
'BOOL' => Type::Bool,
'JSON' => Type::JSON,
static $patterns = [
'^_' => Type::TEXT, // PostgreSQL arrays
'BYTEA|BLOB|BIN' => Type::BINARY,
'TEXT|CHAR|POINT|INTERVAL|STRING' => Type::TEXT,
'YEAR|BYTE|COUNTER|SERIAL|INT|LONG|SHORT|^TINY$' => Type::INTEGER,
'CURRENCY|REAL|MONEY|FLOAT|DOUBLE|DECIMAL|NUMERIC|NUMBER' => Type::FLOAT,
'^TIME$' => Type::TIME,
'TIME' => Type::DATETIME, // DATETIME, TIMESTAMP
'DATE' => Type::DATE,
'BOOL' => Type::BOOL,
];
foreach ($patterns as $s => $val) {
@@ -199,26 +197,30 @@ class Helpers
return $val;
}
}
return null;
return NULL;
}
/** @internal */
public static function getTypeCache(): HashMap
/**
* @internal
*/
public static function getTypeCache()
{
if (!isset(self::$types)) {
self::$types = new HashMap([self::class, 'detectType']);
if (self::$types === NULL) {
self::$types = new HashMap([__CLASS__, 'detectType']);
}
return self::$types;
}
/**
* Apply configuration alias or default values.
* @param array connect configuration
* @param string key
* @param string alias key
* @return void
*/
public static function alias(array &$config, string $key, string $alias): void
public static function alias(&$config, $key, $alias)
{
$foo = &$config;
foreach (explode('|', $key) as $key) {
@@ -234,9 +236,9 @@ class Helpers
/**
* Import SQL dump from file.
* Returns count of sql commands
* @return int count of sql commands
*/
public static function loadFromFile(Connection $connection, string $file, ?callable $onProgress = null): int
public static function loadFromFile(Connection $connection, $file)
{
@set_time_limit(0); // intentionally @
@@ -245,62 +247,31 @@ class Helpers
throw new \RuntimeException("Cannot open file '$file'.");
}
$stat = fstat($handle);
$count = $size = 0;
$count = 0;
$delimiter = ';';
$sql = '';
$driver = $connection->getDriver();
while (($s = fgets($handle)) !== false) {
$size += strlen($s);
if (strtoupper(substr($s, 0, 10)) === 'DELIMITER ') {
$delimiter = trim(substr($s, 10));
while (!feof($handle)) {
$s = rtrim(fgets($handle));
if (substr($s, 0, 10) === 'DELIMITER ') {
$delimiter = substr($s, 10);
} elseif (str_ends_with($ts = rtrim($s), $delimiter)) {
$sql .= substr($ts, 0, -strlen($delimiter));
} elseif (substr($s, -strlen($delimiter)) === $delimiter) {
$sql .= substr($s, 0, -strlen($delimiter));
$driver->query($sql);
$sql = '';
$count++;
if ($onProgress) {
$onProgress($count, isset($stat['size']) ? $size * 100 / $stat['size'] : null);
}
} else {
$sql .= $s;
$sql .= $s . "\n";
}
}
if (rtrim($sql) !== '') {
if (trim($sql) !== '') {
$driver->query($sql);
$count++;
if ($onProgress) {
$onProgress($count, isset($stat['size']) ? 100 : null);
}
}
fclose($handle);
return $count;
}
/** @internal */
public static function false2Null(mixed $val): mixed
{
return $val === false ? null : $val;
}
/** @internal */
public static function intVal(mixed $value): int
{
if (is_int($value)) {
return $value;
} elseif (is_string($value) && preg_match('#-?\d++\z#A', $value)) {
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

@@ -1,12 +1,10 @@
<?php
/**
* This file is part of the Dibi, smart database abstraction layer (https://dibiphp.com)
* This file is part of the "dibi" - smart database abstraction layer.
* Copyright (c) 2005 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Dibi;
@@ -15,7 +13,10 @@ namespace Dibi;
*/
class Literal
{
private string $value;
use Strict;
/** @var string */
private $value;
public function __construct($value)
@@ -24,8 +25,12 @@ class Literal
}
public function __toString(): string
/**
* @return string
*/
public function __toString()
{
return $this->value;
}
}

View File

@@ -1,72 +1,76 @@
<?php
/**
* This file is part of the Dibi, smart database abstraction layer (https://dibiphp.com)
* This file is part of the "dibi" - smart database abstraction layer.
* Copyright (c) 2005 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Dibi\Loggers;
use Dibi;
/**
* Dibi file logger.
* dibi file logger.
*/
class FileLogger
{
public function __construct(
public string $file,
public int $filter = Dibi\Event::QUERY,
private bool $errorsOnly = false,
) {
use Dibi\Strict;
/** @var string Name of the file where SQL errors should be logged */
public $file;
/** @var int */
public $filter;
public function __construct($file, $filter = NULL)
{
$this->file = $file;
$this->filter = $filter ? (int) $filter : Dibi\Event::QUERY;
}
/**
* After event notification.
* @return void
*/
public function logEvent(Dibi\Event $event): void
public function logEvent(Dibi\Event $event)
{
if (
(($event->type & $this->filter) === 0)
|| ($this->errorsOnly === true && !$event->result instanceof \Exception)
) {
if (($event->type & $this->filter) === 0) {
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";
}
$this->writeToFile(
$event,
fwrite($handle,
"ERROR: $message"
. "\n-- SQL: " . $event->sql,
. "\n-- SQL: " . $event->sql
. "\n-- driver: " . $event->connection->getConfig('driver') . '/' . $event->connection->getConfig('name')
. ";\n-- " . date('Y-m-d H:i:s')
. "\n\n"
);
} else {
$this->writeToFile(
$event,
fwrite($handle,
'OK: ' . $event->sql
. ($event->count ? ";\n-- rows: " . $event->count : '')
. "\n-- takes: " . sprintf('%0.3f ms', $event->time * 1000)
. "\n-- source: " . implode(':', $event->source),
. ($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"
);
}
fclose($handle);
}
private function writeToFile(Dibi\Event $event, string $message): void
{
$driver = $event->connection->getConfig('driver');
$message .=
"\n-- driver: " . get_debug_type($driver) . '/' . $event->connection->getConfig('name')
. "\n-- " . date('Y-m-d H:i:s')
. "\n\n";
file_put_contents($this->file, $message, FILE_APPEND | LOCK_EX);
}
}

View File

@@ -0,0 +1,95 @@
<?php
/**
* This file is part of the "dibi" - smart database abstraction layer.
* Copyright (c) 2005 David Grudl (https://davidgrudl.com)
*/
namespace Dibi\Loggers;
use Dibi;
/**
* dibi FirePHP logger.
*/
class FirePhpLogger
{
use Dibi\Strict;
/** maximum number of rows */
public static $maxQueries = 30;
/** maximum SQL length */
public static $maxLength = 1000;
/** size of json stream chunk */
public static $streamChunkSize = 4990;
/** @var int */
public $filter;
/** @var int Elapsed time for all queries */
public $totalTime = 0;
/** @var int Number of all queries */
public $numOfQueries = 0;
/** @var array */
private static $fireTable = [['Time', 'SQL Statement', 'Rows', 'Connection']];
/**
* @return bool
*/
public static function isAvailable()
{
return isset($_SERVER['HTTP_USER_AGENT']) && strpos($_SERVER['HTTP_USER_AGENT'], 'FirePHP/');
}
public function __construct($filter = NULL)
{
$this->filter = $filter ? (int) $filter : Dibi\Event::QUERY;
}
/**
* After event notification.
* @return void
*/
public function logEvent(Dibi\Event $event)
{
if (headers_sent() || ($event->type & $this->filter) === 0 || count(self::$fireTable) > self::$maxQueries) {
return;
}
if (!$this->numOfQueries) {
header('X-Wf-Protocol-dibi: http://meta.wildfirehq.org/Protocol/JsonStream/0.2');
header('X-Wf-dibi-Plugin-1: http://meta.firephp.org/Wildfire/Plugin/FirePHP/Library-FirePHPCore/0.2.0');
header('X-Wf-dibi-Structure-1: http://meta.firephp.org/Wildfire/Structure/FirePHP/FirebugConsole/0.1');
}
$this->totalTime += $event->time;
$this->numOfQueries++;
self::$fireTable[] = [
sprintf('%0.3f', $event->time * 1000),
strlen($event->sql) > self::$maxLength ? substr($event->sql, 0, self::$maxLength) . '...' : $event->sql,
$event->result instanceof \Exception ? 'ERROR' : (string) $event->count,
$event->connection->getConfig('driver') . '/' . $event->connection->getConfig('name'),
];
$payload = json_encode([
[
'Type' => 'TABLE',
'Label' => 'dibi profiler (' . $this->numOfQueries . ' SQL queries took ' . sprintf('%0.3f', $this->totalTime * 1000) . ' ms)',
],
self::$fireTable,
]);
foreach (str_split($payload, self::$streamChunkSize) as $num => $s) {
$num++;
header("X-Wf-dibi-1-1-d$num: |$s|\\"); // protocol-, structure-, plugin-, message-index
}
header("X-Wf-dibi-1-1-d$num: |$s|");
}
}

View File

@@ -1,15 +1,14 @@
<?php
/**
* This file is part of the Dibi, smart database abstraction layer (https://dibiphp.com)
* This file is part of the "dibi" - smart database abstraction layer.
* Copyright (c) 2005 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Dibi\Reflection;
use Dibi;
use Dibi\Type;
/**
@@ -20,94 +19,148 @@ use Dibi;
* @property-read Table $table
* @property-read string $type
* @property-read mixed $nativeType
* @property-read int|null $size
* @property-read bool $nullable
* @property-read bool $autoIncrement
* @property-read int|NULL $size
* @property-read bool|NULL $unsigned
* @property-read bool|NULL $nullable
* @property-read bool|NULL $autoIncrement
* @property-read mixed $default
*/
class Column
{
public function __construct(
private ?Dibi\Reflector $reflector,
private array $info,
) {
use Dibi\Strict;
/** @var Dibi\Reflector|NULL when created by Result */
private $reflector;
/** @var array (name, nativetype, [table], [fullname], [size], [nullable], [default], [autoincrement], [vendor]) */
private $info;
public function __construct(Dibi\Reflector $reflector = NULL, array $info)
{
$this->reflector = $reflector;
$this->info = $info;
}
public function getName(): string
/**
* @return string
*/
public function getName()
{
return $this->info['name'];
}
public function getFullName(): string
/**
* @return string
*/
public function getFullName()
{
return $this->info['fullname'] ?? null;
return isset($this->info['fullname']) ? $this->info['fullname'] : NULL;
}
public function hasTable(): bool
/**
* @return bool
*/
public function hasTable()
{
return !empty($this->info['table']);
}
public function getTable(): Table
/**
* @return Table
*/
public function getTable()
{
if (empty($this->info['table']) || !$this->reflector) {
throw new Dibi\Exception('Table is unknown or not available.');
throw new Dibi\Exception("Table is unknown or not available.");
}
return new Table($this->reflector, ['name' => $this->info['table']]);
}
public function getTableName(): ?string
/**
* @return string|NULL
*/
public function getTableName()
{
return isset($this->info['table']) && $this->info['table'] != null // intentionally ==
? $this->info['table']
: null;
return isset($this->info['table']) && $this->info['table'] != NULL ? $this->info['table'] : NULL; // intentionally ==
}
public function getType(): ?string
/**
* @return string
*/
public function getType()
{
return Dibi\Helpers::getTypeCache()->{$this->info['nativetype']};
}
public function getNativeType(): string
/**
* @return string
*/
public function getNativeType()
{
return $this->info['nativetype'];
}
public function getSize(): ?int
/**
* @return int|NULL
*/
public function getSize()
{
return isset($this->info['size']) ? (int) $this->info['size'] : null;
return isset($this->info['size']) ? (int) $this->info['size'] : NULL;
}
public function isNullable(): bool
/**
* @return bool|NULL
*/
public function isUnsigned()
{
return !empty($this->info['nullable']);
return isset($this->info['unsigned']) ? (bool) $this->info['unsigned'] : NULL;
}
public function isAutoIncrement(): bool
/**
* @return bool|NULL
*/
public function isNullable()
{
return !empty($this->info['autoincrement']);
return isset($this->info['nullable']) ? (bool) $this->info['nullable'] : NULL;
}
public function getDefault(): mixed
/**
* @return bool|NULL
*/
public function isAutoIncrement()
{
return $this->info['default'] ?? null;
return isset($this->info['autoincrement']) ? (bool) $this->info['autoincrement'] : NULL;
}
public function getVendorInfo(string $key): mixed
/**
* @return mixed
*/
public function getDefault()
{
return $this->info['vendor'][$key] ?? null;
return isset($this->info['default']) ? $this->info['default'] : NULL;
}
/**
* @param string
* @return mixed
*/
public function getVendorInfo($key)
{
return isset($this->info['vendor'][$key]) ? $this->info['vendor'][$key] : NULL;
}
}

View File

@@ -1,12 +1,10 @@
<?php
/**
* This file is part of the Dibi, smart database abstraction layer (https://dibiphp.com)
* This file is part of the "dibi" - smart database abstraction layer.
* Copyright (c) 2005 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Dibi\Reflection;
use Dibi;
@@ -21,45 +19,63 @@ use Dibi;
*/
class Database
{
/** @var Table[] */
private array $tables;
use Dibi\Strict;
/** @var Dibi\Reflector */
private $reflector;
/** @var string */
private $name;
/** @var array */
private $tables;
public function __construct(
private Dibi\Reflector $reflector,
private ?string $name = null,
) {
public function __construct(Dibi\Reflector $reflector, $name)
{
$this->reflector = $reflector;
$this->name = $name;
}
public function getName(): ?string
/**
* @return string
*/
public function getName()
{
return $this->name;
}
/** @return Table[] */
public function getTables(): array
/**
* @return Table[]
*/
public function getTables()
{
$this->init();
return array_values($this->tables);
}
/** @return string[] */
public function getTableNames(): array
/**
* @return string[]
*/
public function getTableNames()
{
$this->init();
$res = [];
foreach ($this->tables as $table) {
$res[] = $table->getName();
}
return $res;
}
public function getTable(string $name): Table
/**
* @param string
* @return Table
*/
public function getTable($name)
{
$this->init();
$l = strtolower($name);
@@ -72,20 +88,28 @@ class Database
}
public function hasTable(string $name): bool
/**
* @param string
* @return bool
*/
public function hasTable($name)
{
$this->init();
return isset($this->tables[strtolower($name)]);
}
protected function init(): void
/**
* @return void
*/
protected function init()
{
if (!isset($this->tables)) {
if ($this->tables === NULL) {
$this->tables = [];
foreach ($this->reflector->getTables() as $info) {
$this->tables[strtolower($info['name'])] = new Table($this->reflector, $info);
}
}
}
}

View File

@@ -1,14 +1,13 @@
<?php
/**
* This file is part of the Dibi, smart database abstraction layer (https://dibiphp.com)
* This file is part of the "dibi" - smart database abstraction layer.
* Copyright (c) 2005 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Dibi\Reflection;
use Dibi;
/**
@@ -19,21 +18,37 @@ namespace Dibi\Reflection;
*/
class ForeignKey
{
public function __construct(
private string $name,
private array $references,
) {
use Dibi\Strict;
/** @var string */
private $name;
/** @var array of [local, foreign, onDelete, onUpdate] */
private $references;
public function __construct($name, array $references)
{
$this->name = $name;
$this->references = $references;
}
public function getName(): string
/**
* @return string
*/
public function getName()
{
return $this->name;
}
public function getReferences(): array
/**
* @return array
*/
public function getReferences()
{
return $this->references;
}
}

View File

@@ -1,14 +1,13 @@
<?php
/**
* This file is part of the Dibi, smart database abstraction layer (https://dibiphp.com)
* This file is part of the "dibi" - smart database abstraction layer.
* Copyright (c) 2005 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Dibi\Reflection;
use Dibi;
/**
@@ -21,32 +20,51 @@ namespace Dibi\Reflection;
*/
class Index
{
public function __construct(
private array $info,
) {
use Dibi\Strict;
/** @var array (name, columns, [unique], [primary]) */
private $info;
public function __construct(array $info)
{
$this->info = $info;
}
public function getName(): string
/**
* @return string
*/
public function getName()
{
return $this->info['name'];
}
public function getColumns(): array
/**
* @return array
*/
public function getColumns()
{
return $this->info['columns'];
}
public function isUnique(): bool
/**
* @return bool
*/
public function isUnique()
{
return !empty($this->info['unique']);
}
public function isPrimary(): bool
/**
* @return bool
*/
public function isPrimary()
{
return !empty($this->info['primary']);
}
}

View File

@@ -1,12 +1,10 @@
<?php
/**
* This file is part of the Dibi, smart database abstraction layer (https://dibiphp.com)
* This file is part of the "dibi" - smart database abstraction layer.
* Copyright (c) 2005 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Dibi\Reflection;
use Dibi;
@@ -20,41 +18,54 @@ use Dibi;
*/
class Result
{
/** @var Column[]|null */
private ?array $columns;
use Dibi\Strict;
/** @var Column[]|null */
private ?array $names;
/** @var Dibi\ResultDriver */
private $driver;
/** @var array */
private $columns;
/** @var array */
private $names;
public function __construct(
private Dibi\ResultDriver $driver,
) {
public function __construct(Dibi\ResultDriver $driver)
{
$this->driver = $driver;
}
/** @return Column[] */
public function getColumns(): array
/**
* @return Column[]
*/
public function getColumns()
{
$this->initColumns();
return array_values($this->columns);
}
/** @return string[] */
public function getColumnNames(bool $fullNames = false): array
/**
* @param bool
* @return string[]
*/
public function getColumnNames($fullNames = FALSE)
{
$this->initColumns();
$res = [];
foreach ($this->columns as $column) {
$res[] = $fullNames ? $column->getFullName() : $column->getName();
}
return $res;
}
public function getColumn(string $name): Column
/**
* @param string
* @return Column
*/
public function getColumn($name)
{
$this->initColumns();
$l = strtolower($name);
@@ -67,23 +78,29 @@ class Result
}
public function hasColumn(string $name): bool
/**
* @param string
* @return bool
*/
public function hasColumn($name)
{
$this->initColumns();
return isset($this->names[strtolower($name)]);
}
protected function initColumns(): void
/**
* @return void
*/
protected function initColumns()
{
if (!isset($this->columns)) {
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

@@ -1,12 +1,10 @@
<?php
/**
* This file is part of the Dibi, smart database abstraction layer (https://dibiphp.com)
* This file is part of the "dibi" - smart database abstraction layer.
* Copyright (c) 2005 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Dibi\Reflection;
use Dibi;
@@ -25,19 +23,28 @@ use Dibi;
*/
class Table
{
private Dibi\Reflector $reflector;
private string $name;
private bool $view;
use Dibi\Strict;
/** @var Column[] */
private array $columns;
/** @var Dibi\Reflector */
private $reflector;
/** @var ForeignKey[] */
private array $foreignKeys;
/** @var string */
private $name;
/** @var Index[] */
private array $indexes;
private ?Index $primaryKey;
/** @var bool */
private $view;
/** @var array */
private $columns;
/** @var array */
private $foreignKeys;
/** @var array */
private $indexes;
/** @var Index */
private $primaryKey;
public function __construct(Dibi\Reflector $reflector, array $info)
@@ -48,40 +55,53 @@ class Table
}
public function getName(): string
/**
* @return string
*/
public function getName()
{
return $this->name;
}
public function isView(): bool
/**
* @return bool
*/
public function isView()
{
return $this->view;
}
/** @return Column[] */
public function getColumns(): array
/**
* @return Column[]
*/
public function getColumns()
{
$this->initColumns();
return array_values($this->columns);
}
/** @return string[] */
public function getColumnNames(): array
/**
* @return string[]
*/
public function getColumnNames()
{
$this->initColumns();
$res = [];
foreach ($this->columns as $column) {
$res[] = $column->getName();
}
return $res;
}
public function getColumn(string $name): Column
/**
* @param string
* @return Column
*/
public function getColumn($name)
{
$this->initColumns();
$l = strtolower($name);
@@ -94,39 +114,53 @@ class Table
}
public function hasColumn(string $name): bool
/**
* @param string
* @return bool
*/
public function hasColumn($name)
{
$this->initColumns();
return isset($this->columns[strtolower($name)]);
}
/** @return ForeignKey[] */
public function getForeignKeys(): array
/**
* @return ForeignKey[]
*/
public function getForeignKeys()
{
$this->initForeignKeys();
return $this->foreignKeys;
}
/** @return Index[] */
public function getIndexes(): array
/**
* @return Index[]
*/
public function getIndexes()
{
$this->initIndexes();
return $this->indexes;
}
public function getPrimaryKey(): Index
/**
* @return Index
*/
public function getPrimaryKey()
{
$this->initIndexes();
return $this->primaryKey;
}
protected function initColumns(): void
/**
* @return void
*/
protected function initColumns()
{
if (!isset($this->columns)) {
if ($this->columns === NULL) {
$this->columns = [];
foreach ($this->reflector->getColumns($this->name) as $info) {
$this->columns[strtolower($info['name'])] = new Column($this->reflector, $info);
@@ -135,16 +169,18 @@ class Table
}
protected function initIndexes(): void
/**
* @return void
*/
protected function initIndexes()
{
if (!isset($this->indexes)) {
if ($this->indexes === NULL) {
$this->initColumns();
$this->indexes = [];
foreach ($this->reflector->getIndexes($this->name) as $info) {
foreach ($info['columns'] as $key => $name) {
$info['columns'][$key] = $this->columns[strtolower($name)];
}
$this->indexes[strtolower($info['name'])] = new Index($info);
if (!empty($info['primary'])) {
$this->primaryKey = $this->indexes[strtolower($info['name'])];
@@ -154,8 +190,12 @@ class Table
}
protected function initForeignKeys(): void
/**
* @return void
*/
protected function initForeignKeys()
{
throw new Dibi\NotImplementedException;
}
}

View File

@@ -1,67 +1,98 @@
<?php
/**
* This file is part of the Dibi, smart database abstraction layer (https://dibiphp.com)
* This file is part of the "dibi" - smart database abstraction layer.
* Copyright (c) 2005 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Dibi;
/**
* Query result.
* dibi result set.
*
* <code>
* $result = dibi::query('SELECT * FROM [table]');
*
* $row = $result->fetch();
* $value = $result->fetchSingle();
* $table = $result->fetchAll();
* $pairs = $result->fetchPairs();
* $assoc = $result->fetchAssoc('col1');
* $assoc = $result->fetchAssoc('col1[]col2->col3');
*
* unset($result);
* </code>
*
* @property-read int $rowCount
*/
class Result implements IDataSource
{
private ?ResultDriver $driver;
use Strict;
/** Translate table */
private array $types = [];
private ?Reflection\Result $meta;
/** @var array ResultDriver */
private $driver;
/** Already fetched? Used for allowance for first seek(0) */
private bool $fetched = false;
/** @var array Translate table */
private $types = [];
/** returned object class */
private ?string $rowClass = Row::class;
/** @var Reflection\Result */
private $meta;
/** @var callable|null returned object factory */
/** @var bool Already fetched? Used for allowance for first seek(0) */
private $fetched = FALSE;
/** @var string returned object class */
private $rowClass = 'Dibi\Row';
/** @var callable returned object factory*/
private $rowFactory;
private array $formats = [];
/** @var array format */
private $formats = [];
public function __construct(ResultDriver $driver, bool $normalize = true)
/**
* @param ResultDriver
*/
public function __construct($driver)
{
$this->driver = $driver;
if ($normalize) {
$this->detectTypes();
}
$this->detectTypes();
}
/**
* @deprecated
*/
final public function getResource()
{
trigger_error(__METHOD__ . '() is deprecated, use getResultDriver()->getResultResource().', E_USER_DEPRECATED);
return $this->getResultDriver()->getResultResource();
}
/**
* Frees the resources allocated for this result set.
* @return void
*/
final public function free(): void
final public function free()
{
if ($this->driver !== null) {
if ($this->driver !== NULL) {
$this->driver->free();
$this->driver = $this->meta = null;
$this->driver = $this->meta = NULL;
}
}
/**
* Safe access to property $driver.
* @return ResultDriver
* @throws \RuntimeException
*/
final public function getResultDriver(): ResultDriver
final public function getResultDriver()
{
if ($this->driver === null) {
if ($this->driver === NULL) {
throw new \RuntimeException('Result-set was released from memory.');
}
@@ -74,20 +105,21 @@ class Result implements IDataSource
/**
* Moves cursor position without fetching row.
* @param int the 0-based cursor pos to seek to
* @return bool TRUE on success, FALSE if unable to seek to specified record
* @throws Exception
*/
final public function seek(int $row): bool
final public function seek($row)
{
return ($row !== 0 || $this->fetched)
? $this->getResultDriver()->seek($row)
: true;
return ($row !== 0 || $this->fetched) ? (bool) $this->getResultDriver()->seek($row) : TRUE;
}
/**
* Required by the Countable interface.
* @return int
*/
final public function count(): int
final public function count()
{
return $this->getResultDriver()->getRowCount();
}
@@ -95,8 +127,9 @@ class Result implements IDataSource
/**
* Returns the number of rows in a result set.
* @return int
*/
final public function getRowCount(): int
final public function getRowCount()
{
return $this->getResultDriver()->getRowCount();
}
@@ -104,29 +137,23 @@ class Result implements IDataSource
/**
* Required by the IteratorAggregate interface.
* @return ResultIterator
*/
final public function getIterator(): ResultIterator
final public function getIterator()
{
return new ResultIterator($this);
}
/**
* Returns the number of columns in a result set.
*/
final public function getColumnCount(): int
{
return count($this->types);
}
/********************* fetching rows ****************d*g**/
/**
* Set fetched object class. This class should extend the Row class.
* @param string
* @return self
*/
public function setRowClass(?string $class): static
public function setRowClass($class)
{
$this->rowClass = $class;
return $this;
@@ -135,8 +162,9 @@ class Result implements IDataSource
/**
* Returns fetched object class name.
* @return string
*/
public function getRowClass(): ?string
public function getRowClass()
{
return $this->rowClass;
}
@@ -144,8 +172,9 @@ class Result implements IDataSource
/**
* Set a factory to create fetched object instances. These should extend the Row class.
* @return self
*/
public function setRowFactory(callable $callback): static
public function setRowFactory(callable $callback)
{
$this->rowFactory = $callback;
return $this;
@@ -155,38 +184,36 @@ class Result implements IDataSource
/**
* Fetches the row at current position, process optional type conversion.
* and moves the internal cursor to the next position
* @return Row|FALSE
*/
final public function fetch(): mixed
final public function fetch()
{
$row = $this->getResultDriver()->fetch(true);
if ($row === null) {
return null;
$row = $this->getResultDriver()->fetch(TRUE);
if (!is_array($row)) {
return FALSE;
}
$this->fetched = true;
$this->fetched = TRUE;
$this->normalize($row);
if ($this->rowFactory) {
return ($this->rowFactory)($row);
return call_user_func($this->rowFactory, $row);
} elseif ($this->rowClass) {
return new $this->rowClass($row);
$row = new $this->rowClass($row);
}
return $row;
}
/**
* Like fetch(), but returns only first field.
* Returns value on success, null if no next record
* @return mixed value on success, FALSE if no next record
*/
final public function fetchSingle(): mixed
final public function fetchSingle()
{
$row = $this->getResultDriver()->fetch(true);
if ($row === null) {
return null;
$row = $this->getResultDriver()->fetch(TRUE);
if (!is_array($row)) {
return FALSE;
}
$this->fetched = true;
$this->fetched = TRUE;
$this->normalize($row);
return reset($row);
}
@@ -194,12 +221,14 @@ class Result implements IDataSource
/**
* Fetches all records from table.
* @return Row[]|array[]
* @param int offset
* @param int limit
* @return Row[]
*/
final public function fetchAll(?int $offset = null, ?int $limit = null): array
final public function fetchAll($offset = NULL, $limit = NULL)
{
$limit ??= -1;
$this->seek($offset ?: 0);
$limit = $limit === NULL ? -1 : (int) $limit;
$this->seek((int) $offset);
$row = $this->fetch();
if (!$row) {
return []; // empty result set
@@ -210,7 +239,6 @@ class Result implements IDataSource
if ($limit === 0) {
break;
}
$limit--;
$data[] = $row;
} while ($row = $this->fetch());
@@ -226,11 +254,13 @@ class Result implements IDataSource
* builds a tree: $tree[$val1][$index][$val2]->col3[$val3] = {record}
* - associative descriptor: col1|col2->col3=col4
* builds a tree: $tree[$val1][$val2]->col3[$val3] = val4
* @param string associative descriptor
* @return array
* @throws \InvalidArgumentException
*/
final public function fetchAssoc(string $assoc): array
final public function fetchAssoc($assoc)
{
if (str_contains($assoc, ',')) {
if (strpos($assoc, ',') !== FALSE) {
return $this->oldFetchAssoc($assoc);
}
@@ -240,15 +270,12 @@ class Result implements IDataSource
return []; // empty result set
}
$data = null;
$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) {
// offsetExists ignores null in PHP 5.2.1, isset() surprisingly null accepts
// offsetExists ignores NULL in PHP 5.2.1, isset() surprisingly NULL accepts
if ($as !== '[]' && $as !== '=' && $as !== '->' && $as !== '|' && !property_exists($row, $as)) {
throw new \InvalidArgumentException("Unknown column '$as' in associative descriptor.");
}
@@ -276,31 +303,34 @@ class Result implements IDataSource
continue 2;
} elseif ($as === '->') { // "object" node
if ($x === null) {
if ($x === NULL) {
$x = clone $row;
$x = &$x->{$assoc[$i + 1]};
$x = null; // prepare child node
$x = NULL; // prepare child node
} else {
$x = &$x->{$assoc[$i + 1]};
}
} elseif ($as !== '|') { // associative-array node
$x = &$x[(string) $row->$as];
$x = &$x[$row->$as];
}
}
if ($x === null) { // build leaf
if ($x === NULL) { // build leaf
$x = $row;
}
} while ($row = $this->fetch());
unset($x);
/** @var mixed[] $data */
return $data;
}
/** @deprecated */
private function oldFetchAssoc(string $assoc)
/**
* @deprecated
*/
private function oldFetchAssoc($assoc)
{
$this->seek(0);
$row = $this->fetch();
@@ -308,7 +338,7 @@ class Result implements IDataSource
return []; // empty result set
}
$data = null;
$data = NULL;
$assoc = explode(',', $assoc);
// strip leading = and @
@@ -333,31 +363,36 @@ class Result implements IDataSource
$x = &$x[];
} elseif ($as === '=') { // "record" node
if ($x === null) {
if ($x === NULL) {
$x = $row->toArray();
$x = &$x[$assoc[$i + 1]];
$x = null; // prepare child node
$x = &$x[ $assoc[$i + 1] ];
$x = NULL; // prepare child node
} else {
$x = &$x[$assoc[$i + 1]];
$x = &$x[ $assoc[$i + 1] ];
}
} elseif ($as === '@') { // "object" node
if ($x === null) {
if ($x === NULL) {
$x = clone $row;
$x = &$x->{$assoc[$i + 1]};
$x = null; // prepare child node
$x = NULL; // prepare child node
} else {
$x = &$x->{$assoc[$i + 1]};
}
} else { // associative-array node
$x = &$x[(string) $row->$as];
$x = &$x[$row->$as];
}
}
if ($x === null) { // build leaf
$x = $leaf === '='
? $row->toArray()
: $row;
if ($x === NULL) { // build leaf
if ($leaf === '=') {
$x = $row->toArray();
} else {
$x = $row;
}
}
} while ($row = $this->fetch());
unset($x);
@@ -367,9 +402,12 @@ class Result implements IDataSource
/**
* Fetches all records from table like $key => $value pairs.
* @param string associative key
* @param string value
* @return array
* @throws \InvalidArgumentException
*/
final public function fetchPairs(?string $key = null, ?string $value = null): array
final public function fetchPairs($key = NULL, $value = NULL)
{
$this->seek(0);
$row = $this->fetch();
@@ -379,8 +417,8 @@ class Result implements IDataSource
$data = [];
if ($value === null) {
if ($key !== null) {
if ($value === NULL) {
if ($key !== NULL) {
throw new \InvalidArgumentException('Either none or both columns must be specified.');
}
@@ -391,7 +429,6 @@ class Result implements IDataSource
do {
$data[] = $row[$key];
} while ($row = $this->fetch());
return $data;
}
@@ -402,11 +439,10 @@ class Result implements IDataSource
throw new \InvalidArgumentException("Unknown value column '$value'.");
}
if ($key === null) { // indexed-array
if ($key === NULL) { // indexed-array
do {
$data[] = $row[$value];
} while ($row = $this->fetch());
return $data;
}
@@ -416,7 +452,7 @@ class Result implements IDataSource
}
do {
$data[(string) $row[$key]] = $row[$value];
$data[ (string) $row[$key] ] = $row[$value];
} while ($row = $this->fetch());
return $data;
@@ -428,13 +464,14 @@ class Result implements IDataSource
/**
* Autodetect column types.
* @return void
*/
private function detectTypes(): void
private function detectTypes()
{
$cache = Helpers::getTypeCache();
try {
foreach ($this->getResultDriver()->getResultColumns() as $col) {
$this->types[$col['name']] = $col['type'] ?? $cache->{$col['nativetype']};
$this->types[$col['name']] = isset($col['type']) ? $col['type'] : $cache->{$col['nativetype']};
}
} catch (NotSupportedException $e) {
}
@@ -443,85 +480,55 @@ class Result implements IDataSource
/**
* Converts values to specified type and format.
* @param array
* @return void
*/
private function normalize(array &$row): void
private function normalize(array &$row)
{
foreach ($this->types as $key => $type) {
if (!isset($row[$key])) { // null
if (!isset($row[$key])) { // NULL
continue;
}
$value = $row[$key];
$format = $this->formats[$type] ?? null;
if ($type === null || $format === 'native') {
$row[$key] = $value;
} elseif ($type === Type::Text) {
if ($type === Type::TEXT) {
$row[$key] = (string) $value;
} elseif ($type === Type::Integer) {
} elseif ($type === Type::INTEGER) {
$row[$key] = is_float($tmp = $value * 1)
? (is_string($value) ? $value : (int) $value)
: $tmp;
} elseif ($type === Type::Float) {
if (!is_string($value)) {
$row[$key] = (float) $value;
continue;
}
$negative = ($value[0] ?? null) === '-';
$value = ltrim($value, '0-');
} elseif ($type === Type::FLOAT) {
$value = ltrim((string) $value, '0');
$p = strpos($value, '.');
$e = strpos($value, 'e');
if ($p !== false && $e === false) {
if ($p !== FALSE) {
$value = rtrim(rtrim($value, '0'), '.');
} elseif ($p !== false && $e !== false) {
$value = rtrim($value, '.');
}
if ($value === '' || $value[0] === '.') {
$value = '0' . $value;
}
if ($negative) {
$value = '-' . $value;
}
$row[$key] = $value === str_replace(',', '.', (string) ($float = (float) $value))
? $float
: $value;
} elseif ($type === Type::Bool) {
} elseif ($type === Type::BOOL) {
$row[$key] = ((bool) $value) && $value !== 'f' && $value !== 'F';
} elseif ($type === Type::DateTime || $type === Type::Date || $type === Type::Time) {
if ($value && !str_starts_with((string) $value, '0000-00')) { // '', null, false, '0000-00-00', ...
} elseif ($type === Type::DATETIME || $type === Type::DATE || $type === Type::TIME) {
if ($value && substr((string) $value, 0, 3) !== '000') { // '', NULL, FALSE, '0000-00-00', ...
$value = new DateTime($value);
$row[$key] = $format ? $value->format($format) : $value;
$row[$key] = empty($this->formats[$type]) ? $value : $value->format($this->formats[$type]);
} else {
$row[$key] = null;
$row[$key] = NULL;
}
} elseif ($type === Type::TimeInterval) {
} elseif ($type === Type::TIME_INTERVAL) {
preg_match('#^(-?)(\d+)\D(\d+)\D(\d+)\z#', $value, $m);
$value = new \DateInterval("PT$m[2]H$m[3]M$m[4]S");
$value->invert = (int) (bool) $m[1];
$row[$key] = $format ? $value->format($format) : $value;
$row[$key] = new \DateInterval("PT$m[2]H$m[3]M$m[4]S");
$row[$key]->invert = (int) (bool) $m[1];
} elseif ($type === Type::Binary) {
$row[$key] = is_string($value)
? $this->getResultDriver()->unescapeBinary($value)
: $value;
} elseif ($type === Type::JSON) {
if ($format === 'string') { // back compatibility with 'native'
$row[$key] = $value;
} else {
$row[$key] = json_decode($value, $format === 'array');
}
} else {
throw new \RuntimeException('Unexpected type ' . $type);
} elseif ($type === Type::BINARY) {
$row[$key] = $this->getResultDriver()->unescapeBinary($value);
}
}
}
@@ -529,59 +536,47 @@ class Result implements IDataSource
/**
* Define column type.
* @param string|null $type use constant Type::*
* @param string column
* @param string type (use constant Type::*)
* @return self
*/
final public function setType(string $column, ?string $type): static
final public function setType($col, $type)
{
$this->types[$column] = $type;
$this->types[$col] = $type;
return $this;
}
/**
* Returns column type.
* @return string
*/
final public function getType(string $column): ?string
final public function getType($col)
{
return $this->types[$column] ?? null;
return isset($this->types[$col]) ? $this->types[$col] : NULL;
}
/**
* Returns columns type.
* Sets date format.
* @param string
* @param string|NULL format
* @return self
*/
final public function getTypes(): array
{
return $this->types;
}
/**
* Sets type format.
*/
final public function setFormat(string $type, ?string $format): static
final public function setFormat($type, $format)
{
$this->formats[$type] = $format;
return $this;
}
/**
* Sets type formats.
*/
final public function setFormats(array $formats): static
{
$this->formats = $formats;
return $this;
}
/**
* Returns data format.
* @return string|NULL
*/
final public function getFormat(string $type): ?string
final public function getFormat($type)
{
return $this->formats[$type] ?? null;
return isset($this->formats[$type]) ? $this->formats[$type] : NULL;
}
@@ -590,19 +585,21 @@ class Result implements IDataSource
/**
* Returns a meta information about the current result set.
* @return Reflection\Result
*/
public function getInfo(): Reflection\Result
public function getInfo()
{
if (!isset($this->meta)) {
if ($this->meta === NULL) {
$this->meta = new Reflection\Result($this->getResultDriver());
}
return $this->meta;
}
/** @return Reflection\Column[] */
final public function getColumns(): array
/**
* @return Reflection\Column[]
*/
final public function getColumns()
{
return $this->getInfo()->getColumns();
}
@@ -613,9 +610,11 @@ class Result implements IDataSource
/**
* Displays complete result set as HTML or text table for debug purposes.
* @return void
*/
final public function dump(): void
final public function dump()
{
echo Helpers::dump($this);
}
}

View File

@@ -1,34 +1,53 @@
<?php
/**
* This file is part of the Dibi, smart database abstraction layer (https://dibiphp.com)
* This file is part of the "dibi" - smart database abstraction layer.
* Copyright (c) 2005 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Dibi;
/**
* External result set iterator.
*
* This can be returned by Result::getIterator() method or using foreach
* <code>
* $result = dibi::query('SELECT * FROM table');
* foreach ($result as $row) {
* print_r($row);
* }
* unset($result);
* </code>
*/
class ResultIterator implements \Iterator, \Countable
{
private mixed $row;
private int $pointer = 0;
use Strict;
/** @var Result */
private $result;
/** @var mixed */
private $row;
/** @var int */
private $pointer;
public function __construct(
private Result $result,
) {
/**
* @param Result
*/
public function __construct(Result $result)
{
$this->result = $result;
}
/**
* Rewinds the iterator to the first element.
* @return void
*/
public function rewind(): void
public function rewind()
{
$this->pointer = 0;
$this->result->seek(0);
@@ -38,9 +57,9 @@ class ResultIterator implements \Iterator, \Countable
/**
* Returns the key of the current element.
* @return mixed
*/
#[\ReturnTypeWillChange]
public function key(): mixed
public function key()
{
return $this->pointer;
}
@@ -48,9 +67,9 @@ class ResultIterator implements \Iterator, \Countable
/**
* Returns the current element.
* @return mixed
*/
#[\ReturnTypeWillChange]
public function current(): mixed
public function current()
{
return $this->row;
}
@@ -58,8 +77,9 @@ class ResultIterator implements \Iterator, \Countable
/**
* Moves forward to next element.
* @return void
*/
public function next(): void
public function next()
{
$this->row = $this->result->fetch();
$this->pointer++;
@@ -68,8 +88,9 @@ class ResultIterator implements \Iterator, \Countable
/**
* Checks if there is a current element after calls to rewind() or next().
* @return bool
*/
public function valid(): bool
public function valid()
{
return !empty($this->row);
}
@@ -77,9 +98,11 @@ class ResultIterator implements \Iterator, \Countable
/**
* Required by the Countable interface.
* @return int
*/
public function count(): int
public function count()
{
return $this->result->getRowCount();
}
}

View File

@@ -1,22 +1,20 @@
<?php
/**
* This file is part of the Dibi, smart database abstraction layer (https://dibiphp.com)
* This file is part of the "dibi" - smart database abstraction layer.
* Copyright (c) 2005 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Dibi;
/**
* Result set single row.
*/
#[\AllowDynamicProperties]
class Row implements \ArrayAccess, \IteratorAggregate, \Countable
{
public function __construct(array $arr)
public function __construct($arr)
{
foreach ($arr as $k => $v) {
$this->$k = $v;
@@ -24,7 +22,7 @@ class Row implements \ArrayAccess, \IteratorAggregate, \Countable
}
public function toArray(): array
public function toArray()
{
return (array) $this;
}
@@ -32,71 +30,66 @@ class Row implements \ArrayAccess, \IteratorAggregate, \Countable
/**
* Converts value to DateTime object.
* @param string key
* @param string format
* @return \DateTime
*/
public function asDateTime(string $key, ?string $format = null): DateTime|string|null
public function asDateTime($key, $format = NULL)
{
$time = $this[$key];
if (!$time instanceof DateTime) {
if (!$time || str_starts_with((string) $time, '0000-00')) { // '', null, false, '0000-00-00', ...
return null;
if (!$time || substr((string) $time, 0, 3) === '000') { // '', NULL, FALSE, '0000-00-00', ...
return NULL;
}
$time = new DateTime($time);
}
return $format === null ? $time : $time->format($format);
return $format === NULL ? $time : $time->format($format);
}
public function __get(string $key): mixed
public function __get($key)
{
$hint = Helpers::getSuggestion(array_keys((array) $this), $key);
trigger_error("Attempt to read missing column '$key'" . ($hint ? ", did you mean '$hint'?" : '.'), E_USER_NOTICE);
return null;
}
public function __isset(string $key): bool
{
return false;
}
/********************* interfaces ArrayAccess, Countable & IteratorAggregate ****************d*g**/
final public function count(): int
final public function count()
{
return count((array) $this);
}
final public function getIterator(): \ArrayIterator
final public function getIterator()
{
return new \ArrayIterator($this);
}
final public function offsetSet($nm, $val): void
final public function offsetSet($nm, $val)
{
$this->$nm = $val;
}
final public function offsetGet($nm): mixed
final public function offsetGet($nm)
{
return $this->$nm;
}
final public function offsetExists($nm): bool
final public function offsetExists($nm)
{
return isset($this->$nm);
}
final public function offsetUnset($nm): void
final public function offsetUnset($nm)
{
unset($this->$nm);
}
}

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

@@ -0,0 +1,152 @@
<?php
/**
* This file is part of the "dibi" - smart database abstraction layer.
* Copyright (c) 2005 David Grudl (https://davidgrudl.com)
*/
namespace Dibi;
use ReflectionClass;
use ReflectionMethod;
use ReflectionProperty;
/**
* Better OOP experience.
*/
trait Strict
{
/** @var array [method => [type => callback]] */
private static $extMethods;
/**
* Call to undefined method.
* @throws \LogicException
*/
public function __call($name, $args)
{
if ($cb = self::extensionMethod(get_class($this) . '::' . $name)) { // back compatiblity
array_unshift($args, $this);
return call_user_func_array($cb, $args);
}
$class = method_exists($this, $name) ? 'parent' : get_class($this);
$items = (new ReflectionClass($this))->getMethods(ReflectionMethod::IS_PUBLIC);
$hint = ($t = Helpers::getSuggestion($items, $name)) ? ", did you mean $t()?" : '.';
throw new \LogicException("Call to undefined method $class::$name()$hint");
}
/**
* Call to undefined static method.
* @throws \LogicException
*/
public static function __callStatic($name, $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()?" : '.';
throw new \LogicException("Call to undefined static method {$rc->getName()}::$name()$hint");
}
/**
* Access to undeclared property.
* @throws \LogicException
*/
public function &__get($name)
{
if ((method_exists($this, $m = 'get' . $name) || method_exists($this, $m = 'is' . $name))
&& (new ReflectionMethod($this, $m))->isPublic()
) { // back compatiblity
$ret = $this->$m();
return $ret;
}
$rc = new ReflectionClass($this);
$items = array_diff($rc->getProperties(ReflectionProperty::IS_PUBLIC), $rc->getProperties(ReflectionProperty::IS_STATIC));
$hint = ($t = Helpers::getSuggestion($items, $name)) ? ", did you mean $$t?" : '.';
throw new \LogicException("Attempt to read undeclared property {$rc->getName()}::$$name$hint");
}
/**
* Access to undeclared property.
* @throws \LogicException
*/
public function __set($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?" : '.';
throw new \LogicException("Attempt to write to undeclared property {$rc->getName()}::$$name$hint");
}
/**
* @return bool
*/
public function __isset($name)
{
return FALSE;
}
/**
* Access to undeclared property.
* @throws \LogicException
*/
public function __unset($name)
{
$class = get_class($this);
throw new \LogicException("Attempt to unset undeclared property $class::$$name.");
}
/**
* @param string method name
* @param callable
* @return mixed
*/
public static function extensionMethod($name, $callback = NULL)
{
if (strpos($name, '::') === FALSE) {
$class = get_called_class();
} else {
list($class, $name) = explode('::', $name);
$class = (new ReflectionClass($class))->getName();
}
if (self::$extMethods === NULL) { // for backwards compatibility
$list = get_defined_functions();
foreach ($list['user'] as $fce) {
$pair = explode('_prototype_', $fce);
if (count($pair) === 2) {
trigger_error("Extension method defined as $fce() is deprecated, use $class::extensionMethod('$name', ...).", E_USER_DEPRECATED);
self::$extMethods[$pair[1]][(new ReflectionClass($pair[0]))->getName()] = $fce;
self::$extMethods[$pair[1]][''] = NULL;
}
}
}
$list = & self::$extMethods[strtolower($name)];
if ($callback === NULL) { // getter
$cache = &$list[''][$class];
if (isset($cache)) {
return $cache;
}
foreach ([$class] + class_parents($class) + class_implements($class) as $cl) {
if (isset($list[$cl])) {
return $cache = $list[$cl];
}
}
return $cache = FALSE;
} else { // setter
$list[$class] = $callback;
$list[''] = NULL;
}
}
}

View File

@@ -1,33 +1,52 @@
<?php
/**
* This file is part of the Dibi, smart database abstraction layer (https://dibiphp.com)
* This file is part of the "dibi" - smart database abstraction layer.
* Copyright (c) 2005 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Dibi;
/**
* SQL translator.
* dibi SQL translator.
*/
final class Translator
{
private Connection $connection;
private Driver $driver;
private int $cursor = 0;
private array $args;
use Strict;
/** @var Connection */
private $connection;
/** @var Driver */
private $driver;
/** @var int */
private $cursor = 0;
/** @var array */
private $args;
/** @var string[] */
private array $errors;
private bool $comment = false;
private int $ifLevel = 0;
private int $ifLevelStart = 0;
private ?int $limit = null;
private ?int $offset = null;
private HashMap $identifiers;
private $errors;
/** @var bool */
private $comment = FALSE;
/** @var int */
private $ifLevel = 0;
/** @var int */
private $ifLevelStart = 0;
/** @var int */
private $limit;
/** @var int */
private $offset;
/** @var HashMap */
private $identifiers;
public function __construct(Connection $connection)
@@ -40,20 +59,20 @@ final class Translator
/**
* Generates SQL. Can be called only once.
* @param array
* @return string
* @throws Exception
*/
public function translate(array $args): string
public function translate(array $args)
{
$args = array_values($args);
while (count($args) === 1 && is_array($args[0])) { // implicit array expansion
$args = array_values($args[0]);
}
$this->args = $args;
$this->errors = [];
$commandIns = null;
$lastArr = null;
$commandIns = NULL;
$lastArr = NULL;
$cursor = &$this->cursor;
$comment = &$this->comment;
@@ -72,30 +91,28 @@ final class Translator
$sql[] = $arg;
} else {
$sql[] = substr($arg, 0, $toSkip)
// 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,
/*
. 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',
[$this, 'cb'],
substr($arg, $toSkip),
);
substr($arg, $toSkip)
);
if (preg_last_error()) {
throw new PcreException;
}
}
continue;
}
@@ -110,7 +127,7 @@ final class Translator
if (is_array($arg) && is_string(key($arg))) {
// associative array -> autoselect between SET or VALUES & LIST
if ($commandIns === null) {
if ($commandIns === NULL) {
$commandIns = strtoupper(substr(ltrim($this->args[0]), 0, 6));
$commandIns = $commandIns === 'INSERT' || $commandIns === 'REPLAC';
$sql[] = $this->formatValue($arg, $commandIns ? 'v' : 'a');
@@ -118,30 +135,29 @@ final class Translator
if ($lastArr === $cursor - 1) {
$sql[] = ',';
}
$sql[] = $this->formatValue($arg, $commandIns ? 'l' : 'a');
}
$lastArr = $cursor;
continue;
}
// default processing
$sql[] = $this->formatValue($arg, null);
$sql[] = $this->formatValue($arg, FALSE);
} // while
if ($comment) {
$sql[] = '*/';
}
$sql = trim(implode(' ', $sql), ' ');
$sql = implode(' ', $sql);
if ($this->errors) {
throw new Exception('SQL translate error: ' . trim(reset($this->errors), '*'), 0, $sql);
}
// apply limit
if ($this->limit !== null || $this->offset !== null) {
if ($this->limit !== NULL || $this->offset !== NULL) {
$this->driver->applyLimit($sql, $this->limit, $this->offset);
}
@@ -151,8 +167,11 @@ final class Translator
/**
* Apply modifier to single value.
* @param mixed
* @param string
* @return string
*/
public function formatValue(mixed $value, ?string $modifier): string
public function formatValue($value, $modifier)
{
if ($this->comment) {
return '...';
@@ -177,7 +196,7 @@ final class Translator
$pair = explode('%', $k, 2); // split into identifier & modifier
$k = $this->identifiers->{$pair[0]} . ' ';
if (!isset($pair[1])) {
$v = $this->formatValue($v, null);
$v = $this->formatValue($v, FALSE);
$vx[] = $k . ($v === 'NULL' ? 'IS ' : '= ') . $v;
} elseif ($pair[1] === 'ex') {
@@ -187,21 +206,20 @@ final class Translator
$v = $this->formatValue($v, $pair[1]);
if ($pair[1] === 'l' || $pair[1] === 'in') {
$op = 'IN ';
} elseif (str_contains($pair[1], 'like')) {
} elseif (strpos($pair[1], 'like') !== FALSE) {
$op = 'LIKE ';
} elseif ($v === 'NULL') {
$op = 'IS ';
} else {
$op = '= ';
}
$vx[] = $k . $op . $v;
}
} else {
$vx[] = $this->formatValue($v, 'ex');
}
}
return '(' . implode(') ' . strtoupper($modifier) . ' (', $vx) . ')';
case 'n': // key, key, ... identifier names
@@ -213,17 +231,15 @@ final class Translator
$vx[] = $this->identifiers->{$pair[0]};
}
}
return implode(', ', $vx);
case 'a': // key=val, key=val, ...
foreach ($value as $k => $v) {
$pair = explode('%', (string) $k, 2); // split into identifier & modifier
$pair = explode('%', $k, 2); // split into identifier & modifier
$vx[] = $this->identifiers->{$pair[0]} . '='
. $this->formatValue($v, $pair[1] ?? (is_array($v) ? 'ex!' : null));
. $this->formatValue($v, isset($pair[1]) ? $pair[1] : (is_array($v) ? 'ex' : FALSE));
}
return implode(', ', $vx);
@@ -231,9 +247,8 @@ final class Translator
case 'l': // (val, val, ...)
foreach ($value as $k => $v) {
$pair = explode('%', (string) $k, 2); // split into identifier & modifier
$vx[] = $this->formatValue($v, $pair[1] ?? (is_array($v) ? 'ex!' : null));
$vx[] = $this->formatValue($v, isset($pair[1]) ? $pair[1] : (is_array($v) ? 'ex' : FALSE));
}
return '(' . (($vx || $modifier === 'l') ? implode(', ', $vx) : 'NULL') . ')';
@@ -241,9 +256,8 @@ final class Translator
foreach ($value as $k => $v) {
$pair = explode('%', $k, 2); // split into identifier & modifier
$kx[] = $this->identifiers->{$pair[0]};
$vx[] = $this->formatValue($v, $pair[1] ?? (is_array($v) ? 'ex!' : null));
$vx[] = $this->formatValue($v, isset($pair[1]) ? $pair[1] : (is_array($v) ? 'ex' : FALSE));
}
return '(' . implode(', ', $kx) . ') VALUES (' . implode(', ', $vx) . ')';
case 'm': // (key, key, ...) VALUES (val, val, ...), (val, val, ...), ...
@@ -257,20 +271,18 @@ final class Translator
$proto = array_keys($v);
}
} else {
return $this->errors[] = '**Unexpected type ' . get_debug_type($v) . '**';
return $this->errors[] = '**Unexpected type ' . (is_object($v) ? get_class($v) : gettype($v)) . '**';
}
$pair = explode('%', $k, 2); // split into identifier & modifier
$kx[] = $this->identifiers->{$pair[0]};
foreach ($v as $k2 => $v2) {
$vx[$k2][] = $this->formatValue($v2, $pair[1] ?? (is_array($v2) ? 'ex!' : null));
$vx[$k2][] = $this->formatValue($v2, isset($pair[1]) ? $pair[1] : (is_array($v2) ? 'ex' : FALSE));
}
}
foreach ($vx as $k => $v) {
$vx[$k] = '(' . implode(', ', $v) . ')';
}
return '(' . implode(', ', $kx) . ') VALUES ' . implode(', ', $vx);
case 'by': // key ASC, key DESC
@@ -278,135 +290,95 @@ 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!':
trigger_error('Use Dibi\Expression instead of array: ' . implode(', ', array_filter($value, 'is_scalar')), E_USER_WARNING);
// break omitted
case 'ex':
case 'sql':
return $this->connection->translate(...$value);
return call_user_func_array([$this->connection, 'translate'], $value);
default: // value, value, value - all with the same modifier
foreach ($value as $v) {
$vx[] = $this->formatValue($v, $modifier);
}
return implode(', ', $vx);
}
}
if (is_object($value)
&& $modifier === null
&& !$value instanceof Literal
&& !$value instanceof Expression
&& $result = $this->connection->translateObject($value)
) {
return $this->connection->translate(...$result->getValues());
}
// object-to-scalar procession
if ($value instanceof \BackedEnum && is_scalar($value->value)) {
$value = $value->value;
}
// with modifier procession
if ($modifier) {
if ($value !== null && !is_scalar($value)) { // array is already processed
if ($value !== NULL && !is_scalar($value)) { // array is already processed
if ($value instanceof Literal && ($modifier === 'sql' || $modifier === 'SQL')) {
return (string) $value;
} elseif ($value instanceof Expression && $modifier === 'ex') {
return $this->connection->translate(...$value->getValues());
} elseif (
$value instanceof \DateTimeInterface
&& ($modifier === 'd'
|| $modifier === 't'
|| $modifier === 'dt'
)
) {
$modifier = 'SQL';
} elseif (($value instanceof \DateTime || $value instanceof \DateTimeInterface) && ($modifier === 'd' || $modifier === 't' || $modifier === 'dt')) {
// continue
} else {
$type = get_debug_type($value);
$type = is_object($value) ? get_class($value) : gettype($value);
return $this->errors[] = "**Invalid combination of type $type and modifier %$modifier**";
}
}
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($value);
case 'sN': // string or null
case 'sN': // string or NULL
case 'sn':
return $value === '' || $value === 0 || $value === null
? 'NULL'
: $this->driver->escapeText((string) $value);
return $value == '' ? 'NULL' : $this->driver->escapeText((string) $value); // notice two equal signs
case 'iN': // signed int or null
if ($value === '' || $value === 0 || $value === null) {
return 'NULL';
case 'in': // deprecated
trigger_error('Modifier %in is deprecated, use %iN.', E_USER_DEPRECATED);
// intentionally break omitted
case 'iN': // signed int or NULL
if ($value == '') {
$value = NULL;
}
// break omitted
// intentionally break omitted
case 'i': // signed int
case 'u': // unsigned int, ignored
if ($value === null) {
if ($value === NULL) {
return 'NULL';
} elseif (is_string($value)) {
if (preg_match('#[+-]?\d++(?:e\d+)?\z#A', $value)) {
return $value; // support for long numbers - keep them unchanged
} else {
throw new Exception("Expected number, '$value' given.");
}
} elseif (is_string($value) && preg_match('#[+-]?\d++(?:e\d+)?\z#A', $value)) {
return $value; // support for long numbers - keep them unchanged
} elseif (is_string($value) && substr($value, 1, 1) === 'x' && is_numeric($value)) {
trigger_error('Support for hex strings has been deprecated.', E_USER_DEPRECATED);
return (string) hexdec($value);
} else {
return (string) (int) $value;
}
// break omitted
case 'f': // float
if ($value === null) {
if ($value === NULL) {
return 'NULL';
} elseif (is_string($value)) {
if (is_numeric($value) && substr($value, 1, 1) !== 'x') {
return $value; // support for long numbers - keep them unchanged
} else {
throw new Exception("Expected number, '$value' given.");
}
} elseif (is_string($value) && is_numeric($value) && substr($value, 1, 1) !== 'x') {
return $value; // support for extreme numbers - keep them unchanged
} else {
return rtrim(rtrim(number_format($value + 0, 10, '.', ''), '0'), '.');
}
// break omitted
case 'd': // date
case 't': // datetime
case 'dt': // datetime
if ($value === null) {
if ($value === NULL) {
return 'NULL';
} elseif (!$value instanceof \DateTimeInterface) {
$value = new DateTime($value);
} else {
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
return $this->identifiers->$value;
@@ -421,42 +393,27 @@ final class Translator
$toSkip = strcspn($value, '`[\'":');
if (strlen($value) !== $toSkip) {
$value = substr($value, 0, $toSkip)
. preg_replace_callback(
<<<'XX'
/
(?=[`['":])
(?:
`(.+?)`|
\[(.+?)]|
(')((?:''|[^'])*)'|
(")((?:""|[^"])*)"|
(['"])|
:(\S*?:)([a-zA-Z0-9._]?)
)/sx
XX,
[$this, 'cb'],
substr($value, $toSkip),
);
. preg_replace_callback(
'/(?=[`[\'":])(?:`(.+?)`|\[(.+?)\]|(\')((?:\'\'|[^\'])*)\'|(")((?:""|[^"])*)"|(\'|")|:(\S*?:)([a-zA-Z0-9._]?))/s',
[$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, 2);
case '~like': // LIKE %string
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, -1);
case 'like': // LIKE string
case '~like~': // LIKE %string%
return $this->driver->escapeLike($value, 0);
case 'and':
@@ -472,6 +429,7 @@ final class Translator
}
}
// without modifier procession
if (is_string($value)) {
return $this->driver->escapeText($value);
@@ -485,23 +443,17 @@ final class Translator
} elseif (is_bool($value)) {
return $this->driver->escapeBool($value);
} elseif ($value === null) {
} elseif ($value === NULL) {
return 'NULL';
} elseif ($value instanceof \DateTimeInterface) {
} elseif ($value instanceof \DateTime || $value instanceof \DateTimeInterface) {
return $this->driver->escapeDateTime($value);
} elseif ($value instanceof \DateInterval) {
return $this->driver->escapeDateInterval($value);
} elseif ($value instanceof Literal) {
return (string) $value;
} elseif ($value instanceof Expression) {
return $this->connection->translate(...$value->getValues());
} else {
$type = get_debug_type($value);
$type = is_object($value) ? get_class($value) : gettype($value);
return $this->errors[] = "**Unexpected $type**";
}
}
@@ -509,8 +461,10 @@ final class Translator
/**
* PREG callback from translate() or formatValue().
* @param array
* @return string
*/
private function cb(array $matches): string
private function cb($matches)
{
// [1] => `ident`
// [2] => [ident]
@@ -533,7 +487,7 @@ final class Translator
}
$cursor++;
return $this->formatValue($this->args[$cursor - 1], null);
return $this->formatValue($this->args[$cursor - 1], FALSE);
}
if (!empty($matches[10])) { // modifier
@@ -550,31 +504,30 @@ final class Translator
if (!$this->comment && !$this->args[$cursor - 1]) {
// open comment
$this->ifLevelStart = $this->ifLevel;
$this->comment = true;
$this->comment = TRUE;
return '/*';
}
return '';
} elseif ($mod === 'else') {
if ($this->ifLevelStart === $this->ifLevel) {
$this->ifLevelStart = 0;
$this->comment = false;
$this->comment = FALSE;
return '*/';
} elseif (!$this->comment) {
$this->ifLevelStart = $this->ifLevel;
$this->comment = true;
$this->comment = TRUE;
return '/*';
}
} elseif ($mod === 'end') {
$this->ifLevel--;
if ($this->ifLevelStart === $this->ifLevel + 1) {
// close comment
$this->ifLevelStart = 0;
$this->comment = false;
$this->comment = FALSE;
return '*/';
}
return '';
} elseif ($mod === 'ex') { // array expansion
@@ -583,24 +536,22 @@ final class Translator
} elseif ($mod === 'lmt') { // apply limit
$arg = $this->args[$cursor++];
if ($arg === null) {
if ($arg === NULL) {
} elseif ($this->comment) {
return "(limit $arg)";
} else {
$this->limit = Helpers::intVal($arg);
$this->limit = (int) $arg;
}
return '';
} elseif ($mod === 'ofs') { // apply offset
$arg = $this->args[$cursor++];
if ($arg === null) {
if ($arg === NULL) {
} elseif ($this->comment) {
return "(offset $arg)";
} else {
$this->offset = Helpers::intVal($arg);
$this->offset = (int) $arg;
}
return '';
} else { // default processing
@@ -632,9 +583,7 @@ 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, FALSE) : $m . $matches[9]; // value or identifier
}
throw new \Exception('this should be never executed');
@@ -642,10 +591,12 @@ final class Translator
/**
* Apply substitutions to identifier and delimites it.
* Apply substitutions to indentifier and delimites it.
* @param string indentifier
* @return string
* @internal
*/
public function delimite(string $value): string
public function delimite($value)
{
$value = $this->connection->substitute($value);
$parts = explode('.', $value);
@@ -654,7 +605,7 @@ final class Translator
$v = $this->driver->escapeIdentifier($v);
}
}
return implode('.', $parts);
}
}

View File

@@ -1,12 +1,10 @@
<?php
/**
* This file is part of the Dibi, smart database abstraction layer (https://dibiphp.com)
* This file is part of the "dibi" - smart database abstraction layer.
* Copyright (c) 2005 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Dibi;
@@ -15,48 +13,20 @@ namespace Dibi;
*/
class Type
{
public const
Text = 's', // as 'string'
Binary = 'bin',
JSON = 'json',
Bool = 'b',
Integer = 'i',
Float = 'f',
Date = 'd',
DateTime = 'dt',
Time = 't',
TimeInterval = 'ti';
/** @deprecated use Type::Text */
public const TEXT = self::Text;
/** @deprecated use Type::Binary */
public const BINARY = self::Binary;
/** @deprecated use Type::Bool */
public const BOOL = self::Bool;
/** @deprecated use Type::Integer */
public const INTEGER = self::Integer;
/** @deprecated use Type::Float */
public const FLOAT = self::Float;
/** @deprecated use Type::Date */
public const DATE = self::Date;
/** @deprecated use Type::DateTime */
public const DATETIME = self::DateTime;
/** @deprecated use Type::Time */
public const TIME = self::Time;
/** @deprecated use Type::TimeInterval */
public const TIME_INTERVAL = self::TimeInterval;
const
TEXT = 's', // as 'string'
BINARY = 'bin',
BOOL = 'b',
INTEGER = 'i',
FLOAT = 'f',
DATE = 'd',
DATETIME = 'dt',
TIME = 't',
TIME_INTERVAL = 'ti';
final public function __construct()
{
throw new \LogicException('Cannot instantiate static class ' . self::class);
throw new \LogicException('Cannot instantiate static class ' . __CLASS__);
}
}

View File

@@ -1,75 +1,74 @@
<?php
/**
* This file is part of the Dibi, smart database abstraction layer (https://dibiphp.com)
* This file is part of the "dibi" - smart database abstraction layer.
* Copyright (c) 2005 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
use Dibi\Type;
/**
* Static container class for Dibi connections.
*
* @method static void disconnect()
* @method static Dibi\Result query(...$args)
* @method static Dibi\Result nativeQuery(...$args)
* @method static bool test(...$args)
* @method static Dibi\DataSource dataSource(...$args)
* @method static Dibi\Row|null fetch(...$args)
* @method static array fetchAll(...$args)
* @method static mixed fetchSingle(...$args)
* @method static array fetchPairs(...$args)
* @method static int getAffectedRows()
* @method static int getInsertId(string $sequence = null)
* @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)
* @method static Dibi\Fluent update(string|string[] $table, array $args)
* @method static Dibi\Fluent insert(string $table, array $args)
* @method static Dibi\Fluent delete(string $table)
* @method static Dibi\HashMap getSubstitutes()
* @method static int loadFile(string $file)
* This class is static container class for creating DB objects and
* store connections info.
*/
class dibi
{
public const Version = '5.0.2';
use Dibi\Strict;
/** @deprecated use dibi::Version */
public const VERSION = self::Version;
const
AFFECTED_ROWS = 'a',
IDENTIFIER = 'n';
/** @deprecated use Dibi\Fluent::AffectedRows */
public const AFFECTED_ROWS = Dibi\Fluent::AffectedRows;
/** @deprecated use Dibi\Fluent::Identifier */
public const IDENTIFIER = Dibi\Fluent::Identifier;
/** version */
const
VERSION = '3.0.9',
REVISION = 'released on 2018-03-09';
/** sorting order */
public const
const
ASC = 'ASC',
DESC = 'DESC';
/** Last SQL command @see dibi::query() */
public static ?string $sql = null;
/** @deprecated */
const
TEXT = Type::TEXT,
BINARY = Type::BINARY,
BOOL = Type::BOOL,
INTEGER = Type::INTEGER,
FLOAT = Type::FLOAT,
DATE = Type::DATE,
DATETIME = Type::DATETIME,
TIME = Type::TIME,
FIELD_TEXT = Type::TEXT,
FIELD_BINARY = Type::BINARY,
FIELD_BOOL = Type::BOOL,
FIELD_INTEGER = Type::INTEGER,
FIELD_FLOAT = Type::FLOAT,
FIELD_DATE = Type::DATE,
FIELD_DATETIME = Type::DATETIME,
FIELD_TIME = Type::TIME;
/** Elapsed time for last query */
public static ?float $elapsedTime = null;
/** @var Dibi\Connection[] Connection registry storage for DibiConnection objects */
private static $registry = [];
/** Elapsed time for all queries */
public static float $totalTime = 0;
/** @var Dibi\Connection Current connection */
private static $connection;
/** Number or queries */
public static int $numOfQueries = 0;
/** @var string Last SQL command @see dibi::query() */
public static $sql;
/** @var Dibi\Connection[] Connection registry storage for Dibi\Connection objects */
private static array $registry = [];
/** @var int Elapsed time for last query */
public static $elapsedTime;
/** Current connection */
private static Dibi\Connection $connection;
/** @var int Elapsed time for all queries */
public static $totalTime;
/** @var int Number or queries */
public static $numOfQueries = 0;
/** @var string Default dibi driver */
public static $defaultDriver = 'mysqli';
/**
@@ -77,7 +76,7 @@ class dibi
*/
final public function __construct()
{
throw new LogicException('Cannot instantiate static class ' . static::class);
throw new LogicException('Cannot instantiate static class ' . get_class($this));
}
@@ -86,32 +85,47 @@ class dibi
/**
* Creates a new Connection object and connects it to specified database.
* @param array $config connection parameters
* @param mixed connection parameters
* @param string connection name
* @return Dibi\Connection
* @throws Dibi\Exception
*/
public static function connect(array $config = [], string $name = '0'): Dibi\Connection
public static function connect($config = [], $name = '0')
{
return self::$connection = self::$registry[$name] = new Dibi\Connection($config, $name);
}
/**
* Returns true when connection was established.
* Disconnects from database (doesn't destroy Connection object).
* @return void
*/
public static function isConnected(): bool
public static function disconnect()
{
return (self::$connection !== null) && self::$connection->isConnected();
self::getConnection()->disconnect();
}
/**
* Returns TRUE when connection was established.
* @return bool
*/
public static function isConnected()
{
return (self::$connection !== NULL) && self::$connection->isConnected();
}
/**
* Retrieve active connection.
* @param string connection registy name
* @return Dibi\Connection
* @throws Dibi\Exception
*/
public static function getConnection(?string $name = null): Dibi\Connection
public static function getConnection($name = NULL)
{
if ($name === null) {
if (self::$connection === null) {
if ($name === NULL) {
if (self::$connection === NULL) {
throw new Dibi\Exception('Dibi is not connected to database.');
}
@@ -128,22 +142,298 @@ class dibi
/**
* Sets connection.
* @param Dibi\Connection
* @return Dibi\Connection
*/
public static function setConnection(Dibi\Connection $connection): Dibi\Connection
public static function setConnection(Dibi\Connection $connection)
{
return self::$connection = $connection;
}
/**
* @deprecated
*/
public static function activate($name)
{
trigger_error(__METHOD__ . '() is deprecated.', E_USER_DEPRECATED);
self::$connection = self::getConnection($name);
}
/********************* monostate for active connection ****************d*g**/
/**
* Monostate for Dibi\Connection.
* Generates and executes SQL query - Monostate for Dibi\Connection::query().
* @param array|mixed one or more arguments
* @return Dibi\Result|int result set or number of affected rows
* @throws Dibi\Exception
*/
public static function __callStatic(string $name, array $args)
public static function query($args)
{
return self::getConnection()->$name(...$args);
$args = func_get_args();
return self::getConnection()->query($args);
}
/**
* Executes the SQL query - Monostate for Dibi\Connection::nativeQuery().
* @param string SQL statement.
* @return Dibi\Result|int result set or number of affected rows
*/
public static function nativeQuery($sql)
{
return self::getConnection()->nativeQuery($sql);
}
/**
* Generates and prints SQL query - Monostate for Dibi\Connection::test().
* @param array|mixed one or more arguments
* @return bool
*/
public static function test($args)
{
$args = func_get_args();
return self::getConnection()->test($args);
}
/**
* Generates and returns SQL query as DataSource - Monostate for Dibi\Connection::test().
* @param array|mixed one or more arguments
* @return Dibi\DataSource
*/
public static function dataSource($args)
{
$args = func_get_args();
return self::getConnection()->dataSource($args);
}
/**
* Executes SQL query and fetch result - Monostate for Dibi\Connection::query() & fetch().
* @param array|mixed one or more arguments
* @return Dibi\Row
* @throws Dibi\Exception
*/
public static function fetch($args)
{
$args = func_get_args();
return self::getConnection()->query($args)->fetch();
}
/**
* Executes SQL query and fetch results - Monostate for Dibi\Connection::query() & fetchAll().
* @param array|mixed one or more arguments
* @return Dibi\Row[]
* @throws Dibi\Exception
*/
public static function fetchAll($args)
{
$args = func_get_args();
return self::getConnection()->query($args)->fetchAll();
}
/**
* Executes SQL query and fetch first column - Monostate for Dibi\Connection::query() & fetchSingle().
* @param array|mixed one or more arguments
* @return mixed
* @throws Dibi\Exception
*/
public static function fetchSingle($args)
{
$args = func_get_args();
return self::getConnection()->query($args)->fetchSingle();
}
/**
* Executes SQL query and fetch pairs - Monostate for Dibi\Connection::query() & fetchPairs().
* @param array|mixed one or more arguments
* @return array
* @throws Dibi\Exception
*/
public static function fetchPairs($args)
{
$args = func_get_args();
return self::getConnection()->query($args)->fetchPairs();
}
/**
* Gets the number of affected rows.
* Monostate for Dibi\Connection::getAffectedRows()
* @return int number of rows
* @throws Dibi\Exception
*/
public static function getAffectedRows()
{
return self::getConnection()->getAffectedRows();
}
/**
* Gets the number of affected rows. Alias for getAffectedRows().
* @return int number of rows
* @throws Dibi\Exception
*/
public static function affectedRows()
{
return self::getConnection()->getAffectedRows();
}
/**
* Retrieves the ID generated for an AUTO_INCREMENT column by the previous INSERT query.
* Monostate for Dibi\Connection::getInsertId()
* @param string optional sequence name
* @return int
* @throws Dibi\Exception
*/
public static function getInsertId($sequence = NULL)
{
return self::getConnection()->getInsertId($sequence);
}
/**
* Retrieves the ID generated for an AUTO_INCREMENT column. Alias for getInsertId().
* @param string optional sequence name
* @return int
* @throws Dibi\Exception
*/
public static function insertId($sequence = NULL)
{
return self::getConnection()->getInsertId($sequence);
}
/**
* Begins a transaction - Monostate for Dibi\Connection::begin().
* @param string optional savepoint name
* @return void
* @throws Dibi\Exception
*/
public static function begin($savepoint = NULL)
{
self::getConnection()->begin($savepoint);
}
/**
* Commits statements in a transaction - Monostate for Dibi\Connection::commit($savepoint = NULL).
* @param string optional savepoint name
* @return void
* @throws Dibi\Exception
*/
public static function commit($savepoint = NULL)
{
self::getConnection()->commit($savepoint);
}
/**
* Rollback changes in a transaction - Monostate for Dibi\Connection::rollback().
* @param string optional savepoint name
* @return void
* @throws Dibi\Exception
*/
public static function rollback($savepoint = NULL)
{
self::getConnection()->rollback($savepoint);
}
/**
* Gets a information about the current database - Monostate for Dibi\Connection::getDatabaseInfo().
* @return Dibi\Reflection\Database
*/
public static function getDatabaseInfo()
{
return self::getConnection()->getDatabaseInfo();
}
/**
* Import SQL dump from file - extreme fast!
* @param string filename
* @return int count of sql commands
*/
public static function loadFile($file)
{
return Dibi\Helpers::loadFromFile(self::getConnection(), $file);
}
/********************* fluent SQL builders ****************d*g**/
/**
* @return Dibi\Fluent
*/
public static function command()
{
return self::getConnection()->command();
}
/**
* @param mixed column name
* @return Dibi\Fluent
*/
public static function select($args)
{
$args = func_get_args();
return call_user_func_array([self::getConnection(), 'select'], $args);
}
/**
* @param string table
* @param array
* @return Dibi\Fluent
*/
public static function update($table, $args)
{
return self::getConnection()->update($table, $args);
}
/**
* @param string table
* @param array
* @return Dibi\Fluent
*/
public static function insert($table, $args)
{
return self::getConnection()->insert($table, $args);
}
/**
* @param string table
* @return Dibi\Fluent
*/
public static function delete($table)
{
return self::getConnection()->delete($table);
}
/********************* substitutions ****************d*g**/
/**
* Returns substitution hashmap - Monostate for Dibi\Connection::getSubstitutes().
* @return Dibi\HashMap
*/
public static function getSubstitutes()
{
return self::getConnection()->getSubstitutes();
}
@@ -152,20 +442,13 @@ class dibi
/**
* Prints out a syntax highlighted version of the SQL command or Result.
* @param bool $return return output instead of printing it?
* @param string|Result
* @param bool return output instead of printing it?
* @return string
*/
public static function dump(string|Dibi\Result|null $sql = null, bool $return = false): ?string
public static function dump($sql = NULL, $return = FALSE)
{
return Dibi\Helpers::dump($sql, $return);
}
/**
* Strips microseconds part.
*/
public static function stripMicroseconds(DateTimeInterface $dt): DateTimeInterface
{
$class = $dt::class;
return new $class($dt->format('Y-m-d H:i:s'), $dt->getTimezone());
}
}

View File

@@ -1,50 +1,58 @@
<?php
/**
* This file is part of the Dibi, smart database abstraction layer (https://dibiphp.com)
* This file is part of the "dibi" - smart database abstraction layer.
* Copyright (c) 2005 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Dibi;
/**
* A database operation failed.
* dibi common exception.
*/
class Exception extends \Exception
{
private ?string $sql;
/** @var string|NULL */
private $sql;
public function __construct(
string $message = '',
int|string $code = 0,
?string $sql = null,
?\Throwable $previous = null,
) {
parent::__construct($message, 0, $previous);
/**
* Construct a dibi exception.
* @param string Message describing the exception
* @param mixed
* @param string SQL command
*/
public function __construct($message = '', $code = 0, $sql = NULL)
{
parent::__construct($message);
$this->code = $code;
$this->sql = $sql;
}
final public function getSql(): ?string
/**
* @return string The SQL passed to the constructor
*/
final public function getSql()
{
return $this->sql;
}
public function __toString(): string
/**
* @return string string represenation of exception with SQL command
*/
public function __toString()
{
return parent::__toString() . ($this->sql ? "\nSQL: " . $this->sql : '');
}
}
/**
* The database server reported an error.
* database server exception.
*/
class DriverException extends Exception
{
@@ -52,63 +60,72 @@ class DriverException extends Exception
/**
* Regular expression pattern or execution failed.
* PCRE exception.
*/
class PcreException extends Exception
{
public function __construct()
public function __construct($message = '%msg.')
{
parent::__construct(preg_last_error_msg(), preg_last_error());
static $messages = [
PREG_INTERNAL_ERROR => 'Internal error',
PREG_BACKTRACK_LIMIT_ERROR => 'Backtrack limit was exhausted',
PREG_RECURSION_LIMIT_ERROR => 'Recursion limit was exhausted',
PREG_BAD_UTF8_ERROR => 'Malformed UTF-8 data',
5 => 'Offset didn\'t correspond to the begin of a valid UTF-8 code point', // PREG_BAD_UTF8_OFFSET_ERROR
];
$code = preg_last_error();
parent::__construct(str_replace('%msg', isset($messages[$code]) ? $messages[$code] : 'Unknown error', $message), $code);
}
}
/**
* The requested feature is not implemented.
*/
class NotImplementedException extends Exception
{
}
/**
* The requested operation is not supported.
*/
class NotSupportedException extends Exception
{
}
/**
* A database stored procedure failed.
* Database procedure exception.
*/
class ProcedureException extends Exception
{
protected string $severity;
/** @var string */
protected $severity;
/**
* Construct the exception.
* @param string Message describing the exception
* @param int Some code
* @param string SQL command
*/
public function __construct(string $message = '', int $code = 0, string $severity = '', ?string $sql = null)
public function __construct($message = NULL, $code = 0, $severity = NULL, $sql = NULL)
{
parent::__construct($message, $code, $sql);
parent::__construct($message, (int) $code, $sql);
$this->severity = $severity;
}
/**
* Gets the exception severity.
* @return string
*/
public function getSeverity(): string
public function getSeverity()
{
return $this->severity;
$this->severity;
}
}
/**
* A database constraint was violated.
* Base class for all constraint violation related exceptions.
*/
class ConstraintViolationException extends DriverException
{
@@ -116,7 +133,7 @@ class ConstraintViolationException extends DriverException
/**
* The foreign key constraint check failed.
* Exception for a foreign key constraint violation.
*/
class ForeignKeyConstraintViolationException extends ConstraintViolationException
{
@@ -124,7 +141,7 @@ class ForeignKeyConstraintViolationException extends ConstraintViolationExceptio
/**
* The NOT NULL constraint check failed.
* Exception for a NOT NULL constraint violation.
*/
class NotNullConstraintViolationException extends ConstraintViolationException
{
@@ -132,7 +149,7 @@ class NotNullConstraintViolationException extends ConstraintViolationException
/**
* The unique constraint check failed.
* Exception for a unique constraint violation.
*/
class UniqueConstraintViolationException extends ConstraintViolationException
{

View File

@@ -1,12 +1,10 @@
<?php
/**
* This file is part of the Dibi, smart database abstraction layer (https://dibiphp.com)
* This file is part of the "dibi" - smart database abstraction layer.
* Copyright (c) 2005 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Dibi;
@@ -21,220 +19,227 @@ interface IDataSource extends \Countable, \IteratorAggregate
/**
* Driver interface.
* dibi driver interface.
*/
interface Driver
{
/**
* Disconnects from a database.
* Connects to a database.
* @param array
* @return void
* @throws Exception
*/
function disconnect(): void;
function connect(array &$config);
/**
* Disconnects from a database.
* @return void
* @throws Exception
*/
function disconnect();
/**
* Internal: Executes the SQL query.
* @param string SQL statement.
* @return ResultDriver|NULL
* @throws DriverException
*/
function query(string $sql): ?ResultDriver;
function query($sql);
/**
* Gets the number of affected rows by the last INSERT, UPDATE or DELETE query.
* @return int|FALSE number of rows or FALSE on error
*/
function getAffectedRows(): ?int;
function getAffectedRows();
/**
* Retrieves the ID generated for an AUTO_INCREMENT column by the previous INSERT query.
* @return int|FALSE int on success or FALSE on failure
*/
function getInsertId(?string $sequence): ?int;
function getInsertId($sequence);
/**
* Begins a transaction (if supported).
* @param string optional savepoint name
* @return void
* @throws DriverException
*/
function begin(?string $savepoint = null): void;
function begin($savepoint = NULL);
/**
* Commits statements in a transaction.
* @param string optional savepoint name
* @return void
* @throws DriverException
*/
function commit(?string $savepoint = null): void;
function commit($savepoint = NULL);
/**
* Rollback changes in a transaction.
* @param string optional savepoint name
* @return void
* @throws DriverException
*/
function rollback(?string $savepoint = null): void;
function rollback($savepoint = NULL);
/**
* Returns the connection resource.
* @return mixed
*/
function getResource(): mixed;
function getResource();
/**
* Returns the connection reflector.
* @return Reflector
*/
function getReflector(): Reflector;
function getReflector();
/**
* Encodes data for use in a SQL statement.
* @param string value
* @return string encoded value
*/
function escapeText(string $value): string;
function escapeText($value);
function escapeBinary(string $value): string;
/**
* @param string
* @return string
*/
function escapeBinary($value);
function escapeIdentifier(string $value): string;
/**
* @param string
* @return string
*/
function escapeIdentifier($value);
function escapeBool(bool $value): string;
/**
* @param bool
* @return string
*/
function escapeBool($value);
function escapeDate(\DateTimeInterface $value): string;
/**
* @param \DateTime|\DateTimeInterface|string|int
* @return string
*/
function escapeDate($value);
function escapeDateTime(\DateTimeInterface $value): string;
function escapeDateInterval(\DateInterval $value): string;
/**
* @param \DateTime|\DateTimeInterface|string|int
* @return string
*/
function escapeDateTime($value);
/**
* Encodes string for use in a LIKE statement.
* @param string
* @param int
* @return string
*/
function escapeLike(string $value, int $pos): string;
function escapeLike($value, $pos);
/**
* Injects LIMIT/OFFSET to the SQL query.
* @param string
* @param int|NULL
* @param int|NULL
* @return void
*/
function applyLimit(string &$sql, ?int $limit, ?int $offset): void;
function applyLimit(&$sql, $limit, $offset);
}
/**
* Result set driver interface.
* dibi result set driver interface.
*/
interface ResultDriver
{
/**
* Returns the number of rows in a result set.
* @return int
*/
function getRowCount(): int;
function getRowCount();
/**
* Moves cursor position without fetching row.
* @param int the 0-based cursor pos to seek to
* @return boolean TRUE on success, FALSE if unable to seek to specified record
* @throws Exception
*/
function seek(int $row): bool;
function seek($row);
/**
* Fetches the row at current position and moves the internal cursor to the next position.
* @param bool $type true for associative array, false for numeric
* @param bool TRUE for associative array, FALSE for numeric
* @return array array on success, nonarray if no next record
* @internal
*/
function fetch(bool $type): ?array;
function fetch($type);
/**
* Frees the resources allocated for this result set.
* @param resource result set resource
* @return void
*/
function free(): void;
function free();
/**
* Returns metadata for all columns in a result set.
* @return array of {name, nativetype [, table, fullname, (int) size, (bool) nullable, (mixed) default, (bool) autoincrement, (array) vendor ]}
*/
function getResultColumns(): array;
function getResultColumns();
/**
* Returns the result set resource.
* @return mixed
*/
function getResultResource(): mixed;
function getResultResource();
/**
* Decodes data from result set.
* @param string
* @return string
*/
function unescapeBinary(string $value): string;
function unescapeBinary($value);
}
/**
* Reflection driver.
* dibi driver reflection.
*/
interface Reflector
{
/**
* Returns list of tables.
* @return array of {name [, (bool) view ]}
*/
function getTables(): array;
function getTables();
/**
* Returns metadata for all columns in a table.
* @param string
* @return array of {name, nativetype [, table, fullname, (int) size, (bool) nullable, (mixed) default, (bool) autoincrement, (array) vendor ]}
*/
function getColumns(string $table): array;
function getColumns($table);
/**
* Returns metadata for all indexes in a table.
* @param string
* @return array of {name, (array of names) columns [, (bool) unique, (bool) primary ]}
*/
function getIndexes(string $table): array;
function getIndexes($table);
/**
* Returns metadata for all foreign keys in a table.
* @param string
* @return array
*/
function getForeignKeys(string $table): array;
}
/**
* Dibi connection.
*/
interface IConnection
{
/**
* Connects to a database.
*/
function connect(): void;
/**
* Disconnects from a database.
*/
function disconnect(): void;
/**
* Returns true when connection was established.
*/
function isConnected(): bool;
/**
* Returns the driver and connects to a database in lazy mode.
*/
function getDriver(): Driver;
/**
* Generates (translates) and executes SQL query.
* @throws Exception
*/
function query(...$args): Result;
/**
* Gets the number of affected rows by the last INSERT, UPDATE or DELETE query.
* @throws Exception
*/
function getAffectedRows(): int;
/**
* Retrieves the ID generated for an AUTO_INCREMENT column by the previous INSERT query.
* @throws Exception
*/
function getInsertId(?string $sequence = null): int;
/**
* Begins a transaction (if supported).
*/
function begin(?string $savepoint = null): void;
/**
* Commits statements in a transaction.
*/
function commit(?string $savepoint = null): void;
/**
* Rollback changes in a transaction.
*/
function rollback(?string $savepoint = null): void;
function getForeignKeys($table);
}

147
src/loader.php Normal file
View File

@@ -0,0 +1,147 @@
<?php
/**
* This file is part of the "dibi" - smart database abstraction layer.
* Copyright (c) 2005 David Grudl (https://davidgrudl.com)
*/
if (PHP_VERSION_ID < 50404) {
throw new Exception('Dibi requires PHP 5.4.4 or newer.');
}
spl_autoload_register(function ($class) {
static $map = [
'dibi' => 'dibi.php',
'Dibi\Bridges\Nette\DibiExtension21' => 'Bridges/Nette/DibiExtension21.php',
'Dibi\Bridges\Nette\DibiExtension22' => 'Bridges/Nette/DibiExtension22.php',
'Dibi\Bridges\Nette\Panel' => 'Bridges/Nette/Panel.php',
'Dibi\Bridges\Tracy\Panel' => 'Bridges/Tracy/Panel.php',
'Dibi\Connection' => 'Connection.php',
'Dibi\ConstraintViolationException' => 'exceptions.php',
'Dibi\DataSource' => 'DataSource.php',
'Dibi\DateTime' => 'DateTime.php',
'Dibi\Driver' => 'interfaces.php',
'Dibi\DriverException' => 'exceptions.php',
'Dibi\Drivers\FirebirdDriver' => 'Drivers/FirebirdDriver.php',
'Dibi\Drivers\MsSqlDriver' => 'Drivers/MsSqlDriver.php',
'Dibi\Drivers\MsSqlReflector' => 'Drivers/MsSqlReflector.php',
'Dibi\Drivers\MySqlDriver' => 'Drivers/MySqlDriver.php',
'Dibi\Drivers\MySqlReflector' => 'Drivers/MySqlReflector.php',
'Dibi\Drivers\MySqliDriver' => 'Drivers/MySqliDriver.php',
'Dibi\Drivers\OdbcDriver' => 'Drivers/OdbcDriver.php',
'Dibi\Drivers\OracleDriver' => 'Drivers/OracleDriver.php',
'Dibi\Drivers\PdoDriver' => 'Drivers/PdoDriver.php',
'Dibi\Drivers\PostgreDriver' => 'Drivers/PostgreDriver.php',
'Dibi\Drivers\Sqlite3Driver' => 'Drivers/Sqlite3Driver.php',
'Dibi\Drivers\SqliteReflector' => 'Drivers/SqliteReflector.php',
'Dibi\Drivers\SqlsrvDriver' => 'Drivers/SqlsrvDriver.php',
'Dibi\Drivers\SqlsrvReflector' => 'Drivers/SqlsrvReflector.php',
'Dibi\Event' => 'Event.php',
'Dibi\Exception' => 'exceptions.php',
'Dibi\Fluent' => 'Fluent.php',
'Dibi\ForeignKeyConstraintViolationException' => 'exceptions.php',
'Dibi\HashMap' => 'HashMap.php',
'Dibi\HashMapBase' => 'HashMap.php',
'Dibi\Helpers' => 'Helpers.php',
'Dibi\IDataSource' => 'interfaces.php',
'Dibi\Literal' => 'Literal.php',
'Dibi\Loggers\FileLogger' => 'Loggers/FileLogger.php',
'Dibi\Loggers\FirePhpLogger' => 'Loggers/FirePhpLogger.php',
'Dibi\NotImplementedException' => 'exceptions.php',
'Dibi\NotNullConstraintViolationException' => 'exceptions.php',
'Dibi\NotSupportedException' => 'exceptions.php',
'Dibi\PcreException' => 'exceptions.php',
'Dibi\ProcedureException' => 'exceptions.php',
'Dibi\Reflection\Column' => 'Reflection/Column.php',
'Dibi\Reflection\Database' => 'Reflection/Database.php',
'Dibi\Reflection\ForeignKey' => 'Reflection/ForeignKey.php',
'Dibi\Reflection\Index' => 'Reflection/Index.php',
'Dibi\Reflection\Result' => 'Reflection/Result.php',
'Dibi\Reflection\Table' => 'Reflection/Table.php',
'Dibi\Reflector' => 'interfaces.php',
'Dibi\Result' => 'Result.php',
'Dibi\ResultDriver' => 'interfaces.php',
'Dibi\ResultIterator' => 'ResultIterator.php',
'Dibi\Row' => 'Row.php',
'Dibi\Strict' => 'Strict.php',
'Dibi\Translator' => 'Translator.php',
'Dibi\Type' => 'Type.php',
'Dibi\UniqueConstraintViolationException' => 'exceptions.php',
], $old2new = [
'DibiColumnInfo' => 'Dibi\Reflection\Column',
'DibiConnection' => 'Dibi\Connection',
'DibiDatabaseInfo' => 'Dibi\Reflection\Database',
'DibiDataSource' => 'Dibi\DataSource',
'DibiDateTime' => 'Dibi\DateTime',
'DibiDriverException' => 'Dibi\DriverException',
'DibiEvent' => 'Dibi\Event',
'DibiException' => 'Dibi\Exception',
'DibiFileLogger' => 'Dibi\Loggers\FileLogger',
'DibiFirebirdDriver' => 'Dibi\Drivers\FirebirdDriver',
'DibiFirePhpLogger' => 'Dibi\Loggers\FirePhpLogger',
'DibiFluent' => 'Dibi\Fluent',
'DibiForeignKeyInfo' => 'Dibi\Reflection\ForeignKey',
'DibiHashMap' => 'Dibi\HashMap',
'DibiHashMapBase' => 'Dibi\HashMapBase',
'DibiIndexInfo' => 'Dibi\Reflection\Index',
'DibiLiteral' => 'Dibi\Literal',
'DibiMsSql2005Driver' => 'Dibi\Drivers\SqlsrvDriver',
'DibiMsSql2005Reflector' => 'Dibi\Drivers\SqlsrvReflector',
'DibiMsSqlDriver' => 'Dibi\Drivers\MsSqlDriver',
'DibiMsSqlReflector' => 'Dibi\Drivers\MsSqlReflector',
'DibiMySqlDriver' => 'Dibi\Drivers\MySqlDriver',
'DibiMySqliDriver' => 'Dibi\Drivers\MySqliDriver',
'DibiMySqlReflector' => 'Dibi\Drivers\MySqlReflector',
'DibiNette21Extension' => 'Dibi\Bridges\Nette\DibiExtension21',
'DibiNettePanel' => 'Dibi\Bridges\Nette\Panel',
'DibiNotImplementedException' => 'Dibi\NotImplementedException',
'DibiNotSupportedException' => 'Dibi\NotSupportedException',
'DibiOdbcDriver' => 'Dibi\Drivers\OdbcDriver',
'DibiOracleDriver' => 'Dibi\Drivers\OracleDriver',
'DibiPcreException' => 'Dibi\PcreException',
'DibiPdoDriver' => 'Dibi\Drivers\PdoDriver',
'DibiPostgreDriver' => 'Dibi\Drivers\PostgreDriver',
'DibiProcedureException' => 'Dibi\ProcedureException',
'DibiResult' => 'Dibi\Result',
'DibiResultInfo' => 'Dibi\Reflection\Result',
'DibiResultIterator' => 'Dibi\ResultIterator',
'DibiRow' => 'Dibi\Row',
'DibiSqlite3Driver' => 'Dibi\Drivers\Sqlite3Driver',
'DibiSqliteReflector' => 'Dibi\Drivers\SqliteReflector',
'DibiTableInfo' => 'Dibi\Reflection\Table',
'DibiTranslator' => 'Dibi\Translator',
'IDataSource' => 'Dibi\IDataSource',
'IDibiDriver' => 'Dibi\Driver',
'IDibiReflector' => 'Dibi\Reflector',
'IDibiResultDriver' => 'Dibi\ResultDriver',
'Dibi\Drivers\MsSql2005Driver' => 'Dibi\Drivers\SqlsrvDriver',
'Dibi\Drivers\MsSql2005Reflector' => 'Dibi\Drivers\SqlsrvReflector',
];
if (isset($map[$class])) {
require __DIR__ . '/Dibi/' . $map[$class];
} elseif (isset($old2new[$class])) {
class_alias($old2new[$class], $class);
}
});
// preload for compatiblity
array_map('class_exists', [
'DibiConnection',
'DibiDateTime',
'DibiDriverException',
'DibiEvent',
'DibiException',
'DibiFluent',
'DibiLiteral',
'DibiNotImplementedException',
'DibiNotSupportedException',
'DibiPcreException',
'DibiProcedureException',
'DibiResult',
'DibiRow',
'IDataSource',
'IDibiDriver',
]);

View File

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

View File

@@ -1,5 +1,5 @@
[sqlite] ; default
driver = sqlite
driver = sqlite3
database = :memory:
system = sqlite
@@ -8,30 +8,37 @@ driver = pdo
dsn = "sqlite::memory:"
system = sqlite
[mysql]
driver = mysql
host = 127.0.0.1
username = root
password = "Password12!"
charset = utf8
system = mysql
[mysql improved]
driver = mysqli
host = 127.0.0.1
username = root
password = "Password12!"
database = dibi_test
charset = utf8
system = mysql
[mysql pdo]
driver = pdo
dsn = "mysql:host=127.0.0.1;dbname=dibi_test"
dsn = "mysql:host=127.0.0.1"
username = root
password = "Password12!"
system = mysql
[odbc]
driver = odbc
dsn = "Driver={Microsoft Access Driver (*.mdb, *.accdb)};Dbq=data/odbc.mdb"
dsn = "Driver={Microsoft Access Driver (*.mdb)};Dbq=data/odbc.mdb"
system = odbc
[odbc pdo]
driver = pdo
dsn = "odbc:Driver={Microsoft Access Driver (*.mdb, *.accdb)};Dbq=data/odbc.mdb"
dsn = "odbc:Driver={Microsoft Access Driver (*.mdb)};Dbq=data/odbc.mdb"
username =
password =
system = odbc

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