first commit

This commit is contained in:
2025-06-17 11:53:18 +02:00
commit 9f0f7ba12b
8804 changed files with 1369176 additions and 0 deletions

View File

@ -0,0 +1,275 @@
local volumes = [
{
name: 'composer-cache',
path: '/tmp/composer-cache',
},
];
local hostvolumes = [
{
name: 'composer-cache',
host: { path: '/tmp/composer-cache' },
},
];
local composer(phpversion, params) = {
name: 'Composer',
image: 'joomlaprojects/docker-images:php' + phpversion,
volumes: volumes,
commands: [
'php -v',
'sleep 20',
'composer update ' + params,
],
};
local phpunit_common(phpversion) = {
name: 'PHPUnit',
image: 'joomlaprojects/docker-images:php' + phpversion,
[if phpversion == '8.3' then 'failure']: 'ignore',
commands: [
'vendor/bin/phpunit --configuration phpunit.xml.dist --testdox',
],
};
local phpunit_mysql(phpversion, driver) = {
name: 'PHPUnit',
image: 'joomlaprojects/docker-images:php' + phpversion,
[if phpversion == '8.3' then 'failure']: 'ignore',
commands: [
'php --ri ' + driver + ' || true',
'sleep 20',
'vendor/bin/phpunit --configuration phpunit.' + driver + '.xml.dist --testdox',
],
};
local phpunit(phpversion, driver) = {
name: 'PHPUnit',
image: 'joomlaprojects/docker-images:php' + phpversion,
[if phpversion == '8.3' then 'failure']: 'ignore',
commands: [
'php --ri ' + driver + ' || true',
'vendor/bin/phpunit --configuration phpunit.' + driver + '.xml.dist --testdox',
],
};
local phpunit_sqlsrv(phpversion) = {
name: 'PHPUnit with MS SQL Server',
image: 'joomlaprojects/docker-images:php' + phpversion,
commands: [
'apt-get update',
'apt-get install -y software-properties-common lsb-release gnupg',
'curl https://packages.microsoft.com/keys/microsoft.asc | apt-key add -',
'echo "deb [arch=amd64,armhf,arm64] https://packages.microsoft.com/ubuntu/22.04/prod jammy main" >> /etc/apt/sources.list',
'apt-get update',
'ACCEPT_EULA=Y apt-get install -y msodbcsql18 unixodbc-dev',
'pecl install sqlsrv && docker-php-ext-enable sqlsrv',
'pecl install pdo_sqlsrv && docker-php-ext-enable pdo_sqlsrv',
'php --ri sqlsrv',
'php --ri pdo_sqlsrv',
'vendor/bin/phpunit --configuration phpunit.sqlsrv.xml.dist --testdox',
],
};
local pipeline_sqlite(phpversion, driver, params) = {
kind: 'pipeline',
name: 'PHP ' + phpversion + ' with SQLite (' + driver + ')',
environment: { DB: driver },
volumes: hostvolumes,
steps: [
composer(phpversion, params),
phpunit(phpversion, driver),
],
};
local pipeline_mysql(phpversion, driver, dbversion, params) = {
kind: 'pipeline',
name: 'PHP ' + phpversion + ' with MySQL ' + dbversion + ' (' + driver + ')',
environment: { DB: driver },
volumes: hostvolumes,
steps: [
composer(phpversion, params),
phpunit_mysql(phpversion, driver),
],
services: [
{
name: driver,
image: 'bitnami/mysql:' + dbversion,
environment: {
ALLOW_EMPTY_PASSWORD: 'yes',
MYSQL_DATABASE: 'joomla_ut',
MYSQL_ROOT_PASSWORD: '',
MYSQL_AUTHENTICATION_PLUGIN: 'mysql_native_password',
},
ports: [
{
container: 3306,
host: 3306,
},
],
},
],
};
local pipeline_mariadb(phpversion, driver, dbversion, params) = {
kind: 'pipeline',
name: 'PHP ' + phpversion + ' with MariaDB ' + dbversion + ' (' + driver + ')',
environment: { DB: driver },
volumes: hostvolumes,
steps: [
composer(phpversion, params),
phpunit(phpversion, driver),
],
services: [
{
name: driver,
image: 'mariadb:' + dbversion,
environment: {
MARIADB_ALLOW_EMPTY_ROOT_PASSWORD: 'yes',
MARIADB_DATABASE: 'joomla_ut',
MARIADB_ROOT_PASSWORD: '',
# Provide MySQL environments variables for MariaDB < 10.2.
MYSQL_ALLOW_EMPTY_PASSWORD: 'yes',
MYSQL_DATABASE: 'joomla_ut',
MYSQL_ROOT_PASSWORD: '',
},
ports: [
{
container: 3306,
host: 3306,
},
],
},
],
};
local pipeline_postgres(phpversion, driver, dbversion, params) = {
kind: 'pipeline',
name: 'PHP ' + phpversion + ' with PostgreSQL ' + dbversion + ' (' + driver + ')',
environment: { DB: driver },
volumes: hostvolumes,
steps: [
composer(phpversion, params),
phpunit(phpversion, driver),
],
services: [
{
name: driver,
image: 'postgres:' + dbversion,
environment: {
POSTGRES_HOST_AUTH_METHOD: 'trust',
POSTGRES_PASSWORD: '',
POSTGRES_USER: 'postgres',
},
ports: [
{
container: 5432,
host: 5432,
},
],
commands: [
'psql -U postgres -c ',
'psql -U postgres -d joomla_ut -a -f Tests/Stubs/Schema/pgsql.sql',
],
},
],
};
local pipeline_sqlsrv(phpversion, driver, dbversion, params) = {
kind: 'pipeline',
name: 'PHP ' + phpversion + ' with MS SQL Server ' + dbversion + ' (' + driver + ')',
environment: { DB: driver },
volumes: hostvolumes,
steps: [
composer(phpversion, params),
phpunit_sqlsrv(phpversion),
],
services: [
{
name: driver,
image: 'mcr.microsoft.com/mssql/server:' + dbversion,
environment: {
ACCEPT_EULA: 'Y',
SA_PASSWORD: 'JoomlaFramework123',
},
ports: [
{
container: 1433,
host: 1433,
},
],
},
],
};
[
{
kind: 'pipeline',
name: 'Codequality',
volumes: hostvolumes,
steps: [
{
name: 'composer',
image: 'joomlaprojects/docker-images:php8.1',
volumes: volumes,
commands: [
'php -v',
'composer update'
],
},
{
name: 'phpcs',
image: 'joomlaprojects/docker-images:php8.1',
depends: [ 'composer' ],
commands: [
'vendor/bin/phpcs --standard=ruleset.xml src/',
],
},
{
name: 'phan',
image: 'joomlaprojects/docker-images:php8.1-ast',
depends: [ 'composer' ],
failure: 'ignore',
commands: [
'vendor/bin/phan'
],
},
{
name: 'phpstan',
image: 'joomlaprojects/docker-images:php8.1',
depends: [ 'composer' ],
failure: 'ignore',
commands: [
'vendor/bin/phpstan analyse src',
],
},
{
name: 'phploc',
image: 'joomlaprojects/docker-images:php8.1',
depends: [ 'composer' ],
failure: 'ignore',
commands: [
'phploc src',
],
},
],
},
pipeline_sqlite('8.1', 'sqlite', '--prefer-stable'),
pipeline_sqlite('8.2', 'sqlite', '--prefer-stable'),
pipeline_mysql('8.1', 'mysql', '5.7', '--prefer-stable'),
pipeline_mysql('8.2', 'mysql', '5.7', '--prefer-stable'),
pipeline_mysql('8.1', 'mysql', '8.0', '--prefer-stable'),
pipeline_mysql('8.2', 'mysql', '8.0', '--prefer-stable'),
pipeline_mysql('8.1', 'mysqli', '5.7', '--prefer-stable'),
pipeline_mysql('8.2', 'mysqli', '5.7', '--prefer-stable'),
pipeline_mysql('8.1', 'mysqli', '8.0', '--prefer-stable'),
pipeline_mysql('8.2', 'mysqli', '8.0', '--prefer-stable'),
pipeline_mariadb('8.1', 'mariadb', '10.2', '--prefer-stable'),
pipeline_mariadb('8.2', 'mariadb', '10.2', '--prefer-stable'),
pipeline_postgres('8.1', 'pgsql', '10', '--prefer-stable'),
pipeline_postgres('8.2', 'pgsql', '10', '--prefer-stable'),
pipeline_postgres('8.1', 'pgsql', '11', '--prefer-stable'),
pipeline_postgres('8.2', 'pgsql', '11', '--prefer-stable'),
pipeline_sqlsrv('8.1', 'sqlsrv', '2017-latest', '--prefer-stable'),
pipeline_sqlsrv('8.2', 'sqlsrv', '2017-latest', '--prefer-stable'),
]

View File

@ -0,0 +1,692 @@
---
kind: pipeline
name: Codequality
steps:
- commands:
- php -v
- composer update
image: joomlaprojects/docker-images:php8.1
name: composer
volumes:
- name: composer-cache
path: /tmp/composer-cache
- commands:
- vendor/bin/phpcs --standard=ruleset.xml src/
depends:
- composer
image: joomlaprojects/docker-images:php8.1
name: phpcs
- commands:
- vendor/bin/phan
depends:
- composer
failure: ignore
image: joomlaprojects/docker-images:php8.1-ast
name: phan
- commands:
- vendor/bin/phpstan analyse src
depends:
- composer
failure: ignore
image: joomlaprojects/docker-images:php8.1
name: phpstan
- commands:
- phploc src
depends:
- composer
failure: ignore
image: joomlaprojects/docker-images:php8.1
name: phploc
volumes:
- host:
path: /tmp/composer-cache
name: composer-cache
---
environment:
DB: sqlite
kind: pipeline
name: PHP 8.1 with SQLite (sqlite)
steps:
- commands:
- php -v
- sleep 20
- composer update --prefer-stable
image: joomlaprojects/docker-images:php8.1
name: Composer
volumes:
- name: composer-cache
path: /tmp/composer-cache
- commands:
- php --ri sqlite || true
- vendor/bin/phpunit --configuration phpunit.sqlite.xml.dist --testdox
image: joomlaprojects/docker-images:php8.1
name: PHPUnit
volumes:
- host:
path: /tmp/composer-cache
name: composer-cache
---
environment:
DB: sqlite
kind: pipeline
name: PHP 8.2 with SQLite (sqlite)
steps:
- commands:
- php -v
- sleep 20
- composer update --prefer-stable
image: joomlaprojects/docker-images:php8.2
name: Composer
volumes:
- name: composer-cache
path: /tmp/composer-cache
- commands:
- php --ri sqlite || true
- vendor/bin/phpunit --configuration phpunit.sqlite.xml.dist --testdox
image: joomlaprojects/docker-images:php8.2
name: PHPUnit
volumes:
- host:
path: /tmp/composer-cache
name: composer-cache
---
environment:
DB: mysql
kind: pipeline
name: PHP 8.1 with MySQL 5.7 (mysql)
services:
- environment:
ALLOW_EMPTY_PASSWORD: "yes"
MYSQL_AUTHENTICATION_PLUGIN: mysql_native_password
MYSQL_DATABASE: joomla_ut
MYSQL_ROOT_PASSWORD: ""
image: bitnami/mysql:5.7
name: mysql
ports:
- container: 3306
host: 3306
steps:
- commands:
- php -v
- sleep 20
- composer update --prefer-stable
image: joomlaprojects/docker-images:php8.1
name: Composer
volumes:
- name: composer-cache
path: /tmp/composer-cache
- commands:
- php --ri mysql || true
- sleep 20
- vendor/bin/phpunit --configuration phpunit.mysql.xml.dist --testdox
image: joomlaprojects/docker-images:php8.1
name: PHPUnit
volumes:
- host:
path: /tmp/composer-cache
name: composer-cache
---
environment:
DB: mysql
kind: pipeline
name: PHP 8.2 with MySQL 5.7 (mysql)
services:
- environment:
ALLOW_EMPTY_PASSWORD: "yes"
MYSQL_AUTHENTICATION_PLUGIN: mysql_native_password
MYSQL_DATABASE: joomla_ut
MYSQL_ROOT_PASSWORD: ""
image: bitnami/mysql:5.7
name: mysql
ports:
- container: 3306
host: 3306
steps:
- commands:
- php -v
- sleep 20
- composer update --prefer-stable
image: joomlaprojects/docker-images:php8.2
name: Composer
volumes:
- name: composer-cache
path: /tmp/composer-cache
- commands:
- php --ri mysql || true
- sleep 20
- vendor/bin/phpunit --configuration phpunit.mysql.xml.dist --testdox
image: joomlaprojects/docker-images:php8.2
name: PHPUnit
volumes:
- host:
path: /tmp/composer-cache
name: composer-cache
---
environment:
DB: mysql
kind: pipeline
name: PHP 8.1 with MySQL 8.0 (mysql)
services:
- environment:
ALLOW_EMPTY_PASSWORD: "yes"
MYSQL_AUTHENTICATION_PLUGIN: mysql_native_password
MYSQL_DATABASE: joomla_ut
MYSQL_ROOT_PASSWORD: ""
image: bitnami/mysql:8.0
name: mysql
ports:
- container: 3306
host: 3306
steps:
- commands:
- php -v
- sleep 20
- composer update --prefer-stable
image: joomlaprojects/docker-images:php8.1
name: Composer
volumes:
- name: composer-cache
path: /tmp/composer-cache
- commands:
- php --ri mysql || true
- sleep 20
- vendor/bin/phpunit --configuration phpunit.mysql.xml.dist --testdox
image: joomlaprojects/docker-images:php8.1
name: PHPUnit
volumes:
- host:
path: /tmp/composer-cache
name: composer-cache
---
environment:
DB: mysql
kind: pipeline
name: PHP 8.2 with MySQL 8.0 (mysql)
services:
- environment:
ALLOW_EMPTY_PASSWORD: "yes"
MYSQL_AUTHENTICATION_PLUGIN: mysql_native_password
MYSQL_DATABASE: joomla_ut
MYSQL_ROOT_PASSWORD: ""
image: bitnami/mysql:8.0
name: mysql
ports:
- container: 3306
host: 3306
steps:
- commands:
- php -v
- sleep 20
- composer update --prefer-stable
image: joomlaprojects/docker-images:php8.2
name: Composer
volumes:
- name: composer-cache
path: /tmp/composer-cache
- commands:
- php --ri mysql || true
- sleep 20
- vendor/bin/phpunit --configuration phpunit.mysql.xml.dist --testdox
image: joomlaprojects/docker-images:php8.2
name: PHPUnit
volumes:
- host:
path: /tmp/composer-cache
name: composer-cache
---
environment:
DB: mysqli
kind: pipeline
name: PHP 8.1 with MySQL 5.7 (mysqli)
services:
- environment:
ALLOW_EMPTY_PASSWORD: "yes"
MYSQL_AUTHENTICATION_PLUGIN: mysql_native_password
MYSQL_DATABASE: joomla_ut
MYSQL_ROOT_PASSWORD: ""
image: bitnami/mysql:5.7
name: mysqli
ports:
- container: 3306
host: 3306
steps:
- commands:
- php -v
- sleep 20
- composer update --prefer-stable
image: joomlaprojects/docker-images:php8.1
name: Composer
volumes:
- name: composer-cache
path: /tmp/composer-cache
- commands:
- php --ri mysqli || true
- sleep 20
- vendor/bin/phpunit --configuration phpunit.mysqli.xml.dist --testdox
image: joomlaprojects/docker-images:php8.1
name: PHPUnit
volumes:
- host:
path: /tmp/composer-cache
name: composer-cache
---
environment:
DB: mysqli
kind: pipeline
name: PHP 8.2 with MySQL 5.7 (mysqli)
services:
- environment:
ALLOW_EMPTY_PASSWORD: "yes"
MYSQL_AUTHENTICATION_PLUGIN: mysql_native_password
MYSQL_DATABASE: joomla_ut
MYSQL_ROOT_PASSWORD: ""
image: bitnami/mysql:5.7
name: mysqli
ports:
- container: 3306
host: 3306
steps:
- commands:
- php -v
- sleep 20
- composer update --prefer-stable
image: joomlaprojects/docker-images:php8.2
name: Composer
volumes:
- name: composer-cache
path: /tmp/composer-cache
- commands:
- php --ri mysqli || true
- sleep 20
- vendor/bin/phpunit --configuration phpunit.mysqli.xml.dist --testdox
image: joomlaprojects/docker-images:php8.2
name: PHPUnit
volumes:
- host:
path: /tmp/composer-cache
name: composer-cache
---
environment:
DB: mysqli
kind: pipeline
name: PHP 8.1 with MySQL 8.0 (mysqli)
services:
- environment:
ALLOW_EMPTY_PASSWORD: "yes"
MYSQL_AUTHENTICATION_PLUGIN: mysql_native_password
MYSQL_DATABASE: joomla_ut
MYSQL_ROOT_PASSWORD: ""
image: bitnami/mysql:8.0
name: mysqli
ports:
- container: 3306
host: 3306
steps:
- commands:
- php -v
- sleep 20
- composer update --prefer-stable
image: joomlaprojects/docker-images:php8.1
name: Composer
volumes:
- name: composer-cache
path: /tmp/composer-cache
- commands:
- php --ri mysqli || true
- sleep 20
- vendor/bin/phpunit --configuration phpunit.mysqli.xml.dist --testdox
image: joomlaprojects/docker-images:php8.1
name: PHPUnit
volumes:
- host:
path: /tmp/composer-cache
name: composer-cache
---
environment:
DB: mysqli
kind: pipeline
name: PHP 8.2 with MySQL 8.0 (mysqli)
services:
- environment:
ALLOW_EMPTY_PASSWORD: "yes"
MYSQL_AUTHENTICATION_PLUGIN: mysql_native_password
MYSQL_DATABASE: joomla_ut
MYSQL_ROOT_PASSWORD: ""
image: bitnami/mysql:8.0
name: mysqli
ports:
- container: 3306
host: 3306
steps:
- commands:
- php -v
- sleep 20
- composer update --prefer-stable
image: joomlaprojects/docker-images:php8.2
name: Composer
volumes:
- name: composer-cache
path: /tmp/composer-cache
- commands:
- php --ri mysqli || true
- sleep 20
- vendor/bin/phpunit --configuration phpunit.mysqli.xml.dist --testdox
image: joomlaprojects/docker-images:php8.2
name: PHPUnit
volumes:
- host:
path: /tmp/composer-cache
name: composer-cache
---
environment:
DB: mariadb
kind: pipeline
name: PHP 8.1 with MariaDB 10.2 (mariadb)
services:
- environment:
MARIADB_ALLOW_EMPTY_ROOT_PASSWORD: "yes"
MARIADB_DATABASE: joomla_ut
MARIADB_ROOT_PASSWORD: ""
MYSQL_ALLOW_EMPTY_PASSWORD: "yes"
MYSQL_DATABASE: joomla_ut
MYSQL_ROOT_PASSWORD: ""
image: mariadb:10.2
name: mariadb
ports:
- container: 3306
host: 3306
steps:
- commands:
- php -v
- sleep 20
- composer update --prefer-stable
image: joomlaprojects/docker-images:php8.1
name: Composer
volumes:
- name: composer-cache
path: /tmp/composer-cache
- commands:
- php --ri mariadb || true
- vendor/bin/phpunit --configuration phpunit.mariadb.xml.dist --testdox
image: joomlaprojects/docker-images:php8.1
name: PHPUnit
volumes:
- host:
path: /tmp/composer-cache
name: composer-cache
---
environment:
DB: mariadb
kind: pipeline
name: PHP 8.2 with MariaDB 10.2 (mariadb)
services:
- environment:
MARIADB_ALLOW_EMPTY_ROOT_PASSWORD: "yes"
MARIADB_DATABASE: joomla_ut
MARIADB_ROOT_PASSWORD: ""
MYSQL_ALLOW_EMPTY_PASSWORD: "yes"
MYSQL_DATABASE: joomla_ut
MYSQL_ROOT_PASSWORD: ""
image: mariadb:10.2
name: mariadb
ports:
- container: 3306
host: 3306
steps:
- commands:
- php -v
- sleep 20
- composer update --prefer-stable
image: joomlaprojects/docker-images:php8.2
name: Composer
volumes:
- name: composer-cache
path: /tmp/composer-cache
- commands:
- php --ri mariadb || true
- vendor/bin/phpunit --configuration phpunit.mariadb.xml.dist --testdox
image: joomlaprojects/docker-images:php8.2
name: PHPUnit
volumes:
- host:
path: /tmp/composer-cache
name: composer-cache
---
environment:
DB: pgsql
kind: pipeline
name: PHP 8.1 with PostgreSQL 10 (pgsql)
services:
- commands:
- 'psql -U postgres -c '
- psql -U postgres -d joomla_ut -a -f Tests/Stubs/Schema/pgsql.sql
environment:
POSTGRES_HOST_AUTH_METHOD: trust
POSTGRES_PASSWORD: ""
POSTGRES_USER: postgres
image: postgres:10
name: pgsql
ports:
- container: 5432
host: 5432
steps:
- commands:
- php -v
- sleep 20
- composer update --prefer-stable
image: joomlaprojects/docker-images:php8.1
name: Composer
volumes:
- name: composer-cache
path: /tmp/composer-cache
- commands:
- php --ri pgsql || true
- vendor/bin/phpunit --configuration phpunit.pgsql.xml.dist --testdox
image: joomlaprojects/docker-images:php8.1
name: PHPUnit
volumes:
- host:
path: /tmp/composer-cache
name: composer-cache
---
environment:
DB: pgsql
kind: pipeline
name: PHP 8.2 with PostgreSQL 10 (pgsql)
services:
- commands:
- 'psql -U postgres -c '
- psql -U postgres -d joomla_ut -a -f Tests/Stubs/Schema/pgsql.sql
environment:
POSTGRES_HOST_AUTH_METHOD: trust
POSTGRES_PASSWORD: ""
POSTGRES_USER: postgres
image: postgres:10
name: pgsql
ports:
- container: 5432
host: 5432
steps:
- commands:
- php -v
- sleep 20
- composer update --prefer-stable
image: joomlaprojects/docker-images:php8.2
name: Composer
volumes:
- name: composer-cache
path: /tmp/composer-cache
- commands:
- php --ri pgsql || true
- vendor/bin/phpunit --configuration phpunit.pgsql.xml.dist --testdox
image: joomlaprojects/docker-images:php8.2
name: PHPUnit
volumes:
- host:
path: /tmp/composer-cache
name: composer-cache
---
environment:
DB: pgsql
kind: pipeline
name: PHP 8.1 with PostgreSQL 11 (pgsql)
services:
- commands:
- 'psql -U postgres -c '
- psql -U postgres -d joomla_ut -a -f Tests/Stubs/Schema/pgsql.sql
environment:
POSTGRES_HOST_AUTH_METHOD: trust
POSTGRES_PASSWORD: ""
POSTGRES_USER: postgres
image: postgres:11
name: pgsql
ports:
- container: 5432
host: 5432
steps:
- commands:
- php -v
- sleep 20
- composer update --prefer-stable
image: joomlaprojects/docker-images:php8.1
name: Composer
volumes:
- name: composer-cache
path: /tmp/composer-cache
- commands:
- php --ri pgsql || true
- vendor/bin/phpunit --configuration phpunit.pgsql.xml.dist --testdox
image: joomlaprojects/docker-images:php8.1
name: PHPUnit
volumes:
- host:
path: /tmp/composer-cache
name: composer-cache
---
environment:
DB: pgsql
kind: pipeline
name: PHP 8.2 with PostgreSQL 11 (pgsql)
services:
- commands:
- 'psql -U postgres -c '
- psql -U postgres -d joomla_ut -a -f Tests/Stubs/Schema/pgsql.sql
environment:
POSTGRES_HOST_AUTH_METHOD: trust
POSTGRES_PASSWORD: ""
POSTGRES_USER: postgres
image: postgres:11
name: pgsql
ports:
- container: 5432
host: 5432
steps:
- commands:
- php -v
- sleep 20
- composer update --prefer-stable
image: joomlaprojects/docker-images:php8.2
name: Composer
volumes:
- name: composer-cache
path: /tmp/composer-cache
- commands:
- php --ri pgsql || true
- vendor/bin/phpunit --configuration phpunit.pgsql.xml.dist --testdox
image: joomlaprojects/docker-images:php8.2
name: PHPUnit
volumes:
- host:
path: /tmp/composer-cache
name: composer-cache
---
environment:
DB: sqlsrv
kind: pipeline
name: PHP 8.1 with MS SQL Server 2017-latest (sqlsrv)
services:
- environment:
ACCEPT_EULA: "Y"
SA_PASSWORD: JoomlaFramework123
image: mcr.microsoft.com/mssql/server:2017-latest
name: sqlsrv
ports:
- container: 1433
host: 1433
steps:
- commands:
- php -v
- sleep 20
- composer update --prefer-stable
image: joomlaprojects/docker-images:php8.1
name: Composer
volumes:
- name: composer-cache
path: /tmp/composer-cache
- commands:
- apt-get update
- apt-get install -y software-properties-common lsb-release gnupg
- curl https://packages.microsoft.com/keys/microsoft.asc | apt-key add -
- echo "deb [arch=amd64,armhf,arm64] https://packages.microsoft.com/ubuntu/22.04/prod
jammy main" >> /etc/apt/sources.list
- apt-get update
- ACCEPT_EULA=Y apt-get install -y msodbcsql18 unixodbc-dev
- pecl install sqlsrv && docker-php-ext-enable sqlsrv
- pecl install pdo_sqlsrv && docker-php-ext-enable pdo_sqlsrv
- php --ri sqlsrv
- php --ri pdo_sqlsrv
- vendor/bin/phpunit --configuration phpunit.sqlsrv.xml.dist --testdox
image: joomlaprojects/docker-images:php8.1
name: PHPUnit with MS SQL Server
volumes:
- host:
path: /tmp/composer-cache
name: composer-cache
---
environment:
DB: sqlsrv
kind: pipeline
name: PHP 8.2 with MS SQL Server 2017-latest (sqlsrv)
services:
- environment:
ACCEPT_EULA: "Y"
SA_PASSWORD: JoomlaFramework123
image: mcr.microsoft.com/mssql/server:2017-latest
name: sqlsrv
ports:
- container: 1433
host: 1433
steps:
- commands:
- php -v
- sleep 20
- composer update --prefer-stable
image: joomlaprojects/docker-images:php8.2
name: Composer
volumes:
- name: composer-cache
path: /tmp/composer-cache
- commands:
- apt-get update
- apt-get install -y software-properties-common lsb-release gnupg
- curl https://packages.microsoft.com/keys/microsoft.asc | apt-key add -
- echo "deb [arch=amd64,armhf,arm64] https://packages.microsoft.com/ubuntu/22.04/prod
jammy main" >> /etc/apt/sources.list
- apt-get update
- ACCEPT_EULA=Y apt-get install -y msodbcsql18 unixodbc-dev
- pecl install sqlsrv && docker-php-ext-enable sqlsrv
- pecl install pdo_sqlsrv && docker-php-ext-enable pdo_sqlsrv
- php --ri sqlsrv
- php --ri pdo_sqlsrv
- vendor/bin/phpunit --configuration phpunit.sqlsrv.xml.dist --testdox
image: joomlaprojects/docker-images:php8.2
name: PHPUnit with MS SQL Server
volumes:
- host:
path: /tmp/composer-cache
name: composer-cache
---
kind: signature
hmac: e511351199fd2fa0a45c01a540e0c31ecbe7c72b30075fa2dea06b11ca5c7f58
...

340
libraries/vendor/joomla/database/LICENSE vendored Normal file
View File

@ -0,0 +1,340 @@
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc.
59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Library General Public License instead.) You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.
We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and
modification follow.
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
c) If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary way, to print or display an
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.
In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:
a) Accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of Sections
1 and 2 above on a medium customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.
5. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.
7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.
10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
`Gnomovision' (which makes passes at compilers) written by James Hacker.
<signature of Ty Coon>, 1 April 1989
Ty Coon, President of Vice
This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Library General
Public License instead of this License.

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="Tests/bootstrap.php" colors="false">
<php>
<env name="JOOMLA_TEST_DB_DRIVER" value="sqlsrv" />
<env name="JOOMLA_TEST_DB_HOST" value="(local)\SQL2012SP1" />
<env name="JOOMLA_TEST_DB_PORT" value="" />
<env name="JOOMLA_TEST_DB_USER" value="sa" />
<env name="JOOMLA_TEST_DB_PASSWORD" value="Password12!" />
<env name="JOOMLA_TEST_DB_DATABASE" value="joomla_ut" />
<env name="JOOMLA_TEST_DB_PREFIX" value="" />
<env name="SYMFONY_DEPRECATIONS_HELPER" value="weak" />
</php>
<testsuites>
<testsuite name="Unit">
<directory>Tests</directory>
</testsuite>
</testsuites>
</phpunit>

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="Tests/bootstrap.php" colors="false">
<php>
<env name="JOOMLA_TEST_DB_DRIVER" value="sqlsrv" />
<env name="JOOMLA_TEST_DB_HOST" value="(local)\SQL2014" />
<env name="JOOMLA_TEST_DB_PORT" value="" />
<env name="JOOMLA_TEST_DB_USER" value="sa" />
<env name="JOOMLA_TEST_DB_PASSWORD" value="Password12!" />
<env name="JOOMLA_TEST_DB_DATABASE" value="joomla_ut" />
<env name="JOOMLA_TEST_DB_PREFIX" value="" />
<env name="SYMFONY_DEPRECATIONS_HELPER" value="weak" />
</php>
<testsuites>
<testsuite name="Unit">
<directory>Tests</directory>
</testsuite>
</testsuites>
</phpunit>

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="Tests/bootstrap.php" colors="false">
<php>
<env name="JOOMLA_TEST_DB_DRIVER" value="sqlsrv" />
<env name="JOOMLA_TEST_DB_HOST" value="(local)\SQL2017" />
<env name="JOOMLA_TEST_DB_PORT" value="" />
<env name="JOOMLA_TEST_DB_USER" value="sa" />
<env name="JOOMLA_TEST_DB_PASSWORD" value="Password12!" />
<env name="JOOMLA_TEST_DB_DATABASE" value="joomla_ut" />
<env name="JOOMLA_TEST_DB_PREFIX" value="" />
<env name="SYMFONY_DEPRECATIONS_HELPER" value="weak" />
</php>
<testsuites>
<testsuite name="Unit">
<directory>Tests</directory>
</testsuite>
</testsuites>
</phpunit>

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="Tests/bootstrap.php" colors="false">
<php>
<env name="JOOMLA_TEST_DB_DRIVER" value="mysqli" />
<env name="JOOMLA_TEST_DB_HOST" value="mariadb" />
<env name="JOOMLA_TEST_DB_PORT" value="3306" />
<env name="JOOMLA_TEST_DB_USER" value="root" />
<env name="JOOMLA_TEST_DB_PASSWORD" value="" />
<env name="JOOMLA_TEST_DB_DATABASE" value="joomla_ut" />
<env name="JOOMLA_TEST_DB_PREFIX" value="" />
<env name="SYMFONY_DEPRECATIONS_HELPER" value="weak" />
</php>
<testsuites>
<testsuite name="Unit">
<directory>Tests/Mysqli</directory>
</testsuite>
</testsuites>
</phpunit>

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="Tests/bootstrap.php" colors="false">
<php>
<env name="JOOMLA_TEST_DB_DRIVER" value="mysql" />
<env name="JOOMLA_TEST_DB_HOST" value="mysql" />
<env name="JOOMLA_TEST_DB_PORT" value="3306" />
<env name="JOOMLA_TEST_DB_USER" value="root" />
<env name="JOOMLA_TEST_DB_PASSWORD" value="" />
<env name="JOOMLA_TEST_DB_DATABASE" value="joomla_ut" />
<env name="JOOMLA_TEST_DB_PREFIX" value="" />
<env name="SYMFONY_DEPRECATIONS_HELPER" value="weak" />
</php>
<testsuites>
<testsuite name="Unit">
<directory>Tests/Mysql</directory>
</testsuite>
</testsuites>
</phpunit>

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="Tests/bootstrap.php" colors="false">
<php>
<env name="JOOMLA_TEST_DB_DRIVER" value="mysqli" />
<env name="JOOMLA_TEST_DB_HOST" value="mysqli" />
<env name="JOOMLA_TEST_DB_PORT" value="3306" />
<env name="JOOMLA_TEST_DB_USER" value="root" />
<env name="JOOMLA_TEST_DB_PASSWORD" value="" />
<env name="JOOMLA_TEST_DB_DATABASE" value="joomla_ut" />
<env name="JOOMLA_TEST_DB_PREFIX" value="" />
<env name="SYMFONY_DEPRECATIONS_HELPER" value="weak" />
</php>
<testsuites>
<testsuite name="Unit">
<directory>Tests/Mysqli</directory>
</testsuite>
</testsuites>
</phpunit>

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="Tests/bootstrap.php" colors="false">
<php>
<env name="JOOMLA_TEST_DB_DRIVER" value="pgsql" />
<env name="JOOMLA_TEST_DB_HOST" value="pgsql" />
<env name="JOOMLA_TEST_DB_PORT" value="5433" />
<env name="JOOMLA_TEST_DB_USER" value="postgres" />
<env name="JOOMLA_TEST_DB_PASSWORD" value="" />
<env name="JOOMLA_TEST_DB_DATABASE" value="joomla_ut" />
<env name="JOOMLA_TEST_DB_PREFIX" value="" />
<env name="JOOMLA_TEST_DB_SELECT" value="yes" />
<env name="SYMFONY_DEPRECATIONS_HELPER" value="weak" />
</php>
<testsuites>
<testsuite name="Unit">
<directory>Tests/Pgsql</directory>
</testsuite>
</testsuites>
</phpunit>

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="Tests/bootstrap.php" colors="false">
<php>
<env name="JOOMLA_TEST_DB_DRIVER" value="sqlite" />
<env name="JOOMLA_TEST_DB_HOST" value="" />
<env name="JOOMLA_TEST_DB_PORT" value="" />
<env name="JOOMLA_TEST_DB_USER" value="" />
<env name="JOOMLA_TEST_DB_PASSWORD" value="" />
<env name="JOOMLA_TEST_DB_DATABASE" value=":memory:" />
<env name="JOOMLA_TEST_DB_PREFIX" value="" />
<env name="SYMFONY_DEPRECATIONS_HELPER" value="weak" />
</php>
<testsuites>
<testsuite name="Unit">
<directory>Tests/Sqlite</directory>
</testsuite>
</testsuites>
</phpunit>

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="Tests/bootstrap.php" colors="false">
<php>
<env name="JOOMLA_TEST_DB_DRIVER" value="sqlsrv" />
<env name="JOOMLA_TEST_DB_HOST" value="sqlsrv" />
<env name="JOOMLA_TEST_DB_PORT" value="1433" />
<env name="JOOMLA_TEST_DB_USER" value="sa" />
<env name="JOOMLA_TEST_DB_PASSWORD" value="JoomlaFramework123" />
<env name="JOOMLA_TEST_DB_DATABASE" value="joomla_ut" />
<env name="JOOMLA_TEST_DB_PREFIX" value="" />
<env name="SYMFONY_DEPRECATIONS_HELPER" value="weak" />
</php>
<testsuites>
<testsuite name="Unit">
<directory>Tests/Sqlsrv</directory>
</testsuite>
</testsuites>
</phpunit>

View File

@ -0,0 +1,168 @@
<?php
/**
* Part of the Joomla Framework Database Package
*
* @copyright Copyright (C) 2005 - 2021 Open Source Matters, Inc. All rights reserved.
* @license GNU General Public License version 2 or later; see LICENSE
*/
namespace Joomla\Database\Command;
use Joomla\Archive\Archive;
use Joomla\Archive\Zip;
use Joomla\Console\Command\AbstractCommand;
use Joomla\Database\DatabaseDriver;
use Joomla\Database\Exception\UnsupportedAdapterException;
use Joomla\Filesystem\File;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Console\Input\InputOption;
/**
* Console command for exporting the database
*
* @since 2.0.0
*/
class ExportCommand extends AbstractCommand
{
/**
* The default command name
*
* @var string
* @since 2.0.0
*/
protected static $defaultName = 'database:export';
/**
* Database connector
*
* @var DatabaseDriver
* @since 2.0.0
*/
private $db;
/**
* Instantiate the command.
*
* @param DatabaseDriver $db Database connector
*
* @since 2.0.0
*/
public function __construct(DatabaseDriver $db)
{
$this->db = $db;
parent::__construct();
}
/**
* Internal function to execute the command.
*
* @param InputInterface $input The input to inject into the command.
* @param OutputInterface $output The output to inject into the command.
*
* @return integer The command exit code
*
* @since 2.0.0
*/
protected function doExecute(InputInterface $input, OutputInterface $output): int
{
$symfonyStyle = new SymfonyStyle($input, $output);
$symfonyStyle->title('Exporting Database');
$totalTime = microtime(true);
if (!class_exists(File::class)) {
$symfonyStyle->error('The "joomla/filesystem" Composer package is not installed, cannot create an export.');
return 1;
}
// Make sure the database supports exports before we get going
try {
$exporter = $this->db->getExporter()
->withStructure();
} catch (UnsupportedAdapterException $e) {
$symfonyStyle->error(sprintf('The "%s" database driver does not support exporting data.', $this->db->getName()));
return 1;
}
$folderPath = $input->getOption('folder');
$tableName = $input->getOption('table');
$zip = $input->getOption('zip');
$zipFile = $folderPath . '/data_exported_' . date("Y-m-d\TH-i-s") . '.zip';
$tables = $this->db->getTableList();
$prefix = $this->db->getPrefix();
if ($tableName) {
if (!\in_array($tableName, $tables)) {
$symfonyStyle->error(sprintf('The %s table does not exist in the database.', $tableName));
return 1;
}
$tables = [$tableName];
}
if ($zip) {
if (!class_exists(Archive::class)) {
$symfonyStyle->error('The "joomla/archive" Composer package is not installed, cannot create ZIP files.');
return 1;
}
/** @var Zip $zipArchive */
$zipArchive = (new Archive())->getAdapter('zip');
}
foreach ($tables as $table) {
// If an empty prefix is in use then we will dump all tables, otherwise the prefix must match
if (strlen($prefix) === 0 || strpos(substr($table, 0, strlen($prefix)), $prefix) !== false) {
$taskTime = microtime(true);
$filename = $folderPath . '/' . $table . '.xml';
$symfonyStyle->text(sprintf('Processing the %s table', $table));
$data = (string) $exporter->from($table)->withData(true);
if (file_exists($filename)) {
File::delete($filename);
}
File::write($filename, $data);
if ($zip) {
$zipFilesArray = [['name' => $table . '.xml', 'data' => $data]];
$zipArchive->create($zipFile, $zipFilesArray);
File::delete($filename);
}
$symfonyStyle->text(sprintf('Exported data for %s in %d seconds', $table, round(microtime(true) - $taskTime, 3)));
}
}
$symfonyStyle->success(sprintf('Export completed in %d seconds', round(microtime(true) - $totalTime, 3)));
return 0;
}
/**
* Configure the command.
*
* @return void
*
* @since 2.0.0
*/
protected function configure(): void
{
$this->setDescription('Export the database');
$this->addOption('folder', null, InputOption::VALUE_OPTIONAL, 'Path to write the export files to', '.');
$this->addOption('table', null, InputOption::VALUE_REQUIRED, 'The name of the database table to export');
$this->addOption('zip', null, InputOption::VALUE_NONE, 'Flag indicating the export will be saved to a ZIP archive');
}
}

View File

@ -0,0 +1,251 @@
<?php
/**
* Part of the Joomla Framework Database Package
*
* @copyright Copyright (C) 2005 - 2021 Open Source Matters, Inc. All rights reserved.
* @license GNU General Public License version 2 or later; see LICENSE
*/
// phpcs:disable Generic.PHP.DeprecatedFunctions.Deprecated
namespace Joomla\Database\Command;
use Joomla\Archive\Archive;
use Joomla\Archive\Exception\UnknownArchiveException;
use Joomla\Console\Command\AbstractCommand;
use Joomla\Database\DatabaseDriver;
use Joomla\Database\Exception\ExecutionFailureException;
use Joomla\Database\Exception\UnsupportedAdapterException;
use Joomla\Filesystem\Exception\FilesystemException;
use Joomla\Filesystem\File;
use Joomla\Filesystem\Folder;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
/**
* Console command for importing the database
*
* @since 2.0.0
*/
class ImportCommand extends AbstractCommand
{
/**
* The default command name
*
* @var string
* @since 2.0.0
*/
protected static $defaultName = 'database:import';
/**
* Database connector
*
* @var DatabaseDriver
* @since 2.0.0
*/
private $db;
/**
* Instantiate the command.
*
* @param DatabaseDriver $db Database connector
*
* @since 2.0.0
*/
public function __construct(DatabaseDriver $db)
{
$this->db = $db;
parent::__construct();
}
/**
* Checks if the zip file contains database export files
*
* @param string $archive A zip archive to analyze
*
* @return void
*
* @since 2.0.0
* @throws \RuntimeException
*/
private function checkZipFile(string $archive): void
{
if (!extension_loaded('zip')) {
throw new \RuntimeException('The PHP zip extension is not installed or is disabled');
}
$zip = zip_open($archive);
if (!\is_resource($zip)) {
throw new \RuntimeException('Unable to open archive');
}
while ($file = @zip_read($zip)) {
if (strpos(zip_entry_name($file), $this->db->getPrefix()) === false) {
zip_entry_close($file);
@zip_close($zip);
throw new \RuntimeException('Unable to find table matching database prefix');
}
zip_entry_close($file);
}
@zip_close($zip);
}
/**
* Internal function to execute the command.
*
* @param InputInterface $input The input to inject into the command.
* @param OutputInterface $output The output to inject into the command.
*
* @return integer The command exit code
*
* @since 2.0.0
*/
protected function doExecute(InputInterface $input, OutputInterface $output): int
{
$symfonyStyle = new SymfonyStyle($input, $output);
$symfonyStyle->title('Importing Database');
$totalTime = microtime(true);
// Make sure the database supports imports before we get going
try {
$importer = $this->db->getImporter()
->withStructure()
->asXml();
} catch (UnsupportedAdapterException $e) {
$symfonyStyle->error(sprintf('The "%s" database driver does not support importing data.', $this->db->getName()));
return 1;
}
$folderPath = $input->getOption('folder');
$tableName = $input->getOption('table');
$zipFile = $input->getOption('zip');
if ($zipFile) {
if (!class_exists(File::class)) {
$symfonyStyle->error('The "joomla/filesystem" Composer package is not installed, cannot process ZIP files.');
return 1;
}
if (!class_exists(Archive::class)) {
$symfonyStyle->error('The "joomla/archive" Composer package is not installed, cannot process ZIP files.');
return 1;
}
$zipPath = $folderPath . '/' . $zipFile;
try {
$this->checkZipFile($zipPath);
} catch (\RuntimeException $e) {
$symfonyStyle->error($e->getMessage());
return 1;
}
$folderPath .= File::stripExt($zipFile);
try {
Folder::create($folderPath);
} catch (FilesystemException $e) {
$symfonyStyle->error($e->getMessage());
return 1;
}
try {
(new Archive())->extract($zipPath, $folderPath);
} catch (UnknownArchiveException $e) {
$symfonyStyle->error($e->getMessage());
Folder::delete($folderPath);
return 1;
}
}
if ($tableName) {
$tables = [$tableName . '.xml'];
} else {
$tables = Folder::files($folderPath, '\.xml$');
}
foreach ($tables as $table) {
$taskTime = microtime(true);
$percorso = $folderPath . '/' . $table;
// Check file
if (!file_exists($percorso)) {
$symfonyStyle->error(sprintf('The %s file does not exist.', $table));
return 1;
}
$tableName = str_replace('.xml', '', $table);
$symfonyStyle->text(sprintf('Importing %1$s from %2$s', $tableName, $table));
$importer->from(file_get_contents($percorso));
$symfonyStyle->text(sprintf('Processing the %s table', $tableName));
try {
$this->db->dropTable($tableName, true);
} catch (ExecutionFailureException $e) {
$symfonyStyle->error(sprintf('Error executing the DROP TABLE statement for %1$s: %2$s', $tableName, $e->getMessage()));
return 1;
}
try {
$importer->mergeStructure();
} catch (\Exception $e) {
$symfonyStyle->error(sprintf('Error merging the structure for %1$s: %2$s', $tableName, $e->getMessage()));
return 1;
}
try {
$importer->importData();
} catch (\Exception $e) {
$symfonyStyle->error(sprintf('Error importing the data for %1$s: %2$s', $tableName, $e->getMessage()));
return 1;
}
$symfonyStyle->text(sprintf('Imported data for %s in %d seconds', $table, round(microtime(true) - $taskTime, 3)));
}
if ($zipFile) {
Folder::delete($folderPath);
}
$symfonyStyle->success(sprintf('Import completed in %d seconds', round(microtime(true) - $totalTime, 3)));
return 0;
}
/**
* Configure the command.
*
* @return void
*
* @since 2.0.0
*/
protected function configure(): void
{
$this->setDescription('Import the database');
$this->addOption('folder', null, InputOption::VALUE_OPTIONAL, 'Path to the folder containing files to import', '.');
$this->addOption('zip', null, InputOption::VALUE_REQUIRED, 'The name of a ZIP file to import');
$this->addOption('table', null, InputOption::VALUE_REQUIRED, 'The name of the database table to import');
}
}

View File

@ -0,0 +1,29 @@
<?php
/**
* Part of the Joomla Framework Database Package
*
* @copyright Copyright (C) 2022 Open Source Matters, Inc. All rights reserved.
* @license GNU General Public License version 2 or later; see LICENSE
*/
namespace Joomla\Database;
/**
* Defines the interface for a DatabaseInterface aware class.
*
* @since 2.1.0
*/
interface DatabaseAwareInterface
{
/**
* Set the database.
*
* @param DatabaseInterface $db The database.
*
* @return void
*
* @since 2.1.0
*/
public function setDatabase(DatabaseInterface $db): void;
}

View File

@ -0,0 +1,59 @@
<?php
/**
* Part of the Joomla Framework Database Package
*
* @copyright Copyright (C) 2022 Open Source Matters, Inc. All rights reserved.
* @license GNU General Public License version 2 or later; see LICENSE
*/
namespace Joomla\Database;
use Joomla\Database\Exception\DatabaseNotFoundException;
/**
* Defines the trait for a Database Aware Class.
*
* @since 2.1.0
*/
trait DatabaseAwareTrait
{
/**
* Database
*
* @var DatabaseInterface
* @since 2.1.0
*/
private $databaseAwareTraitDatabase;
/**
* Get the database.
*
* @return DatabaseInterface
*
* @since 2.1.0
* @throws DatabaseNotFoundException May be thrown if the database has not been set.
*/
protected function getDatabase(): DatabaseInterface
{
if ($this->databaseAwareTraitDatabase) {
return $this->databaseAwareTraitDatabase;
}
throw new DatabaseNotFoundException('Database not set in ' . \get_class($this));
}
/**
* Set the database.
*
* @param DatabaseInterface $db The database.
*
* @return void
*
* @since 2.1.0
*/
public function setDatabase(DatabaseInterface $db): void
{
$this->databaseAwareTraitDatabase = $db;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,47 @@
<?php
/**
* Part of the Joomla Framework Database Package
*
* @copyright Copyright (C) 2005 - 2021 Open Source Matters, Inc. All rights reserved.
* @license GNU General Public License version 2 or later; see LICENSE
*/
namespace Joomla\Database;
/**
* Class defining the events dispatched by the database API
*
* @since 2.0.0
*/
final class DatabaseEvents
{
/**
* Private constructor to prevent instantiation of this class
*
* @since 2.0.0
*/
private function __construct()
{
}
/**
* Database event which is dispatched after the connection to the database server is opened.
*
* Listeners to this event receive a `Joomla\Database\Event\ConnectionEvent` object.
*
* @var string
* @since 2.0.0
*/
public const POST_CONNECT = 'onAfterConnect';
/**
* Database event which is dispatched after the connection to the database server is closed.
*
* Listeners to this event receive a `Joomla\Database\Event\ConnectionEvent` object.
*
* @var string
* @since 2.0.0
*/
public const POST_DISCONNECT = 'onAfterDisconnect';
}

View File

@ -0,0 +1,308 @@
<?php
/**
* Part of the Joomla Framework Database Package
*
* @copyright Copyright (C) 2005 - 2021 Open Source Matters, Inc. All rights reserved.
* @license GNU General Public License version 2 or later; see LICENSE
*/
namespace Joomla\Database;
/**
* Joomla Framework Database Exporter Class
*
* @since 1.0
*/
abstract class DatabaseExporter
{
/**
* The type of output format.
*
* @var string
* @since 1.0
*/
protected $asFormat = 'xml';
/**
* An array of cached data.
*
* @var array
* @since 1.0
*/
protected $cache = ['columns' => [], 'keys' => []];
/**
* The database connector to use for exporting structure and/or data.
*
* @var DatabaseInterface
* @since 1.0
*/
protected $db;
/**
* An array input sources (table names).
*
* @var string[]
* @since 1.0
*/
protected $from = [];
/**
* An array of options for the exporter.
*
* @var \stdClass
* @since 1.0
*/
protected $options;
/**
* Constructor.
*
* Sets up the default options for the exporter.
*
* @since 1.0
*/
public function __construct()
{
$this->options = new \stdClass();
// Set up the class defaults:
// Export not only structure
$this->withStructure();
$this->withData();
// Export as xml.
$this->asXml();
// Default destination is a string using $output = (string) $exporter;
}
/**
* Magic function to exports the data to a string.
*
* @return string
*
* @since 1.0
*/
public function __toString()
{
$buffer = '';
try {
// Check everything is ok to run first.
$this->check();
// Get the format.
switch ($this->asFormat) {
case 'xml':
default:
$buffer = $this->buildXml();
break;
}
} catch (\Exception $e) {
// Do nothing
}
return $buffer;
}
/**
* Set the output option for the exporter to XML format.
*
* @return $this
*
* @since 1.0
*/
public function asXml()
{
$this->asFormat = 'xml';
return $this;
}
/**
* Builds the XML data for the tables to export.
*
* @return string An XML string
*
* @since 1.0
* @throws \Exception if an error occurs.
*/
abstract protected function buildXml();
/**
* Builds the XML structure to export.
*
* @return array An array of XML lines (strings).
*
* @since 1.0
* @throws \Exception if an error occurs.
*/
abstract protected function buildXmlStructure();
/**
* Checks if all data and options are in order prior to exporting.
*
* @return $this
*
* @since 1.0
* @throws \Exception if an error is encountered.
*/
abstract public function check();
/**
* Specifies a list of table names to export.
*
* @param string[]|string $from The name of a single table, or an array of the table names to export.
*
* @return $this
*
* @since 1.0
* @throws \InvalidArgumentException
*/
public function from($from)
{
if (\is_string($from)) {
$this->from = [$from];
} elseif (\is_array($from)) {
$this->from = $from;
} else {
throw new \InvalidArgumentException('The exporter requires either a single table name or array of table names');
}
return $this;
}
/**
* Get the generic name of the table, converting the database prefix to the wildcard string.
*
* @param string $table The name of the table.
*
* @return string The name of the table with the database prefix replaced with #__.
*
* @since 1.0
*/
protected function getGenericTableName($table)
{
$prefix = $this->db->getPrefix();
// Replace the magic prefix if found.
return preg_replace("|^$prefix|", '#__', $table);
}
/**
* Sets the database connector to use for importing structure and/or data.
*
* @param DatabaseInterface $db The database connector.
*
* @return $this
*
* @since 1.0
*/
public function setDbo(DatabaseInterface $db)
{
$this->db = $db;
return $this;
}
/**
* Sets an internal option to export the structure of the input table(s).
*
* @param boolean $setting True to export the structure, false to not.
*
* @return $this
*
* @since 1.0
*/
public function withStructure($setting = true)
{
$this->options->withStructure = (bool) $setting;
return $this;
}
/**
* Sets an internal option to export the data of the input table(s).
*
* @param boolean $setting True to export the data, false to not.
*
* @return $this
*
* @since 2.0.0
*/
public function withData($setting = false)
{
$this->options->withData = (bool) $setting;
return $this;
}
/**
* Builds the XML data to export.
*
* @return array An array of XML lines (strings).
*
* @since 2.0.0
* @throws \Exception if an error occurs.
*/
protected function buildXmlData()
{
$buffer = [];
foreach ($this->from as $table) {
// Replace the magic prefix if found.
$table = $this->getGenericTableName($table);
// Get the details columns information.
$fields = $this->db->getTableColumns($table, false);
$colblob = [];
foreach ($fields as $field) {
// Catch blob for conversion xml
if ($field->Type == 'mediumblob') {
$colblob[] = $field->Field;
}
}
$this->db->setQuery(
$this->db->getQuery(true)
->select($this->db->quoteName(array_keys($fields)))
->from($this->db->quoteName($table))
);
$rows = $this->db->loadObjectList();
if (!count($rows)) {
continue;
}
$buffer[] = ' <table_data name="' . $table . '">';
foreach ($rows as $row) {
$buffer[] = ' <row>';
foreach ($row as $key => $value) {
if (!in_array($key, $colblob)) {
if (is_null($value)) {
$buffer[] = ' <field name="' . $key . '" value_is_null></field>';
} else {
$buffer[] = ' <field name="' . $key . '">' . htmlspecialchars($value, ENT_COMPAT, 'UTF-8') . '</field>';
}
} else {
$buffer[] = ' <field name="' . $key . '">' . base64_encode($value) . '</field>';
}
}
$buffer[] = ' </row>';
}
$buffer[] = ' </table_data>';
}
return $buffer;
}
}

View File

@ -0,0 +1,171 @@
<?php
/**
* Part of the Joomla Framework Database Package
*
* @copyright Copyright (C) 2005 - 2021 Open Source Matters, Inc. All rights reserved.
* @license GNU General Public License version 2 or later; see LICENSE
*/
namespace Joomla\Database;
/**
* Joomla Framework Database Factory class
*
* @since 1.0
*/
class DatabaseFactory
{
/**
* Method to return a database driver based on the given options.
*
* There are three global options and then the rest are specific to the database driver. The 'database' option determines which database is to
* be used for the connection. The 'select' option determines whether the connector should automatically select the chosen database.
*
* @param string $name Name of the database driver you'd like to instantiate
* @param array $options Parameters to be passed to the database driver.
*
* @return DatabaseInterface
*
* @since 1.0
* @throws Exception\UnsupportedAdapterException if there is not a compatible database driver
*/
public function getDriver(string $name = 'mysqli', array $options = []): DatabaseInterface
{
// Sanitize the database connector options.
$options['driver'] = preg_replace('/[^A-Z0-9_\.-]/i', '', $name);
$options['database'] = $options['database'] ?? null;
$options['select'] = $options['select'] ?? true;
$options['factory'] = $options['factory'] ?? $this;
// Derive the class name from the driver.
$class = __NAMESPACE__ . '\\' . ucfirst(strtolower($options['driver'])) . '\\' . ucfirst(strtolower($options['driver'])) . 'Driver';
// If the class still doesn't exist we have nothing left to do but throw an exception. We did our best.
if (!class_exists($class)) {
throw new Exception\UnsupportedAdapterException(sprintf('Unable to load Database Driver: %s', $options['driver']));
}
return new $class($options);
}
/**
* Gets an exporter class object.
*
* @param string $name Name of the driver you want an exporter for.
* @param DatabaseInterface|null $db Optional database driver to inject into the query object.
*
* @return DatabaseExporter
*
* @since 1.0
* @throws Exception\UnsupportedAdapterException if there is not a compatible database exporter
*/
public function getExporter(string $name, ?DatabaseInterface $db = null): DatabaseExporter
{
// Derive the class name from the driver.
$class = __NAMESPACE__ . '\\' . ucfirst(strtolower($name)) . '\\' . ucfirst(strtolower($name)) . 'Exporter';
// Make sure we have an exporter class for this driver.
if (!class_exists($class)) {
// If it doesn't exist we are at an impasse so throw an exception.
throw new Exception\UnsupportedAdapterException('Database Exporter not found.');
}
/** @var DatabaseExporter $o */
$o = new $class();
if ($db) {
$o->setDbo($db);
}
return $o;
}
/**
* Gets an importer class object.
*
* @param string $name Name of the driver you want an importer for.
* @param DatabaseInterface|null $db Optional database driver to inject into the query object.
*
* @return DatabaseImporter
*
* @since 1.0
* @throws Exception\UnsupportedAdapterException if there is not a compatible database importer
*/
public function getImporter(string $name, ?DatabaseInterface $db = null): DatabaseImporter
{
// Derive the class name from the driver.
$class = __NAMESPACE__ . '\\' . ucfirst(strtolower($name)) . '\\' . ucfirst(strtolower($name)) . 'Importer';
// Make sure we have an importer class for this driver.
if (!class_exists($class)) {
// If it doesn't exist we are at an impasse so throw an exception.
throw new Exception\UnsupportedAdapterException('Database importer not found.');
}
/** @var DatabaseImporter $o */
$o = new $class();
if ($db) {
$o->setDbo($db);
}
return $o;
}
/**
* Get a new iterator on the current query.
*
* @param string $name Name of the driver you want an iterator for.
* @param StatementInterface $statement Statement holding the result set to be iterated.
* @param string|null $column An optional column to use as the iterator key.
* @param string $class The class of object that is returned.
*
* @return DatabaseIterator
*
* @since 2.0.0
*/
public function getIterator(
string $name,
StatementInterface $statement,
?string $column = null,
string $class = \stdClass::class
): DatabaseIterator {
// Derive the class name from the driver.
$iteratorClass = __NAMESPACE__ . '\\' . ucfirst($name) . '\\' . ucfirst($name) . 'Iterator';
// Make sure we have an iterator class for this driver.
if (!class_exists($iteratorClass)) {
// We can work with the base iterator class so use that
$iteratorClass = DatabaseIterator::class;
}
// Return a new iterator
return new $iteratorClass($statement, $column, $class);
}
/**
* Get the current query object or a new Query object.
*
* @param string $name Name of the driver you want an query object for.
* @param DatabaseInterface|null $db Optional database driver to inject into the query object.
*
* @return QueryInterface
*
* @since 1.0
* @throws Exception\UnsupportedAdapterException if there is not a compatible database query object
*/
public function getQuery(string $name, ?DatabaseInterface $db = null): QueryInterface
{
// Derive the class name from the driver.
$class = __NAMESPACE__ . '\\' . ucfirst(strtolower($name)) . '\\' . ucfirst(strtolower($name)) . 'Query';
// Make sure we have a query class for this driver.
if (!class_exists($class)) {
// If it doesn't exist we are at an impasse so throw an exception.
throw new Exception\UnsupportedAdapterException('Database Query class not found');
}
return new $class($db);
}
}

View File

@ -0,0 +1,376 @@
<?php
/**
* Part of the Joomla Framework Database Package
*
* @copyright Copyright (C) 2005 - 2021 Open Source Matters, Inc. All rights reserved.
* @license GNU General Public License version 2 or later; see LICENSE
*/
namespace Joomla\Database;
/**
* Joomla Framework Database Importer Class
*
* @since 1.0
*/
abstract class DatabaseImporter
{
/**
* An array of cached data.
*
* @var array
* @since 1.0
*/
protected $cache = ['columns' => [], 'keys' => []];
/**
* The database connector to use for exporting structure and/or data.
*
* @var DatabaseInterface
* @since 1.0
*/
protected $db;
/**
* The input source.
*
* @var mixed
* @since 1.0
*/
protected $from = [];
/**
* The type of input format.
*
* @var string
* @since 1.0
*/
protected $asFormat = 'xml';
/**
* An array of options for the exporter.
*
* @var \stdClass
* @since 1.0
*/
protected $options;
/**
* Constructor.
*
* Sets up the default options for the importer.
*
* @since 1.0
*/
public function __construct()
{
$this->options = new \stdClass();
// Set up the class defaults:
// Import with only structure
$this->withStructure();
// Export as XML.
$this->asXml();
// Default destination is a string using $output = (string) $importer;
}
/**
* Set the output option for the importer to XML format.
*
* @return $this
*
* @since 1.0
*/
public function asXml()
{
$this->asFormat = 'xml';
return $this;
}
/**
* Checks if all data and options are in order prior to importer.
*
* @return $this
*
* @since 1.0
* @throws \RuntimeException
*/
abstract public function check();
/**
* Specifies the data source to import.
*
* @param \SimpleXMLElement|string $from The data source to import, either as a SimpleXMLElement object or XML string.
*
* @return $this
*
* @since 1.0
*/
public function from($from)
{
$this->from = $from;
return $this;
}
/**
* Get the SQL syntax to add a column.
*
* @param string $table The table name.
* @param \SimpleXMLElement $field The XML field definition.
*
* @return string
*
* @since 1.0
*/
protected function getAddColumnSql($table, \SimpleXMLElement $field)
{
return 'ALTER TABLE ' . $this->db->quoteName($table) . ' ADD COLUMN ' . $this->getColumnSQL($field);
}
/**
* Get alters for table if there is a difference.
*
* @param \SimpleXMLElement $structure The XML structure of the table.
*
* @return array
*
* @since 2.0.0
*/
abstract protected function getAlterTableSql(\SimpleXMLElement $structure);
/**
* Get the syntax to alter a column.
*
* @param string $table The name of the database table to alter.
* @param \SimpleXMLElement $field The XML definition for the field.
*
* @return string
*
* @since 1.0
*/
protected function getChangeColumnSql($table, \SimpleXMLElement $field)
{
return 'ALTER TABLE ' . $this->db->quoteName($table) . ' CHANGE COLUMN ' . $this->db->quoteName((string) $field['Field']) . ' '
. $this->getColumnSQL($field);
}
/**
* Get the SQL syntax for a single column that would be included in a table create or alter statement.
*
* @param \SimpleXMLElement $field The XML field definition.
*
* @return string
*
* @since 1.0
*/
abstract protected function getColumnSql(\SimpleXMLElement $field);
/**
* Get the SQL syntax to drop a column.
*
* @param string $table The table name.
* @param string $name The name of the field to drop.
*
* @return string
*
* @since 1.0
*/
protected function getDropColumnSql($table, $name)
{
return 'ALTER TABLE ' . $this->db->quoteName($table) . ' DROP COLUMN ' . $this->db->quoteName($name);
}
/**
* Get the details list of keys for a table.
*
* @param array $keys An array of objects that comprise the keys for the table.
*
* @return array The lookup array. array({key name} => array(object, ...))
*
* @since 1.0
*/
protected function getKeyLookup($keys)
{
// First pass, create a lookup of the keys.
$lookup = [];
foreach ($keys as $key) {
if ($key instanceof \SimpleXMLElement) {
$kName = (string) $key['Key_name'];
} else {
$kName = $key->Key_name;
}
if (empty($lookup[$kName])) {
$lookup[$kName] = [];
}
$lookup[$kName][] = $key;
}
return $lookup;
}
/**
* Get the real name of the table, converting the prefix wildcard string if present.
*
* @param string $table The name of the table.
*
* @return string The real name of the table.
*
* @since 1.0
*/
protected function getRealTableName($table)
{
$prefix = $this->db->getPrefix();
// Replace the magic prefix if found.
$table = preg_replace('|^#__|', $prefix, $table);
return $table;
}
/**
* Import the data from the source into the existing tables.
*
* @return void
*
* @note Currently only supports XML format.
* @since 2.0.0
* @throws \RuntimeException on error.
*/
public function importData()
{
if ($this->from instanceof \SimpleXMLElement) {
$xml = $this->from;
} else {
$xml = new \SimpleXMLElement($this->from);
}
// Get all the table definitions.
$xmlTables = $xml->xpath('database/table_data');
foreach ($xmlTables as $table) {
// Convert the magic prefix into the real table name.
$tableName = $this->getRealTableName((string) $table['name']);
$rows = $table->children();
foreach ($rows as $row) {
if ($row->getName() == 'row') {
$entry = new \stdClass();
foreach ($row->children() as $data) {
if (isset($data['value_is_null'])) {
$entry->{(string) $data['name']} = null;
} else {
$entry->{(string) $data['name']} = (string) $data;
}
}
$this->db->insertObject($tableName, $entry);
}
}
}
}
/**
* Merges the incoming structure definition with the existing structure.
*
* @return void
*
* @note Currently only supports XML format.
* @since 1.0
* @throws \RuntimeException on error.
*/
public function mergeStructure()
{
$tables = $this->db->getTableList();
if ($this->from instanceof \SimpleXMLElement) {
$xml = $this->from;
} else {
$xml = new \SimpleXMLElement($this->from);
}
// Get all the table definitions.
$xmlTables = $xml->xpath('database/table_structure');
foreach ($xmlTables as $table) {
// Convert the magic prefix into the real table name.
$tableName = $this->getRealTableName((string) $table['name']);
if (\in_array($tableName, $tables, true)) {
// The table already exists. Now check if there is any difference.
if ($queries = $this->getAlterTableSql($table)) {
// Run the queries to upgrade the data structure.
foreach ($queries as $query) {
$this->db->setQuery((string) $query);
$this->db->execute();
}
}
} else {
// This is a new table.
$sql = $this->xmlToCreate($table);
$queries = explode(';', (string) $sql);
foreach ($queries as $query) {
if (!empty($query)) {
$this->db->setQuery((string) $query);
$this->db->execute();
}
}
}
}
}
/**
* Sets the database connector to use for exporting structure and/or data.
*
* @param DatabaseInterface $db The database connector.
*
* @return $this
*
* @since 1.0
*/
public function setDbo(DatabaseInterface $db)
{
$this->db = $db;
return $this;
}
/**
* Sets an internal option to merge the structure based on the input data.
*
* @param boolean $setting True to import the structure, false to not.
*
* @return $this
*
* @since 1.0
*/
public function withStructure($setting = true)
{
$this->options->withStructure = (bool) $setting;
return $this;
}
/**
* Get the SQL syntax to add a table.
*
* @param \SimpleXMLElement $table The table information.
*
* @return string
*
* @since 2.0.0
* @throws \RuntimeException
*/
abstract protected function xmlToCreate(\SimpleXMLElement $table);
}

View File

@ -0,0 +1,619 @@
<?php
/**
* Part of the Joomla Framework Database Package
*
* @copyright Copyright (C) 2005 - 2021 Open Source Matters, Inc. All rights reserved.
* @license GNU General Public License version 2 or later; see LICENSE
*/
namespace Joomla\Database;
/**
* Joomla Framework Database Interface
*
* @since 1.0
*/
interface DatabaseInterface
{
/**
* Connects to the database if needed.
*
* @return void
*
* @since 2.0.0
* @throws \RuntimeException
*/
public function connect();
/**
* Determines if the connection to the server is active.
*
* @return boolean
*
* @since 2.0.0
*/
public function connected();
/**
* Create a new database using information from $options object.
*
* @param \stdClass $options Object used to pass user and database name to database driver. This object must have "db_name" and "db_user" set.
* @param boolean $utf True if the database supports the UTF-8 character set.
*
* @return boolean|resource
*
* @since 2.0.0
* @throws \RuntimeException
*/
public function createDatabase($options, $utf = true);
/**
* Replace special placeholder representing binary field with the original string.
*
* @param string|resource $data Encoded string or resource.
*
* @return string The original string.
*
* @since 1.7.0
*/
public function decodeBinary($data);
/**
* Disconnects the database.
*
* @return void
*
* @since 2.0.0
*/
public function disconnect();
/**
* Drops a table from the database.
*
* @param string $table The name of the database table to drop.
* @param boolean $ifExists Optionally specify that the table must exist before it is dropped.
*
* @return $this
*
* @since 2.0.0
* @throws \RuntimeException
*/
public function dropTable($table, $ifExists = true);
/**
* Escapes a string for usage in an SQL statement.
*
* @param string $text The string to be escaped.
* @param boolean $extra Optional parameter to provide extra escaping.
*
* @return string The escaped string.
*
* @since 2.0.0
*/
public function escape($text, $extra = false);
/**
* Execute the SQL statement.
*
* @return boolean
*
* @since 2.0.0
* @throws \RuntimeException
*/
public function execute();
/**
* Get the number of affected rows for the previous executed SQL statement.
*
* @return integer
*
* @since 2.0.0
*/
public function getAffectedRows();
/**
* Method to get the database collation in use by sampling a text field of a table in the database.
*
* @return string|boolean The collation in use by the database or boolean false if not supported.
*
* @since 2.0.0
*/
public function getCollation();
/**
* Method that provides access to the underlying database connection.
*
* @return resource The underlying database connection resource.
*
* @since 2.0.0
*/
public function getConnection();
/**
* Method to get the database connection collation, as reported by the driver.
*
* If the connector doesn't support reporting this value please return an empty string.
*
* @return string
*
* @since 2.0.0
*/
public function getConnectionCollation();
/**
* Method to get the database encryption details (cipher and protocol) in use.
*
* @return string The database encryption details.
*
* @since 2.0.0
*/
public function getConnectionEncryption(): string;
/**
* Method to test if the database TLS connections encryption are supported.
*
* @return boolean Whether the database supports TLS connections encryption.
*
* @since 2.0.0
*/
public function isConnectionEncryptionSupported(): bool;
/**
* Method to check whether the installed database version is supported by the database driver
*
* @return boolean True if the database version is supported
*
* @since 2.0.0
*/
public function isMinimumVersion();
/**
* Get the total number of SQL statements executed by the database driver.
*
* @return integer
*
* @since 2.0.0
*/
public function getCount();
/**
* Returns a PHP date() function compliant date format for the database driver.
*
* @return string
*
* @since 2.0.0
*/
public function getDateFormat();
/**
* Get the minimum supported database version.
*
* @return string
*
* @since 2.0.0
*/
public function getMinimum();
/**
* Get the name of the database driver.
*
* @return string
*
* @since 2.0.0
*/
public function getName();
/**
* Get the null or zero representation of a timestamp for the database driver.
*
* @return string
*
* @since 2.0.0
*/
public function getNullDate();
/**
* Get the common table prefix for the database driver.
*
* @return string The common database table prefix.
*
* @since 3.0
*/
public function getPrefix();
/**
* Get the number of returned rows for the previous executed SQL statement.
*
* @return integer
*
* @since 2.0.0
*/
public function getNumRows();
/**
* Get the current query object or a new QueryInterface object.
*
* @param boolean $new False to return the current query object, True to return a new QueryInterface object.
*
* @return QueryInterface
*
* @since 2.0.0
* @throws \RuntimeException
*/
public function getQuery($new = false);
/**
* Get the server family type.
*
* @return string
*
* @since 2.0.0
*/
public function getServerType();
/**
* Retrieves field information about the given tables.
*
* @param string $table The name of the database table.
* @param boolean $typeOnly True (default) to only return field types.
*
* @return array
*
* @since 2.0.0
* @throws \RuntimeException
*/
public function getTableColumns($table, $typeOnly = true);
/**
* Retrieves field information about the given tables.
*
* @param mixed $tables A table name or a list of table names.
*
* @return array
*
* @since 2.0.0
* @throws \RuntimeException
*/
public function getTableKeys($tables);
/**
* Method to get an array of all tables in the database.
*
* @return array
*
* @since 2.0.0
* @throws \RuntimeException
*/
public function getTableList();
/**
* Get the version of the database connector.
*
* @return string
*
* @since 2.0.0
*/
public function getVersion();
/**
* Determine whether or not the database engine supports UTF-8 character encoding.
*
* @return boolean True if the database engine supports UTF-8 character encoding.
*
* @since 2.0.0
*/
public function hasUtfSupport();
/**
* Method to get the auto-incremented value from the last INSERT statement.
*
* @return mixed The value of the auto-increment field from the last inserted row.
*
* @since 2.0.0
*/
public function insertid();
/**
* Inserts a row into a table based on an object's properties.
*
* @param string $table The name of the database table to insert into.
* @param object $object A reference to an object whose public properties match the table fields.
* @param string $key The name of the primary key. If provided the object property is updated.
*
* @return boolean
*
* @since 2.0.0
* @throws \RuntimeException
*/
public function insertObject($table, &$object, $key = null);
/**
* Test to see if the connector is available.
*
* @return boolean
*
* @since 1.0
*/
public static function isSupported();
/**
* Method to get the first row of the result set from the database query as an associative array of ['field_name' => 'row_value'].
*
* @return mixed The return value or null if the query failed.
*
* @since 2.0.0
* @throws \RuntimeException
*/
public function loadAssoc();
/**
* Method to get an array of the result set rows from the database query where each row is an associative array
* of ['field_name' => 'row_value']. The array of rows can optionally be keyed by a field name, but defaults to
* a sequential numeric array.
*
* NOTE: Choosing to key the result array by a non-unique field name can result in unwanted
* behavior and should be avoided.
*
* @param string $key The name of a field on which to key the result array.
* @param string $column An optional column name. Instead of the whole row, only this column value will be in the result array.
*
* @return mixed The return value or null if the query failed.
*
* @since 2.0.0
* @throws \RuntimeException
*/
public function loadAssocList($key = null, $column = null);
/**
* Method to get an array of values from the <var>$offset</var> field in each row of the result set from the database query.
*
* @param integer $offset The row offset to use to build the result array.
*
* @return mixed The return value or null if the query failed.
*
* @since 2.0.0
* @throws \RuntimeException
*/
public function loadColumn($offset = 0);
/**
* Method to get the first row of the result set from the database query as an object.
*
* @param string $class The class name to use for the returned row object.
*
* @return mixed The return value or null if the query failed.
*
* @since 2.0.0
* @throws \RuntimeException
*/
public function loadObject($class = \stdClass::class);
/**
* Method to get an array of the result set rows from the database query where each row is an object. The array
* of objects can optionally be keyed by a field name, but defaults to a sequential numeric array.
*
* NOTE: Choosing to key the result array by a non-unique field name can result in unwanted behavior and should be avoided.
*
* @param string $key The name of a field on which to key the result array.
* @param string $class The class name to use for the returned row objects.
*
* @return mixed The return value or null if the query failed.
*
* @since 2.0.0
* @throws \RuntimeException
*/
public function loadObjectList($key = '', $class = \stdClass::class);
/**
* Method to get the first field of the first row of the result set from the database query.
*
* @return mixed The return value or null if the query failed.
*
* @since 2.0.0
* @throws \RuntimeException
*/
public function loadResult();
/**
* Method to get the first row of the result set from the database query as an array.
*
* Columns are indexed numerically so the first column in the result set would be accessible via <var>$row[0]</var>, etc.
*
* @return mixed The return value or null if the query failed.
*
* @since 2.0.0
* @throws \RuntimeException
*/
public function loadRow();
/**
* Method to get an array of the result set rows from the database query where each row is an array. The array
* of objects can optionally be keyed by a field offset, but defaults to a sequential numeric array.
*
* NOTE: Choosing to key the result array by a non-unique field can result in unwanted behavior and should be avoided.
*
* @param string $key The name of a field on which to key the result array.
*
* @return mixed The return value or null if the query failed.
*
* @since 2.0.0
* @throws \RuntimeException
*/
public function loadRowList($key = null);
/**
* Locks a table in the database.
*
* @param string $tableName The name of the table to unlock.
*
* @return $this
*
* @since 2.0.0
* @throws \RuntimeException
*/
public function lockTable($tableName);
/**
* Quotes and optionally escapes a string to database requirements for use in database queries.
*
* @param array|string $text A string or an array of strings to quote.
* @param boolean $escape True (default) to escape the string, false to leave it unchanged.
*
* @return string
*
* @since 2.0.0
*/
public function quote($text, $escape = true);
/**
* Quotes a binary string to database requirements for use in database queries.
*
* @param string $data A binary string to quote.
*
* @return string The binary quoted input string.
*
* @since 1.7.0
*/
public function quoteBinary($data);
/**
* Wrap an SQL statement identifier name such as column, table or database names in quotes to prevent injection
* risks and reserved word conflicts.
*
* @param array|string $name The identifier name to wrap in quotes, or an array of identifier names to wrap in quotes.
* Each type supports dot-notation name.
* @param array|string $as The AS query part associated to $name. It can be string or array, in latter case it has to be
* same length of $name; if is null there will not be any AS part for string or array element.
*
* @return array|string The quote wrapped name, same type of $name.
*
* @since 2.0.0
*/
public function quoteName($name, $as = null);
/**
* Renames a table in the database.
*
* @param string $oldTable The name of the table to be renamed
* @param string $newTable The new name for the table.
* @param string $backup Table prefix
* @param string $prefix For the table - used to rename constraints in non-mysql databases
*
* @return $this
*
* @since 2.0.0
* @throws \RuntimeException
*/
public function renameTable($oldTable, $newTable, $backup = null, $prefix = null);
/**
* This function replaces a string identifier with the configured table prefix.
*
* @param string $sql The SQL statement to prepare.
* @param string $prefix The table prefix.
*
* @return string The processed SQL statement.
*
* @since 2.0.0
*/
public function replacePrefix($sql, $prefix = '#__');
/**
* Select a database for use.
*
* @param string $database The name of the database to select for use.
*
* @return boolean
*
* @since 2.0.0
* @throws \RuntimeException
*/
public function select($database);
/**
* Sets the SQL statement string for later execution.
*
* @param mixed $query The SQL statement to set either as a Query object or a string.
* @param integer $offset The affected row offset to set. {@deprecated 3.0 Use LimitableInterface::setLimit() instead}
* @param integer $limit The maximum affected rows to set. {@deprecated 3.0 Use LimitableInterface::setLimit() instead}
*
* @return $this
*
* @since 2.0.0
*/
public function setQuery($query, $offset = 0, $limit = 0);
/**
* Method to commit a transaction.
*
* @param boolean $toSavepoint If true, commit to the last savepoint.
*
* @return void
*
* @since 2.0.0
* @throws \RuntimeException
*/
public function transactionCommit($toSavepoint = false);
/**
* Method to roll back a transaction.
*
* @param boolean $toSavepoint If true, rollback to the last savepoint.
*
* @return void
*
* @since 2.0.0
* @throws \RuntimeException
*/
public function transactionRollback($toSavepoint = false);
/**
* Method to initialize a transaction.
*
* @param boolean $asSavepoint If true and a transaction is already active, a savepoint will be created.
*
* @return void
*
* @since 2.0.0
* @throws \RuntimeException
*/
public function transactionStart($asSavepoint = false);
/**
* Method to truncate a table.
*
* @param string $table The table to truncate
*
* @return void
*
* @since 2.0.0
* @throws \RuntimeException
*/
public function truncateTable($table);
/**
* Unlocks tables in the database.
*
* @return $this
*
* @since 2.0.0
* @throws \RuntimeException
*/
public function unlockTables();
/**
* Updates a row in a table based on an object's properties.
*
* @param string $table The name of the database table to update.
* @param object $object A reference to an object whose public properties match the table fields.
* @param array|string $key The name of the primary key.
* @param boolean $nulls True to update null fields or false to ignore them.
*
* @return boolean
*
* @since 2.0.0
* @throws \RuntimeException
*/
public function updateObject($table, &$object, $key, $nulls = false);
}

View File

@ -0,0 +1,246 @@
<?php
/**
* Part of the Joomla Framework Database Package
*
* @copyright Copyright (C) 2005 - 2021 Open Source Matters, Inc. All rights reserved.
* @license GNU General Public License version 2 or later; see LICENSE
*/
namespace Joomla\Database;
/**
* Joomla Framework Database Driver Class
*
* @since 1.0
*/
class DatabaseIterator implements \Countable, \Iterator
{
/**
* The class of object to create.
*
* @var string
* @since 1.0
*/
protected $class;
/**
* The name of the column to use for the key of the database record.
*
* @var mixed
* @since 1.0
*/
private $column;
/**
* The current database record.
*
* @var mixed
* @since 1.0
*/
private $current;
/**
* A numeric or string key for the current database record.
*
* @var scalar
* @since 1.0
*/
private $key;
/**
* The number of fetched records.
*
* @var integer
* @since 1.0
*/
private $fetched = 0;
/**
* The statement holding the result set to iterate.
*
* @var StatementInterface
* @since 1.0
*/
protected $statement;
/**
* Database iterator constructor.
*
* @param StatementInterface $statement The statement holding the result set to iterate.
* @param string $column An option column to use as the iterator key.
* @param string $class The class of object that is returned.
*
* @since 1.0
* @throws \InvalidArgumentException
*/
public function __construct(StatementInterface $statement, $column = null, $class = \stdClass::class)
{
if (!class_exists($class)) {
throw new \InvalidArgumentException(sprintf('new %s(*%s*, cursor)', \get_class($this), \gettype($class)));
}
if ($statement) {
$fetchMode = $class === \stdClass::class ? FetchMode::STANDARD_OBJECT : FetchMode::CUSTOM_OBJECT;
// PDO doesn't allow extra arguments for \PDO::FETCH_CLASS, so only forward the class for the custom object mode
if ($fetchMode === FetchMode::STANDARD_OBJECT) {
$statement->setFetchMode($fetchMode);
} else {
$statement->setFetchMode($fetchMode, $class);
}
}
$this->statement = $statement;
$this->class = $class;
$this->column = $column;
$this->fetched = 0;
$this->next();
}
/**
* Database iterator destructor.
*
* @since 1.0
*/
public function __destruct()
{
if ($this->statement) {
$this->freeResult();
}
}
/**
* Get the number of rows in the result set for the executed SQL given by the cursor.
*
* @return integer The number of rows in the result set.
*
* @see Countable::count()
* @since 1.0
*/
#[\ReturnTypeWillChange]
public function count()
{
if ($this->statement) {
return $this->statement->rowCount();
}
return 0;
}
/**
* The current element in the iterator.
*
* @return object
*
* @see Iterator::current()
* @since 1.0
*/
#[\ReturnTypeWillChange]
public function current()
{
return $this->current;
}
/**
* The key of the current element in the iterator.
*
* @return scalar
*
* @see Iterator::key()
* @since 1.0
*/
#[\ReturnTypeWillChange]
public function key()
{
return $this->key;
}
/**
* Moves forward to the next result from the SQL query.
*
* @return void
*
* @see Iterator::next()
* @since 1.0
*/
#[\ReturnTypeWillChange]
public function next()
{
// Set the default key as being the number of fetched object
$this->key = $this->fetched;
// Try to get an object
$this->current = $this->fetchObject();
// If an object has been found
if ($this->current) {
// Set the key as being the indexed column (if it exists)
if ($this->column && isset($this->current->{$this->column})) {
$this->key = $this->current->{$this->column};
}
// Update the number of fetched object
$this->fetched++;
}
}
/**
* Rewinds the iterator.
*
* This iterator cannot be rewound.
*
* @return void
*
* @see Iterator::rewind()
* @since 1.0
*/
#[\ReturnTypeWillChange]
public function rewind()
{
}
/**
* Checks if the current position of the iterator is valid.
*
* @return boolean
*
* @see Iterator::valid()
* @since 1.0
*/
#[\ReturnTypeWillChange]
public function valid()
{
return (bool) $this->current;
}
/**
* Method to fetch a row from the result set cursor as an object.
*
* @return mixed Either the next row from the result set or false if there are no more rows.
*
* @since 1.0
*/
protected function fetchObject()
{
if ($this->statement) {
return $this->statement->fetch();
}
return false;
}
/**
* Method to free up the memory used for the result set.
*
* @return void
*
* @since 1.0
*/
protected function freeResult()
{
if ($this->statement) {
$this->statement->closeCursor();
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,56 @@
<?php
/**
* Part of the Joomla Framework Database Package
*
* @copyright Copyright (C) 2005 - 2021 Open Source Matters, Inc. All rights reserved.
* @license GNU General Public License version 2 or later; see LICENSE
*/
namespace Joomla\Database\Event;
use Joomla\Database\DatabaseInterface;
use Joomla\Event\Event;
/**
* Database connection event
*
* @since 2.0.0
*/
class ConnectionEvent extends Event
{
/**
* DatabaseInterface object for this event
*
* @var DatabaseInterface
* @since 2.0.0
*/
private $driver;
/**
* Constructor.
*
* @param string $name The event name.
* @param DatabaseInterface $driver The DatabaseInterface object for this event.
*
* @since 2.0.0
*/
public function __construct(string $name, DatabaseInterface $driver)
{
parent::__construct($name);
$this->driver = $driver;
}
/**
* Retrieve the DatabaseInterface object attached to this event.
*
* @return DatabaseInterface
*
* @since 2.0.0
*/
public function getDriver(): DatabaseInterface
{
return $this->driver;
}
}

View File

@ -0,0 +1,35 @@
<?php
/**
* Part of the Joomla Framework Database Package
*
* @copyright Copyright (C) 2005 - 2021 Open Source Matters, Inc. All rights reserved.
* @license GNU General Public License version 2 or later; see LICENSE
*/
namespace Joomla\Database\Exception;
/**
* Exception class defining an error connecting to the database platform
*
* @since 1.5.0
*/
class ConnectionFailureException extends \RuntimeException
{
/**
* Construct the exception
*
* @param string $message The Exception message to throw. [optional]
* @param integer $code The Exception code. [optional]
* @param ?\Exception $previous The previous exception used for the exception chaining. [optional]
*
* @since 2.0.0
*/
public function __construct($message = '', $code = 0, \Exception $previous = null)
{
// PDO uses strings for exception codes, PHP forces numeric codes, so "force" the string code to be used
parent::__construct($message, 0, $previous);
$this->code = $code;
}
}

View File

@ -0,0 +1,19 @@
<?php
/**
* Part of the Joomla Framework Database Package
*
* @copyright Copyright (C) 2022 Open Source Matters, Inc. All rights reserved.
* @license GNU General Public License version 2 or later; see LICENSE
*/
namespace Joomla\Database\Exception;
/**
* No database is available.
*
* @since 2.1.0
*/
class DatabaseNotFoundException extends \RuntimeException
{
}

View File

@ -0,0 +1,57 @@
<?php
/**
* Part of the Joomla Framework Database Package
*
* @copyright Copyright (C) 2005 - 2021 Open Source Matters, Inc. All rights reserved.
* @license GNU General Public License version 2 or later; see LICENSE
*/
namespace Joomla\Database\Exception;
/**
* Exception class defining an error executing a statement
*
* @since 1.5.0
*/
class ExecutionFailureException extends \RuntimeException
{
/**
* The SQL statement that was executed.
*
* @var string
* @since 1.5.0
*/
private $query;
/**
* Construct the exception
*
* @param string $query The SQL statement that was executed.
* @param string $message The Exception message to throw. [optional]
* @param integer $code The Exception code. [optional]
* @param ?\Exception $previous The previous exception used for the exception chaining. [optional]
*
* @since 1.5.0
*/
public function __construct($query, $message = '', $code = 0, \Exception $previous = null)
{
// PDO uses strings for exception codes, PHP forces numeric codes, so "force" the string code to be used
parent::__construct($message, 0, $previous);
$this->code = $code;
$this->query = $query;
}
/**
* Get the SQL statement that was executed
*
* @return string
*
* @since 1.5.0
*/
public function getQuery()
{
return $this->query;
}
}

View File

@ -0,0 +1,35 @@
<?php
/**
* Part of the Joomla Framework Database Package
*
* @copyright Copyright (C) 2005 - 2021 Open Source Matters, Inc. All rights reserved.
* @license GNU General Public License version 2 or later; see LICENSE
*/
namespace Joomla\Database\Exception;
/**
* Exception class defining an error preparing the SQL statement for execution
*
* @since 2.0.0
*/
class PrepareStatementFailureException extends \RuntimeException
{
/**
* Construct the exception
*
* @param string $message The Exception message to throw. [optional]
* @param integer $code The Exception code. [optional]
* @param ?\Exception $previous The previous exception used for the exception chaining. [optional]
*
* @since 2.0.0
*/
public function __construct($message = '', $code = 0, \Exception $previous = null)
{
// PDO uses strings for exception codes, PHP forces numeric codes, so "force" the string code to be used
parent::__construct($message, 0, $previous);
$this->code = $code;
}
}

View File

@ -0,0 +1,19 @@
<?php
/**
* Part of the Joomla Framework Database Package
*
* @copyright Copyright (C) 2005 - 2021 Open Source Matters, Inc. All rights reserved.
* @license GNU General Public License version 2 or later; see LICENSE
*/
namespace Joomla\Database\Exception;
/**
* Exception class defining an exception when attempting to change a query type
*
* @since 2.0.0
*/
class QueryTypeAlreadyDefinedException extends \RuntimeException
{
}

View File

@ -0,0 +1,19 @@
<?php
/**
* Part of the Joomla Framework Database Package
*
* @copyright Copyright (C) 2005 - 2021 Open Source Matters, Inc. All rights reserved.
* @license GNU General Public License version 2 or later; see LICENSE
*/
namespace Joomla\Database\Exception;
/**
* Class representing an unknown type for a given database driver.
*
* @since 2.0.0
*/
class UnknownTypeException extends \InvalidArgumentException
{
}

View File

@ -0,0 +1,19 @@
<?php
/**
* Part of the Joomla Framework Database Package
*
* @copyright Copyright (C) 2005 - 2021 Open Source Matters, Inc. All rights reserved.
* @license GNU General Public License version 2 or later; see LICENSE
*/
namespace Joomla\Database\Exception;
/**
* Exception class defining an unsupported database object
*
* @since 1.5.0
*/
class UnsupportedAdapterException extends \RuntimeException
{
}

View File

@ -0,0 +1,88 @@
<?php
/**
* Part of the Joomla Framework Database Package
*
* @copyright Copyright (C) 2005 - 2021 Open Source Matters, Inc. All rights reserved.
* @license GNU General Public License version 2 or later; see LICENSE
*/
namespace Joomla\Database;
/**
* Class defining the fetch mode for prepared statements
*
* The values of the constants in this class match the `PDO::FETCH_*` constants.
*
* @since 2.0.0
*/
final class FetchMode
{
/**
* Specifies that the fetch method shall return each row as an array indexed by column name as returned in the corresponding result set.
*
* If the result set contains multiple columns with the same name, the statement returns only a single value per column name.
*
* @var integer
* @since 2.0.0
* @see \PDO::FETCH_ASSOC
*/
public const ASSOCIATIVE = 2;
/**
* Specifies that the fetch method shall return each row as an array indexed by column number as returned in the corresponding result set,
* starting at column 0.
*
* @var integer
* @since 2.0.0
* @see \PDO::FETCH_NUM
*/
public const NUMERIC = 3;
/**
* Specifies that the fetch method shall return each row as an array indexed by both column name and number as returned in the corresponding
* result set, starting at column 0.
*
* @var integer
* @since 2.0.0
* @see \PDO::FETCH_BOTH
*/
public const MIXED = 4;
/**
* Specifies that the fetch method shall return each row as an object with property names that correspond to the column names returned in the
* result set.
*
* @var integer
* @since 2.0.0
* @see \PDO::FETCH_OBJ
*/
public const STANDARD_OBJECT = 5;
/**
* Specifies that the fetch method shall return only a single requested column from the next row in the result set.
*
* @var integer
* @since 2.0.0
* @see \PDO::FETCH_COLUMN
*/
public const COLUMN = 7;
/**
* Specifies that the fetch method shall return a new instance of the requested class, mapping the columns to named properties in the class.
*
* @var integer
* @since 2.0.0
* @see \PDO::FETCH_CLASS
*/
public const CUSTOM_OBJECT = 8;
/**
* Private constructor to prevent instantiation of this class
*
* @since 2.0.0
*/
private function __construct()
{
}
}

View File

@ -0,0 +1,77 @@
<?php
/**
* Part of the Joomla Framework Database Package
*
* @copyright Copyright (C) 2005 - 2021 Open Source Matters, Inc. All rights reserved.
* @license GNU General Public License version 2 or later; see LICENSE
*/
namespace Joomla\Database;
/**
* Class defining the fetch orientation for prepared statements
*
* The values of the constants in this class match the `PDO::FETCH_ORI_*` constants.
*
* @since 2.0.0
*/
final class FetchOrientation
{
/**
* Fetch the next row in the result set. Valid only for scrollable cursors.
*
* @var integer
* @since 2.0.0
*/
public const NEXT = 0;
/**
* Fetch the previous row in the result set. Valid only for scrollable cursors.
*
* @var integer
* @since 2.0.0
*/
public const PRIOR = 1;
/**
* Fetch the first row in the result set. Valid only for scrollable cursors.
*
* @var integer
* @since 2.0.0
*/
public const FIRST = 2;
/**
* Fetch the last row in the result set. Valid only for scrollable cursors.
*
* @var integer
* @since 2.0.0
*/
public const LAST = 3;
/**
* Fetch the requested row by row number from the result set. Valid only for scrollable cursors.
*
* @var integer
* @since 2.0.0
*/
public const ABS = 4;
/**
* Fetch the requested row by relative position from the current position of the cursor in the result set. Valid only for scrollable cursors.
*
* @var integer
* @since 2.0.0
*/
public const REL = 5;
/**
* Private constructor to prevent instantiation of this class
*
* @since 2.0.0
*/
private function __construct()
{
}
}

View File

@ -0,0 +1,74 @@
<?php
/**
* Part of the Joomla Framework Database Package
*
* @copyright Copyright (C) 2005 - 2021 Open Source Matters, Inc. All rights reserved.
* @license GNU General Public License version 2 or later; see LICENSE
*/
namespace Joomla\Database\Monitor;
use Joomla\Database\QueryMonitorInterface;
/**
* Chained query monitor allowing multiple monitors to be executed.
*
* @since 2.0.0
*/
class ChainedMonitor implements QueryMonitorInterface
{
/**
* The query monitors stored to this chain
*
* @var QueryMonitorInterface[]
* @since 2.0.0
*/
private $monitors = [];
/**
* Register a monitor to the chain.
*
* @param QueryMonitorInterface $monitor The monitor to add.
*
* @return void
*
* @since 2.0.0
*/
public function addMonitor(QueryMonitorInterface $monitor): void
{
$this->monitors[] = $monitor;
}
/**
* Act on a query being started.
*
* @param string $sql The SQL to be executed.
* @param object[]|null $boundParams List of bound params, used with the query.
* Each item is an object that holds: value, dataType
*
* @return void
*
* @since 2.0.0
*/
public function startQuery(string $sql, ?array $boundParams = null): void
{
foreach ($this->monitors as $monitor) {
$monitor->startQuery($sql, $boundParams);
}
}
/**
* Act on a query being stopped.
*
* @return void
*
* @since 2.0.0
*/
public function stopQuery(): void
{
foreach ($this->monitors as $monitor) {
$monitor->stopQuery();
}
}
}

View File

@ -0,0 +1,156 @@
<?php
/**
* Part of the Joomla Framework Database Package
*
* @copyright Copyright (C) 2005 - 2021 Open Source Matters, Inc. All rights reserved.
* @license GNU General Public License version 2 or later; see LICENSE
*/
namespace Joomla\Database\Monitor;
use Joomla\Database\QueryMonitorInterface;
/**
* Query monitor handling logging of queries.
*
* @since 2.0.0
*/
final class DebugMonitor implements QueryMonitorInterface
{
/**
* The log of executed SQL statements call stacks by the database driver.
*
* @var array
* @since 2.0.0
*/
private $callStacks = [];
/**
* The log of executed SQL statements by the database driver.
*
* @var array
* @since 2.0.0
*/
private $logs = [];
/**
* List of bound params, used with the query.
*
* @var array
* @since 2.0.0
*/
private $boundParams = [];
/**
* The log of executed SQL statements memory usage (start and stop memory_get_usage) by the database driver.
*
* @var array
* @since 2.0.0
*/
private $memoryLogs = [];
/**
* The log of executed SQL statements timings (start and stop microtimes) by the database driver.
*
* @var array
* @since 2.0.0
*/
private $timings = [];
/**
* Act on a query being started.
*
* @param string $sql The SQL to be executed.
* @param object[]|null $boundParams List of bound params, used with the query.
* Each item is an object that holds: value, dataType
*
* @return void
*
* @since 2.0.0
*/
public function startQuery(string $sql, ?array $boundParams = null): void
{
$this->logs[] = $sql;
// Dereference bound parameters to prevent reporting wrong value when reusing the same query object.
$this->boundParams[] = unserialize(serialize($boundParams));
$this->callStacks[] = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
$this->memoryLogs[] = memory_get_usage();
$this->timings[] = microtime(true);
}
/**
* Act on a query being stopped.
*
* @return void
*
* @since 2.0.0
*/
public function stopQuery(): void
{
$this->timings[] = microtime(true);
$this->memoryLogs[] = memory_get_usage();
}
/**
* Get the logged call stacks.
*
* @return array
*
* @since 2.0.0
*/
public function getCallStacks(): array
{
return $this->callStacks;
}
/**
* Get the logged queries.
*
* @return array
*
* @since 2.0.0
*/
public function getLogs(): array
{
return $this->logs;
}
/**
* Get the logged bound params.
*
* @return array
*
* @since 2.0.0
*/
public function getBoundParams(): array
{
return $this->boundParams;
}
/**
* Get the logged memory logs.
*
* @return array
*
* @since 2.0.0
*/
public function getMemoryLogs(): array
{
return $this->memoryLogs;
}
/**
* Get the logged timings.
*
* @return array
*
* @since 2.0.0
*/
public function getTimings(): array
{
return $this->timings;
}
}

View File

@ -0,0 +1,57 @@
<?php
/**
* Part of the Joomla Framework Database Package
*
* @copyright Copyright (C) 2005 - 2021 Open Source Matters, Inc. All rights reserved.
* @license GNU General Public License version 2 or later; see LICENSE
*/
namespace Joomla\Database\Monitor;
use Joomla\Database\QueryMonitorInterface;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerAwareTrait;
/**
* Query monitor handling logging of queries.
*
* @since 2.0.0
*/
class LoggingMonitor implements QueryMonitorInterface, LoggerAwareInterface
{
use LoggerAwareTrait;
/**
* Act on a query being started.
*
* @param string $sql The SQL to be executed.
* @param object[]|null $boundParams List of bound params, used with the query.
* Each item is an object that holds: value, dataType
* @return void
*
* @since 2.0.0
*/
public function startQuery(string $sql, ?array $boundParams = null): void
{
if ($this->logger) {
// Add the query to the object queue.
$this->logger->info(
'Query Executed: {sql}',
['sql' => $sql, 'trace' => debug_backtrace()]
);
}
}
/**
* Act on a query being stopped.
*
* @return void
*
* @since 2.0.0
*/
public function stopQuery(): void
{
// Nothing to do
}
}

View File

@ -0,0 +1,807 @@
<?php
/**
* Part of the Joomla Framework Database Package
*
* @copyright Copyright (C) 2005 - 2021 Open Source Matters, Inc. All rights reserved.
* @license GNU General Public License version 2 or later; see LICENSE
*/
namespace Joomla\Database\Mysql;
use Joomla\Database\Exception\ConnectionFailureException;
use Joomla\Database\Pdo\PdoDriver;
use Joomla\Database\UTF8MB4SupportInterface;
/**
* MySQL database driver supporting PDO based connections
*
* @link https://www.php.net/manual/en/ref.pdo-mysql.php
* @since 1.0
*/
class MysqlDriver extends PdoDriver implements UTF8MB4SupportInterface
{
/**
* The name of the database driver.
*
* @var string
* @since 1.0
*/
public $name = 'mysql';
/**
* The character(s) used to quote SQL statement names such as table names or field names, etc.
*
* If a single character string the same character is used for both sides of the quoted name, else the first character will be used for the
* opening quote and the second for the closing quote.
*
* @var string
* @since 1.0
*/
protected $nameQuote = '`';
/**
* The null or zero representation of a timestamp for the database driver.
*
* @var string
* @since 1.0
*/
protected $nullDate = '0000-00-00 00:00:00';
/**
* True if the database engine supports UTF-8 Multibyte (utf8mb4) character encoding.
*
* @var boolean
* @since 1.4.0
*/
protected $utf8mb4 = false;
/**
* True if the database engine is MariaDB.
*
* @var boolean
* @since 2.0.0
*/
protected $mariadb = false;
/**
* The minimum supported database version.
*
* @var string
* @since 1.0
*/
protected static $dbMinimum = '5.6';
/**
* The minimum supported MariaDB database version.
*
* @var string
* @since 2.0.0
*/
protected static $dbMinMariadb = '10.0';
/**
* The default cipher suite for TLS connections.
*
* @var array
* @since 2.0.0
*/
protected static $defaultCipherSuite = [
'AES128-GCM-SHA256',
'AES256-GCM-SHA384',
'AES128-CBC-SHA256',
'AES256-CBC-SHA384',
'DES-CBC3-SHA',
];
/**
* The default charset.
*
* @var string
* @since 2.0.0
*/
public $charset = 'utf8';
/**
* Constructor.
*
* @param array $options Array of database options with keys: host, user, password, database, select.
*
* @since 1.0
*/
public function __construct(array $options)
{
/**
* sql_mode to MySql 5.7.8+ default strict mode minus ONLY_FULL_GROUP_BY
*
* @link https://dev.mysql.com/doc/relnotes/mysql/5.7/en/news-5-7-8.html#mysqld-5-7-8-sql-mode
*/
$sqlModes = [
'STRICT_TRANS_TABLES',
'ERROR_FOR_DIVISION_BY_ZERO',
'NO_ENGINE_SUBSTITUTION',
];
// Get some basic values from the options.
$options['driver'] = 'mysql';
$options['charset'] = $options['charset'] ?? 'utf8';
$options['sqlModes'] = isset($options['sqlModes']) ? (array) $options['sqlModes'] : $sqlModes;
$this->charset = $options['charset'];
/*
* Pre-populate the UTF-8 Multibyte compatibility flag. Unfortunately PDO won't report the server version unless we're connected to it,
* and we cannot connect to it unless we know if it supports utf8mb4, which requires us knowing the server version. Because of this
* chicken and egg issue, we _assume_ it's supported and we'll just catch any problems at connection time.
*/
$this->utf8mb4 = $options['charset'] === 'utf8mb4';
// Finalize initialisation.
parent::__construct($options);
}
/**
* Connects to the database if needed.
*
* @return void Returns void if the database connected successfully.
*
* @since 1.0
* @throws \RuntimeException
*/
public function connect()
{
if ($this->getConnection()) {
return;
}
// For SSL/TLS connection encryption.
if ($this->options['ssl'] !== [] && $this->options['ssl']['enable'] === true) {
$sslContextIsNull = true;
// If customised, add cipher suite, ca file path, ca path, private key file path and certificate file path to PDO driver options.
foreach (['cipher', 'ca', 'capath', 'key', 'cert'] as $key => $value) {
if ($this->options['ssl'][$value] !== null) {
$this->options['driverOptions'][constant('\PDO::MYSQL_ATTR_SSL_' . strtoupper($value))] = $this->options['ssl'][$value];
$sslContextIsNull = false;
}
}
// PDO, if no cipher, ca, capath, cert and key are set, can't start TLS one-way connection, set a common ciphers suite to force it.
if ($sslContextIsNull === true) {
$this->options['driverOptions'][\PDO::MYSQL_ATTR_SSL_CIPHER] = implode(':', static::$defaultCipherSuite);
}
// If customised, for capable systems (PHP 7.0.14+ and 7.1.4+) verify certificate chain and Common Name to driver options.
if ($this->options['ssl']['verify_server_cert'] !== null && defined('\PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT')) {
$this->options['driverOptions'][\PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT] = $this->options['ssl']['verify_server_cert'];
}
}
try {
// Try to connect to MySQL
parent::connect();
} catch (ConnectionFailureException $e) {
// If the connection failed, but not because of the wrong character set, then bubble up the exception.
if (!$this->utf8mb4) {
throw $e;
}
/*
* Otherwise, try connecting again without using utf8mb4 and see if maybe that was the problem. If the connection succeeds, then we
* will have learned that the client end of the connection does not support utf8mb4.
*/
$this->utf8mb4 = false;
$this->options['charset'] = 'utf8';
parent::connect();
}
$serverVersion = $this->getVersion();
$this->mariadb = stripos($serverVersion, 'mariadb') !== false;
if ($this->utf8mb4) {
// At this point we know the client supports utf8mb4. Now we must check if the server supports utf8mb4 as well.
$this->utf8mb4 = version_compare($serverVersion, '5.5.3', '>=');
if ($this->mariadb && version_compare($serverVersion, '10.0.0', '<')) {
$this->utf8mb4 = false;
}
if (!$this->utf8mb4) {
// Reconnect with the utf8 character set.
parent::disconnect();
$this->options['charset'] = 'utf8';
parent::connect();
}
}
// If needed, set the sql modes.
if ($this->options['sqlModes'] !== []) {
$this->connection->query('SET @@SESSION.sql_mode = \'' . implode(',', $this->options['sqlModes']) . '\';');
}
$this->setOption(\PDO::ATTR_EMULATE_PREPARES, true);
}
/**
* Automatically downgrade a CREATE TABLE or ALTER TABLE query from utf8mb4 (UTF-8 Multibyte) to plain utf8.
*
* Used when the server doesn't support UTF-8 Multibyte.
*
* @param string $query The query to convert
*
* @return string The converted query
*
* @since 1.4.0
*/
public function convertUtf8mb4QueryToUtf8($query)
{
if ($this->hasUTF8mb4Support()) {
return $query;
}
// If it's not an ALTER TABLE or CREATE TABLE command there's nothing to convert
$beginningOfQuery = substr($query, 0, 12);
$beginningOfQuery = strtoupper($beginningOfQuery);
if (!\in_array($beginningOfQuery, ['ALTER TABLE ', 'CREATE TABLE'], true)) {
return $query;
}
// Replace utf8mb4 with utf8
return str_replace('utf8mb4', 'utf8', $query);
}
/**
* Test to see if the MySQL connector is available.
*
* @return boolean True on success, false otherwise.
*
* @since 1.0
*/
public static function isSupported()
{
return class_exists('\\PDO') && \in_array('mysql', \PDO::getAvailableDrivers(), true);
}
/**
* Select a database for use.
*
* @param string $database The name of the database to select for use.
*
* @return boolean
*
* @since 1.0
* @throws \RuntimeException
*/
public function select($database)
{
$this->connect();
$this->setQuery('USE ' . $this->quoteName($database))
->execute();
return true;
}
/**
* Return the query string to alter the database character set.
*
* @param string $dbName The database name
*
* @return string The query that alter the database query string
*
* @since 2.0.0
*/
public function getAlterDbCharacterSet($dbName)
{
$charset = $this->utf8mb4 ? 'utf8mb4' : 'utf8';
return 'ALTER DATABASE ' . $this->quoteName($dbName) . ' CHARACTER SET `' . $charset . '`';
}
/**
* Method to get the database collation in use by sampling a text field of a table in the database.
*
* @return string|boolean The collation in use by the database (string) or boolean false if not supported.
*
* @since 1.0
* @throws \RuntimeException
*/
public function getCollation()
{
$this->connect();
return $this->setQuery('SELECT @@collation_database;')->loadResult();
}
/**
* Method to get the database connection collation in use by sampling a text field of a table in the database.
*
* @return string|boolean The collation in use by the database connection (string) or boolean false if not supported.
*
* @since 1.6.0
* @throws \RuntimeException
*/
public function getConnectionCollation()
{
$this->connect();
return $this->setQuery('SELECT @@collation_connection;')->loadResult();
}
/**
* Method to get the database encryption details (cipher and protocol) in use.
*
* @return string The database encryption details.
*
* @since 2.0.0
* @throws \RuntimeException
*/
public function getConnectionEncryption(): string
{
$this->connect();
$variables = $this->setQuery('SHOW SESSION STATUS WHERE `Variable_name` IN (\'Ssl_version\', \'Ssl_cipher\')')
->loadObjectList('Variable_name');
if (!empty($variables['Ssl_cipher']->Value)) {
return $variables['Ssl_version']->Value . ' (' . $variables['Ssl_cipher']->Value . ')';
}
return '';
}
/**
* Method to test if the database TLS connections encryption are supported.
*
* @return boolean Whether the database supports TLS connections encryption.
*
* @since 2.0.0
*/
public function isConnectionEncryptionSupported(): bool
{
$this->connect();
$variables = $this->setQuery('SHOW SESSION VARIABLES WHERE `Variable_name` IN (\'have_ssl\')')->loadObjectList('Variable_name');
return !empty($variables['have_ssl']->Value) && $variables['have_ssl']->Value === 'YES';
}
/**
* Return the query string to create new Database.
*
* @param \stdClass $options Object used to pass user and database name to database driver. This object must have "db_name" and "db_user" set.
* @param boolean $utf True if the database supports the UTF-8 character set.
*
* @return string The query that creates database
*
* @since 2.0.0
*/
protected function getCreateDatabaseQuery($options, $utf)
{
if ($utf) {
$charset = $this->utf8mb4 ? 'utf8mb4' : 'utf8';
$collation = $charset . '_unicode_ci';
return 'CREATE DATABASE ' . $this->quoteName($options->db_name) . ' CHARACTER SET `' . $charset . '` COLLATE `' . $collation . '`';
}
return 'CREATE DATABASE ' . $this->quoteName($options->db_name);
}
/**
* Shows the table CREATE statement that creates the given tables.
*
* @param array|string $tables A table name or a list of table names.
*
* @return array A list of the create SQL for the tables.
*
* @since 1.0
* @throws \RuntimeException
*/
public function getTableCreate($tables)
{
$this->connect();
// Initialise variables.
$result = [];
// Sanitize input to an array and iterate over the list.
$tables = (array) $tables;
foreach ($tables as $table) {
$row = $this->setQuery('SHOW CREATE TABLE ' . $this->quoteName($table))->loadRow();
// Populate the result array based on the create statements.
$result[$table] = $row[1];
}
return $result;
}
/**
* Retrieves field information about a given table.
*
* @param string $table The name of the database table.
* @param boolean $typeOnly True to only return field types.
*
* @return array An array of fields for the database table.
*
* @since 1.0
* @throws \RuntimeException
*/
public function getTableColumns($table, $typeOnly = true)
{
$this->connect();
$result = [];
// Set the query to get the table fields statement.
$fields = $this->setQuery('SHOW FULL COLUMNS FROM ' . $this->quoteName($table))->loadObjectList();
// If we only want the type as the value add just that to the list.
if ($typeOnly) {
foreach ($fields as $field) {
$result[$field->Field] = preg_replace('/[(0-9)]/', '', $field->Type);
}
} else {
// If we want the whole field data object add that to the list.
foreach ($fields as $field) {
$result[$field->Field] = $field;
}
}
return $result;
}
/**
* Get the details list of keys for a table.
*
* @param string $table The name of the table.
*
* @return array An array of the column specification for the table.
*
* @since 1.0
* @throws \RuntimeException
*/
public function getTableKeys($table)
{
$this->connect();
// Get the details columns information.
return $this->setQuery('SHOW KEYS FROM ' . $this->quoteName($table))->loadObjectList();
}
/**
* Method to get an array of all tables in the database.
*
* @return array An array of all the tables in the database.
*
* @since 1.0
* @throws \RuntimeException
*/
public function getTableList()
{
$this->connect();
// Set the query to get the tables statement.
return $this->setQuery('SHOW TABLES')->loadColumn();
}
/**
* Get the version of the database connector.
*
* @return string The database connector version.
*
* @since 2.0.0
*/
public function getVersion()
{
$this->connect();
$version = $this->getOption(\PDO::ATTR_SERVER_VERSION);
if (stripos($version, 'mariadb') !== false) {
// MariaDB: Strip off any leading '5.5.5-', if present
return preg_replace('/^5\.5\.5-/', '', $version);
}
return $version;
}
/**
* Get the minimum supported database version.
*
* @return string
*
* @since 2.0.0
*/
public function getMinimum()
{
return $this->mariadb ? static::$dbMinMariadb : static::$dbMinimum;
}
/**
* Get the null or zero representation of a timestamp for the database driver.
*
* @return string
*
* @since 2.0.0
*/
public function getNullDate()
{
// Check the session sql mode;
if (\in_array('NO_ZERO_DATE', $this->options['sqlModes']) !== false) {
$this->nullDate = '1000-01-01 00:00:00';
}
return $this->nullDate;
}
/**
* Determine whether the database engine support the UTF-8 Multibyte (utf8mb4) character encoding.
*
* @return boolean True if the database engine supports UTF-8 Multibyte.
*
* @since 2.0.0
*/
public function hasUTF8mb4Support()
{
return $this->utf8mb4;
}
/**
* Determine if the database engine is MariaDB.
*
* @return boolean
*
* @since 2.0.0
*/
public function isMariaDb(): bool
{
$this->connect();
return $this->mariadb;
}
/**
* Locks a table in the database.
*
* @param string $table The name of the table to unlock.
*
* @return $this
*
* @since 1.0
* @throws \RuntimeException
*/
public function lockTable($table)
{
$this->setQuery('LOCK TABLES ' . $this->quoteName($table) . ' WRITE')
->execute();
return $this;
}
/**
* Renames a table in the database.
*
* @param string $oldTable The name of the table to be renamed
* @param string $newTable The new name for the table.
* @param string $backup Not used by MySQL.
* @param string $prefix Not used by MySQL.
*
* @return $this
*
* @since 1.0
* @throws \RuntimeException
*/
public function renameTable($oldTable, $newTable, $backup = null, $prefix = null)
{
$this->setQuery('RENAME TABLE ' . $this->quoteName($oldTable) . ' TO ' . $this->quoteName($newTable))
->execute();
return $this;
}
/**
* Inserts a row into a table based on an object's properties.
*
* @param string $table The name of the database table to insert into.
* @param object $object A reference to an object whose public properties match the table fields.
* @param string $key The name of the primary key. If provided the object property is updated.
*
* @return boolean
*
* @since 2.0.0
* @throws \RuntimeException
*/
public function insertObject($table, &$object, $key = null)
{
$fields = [];
$values = [];
$tableColumns = $this->getTableColumns($table);
// Iterate over the object variables to build the query fields and values.
foreach (get_object_vars($object) as $k => $v) {
// Skip columns that don't exist in the table.
if (!array_key_exists($k, $tableColumns)) {
continue;
}
// Only process non-null scalars.
if (\is_array($v) || \is_object($v) || $v === null) {
continue;
}
// Ignore any internal fields.
if ($k[0] === '_') {
continue;
}
// Ignore null datetime fields.
if ($tableColumns[$k] === 'datetime' && empty($v)) {
continue;
}
// Ignore null integer fields.
if (stristr($tableColumns[$k], 'int') !== false && $v === '') {
continue;
}
// Prepare and sanitize the fields and values for the database query.
$fields[] = $this->quoteName($k);
$values[] = $this->quote($v);
}
// Create the base insert statement.
$query = $this->createQuery()
->insert($this->quoteName($table))
->columns($fields)
->values(implode(',', $values));
// Set the query and execute the insert.
$this->setQuery($query)->execute();
// Update the primary key if it exists.
$id = $this->insertid();
if ($key && $id && \is_string($key)) {
$object->$key = $id;
}
return true;
}
/**
* Method to escape a string for usage in an SQL statement.
*
* Oracle escaping reference:
* http://www.orafaq.com/wiki/SQL_FAQ#How_does_one_escape_special_characters_when_writing_SQL_queries.3F
*
* SQLite escaping notes:
* http://www.sqlite.org/faq.html#q14
*
* Method body is as implemented by the Zend Framework
*
* Note: Using query objects with bound variables is preferable to the below.
*
* @param string $text The string to be escaped.
* @param boolean $extra Unused optional parameter to provide extra escaping.
*
* @return string The escaped string.
*
* @since 1.0
*/
public function escape($text, $extra = false)
{
if (\is_int($text)) {
return $text;
}
if (\is_float($text)) {
// Force the dot as a decimal point.
return str_replace(',', '.', (string) $text);
}
$this->connect();
$result = substr($this->connection->quote($text), 1, -1);
if ($extra) {
$result = addcslashes($result, '%_');
}
return $result;
}
/**
* Unlocks tables in the database.
*
* @return $this
*
* @since 1.0
* @throws \RuntimeException
*/
public function unlockTables()
{
$this->setQuery('UNLOCK TABLES')
->execute();
return $this;
}
/**
* Method to commit a transaction.
*
* @param boolean $toSavepoint If true, commit to the last savepoint.
*
* @return void
*
* @since 1.0
* @throws \RuntimeException
*/
public function transactionCommit($toSavepoint = false)
{
$this->connect();
if (!$toSavepoint || $this->transactionDepth <= 1) {
parent::transactionCommit($toSavepoint);
} else {
$this->transactionDepth--;
}
}
/**
* Method to roll back a transaction.
*
* @param boolean $toSavepoint If true, rollback to the last savepoint.
*
* @return void
*
* @since 1.0
* @throws \RuntimeException
*/
public function transactionRollback($toSavepoint = false)
{
$this->connect();
if (!$toSavepoint || $this->transactionDepth <= 1) {
parent::transactionRollback($toSavepoint);
} else {
$savepoint = 'SP_' . ($this->transactionDepth - 1);
$this->setQuery('ROLLBACK TO SAVEPOINT ' . $this->quoteName($savepoint));
if ($this->execute()) {
$this->transactionDepth--;
}
}
}
/**
* Method to initialize a transaction.
*
* @param boolean $asSavepoint If true and a transaction is already active, a savepoint will be created.
*
* @return void
*
* @since 1.0
* @throws \RuntimeException
*/
public function transactionStart($asSavepoint = false)
{
$this->connect();
if (!$asSavepoint || !$this->transactionDepth) {
parent::transactionStart($asSavepoint);
} else {
$savepoint = 'SP_' . $this->transactionDepth;
$this->setQuery('SAVEPOINT ' . $this->quoteName($savepoint));
if ($this->execute()) {
$this->transactionDepth++;
}
}
}
}

View File

@ -0,0 +1,116 @@
<?php
/**
* Part of the Joomla Framework Database Package
*
* @copyright Copyright (C) 2005 - 2021 Open Source Matters, Inc. All rights reserved.
* @license GNU General Public License version 2 or later; see LICENSE
*/
namespace Joomla\Database\Mysql;
use Joomla\Database\DatabaseExporter;
/**
* MySQL Database Exporter.
*
* @since 1.0
*/
class MysqlExporter extends DatabaseExporter
{
/**
* Builds the XML data for the tables to export.
*
* @return string An XML string
*
* @since 1.0
* @throws \Exception if an error occurs.
*/
protected function buildXml()
{
$buffer = [];
$buffer[] = '<?xml version="1.0"?>';
$buffer[] = '<mysqldump xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">';
$buffer[] = ' <database name="">';
if ($this->options->withStructure) {
$buffer = array_merge($buffer, $this->buildXmlStructure());
}
if ($this->options->withData) {
$buffer = array_merge($buffer, $this->buildXmlData());
}
$buffer[] = ' </database>';
$buffer[] = '</mysqldump>';
return implode("\n", $buffer);
}
/**
* Builds the XML structure to export.
*
* @return array An array of XML lines (strings).
*
* @since 1.0
* @throws \Exception if an error occurs.
*/
protected function buildXmlStructure()
{
$buffer = [];
foreach ($this->from as $table) {
// Replace the magic prefix if found.
$table = $this->getGenericTableName($table);
// Get the details columns information.
$fields = $this->db->getTableColumns($table, false);
$keys = $this->db->getTableKeys($table);
$buffer[] = ' <table_structure name="' . $table . '">';
foreach ($fields as $field) {
$buffer[] = ' <field Field="' . $field->Field . '" Type="' . $field->Type . '" Null="' . $field->Null . '" Key="' .
$field->Key . '"' . (isset($field->Default) ? ' Default="' . $field->Default . '"' : '') . ' Extra="' . $field->Extra . '"' .
' />';
}
foreach ($keys as $key) {
$buffer[] = ' <key Table="' . $table . '" Non_unique="' . $key->Non_unique . '" Key_name="' . $key->Key_name . '"' .
' Seq_in_index="' . $key->Seq_in_index . '" Column_name="' . $key->Column_name . '" Collation="' . $key->Collation . '"' .
' Null="' . $key->Null . '" Index_type="' . $key->Index_type . '"' .
' Sub_part="' . $key->Sub_part . '"' .
' Comment="' . htmlspecialchars($key->Comment, \ENT_COMPAT, 'UTF-8') . '"' .
' />';
}
$buffer[] = ' </table_structure>';
}
return $buffer;
}
/**
* Checks if all data and options are in order prior to exporting.
*
* @return $this
*
* @since 1.0
* @throws \RuntimeException
*/
public function check()
{
// Check if the db connector has been set.
if (!($this->db instanceof MysqlDriver)) {
throw new \RuntimeException('Database connection wrong type.');
}
// Check if the tables have been specified.
if (empty($this->from)) {
throw new \RuntimeException('ERROR: No Tables Specified');
}
return $this;
}
}

View File

@ -0,0 +1,398 @@
<?php
/**
* Part of the Joomla Framework Database Package
*
* @copyright Copyright (C) 2005 - 2021 Open Source Matters, Inc. All rights reserved.
* @license GNU General Public License version 2 or later; see LICENSE
*/
namespace Joomla\Database\Mysql;
use Joomla\Database\DatabaseImporter;
/**
* MySQL Database Importer.
*
* @since 1.0
*/
class MysqlImporter extends DatabaseImporter
{
/**
* Checks if all data and options are in order prior to exporting.
*
* @return $this
*
* @since 1.0
* @throws \RuntimeException
*/
public function check()
{
// Check if the db connector has been set.
if (!($this->db instanceof MysqlDriver)) {
throw new \RuntimeException('Database connection wrong type.');
}
// Check if the tables have been specified.
if (empty($this->from)) {
throw new \RuntimeException('ERROR: No Tables Specified');
}
return $this;
}
/**
* Get the SQL syntax to add a key.
*
* @param string $table The table name.
* @param array $keys An array of the fields pertaining to this key.
*
* @return string
*
* @since 1.0
*/
protected function getAddKeySql($table, $keys)
{
return 'ALTER TABLE ' . $this->db->quoteName($table) . ' ADD ' . $this->getKeySql($keys);
}
/**
* Get alters for table if there is a difference.
*
* @param \SimpleXMLElement $structure The XML structure of the table.
*
* @return array
*
* @since 1.0
*/
protected function getAlterTableSql(\SimpleXMLElement $structure)
{
// Initialise variables.
$table = $this->getRealTableName($structure['name']);
$oldFields = $this->db->getTableColumns($table);
$oldKeys = $this->db->getTableKeys($table);
$alters = [];
// Get the fields and keys from the XML that we are aiming for.
$newFields = $structure->xpath('field');
$newKeys = $structure->xpath('key');
// Loop through each field in the new structure.
foreach ($newFields as $field) {
$fName = (string) $field['Field'];
if (isset($oldFields[$fName])) {
// The field exists, check it's the same.
$column = $oldFields[$fName];
// Test whether there is a change.
$change = ((string) $field['Type'] !== $column->Type) || ((string) $field['Null'] !== $column->Null)
|| ((string) $field['Default'] !== $column->Default) || ((string) $field['Extra'] !== $column->Extra);
if ($change) {
$alters[] = $this->getChangeColumnSql($table, $field);
}
// Unset this field so that what we have left are fields that need to be removed.
unset($oldFields[$fName]);
} else {
// The field is new.
$alters[] = $this->getAddColumnSql($table, $field);
}
}
// Any columns left are orphans
foreach ($oldFields as $name => $column) {
// Delete the column.
$alters[] = $this->getDropColumnSql($table, $name);
}
// Get the lookups for the old and new keys.
$oldLookup = $this->getKeyLookup($oldKeys);
$newLookup = $this->getKeyLookup($newKeys);
// Loop through each key in the new structure.
foreach ($newLookup as $name => $keys) {
// Check if there are keys on this field in the existing table.
if (isset($oldLookup[$name])) {
$same = true;
$newCount = \count($newLookup[$name]);
$oldCount = \count($oldLookup[$name]);
// There is a key on this field in the old and new tables. Are they the same?
if ($newCount === $oldCount) {
// Need to loop through each key and do a fine grained check.
for ($i = 0; $i < $newCount; $i++) {
$same = (((string) $newLookup[$name][$i]['Non_unique'] === $oldLookup[$name][$i]->Non_unique)
&& ((string) $newLookup[$name][$i]['Column_name'] === $oldLookup[$name][$i]->Column_name)
&& ((string) $newLookup[$name][$i]['Seq_in_index'] === $oldLookup[$name][$i]->Seq_in_index)
&& ((string) $newLookup[$name][$i]['Collation'] === $oldLookup[$name][$i]->Collation)
&& ((string) $newLookup[$name][$i]['Sub_part'] === $oldLookup[$name][$i]->Sub_part)
&& ((string) $newLookup[$name][$i]['Index_type'] === $oldLookup[$name][$i]->Index_type));
/*
Debug.
echo '<pre>';
echo '<br>Non_unique: '.
((string) $newLookup[$name][$i]['Non_unique'] == $oldLookup[$name][$i]->Non_unique ? 'Pass' : 'Fail').' '.
(string) $newLookup[$name][$i]['Non_unique'].' vs '.$oldLookup[$name][$i]->Non_unique;
echo '<br>Column_name: '.
((string) $newLookup[$name][$i]['Column_name'] == $oldLookup[$name][$i]->Column_name ? 'Pass' : 'Fail').' '.
(string) $newLookup[$name][$i]['Column_name'].' vs '.$oldLookup[$name][$i]->Column_name;
echo '<br>Seq_in_index: '.
((string) $newLookup[$name][$i]['Seq_in_index'] == $oldLookup[$name][$i]->Seq_in_index ? 'Pass' : 'Fail').' '.
(string) $newLookup[$name][$i]['Seq_in_index'].' vs '.$oldLookup[$name][$i]->Seq_in_index;
echo '<br>Collation: '.
((string) $newLookup[$name][$i]['Collation'] == $oldLookup[$name][$i]->Collation ? 'Pass' : 'Fail').' '.
(string) $newLookup[$name][$i]['Collation'].' vs '.$oldLookup[$name][$i]->Collation;
echo '<br>Sub_part: '.
((string) $newLookup[$name][$i]['Sub_part'] == $oldLookup[$name][$i]->Sub_part ? 'Pass' : 'Fail').' '.
(string) $newLookup[$name][$i]['Sub_part'].' vs '.$oldLookup[$name][$i]->Sub_part;
echo '<br>Index_type: '.
((string) $newLookup[$name][$i]['Index_type'] == $oldLookup[$name][$i]->Index_type ? 'Pass' : 'Fail').' '.
(string) $newLookup[$name][$i]['Index_type'].' vs '.$oldLookup[$name][$i]->Index_type;
echo '<br>Same = '.($same ? 'true' : 'false');
echo '</pre>';
*/
if (!$same) {
// Break out of the loop. No need to check further.
break;
}
}
} else {
// Count is different, just drop and add.
$same = false;
}
if (!$same) {
$alters[] = $this->getDropKeySql($table, $name);
$alters[] = $this->getAddKeySql($table, $keys);
}
// Unset this field so that what we have left are fields that need to be removed.
unset($oldLookup[$name]);
} else {
// This is a new key.
$alters[] = $this->getAddKeySql($table, $keys);
}
}
// Any keys left are orphans.
foreach ($oldLookup as $name => $keys) {
if (strtoupper($name) === 'PRIMARY') {
$alters[] = $this->getDropPrimaryKeySql($table);
} else {
$alters[] = $this->getDropKeySql($table, $name);
}
}
return $alters;
}
/**
* Get the syntax to alter a column.
*
* @param string $table The name of the database table to alter.
* @param \SimpleXMLElement $field The XML definition for the field.
*
* @return string
*
* @since 1.0
*/
protected function getChangeColumnSql($table, \SimpleXMLElement $field)
{
return 'ALTER TABLE ' . $this->db->quoteName($table) . ' CHANGE COLUMN ' . $this->db->quoteName((string) $field['Field']) . ' '
. $this->getColumnSql($field);
}
/**
* Get the SQL syntax for a single column that would be included in a table create or alter statement.
*
* @param \SimpleXMLElement $field The XML field definition.
*
* @return string
*
* @since 1.0
*/
protected function getColumnSql(\SimpleXMLElement $field)
{
// Initialise variables.
// TODO Incorporate into parent class and use $this.
$blobs = ['text', 'smalltext', 'mediumtext', 'largetext'];
$fName = (string) $field['Field'];
$fType = (string) $field['Type'];
$fNull = (string) $field['Null'];
$fDefault = isset($field['Default']) ? (string) $field['Default'] : null;
$fExtra = (string) $field['Extra'];
$sql = $this->db->quoteName($fName) . ' ' . $fType;
if ($fNull === 'NO') {
if ($fDefault === null || \in_array($fType, $blobs, true)) {
$sql .= ' NOT NULL';
} else {
// TODO Don't quote numeric values.
if (stristr($fDefault, 'CURRENT') !== false) {
$sql .= ' NOT NULL DEFAULT CURRENT_TIMESTAMP()';
} else {
$sql .= ' NOT NULL DEFAULT ' . $this->db->quote($fDefault);
}
}
} else {
if ($fDefault === null) {
$sql .= ' DEFAULT NULL';
} else {
// TODO Don't quote numeric values.
$sql .= ' DEFAULT ' . $this->db->quote($fDefault);
}
}
if ($fExtra) {
// MySql 8.0 introduces DEFAULT_GENERATED in the extra column and should be replaced with the default value
if (stristr($fExtra, 'DEFAULT_GENERATED') !== false) {
$sql .= ' ' . strtoupper(str_ireplace('DEFAULT_GENERATED', 'DEFAULT ' . $fDefault, $fExtra));
} else {
$sql .= ' ' . strtoupper($fExtra);
}
}
return $sql;
}
/**
* Get the SQL syntax to drop a key.
*
* @param string $table The table name.
* @param string $name The name of the key to drop.
*
* @return string
*
* @since 1.0
*/
protected function getDropKeySql($table, $name)
{
return 'ALTER TABLE ' . $this->db->quoteName($table) . ' DROP KEY ' . $this->db->quoteName($name);
}
/**
* Get the SQL syntax to drop a key.
*
* @param string $table The table name.
*
* @return string
*
* @since 1.0
*/
protected function getDropPrimaryKeySql($table)
{
return 'ALTER TABLE ' . $this->db->quoteName($table) . ' DROP PRIMARY KEY';
}
/**
* Get the details list of keys for a table.
*
* @param array $keys An array of objects that comprise the keys for the table.
*
* @return array The lookup array. array({key name} => array(object, ...))
*
* @since 1.0
* @throws \Exception
*/
protected function getKeyLookup($keys)
{
// First pass, create a lookup of the keys.
$lookup = [];
foreach ($keys as $key) {
if ($key instanceof \SimpleXMLElement) {
$kName = (string) $key['Key_name'];
} else {
$kName = $key->Key_name;
}
if (empty($lookup[$kName])) {
$lookup[$kName] = [];
}
$lookup[$kName][] = $key;
}
return $lookup;
}
/**
* Get the SQL syntax for a key.
*
* @param array $columns An array of SimpleXMLElement objects comprising the key.
*
* @return string
*
* @since 1.0
*/
protected function getKeySql($columns)
{
$kNonUnique = (string) $columns[0]['Non_unique'];
$kName = (string) $columns[0]['Key_name'];
$prefix = '';
if ($kName === 'PRIMARY') {
$prefix = 'PRIMARY ';
} elseif ($kNonUnique == 0) {
$prefix = 'UNIQUE ';
}
$kColumns = [];
foreach ($columns as $column) {
$kLength = '';
if (!empty($column['Sub_part'])) {
$kLength = '(' . $column['Sub_part'] . ')';
}
$kColumns[] = $this->db->quoteName((string) $column['Column_name']) . $kLength;
}
return $prefix . 'KEY ' . ($kName !== 'PRIMARY' ? $this->db->quoteName($kName) : '') . ' (' . implode(',', $kColumns) . ')';
}
/**
* Get the SQL syntax to add a table.
*
* @param \SimpleXMLElement $table The table information.
*
* @return string
*
* @since 2.0.0
* @throws \RuntimeException
*/
protected function xmlToCreate(\SimpleXMLElement $table)
{
$existingTables = $this->db->getTableList();
$tableName = (string) $table['name'];
if (\in_array($tableName, $existingTables)) {
throw new \RuntimeException('The table you are trying to create already exists');
}
$createTableStatement = 'CREATE TABLE ' . $this->db->quoteName($tableName) . ' (';
foreach ($table->xpath('field') as $field) {
$createTableStatement .= $this->getColumnSql($field) . ', ';
}
$newLookup = $this->getKeyLookup($table->xpath('key'));
foreach ($newLookup as $key) {
$createTableStatement .= $this->getKeySql($key) . ', ';
}
$createTableStatement = rtrim($createTableStatement, ', ');
$createTableStatement .= ')';
return $createTableStatement;
}
}

View File

@ -0,0 +1,31 @@
<?php
/**
* Part of the Joomla Framework Database Package
*
* @copyright Copyright (C) 2005 - 2021 Open Source Matters, Inc. All rights reserved.
* @license GNU General Public License version 2 or later; see LICENSE
*/
namespace Joomla\Database\Mysql;
use Joomla\Database\Pdo\PdoQuery;
use Joomla\Database\Query\MysqlQueryBuilder;
/**
* MySQL Query Building Class.
*
* @since 1.0
*/
class MysqlQuery extends PdoQuery
{
use MysqlQueryBuilder;
/**
* The list of zero or null representation of a datetime.
*
* @var array
* @since 2.0.0
*/
protected $nullDatetimeList = ['0000-00-00 00:00:00', '1000-01-01 00:00:00'];
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,116 @@
<?php
/**
* Part of the Joomla Framework Database Package
*
* @copyright Copyright (C) 2005 - 2021 Open Source Matters, Inc. All rights reserved.
* @license GNU General Public License version 2 or later; see LICENSE
*/
namespace Joomla\Database\Mysqli;
use Joomla\Database\DatabaseExporter;
/**
* MySQLi Database Exporter.
*
* @since 1.0
*/
class MysqliExporter extends DatabaseExporter
{
/**
* Builds the XML data for the tables to export.
*
* @return string An XML string
*
* @since 1.0
* @throws \Exception if an error occurs.
*/
protected function buildXml()
{
$buffer = [];
$buffer[] = '<?xml version="1.0"?>';
$buffer[] = '<mysqldump xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">';
$buffer[] = ' <database name="">';
if ($this->options->withStructure) {
$buffer = array_merge($buffer, $this->buildXmlStructure());
}
if ($this->options->withData) {
$buffer = array_merge($buffer, $this->buildXmlData());
}
$buffer[] = ' </database>';
$buffer[] = '</mysqldump>';
return implode("\n", $buffer);
}
/**
* Builds the XML structure to export.
*
* @return array An array of XML lines (strings).
*
* @since 1.0
* @throws \Exception if an error occurs.
*/
protected function buildXmlStructure()
{
$buffer = [];
foreach ($this->from as $table) {
// Replace the magic prefix if found.
$table = $this->getGenericTableName($table);
// Get the details columns information.
$fields = $this->db->getTableColumns($table, false);
$keys = $this->db->getTableKeys($table);
$buffer[] = ' <table_structure name="' . $table . '">';
foreach ($fields as $field) {
$buffer[] = ' <field Field="' . $field->Field . '" Type="' . $field->Type . '" Null="' . $field->Null . '" Key="' .
$field->Key . '"' . (isset($field->Default) ? ' Default="' . $field->Default . '"' : '') . ' Extra="' . $field->Extra . '"' .
' />';
}
foreach ($keys as $key) {
$buffer[] = ' <key Table="' . $table . '" Non_unique="' . $key->Non_unique . '" Key_name="' . $key->Key_name . '"' .
' Seq_in_index="' . $key->Seq_in_index . '" Column_name="' . $key->Column_name . '" Collation="' . $key->Collation . '"' .
' Null="' . $key->Null . '" Index_type="' . $key->Index_type . '"' .
' Sub_part="' . $key->Sub_part . '"' .
' Comment="' . htmlspecialchars($key->Comment, \ENT_COMPAT, 'UTF-8') . '"' .
' />';
}
$buffer[] = ' </table_structure>';
}
return $buffer;
}
/**
* Checks if all data and options are in order prior to exporting.
*
* @return $this
*
* @since 1.0
* @throws \RuntimeException
*/
public function check()
{
// Check if the db connector has been set.
if (!($this->db instanceof MysqliDriver)) {
throw new \RuntimeException('Database connection wrong type.');
}
// Check if the tables have been specified.
if (empty($this->from)) {
throw new \RuntimeException('ERROR: No Tables Specified');
}
return $this;
}
}

View File

@ -0,0 +1,395 @@
<?php
/**
* Part of the Joomla Framework Database Package
*
* @copyright Copyright (C) 2005 - 2021 Open Source Matters, Inc. All rights reserved.
* @license GNU General Public License version 2 or later; see LICENSE
*/
namespace Joomla\Database\Mysqli;
use Joomla\Database\DatabaseImporter;
/**
* MySQLi Database Importer.
*
* @since 1.0
*/
class MysqliImporter extends DatabaseImporter
{
/**
* Checks if all data and options are in order prior to exporting.
*
* @return $this
*
* @since 1.0
* @throws \RuntimeException
*/
public function check()
{
// Check if the db connector has been set.
if (!($this->db instanceof MysqliDriver)) {
throw new \RuntimeException('Database connection wrong type.');
}
// Check if the tables have been specified.
if (empty($this->from)) {
throw new \RuntimeException('ERROR: No Tables Specified');
}
return $this;
}
/**
* Get the SQL syntax to add a table.
*
* @param \SimpleXMLElement $table The table information.
*
* @return string
*
* @since 1.4.0
* @throws \RuntimeException
*/
protected function xmlToCreate(\SimpleXMLElement $table)
{
$existingTables = $this->db->getTableList();
$tableName = (string) $table['name'];
if (\in_array($tableName, $existingTables, true)) {
throw new \RuntimeException('The table you are trying to create already exists');
}
$createTableStatement = 'CREATE TABLE ' . $this->db->quoteName($tableName) . ' (';
foreach ($table->xpath('field') as $field) {
$createTableStatement .= $this->getColumnSql($field) . ', ';
}
$newLookup = $this->getKeyLookup($table->xpath('key'));
foreach ($newLookup as $key) {
$createTableStatement .= $this->getKeySql($key) . ', ';
}
$createTableStatement = rtrim($createTableStatement, ', ');
$createTableStatement .= ')';
return $createTableStatement;
}
/**
* Get the SQL syntax to add a key.
*
* @param string $table The table name.
* @param array $keys An array of the fields pertaining to this key.
*
* @return string
*
* @since 1.0
*/
protected function getAddKeySql($table, $keys)
{
return 'ALTER TABLE ' . $this->db->quoteName($table) . ' ADD ' . $this->getKeySql($keys);
}
/**
* Get alters for table if there is a difference.
*
* @param \SimpleXMLElement $structure The XML structure of the table.
*
* @return array
*
* @since 1.0
*/
protected function getAlterTableSql(\SimpleXMLElement $structure)
{
$table = $this->getRealTableName($structure['name']);
$oldFields = $this->db->getTableColumns($table, false);
$oldKeys = $this->db->getTableKeys($table);
$alters = [];
// Get the fields and keys from the XML that we are aiming for.
$newFields = $structure->xpath('field');
$newKeys = $structure->xpath('key');
// Loop through each field in the new structure.
foreach ($newFields as $field) {
$fName = (string) $field['Field'];
if (isset($oldFields[$fName])) {
// The field exists, check it's the same.
$column = $oldFields[$fName];
// Test whether there is a change.
$change = ((string) $field['Type'] !== $column->Type) || ((string) $field['Null'] !== $column->Null)
|| ((string) $field['Default'] !== $column->Default) || ((string) $field['Extra'] !== $column->Extra);
if ($change) {
$alters[] = $this->getChangeColumnSql($table, $field);
}
// Unset this field so that what we have left are fields that need to be removed.
unset($oldFields[$fName]);
} else {
// The field is new.
$alters[] = $this->getAddColumnSql($table, $field);
}
}
// Any columns left are orphans
foreach ($oldFields as $name => $column) {
// Delete the column.
$alters[] = $this->getDropColumnSql($table, $name);
}
// Get the lookups for the old and new keys.
$oldLookup = $this->getKeyLookup($oldKeys);
$newLookup = $this->getKeyLookup($newKeys);
// Loop through each key in the new structure.
foreach ($newLookup as $name => $keys) {
// Check if there are keys on this field in the existing table.
if (isset($oldLookup[$name])) {
$same = true;
$newCount = \count($newLookup[$name]);
$oldCount = \count($oldLookup[$name]);
// There is a key on this field in the old and new tables. Are they the same?
if ($newCount === $oldCount) {
// Need to loop through each key and do a fine grained check.
for ($i = 0; $i < $newCount; $i++) {
$same = (((string) $newLookup[$name][$i]['Non_unique'] === $oldLookup[$name][$i]->Non_unique)
&& ((string) $newLookup[$name][$i]['Column_name'] === $oldLookup[$name][$i]->Column_name)
&& ((string) $newLookup[$name][$i]['Seq_in_index'] === $oldLookup[$name][$i]->Seq_in_index)
&& ((string) $newLookup[$name][$i]['Collation'] === $oldLookup[$name][$i]->Collation)
&& ((string) $newLookup[$name][$i]['Sub_part'] == $oldLookup[$name][$i]->Sub_part)
&& ((string) $newLookup[$name][$i]['Index_type'] === $oldLookup[$name][$i]->Index_type));
/*
Debug.
echo '<pre>';
echo '<br>Non_unique: '.
((string) $newLookup[$name][$i]['Non_unique'] == $oldLookup[$name][$i]->Non_unique ? 'Pass' : 'Fail').' '.
(string) $newLookup[$name][$i]['Non_unique'].' vs '.$oldLookup[$name][$i]->Non_unique;
echo '<br>Column_name: '.
((string) $newLookup[$name][$i]['Column_name'] == $oldLookup[$name][$i]->Column_name ? 'Pass' : 'Fail').' '.
(string) $newLookup[$name][$i]['Column_name'].' vs '.$oldLookup[$name][$i]->Column_name;
echo '<br>Seq_in_index: '.
((string) $newLookup[$name][$i]['Seq_in_index'] == $oldLookup[$name][$i]->Seq_in_index ? 'Pass' : 'Fail').' '.
(string) $newLookup[$name][$i]['Seq_in_index'].' vs '.$oldLookup[$name][$i]->Seq_in_index;
echo '<br>Collation: '.
((string) $newLookup[$name][$i]['Collation'] == $oldLookup[$name][$i]->Collation ? 'Pass' : 'Fail').' '.
(string) $newLookup[$name][$i]['Collation'].' vs '.$oldLookup[$name][$i]->Collation;
echo '<br>Sub_part: '.
((string) $newLookup[$name][$i]['Sub_part'] == $oldLookup[$name][$i]->Sub_part ? 'Pass' : 'Fail').' '.
(string) $newLookup[$name][$i]['Sub_part'].' vs '.$oldLookup[$name][$i]->Sub_part;
echo '<br>Index_type: '.
((string) $newLookup[$name][$i]['Index_type'] == $oldLookup[$name][$i]->Index_type ? 'Pass' : 'Fail').' '.
(string) $newLookup[$name][$i]['Index_type'].' vs '.$oldLookup[$name][$i]->Index_type;
echo '<br>Same = '.($same ? 'true' : 'false');
echo '</pre>';
*/
if (!$same) {
// Break out of the loop. No need to check further.
break;
}
}
} else {
// Count is different, just drop and add.
$same = false;
}
if (!$same) {
$alters[] = $this->getDropKeySql($table, $name);
$alters[] = $this->getAddKeySql($table, $keys);
}
// Unset this field so that what we have left are fields that need to be removed.
unset($oldLookup[$name]);
} else {
// This is a new key.
$alters[] = $this->getAddKeySql($table, $keys);
}
}
// Any keys left are orphans.
foreach ($oldLookup as $name => $keys) {
if (strtoupper($name) === 'PRIMARY') {
$alters[] = $this->getDropPrimaryKeySql($table);
} else {
$alters[] = $this->getDropKeySql($table, $name);
}
}
return $alters;
}
/**
* Get the syntax to alter a column.
*
* @param string $table The name of the database table to alter.
* @param \SimpleXMLElement $field The XML definition for the field.
*
* @return string
*
* @since 1.0
*/
protected function getChangeColumnSql($table, \SimpleXMLElement $field)
{
return 'ALTER TABLE ' . $this->db->quoteName($table) . ' CHANGE COLUMN ' . $this->db->quoteName((string) $field['Field']) . ' '
. $this->getColumnSql($field);
}
/**
* Get the SQL syntax for a single column that would be included in a table create or alter statement.
*
* @param \SimpleXMLElement $field The XML field definition.
*
* @return string
*
* @since 1.0
*/
protected function getColumnSql(\SimpleXMLElement $field)
{
// TODO Incorporate into parent class and use $this.
$blobs = ['text', 'smalltext', 'mediumtext', 'largetext'];
$fName = (string) $field['Field'];
$fType = (string) $field['Type'];
$fNull = (string) $field['Null'];
$fDefault = isset($field['Default']) ? (string) $field['Default'] : null;
$fExtra = (string) $field['Extra'];
$sql = $this->db->quoteName($fName) . ' ' . $fType;
if ($fNull === 'NO') {
if ($fDefault === null || \in_array($fType, $blobs, true)) {
$sql .= ' NOT NULL';
} else {
// TODO Don't quote numeric values.
if (stristr($fDefault, 'CURRENT') !== false) {
$sql .= ' NOT NULL DEFAULT CURRENT_TIMESTAMP()';
} else {
$sql .= ' NOT NULL DEFAULT ' . $this->db->quote($fDefault);
}
}
} else {
if ($fDefault === null) {
$sql .= ' DEFAULT NULL';
} else {
// TODO Don't quote numeric values.
$sql .= ' DEFAULT ' . $this->db->quote($fDefault);
}
}
if ($fExtra) {
// MySql 8.0 introduces DEFAULT_GENERATED in the extra column and should be replaced with the default value
if (stristr($fExtra, 'DEFAULT_GENERATED') !== false) {
$sql .= ' ' . strtoupper(str_ireplace('DEFAULT_GENERATED', 'DEFAULT ' . $fDefault, $fExtra));
} else {
$sql .= ' ' . strtoupper($fExtra);
}
}
return $sql;
}
/**
* Get the SQL syntax to drop a key.
*
* @param string $table The table name.
* @param string $name The name of the key to drop.
*
* @return string
*
* @since 1.0
*/
protected function getDropKeySql($table, $name)
{
return 'ALTER TABLE ' . $this->db->quoteName($table) . ' DROP KEY ' . $this->db->quoteName($name);
}
/**
* Get the SQL syntax to drop a key.
*
* @param string $table The table name.
*
* @return string
*
* @since 1.0
*/
protected function getDropPrimaryKeySql($table)
{
return 'ALTER TABLE ' . $this->db->quoteName($table) . ' DROP PRIMARY KEY';
}
/**
* Get the details list of keys for a table.
*
* @param array $keys An array of objects that comprise the keys for the table.
*
* @return array The lookup array. array({key name} => array(object, ...))
*
* @since 1.0
*/
protected function getKeyLookup($keys)
{
// First pass, create a lookup of the keys.
$lookup = [];
foreach ($keys as $key) {
if ($key instanceof \SimpleXMLElement) {
$kName = (string) $key['Key_name'];
} else {
$kName = $key->Key_name;
}
if (empty($lookup[$kName])) {
$lookup[$kName] = [];
}
$lookup[$kName][] = $key;
}
return $lookup;
}
/**
* Get the SQL syntax for a key.
*
* @param array $columns An array of SimpleXMLElement objects comprising the key.
*
* @return string
*
* @since 1.0
*/
protected function getKeySql($columns)
{
$kNonUnique = (string) $columns[0]['Non_unique'];
$kName = (string) $columns[0]['Key_name'];
$prefix = '';
if ($kName === 'PRIMARY') {
$prefix = 'PRIMARY ';
} elseif ($kNonUnique == 0) {
$prefix = 'UNIQUE ';
}
$kColumns = [];
foreach ($columns as $column) {
$kLength = '';
if (!empty($column['Sub_part'])) {
$kLength = '(' . $column['Sub_part'] . ')';
}
$kColumns[] = $this->db->quoteName((string) $column['Column_name']) . $kLength;
}
return $prefix . 'KEY ' . ($kName !== 'PRIMARY' ? $this->db->quoteName($kName) : '') . ' (' . implode(',', $kColumns) . ')';
}
}

View File

@ -0,0 +1,31 @@
<?php
/**
* Part of the Joomla Framework Database Package
*
* @copyright Copyright (C) 2005 - 2021 Open Source Matters, Inc. All rights reserved.
* @license GNU General Public License version 2 or later; see LICENSE
*/
namespace Joomla\Database\Mysqli;
use Joomla\Database\DatabaseQuery;
use Joomla\Database\Query\MysqlQueryBuilder;
/**
* MySQLi Query Building Class.
*
* @since 1.0
*/
class MysqliQuery extends DatabaseQuery
{
use MysqlQueryBuilder;
/**
* The list of zero or null representation of a datetime.
*
* @var array
* @since 2.0.0
*/
protected $nullDatetimeList = ['0000-00-00 00:00:00', '1000-01-01 00:00:00'];
}

View File

@ -0,0 +1,580 @@
<?php
/**
* Part of the Joomla Framework Database Package
*
* @copyright Copyright (C) 2005 - 2021 Open Source Matters, Inc. All rights reserved.
* @license GNU General Public License version 2 or later; see LICENSE
*/
namespace Joomla\Database\Mysqli;
use Joomla\Database\Exception\ExecutionFailureException;
use Joomla\Database\Exception\PrepareStatementFailureException;
use Joomla\Database\FetchMode;
use Joomla\Database\FetchOrientation;
use Joomla\Database\ParameterType;
use Joomla\Database\StatementInterface;
/**
* MySQLi Database Statement.
*
* This class is modeled on \Doctrine\DBAL\Driver\Mysqli\MysqliStatement
*
* @since 2.0.0
*/
class MysqliStatement implements StatementInterface
{
/**
* Values which have been bound to the statement.
*
* @var array
* @since 2.0.0
*/
protected $bindedValues;
/**
* Mapping between named parameters and position in query.
*
* @var array
* @since 2.0.0
*/
protected $parameterKeyMapping;
/**
* Mapping array for parameter types.
*
* @var array
* @since 2.0.0
*/
protected $parameterTypeMapping = [
ParameterType::BOOLEAN => 'i',
ParameterType::INTEGER => 'i',
ParameterType::LARGE_OBJECT => 's',
ParameterType::NULL => 's',
ParameterType::STRING => 's',
];
/**
* Column names from the executed statement.
*
* @var array|boolean|null
* @since 2.0.0
*/
protected $columnNames;
/**
* The database connection resource.
*
* @var \mysqli
* @since 2.0.0
*/
protected $connection;
/**
* The default fetch mode for the statement.
*
* @var integer
* @since 2.0.0
*/
protected $defaultFetchStyle = FetchMode::MIXED;
/**
* The query string being prepared.
*
* @var string
* @since 2.0.0
*/
protected $query;
/**
* Internal tracking flag to set whether there is a result set available for processing
*
* @var boolean
* @since 2.0.0
*/
private $result = false;
/**
* Values which have been bound to the rows of each result set.
*
* @var array
* @since 2.0.0
*/
protected $rowBindedValues;
/**
* The prepared statement.
*
* @var \mysqli_stmt
* @since 2.0.0
*/
protected $statement;
/**
* Bound parameter types.
*
* @var array
* @since 2.0.0
*/
protected $typesKeyMapping;
/**
* Constructor.
*
* @param \mysqli $connection The database connection resource
* @param string $query The query this statement will process
*
* @since 2.0.0
* @throws PrepareStatementFailureException
*/
public function __construct(\mysqli $connection, string $query)
{
$this->connection = $connection;
$this->query = $query;
$query = $this->prepareParameterKeyMapping($query);
$this->statement = $connection->prepare($query);
if (!$this->statement) {
throw new PrepareStatementFailureException($this->connection->error, $this->connection->errno);
}
}
/**
* Replace named parameters with numbered parameters
*
* @param string $sql The SQL statement to prepare.
*
* @return string The processed SQL statement.
*
* @since 2.0.0
*/
public function prepareParameterKeyMapping($sql)
{
$escaped = false;
$startPos = 0;
$quoteChar = '';
$literal = '';
$mapping = [];
$replace = [];
$matches = [];
$pattern = '/([:][a-zA-Z0-9_]+)/';
if (!preg_match($pattern, $sql, $matches)) {
return $sql;
}
$sql = trim($sql);
$n = \strlen($sql);
while ($startPos < $n) {
if (!preg_match($pattern, $sql, $matches, 0, $startPos)) {
break;
}
$j = strpos($sql, "'", $startPos);
$k = strpos($sql, '"', $startPos);
if (($k !== false) && (($k < $j) || ($j === false))) {
$quoteChar = '"';
$j = $k;
} else {
$quoteChar = "'";
}
if ($j === false) {
$j = $n;
}
// Search for named prepared parameters and replace it with ? and save its position
$substring = substr($sql, $startPos, $j - $startPos);
if (preg_match_all($pattern, $substring, $matches, PREG_PATTERN_ORDER + PREG_OFFSET_CAPTURE)) {
foreach ($matches[0] as $i => $match) {
if ($i === 0) {
$literal .= substr($substring, 0, $match[1]);
}
$mapping[$match[0]] = \count($mapping);
$endOfPlaceholder = $match[1] + strlen($match[0]);
$beginOfNextPlaceholder = $matches[0][$i + 1][1] ?? strlen($substring);
$beginOfNextPlaceholder -= $endOfPlaceholder;
$literal .= '?' . substr($substring, $endOfPlaceholder, $beginOfNextPlaceholder);
}
} else {
$literal .= $substring;
}
$startPos = $j;
$j++;
if ($j >= $n) {
break;
}
// Quote comes first, find end of quote
while (true) {
$k = strpos($sql, $quoteChar, $j);
$escaped = false;
if ($k === false) {
break;
}
$l = $k - 1;
while ($l >= 0 && $sql[$l] === '\\') {
$l--;
$escaped = !$escaped;
}
if ($escaped) {
$j = $k + 1;
continue;
}
break;
}
if ($k === false) {
// Error in the query - no end quote; ignore it
break;
}
$literal .= substr($sql, $startPos, $k - $startPos + 1);
$startPos = $k + 1;
}
if ($startPos < $n) {
$literal .= substr($sql, $startPos, $n - $startPos);
}
$this->parameterKeyMapping = $mapping;
return $literal;
}
/**
* Binds a parameter to the specified variable name.
*
* @param string|integer $parameter Parameter identifier. For a prepared statement using named placeholders, this will be a parameter
* name of the form `:name`. For a prepared statement using question mark placeholders, this will be
* the 1-indexed position of the parameter.
* @param mixed $variable Name of the PHP variable to bind to the SQL statement parameter.
* @param integer $dataType Constant corresponding to a SQL datatype, this should be the processed type from the QueryInterface.
* @param integer $length The length of the variable. Usually required for OUTPUT parameters.
* @param array $driverOptions Optional driver options to be used.
*
* @return boolean
*
* @since 2.0.0
*/
public function bindParam($parameter, &$variable, string $dataType = ParameterType::STRING, ?int $length = null, ?array $driverOptions = null)
{
$this->bindedValues[$parameter] =& $variable;
// Validate parameter type
if (!isset($this->parameterTypeMapping[$dataType])) {
throw new \InvalidArgumentException(sprintf('Unsupported parameter type `%s`', $dataType));
}
$this->typesKeyMapping[$parameter] = $this->parameterTypeMapping[$dataType];
return true;
}
/**
* Binds a array of values to bound parameters.
*
* @param array $values The values to bind to the statement
*
* @return boolean
*
* @since 2.0.0
*/
private function bindValues(array $values)
{
$params = [];
$types = str_repeat('s', \count($values));
if (!empty($this->parameterKeyMapping)) {
foreach ($values as $key => &$value) {
$params[$this->parameterKeyMapping[$key]] =& $value;
}
ksort($params);
} else {
foreach ($values as $key => &$value) {
$params[] =& $value;
}
}
array_unshift($params, $types);
return \call_user_func_array([$this->statement, 'bind_param'], $params);
}
/**
* Closes the cursor, enabling the statement to be executed again.
*
* @return void
*
* @since 2.0.0
*/
public function closeCursor(): void
{
$this->statement->free_result();
$this->result = false;
}
/**
* Fetches the SQLSTATE associated with the last operation on the statement handle.
*
* @return int
*
* @since 2.0.0
*/
public function errorCode()
{
return $this->statement->errno;
}
/**
* Fetches extended error information associated with the last operation on the statement handle.
*
* @return string
*
* @since 2.0.0
*/
public function errorInfo()
{
return $this->statement->error;
}
/**
* Executes a prepared statement
*
* @param array|null $parameters An array of values with as many elements as there are bound parameters in the SQL statement being executed.
*
* @return boolean
*
* @since 2.0.0
*/
public function execute(?array $parameters = null)
{
if ($this->bindedValues !== null) {
$params = [];
$types = [];
if (!empty($this->parameterKeyMapping)) {
foreach ($this->bindedValues as $key => &$value) {
$params[$this->parameterKeyMapping[$key]] =& $value;
$types[$this->parameterKeyMapping[$key]] = $this->typesKeyMapping[$key];
}
} else {
foreach ($this->bindedValues as $key => &$value) {
$params[] =& $value;
$types[$key] = $this->typesKeyMapping[$key];
}
}
ksort($params);
ksort($types);
array_unshift($params, implode('', $types));
if (!\call_user_func_array([$this->statement, 'bind_param'], $params)) {
throw new PrepareStatementFailureException($this->statement->error, $this->statement->errno);
}
} elseif ($parameters !== null) {
if (!$this->bindValues($parameters)) {
throw new PrepareStatementFailureException($this->statement->error, $this->statement->errno);
}
}
try {
if (!$this->statement->execute()) {
throw new ExecutionFailureException($this->query, $this->statement->error, $this->statement->errno);
}
} catch (\Throwable $e) {
throw new ExecutionFailureException($this->query, $e->getMessage(), $e->getCode(), $e);
}
if ($this->columnNames === null) {
$meta = $this->statement->result_metadata();
if ($meta !== false) {
$columnNames = [];
foreach ($meta->fetch_fields() as $col) {
$columnNames[] = $col->name;
}
$meta->free();
$this->columnNames = $columnNames;
} else {
$this->columnNames = false;
}
}
if ($this->columnNames !== false) {
$this->statement->store_result();
$this->rowBindedValues = array_fill(0, \count($this->columnNames), null);
$refs = [];
foreach ($this->rowBindedValues as $key => &$value) {
$refs[$key] =& $value;
}
if (!\call_user_func_array([$this->statement, 'bind_result'], $refs)) {
throw new \RuntimeException($this->statement->error, $this->statement->errno);
}
}
$this->result = true;
return true;
}
/**
* Fetches the next row from a result set
*
* @param integer|null $fetchStyle Controls how the next row will be returned to the caller. This value must be one of the
* FetchMode constants, defaulting to value of FetchMode::MIXED.
* @param integer $cursorOrientation For a StatementInterface object representing a scrollable cursor, this value determines which row
* will be returned to the caller. This value must be one of the FetchOrientation constants,
* defaulting to FetchOrientation::NEXT.
* @param integer $cursorOffset For a StatementInterface object representing a scrollable cursor for which the cursorOrientation
* parameter is set to FetchOrientation::ABS, this value specifies the absolute number of the row in
* the result set that shall be fetched. For a StatementInterface object representing a scrollable
* cursor for which the cursorOrientation parameter is set to FetchOrientation::REL, this value
* specifies the row to fetch relative to the cursor position before `fetch()` was called.
*
* @return mixed The return value of this function on success depends on the fetch type. In all cases, boolean false is returned on failure.
*
* @since 2.0.0
*/
public function fetch(?int $fetchStyle = null, int $cursorOrientation = FetchOrientation::NEXT, int $cursorOffset = 0)
{
if (!$this->result) {
return false;
}
$fetchStyle = $fetchStyle ?: $this->defaultFetchStyle;
if ($fetchStyle === FetchMode::COLUMN) {
return $this->fetchColumn();
}
$values = $this->fetchData();
if ($values === null) {
return false;
}
if ($values === false) {
throw new \RuntimeException($this->statement->error, $this->statement->errno);
}
switch ($fetchStyle) {
case FetchMode::NUMERIC:
return $values;
case FetchMode::ASSOCIATIVE:
return array_combine($this->columnNames, $values);
case FetchMode::MIXED:
$ret = array_combine($this->columnNames, $values);
$ret += $values;
return $ret;
case FetchMode::STANDARD_OBJECT:
return (object) array_combine($this->columnNames, $values);
default:
throw new \InvalidArgumentException("Unknown fetch type '{$fetchStyle}'");
}
}
/**
* Returns a single column from the next row of a result set
*
* @param integer $columnIndex 0-indexed number of the column you wish to retrieve from the row.
* If no value is supplied, the first column is retrieved.
*
* @return mixed Returns a single column from the next row of a result set or boolean false if there are no more rows.
*
* @since 2.0.0
*/
public function fetchColumn($columnIndex = 0)
{
$row = $this->fetch(FetchMode::NUMERIC);
if ($row === false) {
return false;
}
return $row[$columnIndex] ?? null;
}
/**
* Fetch the data from the statement.
*
* @return array|boolean
*
* @since 2.0.0
*/
private function fetchData()
{
$return = $this->statement->fetch();
if ($return === true) {
$values = [];
foreach ($this->rowBindedValues as $v) {
$values[] = $v;
}
return $values;
}
return $return;
}
/**
* Returns the number of rows affected by the last SQL statement.
*
* @return integer
*
* @since 2.0.0
*/
public function rowCount(): int
{
if ($this->columnNames === false) {
return $this->statement->affected_rows;
}
return $this->statement->num_rows;
}
/**
* Sets the fetch mode to use while iterating this statement.
*
* @param integer $fetchMode The fetch mode, must be one of the FetchMode constants.
* @param mixed ...$args Optional mode-specific arguments.
*
* @return void
*
* @since 2.0.0
*/
public function setFetchMode(int $fetchMode, ...$args): void
{
$this->defaultFetchStyle = $fetchMode;
}
}

View File

@ -0,0 +1,67 @@
<?php
/**
* Part of the Joomla Framework Database Package
*
* @copyright Copyright (C) 2005 - 2021 Open Source Matters, Inc. All rights reserved.
* @license GNU General Public License version 2 or later; see LICENSE
*/
namespace Joomla\Database;
/**
* Class defining the parameter types for prepared statements
*
* @since 2.0.0
*/
final class ParameterType
{
/**
* Defines a boolean parameter
*
* @var string
* @since 2.0.0
*/
public const BOOLEAN = 'boolean';
/**
* Defines an integer parameter
*
* @var string
* @since 2.0.0
*/
public const INTEGER = 'int';
/**
* Defines a large object parameter
*
* @var string
* @since 2.0.0
*/
public const LARGE_OBJECT = 'lob';
/**
* Defines a null parameter
*
* @var string
* @since 2.0.0
*/
public const NULL = 'null';
/**
* Defines a string parameter
*
* @var string
* @since 2.0.0
*/
public const STRING = 'string';
/**
* Private constructor to prevent instantiation of this class
*
* @since 2.0.0
*/
private function __construct()
{
}
}

View File

@ -0,0 +1,748 @@
<?php
/**
* Part of the Joomla Framework Database Package
*
* @copyright Copyright (C) 2005 - 2021 Open Source Matters, Inc. All rights reserved.
* @license GNU General Public License version 2 or later; see LICENSE
*/
namespace Joomla\Database\Pdo;
use Joomla\Database\DatabaseDriver;
use Joomla\Database\DatabaseEvents;
use Joomla\Database\Event\ConnectionEvent;
use Joomla\Database\Exception\ConnectionFailureException;
use Joomla\Database\Exception\ExecutionFailureException;
use Joomla\Database\Exception\PrepareStatementFailureException;
use Joomla\Database\Exception\UnsupportedAdapterException;
use Joomla\Database\StatementInterface;
/**
* Joomla Framework PDO Database Driver Class
*
* @link https://www.php.net/pdo
* @since 1.0
*/
abstract class PdoDriver extends DatabaseDriver
{
/**
* The database connection resource.
*
* @var \PDO
* @since 1.0
*/
protected $connection;
/**
* The name of the database driver.
*
* @var string
* @since 1.0
*/
public $name = 'pdo';
/**
* The character(s) used to quote SQL statement names such as table names or field names, etc.
*
* If a single character string the same character is used for both sides of the quoted name, else the first character will be used for the
* opening quote and the second for the closing quote.
*
* @var string
* @since 1.0
*/
protected $nameQuote = "'";
/**
* The null or zero representation of a timestamp for the database driver.
*
* @var string
* @since 1.0
*/
protected $nullDate = '0000-00-00 00:00:00';
/**
* Constructor.
*
* @param array $options List of options used to configure the connection
*
* @since 1.0
*/
public function __construct(array $options)
{
// Get some basic values from the options.
$options['driver'] = $options['driver'] ?? 'odbc';
$options['dsn'] = $options['dsn'] ?? '';
$options['host'] = $options['host'] ?? 'localhost';
$options['database'] = $options['database'] ?? '';
$options['user'] = $options['user'] ?? '';
$options['port'] = isset($options['port']) ? (int) $options['port'] : null;
$options['password'] = $options['password'] ?? '';
$options['driverOptions'] = $options['driverOptions'] ?? [];
$options['ssl'] = isset($options['ssl']) ? $options['ssl'] : [];
$options['socket'] = \strpos($options['host'], 'unix:') !== false ? \str_replace('unix:', '', $options['host']) : null;
if ($options['ssl'] !== []) {
$options['ssl']['enable'] = isset($options['ssl']['enable']) ? $options['ssl']['enable'] : false;
$options['ssl']['cipher'] = isset($options['ssl']['cipher']) ? $options['ssl']['cipher'] : null;
$options['ssl']['ca'] = isset($options['ssl']['ca']) ? $options['ssl']['ca'] : null;
$options['ssl']['capath'] = isset($options['ssl']['capath']) ? $options['ssl']['capath'] : null;
$options['ssl']['key'] = isset($options['ssl']['key']) ? $options['ssl']['key'] : null;
$options['ssl']['cert'] = isset($options['ssl']['cert']) ? $options['ssl']['cert'] : null;
$options['ssl']['verify_server_cert'] = isset($options['ssl']['verify_server_cert']) ? $options['ssl']['verify_server_cert'] : null;
}
// Finalize initialisation
parent::__construct($options);
}
/**
* Destructor.
*
* @since 1.0
*/
public function __destruct()
{
$this->disconnect();
}
/**
* Connects to the database if needed.
*
* @return void Returns void if the database connected successfully.
*
* @since 1.0
* @throws \RuntimeException
*/
public function connect()
{
if ($this->connection) {
return;
}
// Make sure the PDO extension for PHP is installed and enabled.
if (!static::isSupported()) {
throw new UnsupportedAdapterException('PDO Extension is not available.', 1);
}
// Find the correct PDO DSN Format to use:
switch ($this->options['driver']) {
case 'cubrid':
$this->options['port'] = $this->options['port'] ?? 33000;
$format = 'cubrid:host=#HOST#;port=#PORT#;dbname=#DBNAME#';
$replace = ['#HOST#', '#PORT#', '#DBNAME#'];
$with = [$this->options['host'], $this->options['port'], $this->options['database']];
break;
case 'dblib':
$this->options['port'] = $this->options['port'] ?? 1433;
$format = 'dblib:host=#HOST#;port=#PORT#;dbname=#DBNAME#';
$replace = ['#HOST#', '#PORT#', '#DBNAME#'];
$with = [$this->options['host'], $this->options['port'], $this->options['database']];
break;
case 'firebird':
$this->options['port'] = $this->options['port'] ?? 3050;
$format = 'firebird:dbname=#DBNAME#';
$replace = ['#DBNAME#'];
$with = [$this->options['database']];
break;
case 'ibm':
$this->options['port'] = $this->options['port'] ?? 56789;
if (!empty($this->options['dsn'])) {
$format = 'ibm:DSN=#DSN#';
$replace = ['#DSN#'];
$with = [$this->options['dsn']];
} else {
$format = 'ibm:hostname=#HOST#;port=#PORT#;database=#DBNAME#';
$replace = ['#HOST#', '#PORT#', '#DBNAME#'];
$with = [$this->options['host'], $this->options['port'], $this->options['database']];
}
break;
case 'informix':
$this->options['port'] = $this->options['port'] ?? 1526;
$this->options['protocol'] = $this->options['protocol'] ?? 'onsoctcp';
if (!empty($this->options['dsn'])) {
$format = 'informix:DSN=#DSN#';
$replace = ['#DSN#'];
$with = [$this->options['dsn']];
} else {
$format = 'informix:host=#HOST#;service=#PORT#;database=#DBNAME#;server=#SERVER#;protocol=#PROTOCOL#';
$replace = ['#HOST#', '#PORT#', '#DBNAME#', '#SERVER#', '#PROTOCOL#'];
$with = [
$this->options['host'],
$this->options['port'],
$this->options['database'],
$this->options['server'],
$this->options['protocol'],
];
}
break;
case 'mssql':
$this->options['port'] = $this->options['port'] ?? 1433;
$format = 'mssql:host=#HOST#;port=#PORT#;dbname=#DBNAME#';
$replace = ['#HOST#', '#PORT#', '#DBNAME#'];
$with = [$this->options['host'], $this->options['port'], $this->options['database']];
break;
case 'mysql':
$this->options['port'] = $this->options['port'] ?? 3306;
if ($this->options['socket'] !== null) {
$format = 'mysql:unix_socket=#SOCKET#;dbname=#DBNAME#;charset=#CHARSET#';
} else {
$format = 'mysql:host=#HOST#;port=#PORT#;dbname=#DBNAME#;charset=#CHARSET#';
}
$replace = ['#HOST#', '#PORT#', '#SOCKET#', '#DBNAME#', '#CHARSET#'];
$with = [
$this->options['host'],
$this->options['port'],
$this->options['socket'],
$this->options['database'],
$this->options['charset'],
];
break;
case 'oci':
$this->options['port'] = $this->options['port'] ?? 1521;
$this->options['charset'] = $this->options['charset'] ?? 'AL32UTF8';
if (!empty($this->options['dsn'])) {
$format = 'oci:dbname=#DSN#';
$replace = ['#DSN#'];
$with = [$this->options['dsn']];
} else {
$format = 'oci:dbname=//#HOST#:#PORT#/#DBNAME#';
$replace = ['#HOST#', '#PORT#', '#DBNAME#'];
$with = [$this->options['host'], $this->options['port'], $this->options['database']];
}
$format .= ';charset=' . $this->options['charset'];
break;
case 'odbc':
$format = 'odbc:DSN=#DSN#;UID:#USER#;PWD=#PASSWORD#';
$replace = ['#DSN#', '#USER#', '#PASSWORD#'];
$with = [$this->options['dsn'], $this->options['user'], $this->options['password']];
break;
case 'pgsql':
$this->options['port'] = $this->options['port'] ?? 5432;
if ($this->options['socket'] !== null) {
$format = 'pgsql:host=#SOCKET#;dbname=#DBNAME#';
} else {
$format = 'pgsql:host=#HOST#;port=#PORT#;dbname=#DBNAME#';
}
$replace = ['#HOST#', '#PORT#', '#SOCKET#', '#DBNAME#'];
$with = [$this->options['host'], $this->options['port'], $this->options['socket'], $this->options['database']];
// For data in transit TLS encryption.
if ($this->options['ssl'] !== [] && $this->options['ssl']['enable'] === true) {
if (isset($this->options['ssl']['verify_server_cert']) && $this->options['ssl']['verify_server_cert'] === true) {
$format .= ';sslmode=verify-full';
} else {
$format .= ';sslmode=require';
}
$sslKeysMapping = [
'cipher' => null,
'ca' => 'sslrootcert',
'capath' => null,
'key' => 'sslkey',
'cert' => 'sslcert',
];
// If customised, add cipher suite, ca file path, ca path, private key file path and certificate file path to PDO driver options.
foreach ($sslKeysMapping as $key => $value) {
if ($value !== null && $this->options['ssl'][$key] !== null) {
$format .= ';' . $value . '=' . $this->options['ssl'][$key];
}
}
}
break;
case 'sqlite':
if (isset($this->options['version']) && $this->options['version'] == 2) {
$format = 'sqlite2:#DBNAME#';
} else {
$format = 'sqlite:#DBNAME#';
}
$replace = ['#DBNAME#'];
$with = [$this->options['database']];
break;
case 'sybase':
$this->options['port'] = $this->options['port'] ?? 1433;
$format = 'mssql:host=#HOST#;port=#PORT#;dbname=#DBNAME#';
$replace = ['#HOST#', '#PORT#', '#DBNAME#'];
$with = [$this->options['host'], $this->options['port'], $this->options['database']];
break;
default:
throw new UnsupportedAdapterException('The ' . $this->options['driver'] . ' driver is not supported.');
}
// Create the connection string:
$connectionString = str_replace($replace, $with, $format);
try {
$this->connection = new \PDO(
$connectionString,
$this->options['user'],
$this->options['password'],
$this->options['driverOptions']
);
} catch (\PDOException $e) {
throw new ConnectionFailureException('Could not connect to PDO: ' . $e->getMessage(), $e->getCode(), $e);
}
$this->setOption(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
$this->dispatchEvent(new ConnectionEvent(DatabaseEvents::POST_CONNECT, $this));
}
/**
* Method to escape a string for usage in an SQL statement.
*
* Oracle escaping reference:
* http://www.orafaq.com/wiki/SQL_FAQ#How_does_one_escape_special_characters_when_writing_SQL_queries.3F
*
* SQLite escaping notes:
* http://www.sqlite.org/faq.html#q14
*
* Method body is as implemented by the Zend Framework
*
* Note: Using query objects with bound variables is preferable to the below.
*
* @param string $text The string to be escaped.
* @param boolean $extra Unused optional parameter to provide extra escaping.
*
* @return string The escaped string.
*
* @since 1.0
*/
public function escape($text, $extra = false)
{
if (\is_int($text)) {
return $text;
}
if (\is_float($text)) {
// Force the dot as a decimal point.
return str_replace(',', '.', (string) $text);
}
$text = str_replace("'", "''", (string) $text);
return addcslashes($text, "\000\n\r\\\032");
}
/**
* Execute the SQL statement.
*
* @return boolean
*
* @since 1.0
* @throws \Exception
* @throws \RuntimeException
*/
public function execute()
{
$this->connect();
// Take a local copy so that we don't modify the original query and cause issues later
$sql = $this->replacePrefix((string) $this->sql);
// Increment the query counter.
$this->count++;
// Get list of bounded parameters
$bounded =& $this->sql->getBounded();
// If there is a monitor registered, let it know we are starting this query
if ($this->monitor) {
$this->monitor->startQuery($sql, $bounded);
}
// Execute the query.
$this->executed = false;
// Bind the variables
foreach ($bounded as $key => $obj) {
$this->statement->bindParam($key, $obj->value, $obj->dataType, $obj->length, $obj->driverOptions);
}
try {
$this->executed = $this->statement->execute();
// If there is a monitor registered, let it know we have finished this query
if ($this->monitor) {
$this->monitor->stopQuery();
}
return true;
} catch (\PDOException $exception) {
// If there is a monitor registered, let it know we have finished this query
if ($this->monitor) {
$this->monitor->stopQuery();
}
// Get the error number and message before we execute any more queries.
$errorNum = (int) $this->statement->errorCode();
$errorMsg = (string) implode(', ', $this->statement->errorInfo());
// Check if the server was disconnected.
try {
if (!$this->connected()) {
try {
// Attempt to reconnect.
$this->connection = null;
$this->connect();
} catch (ConnectionFailureException $e) {
// If connect fails, ignore that exception and throw the normal exception.
throw new ExecutionFailureException($sql, $errorMsg, $errorNum);
}
// Since we were able to reconnect, run the query again.
return $this->execute();
}
} catch (\LogicException $e) {
throw new ExecutionFailureException($sql, $errorMsg, $errorNum, $e);
}
// Throw the normal query exception.
throw new ExecutionFailureException($sql, $errorMsg, $errorNum);
}
}
/**
* Retrieve a PDO database connection attribute
* https://www.php.net/manual/en/pdo.getattribute.php
*
* Usage: $db->getOption(PDO::ATTR_CASE);
*
* @param mixed $key One of the PDO::ATTR_* Constants
*
* @return mixed
*
* @since 1.0
*/
public function getOption($key)
{
$this->connect();
return $this->connection->getAttribute($key);
}
/**
* Get the version of the database connector.
*
* @return string The database connector version.
*
* @since 1.5.0
*/
public function getVersion()
{
$this->connect();
return $this->getOption(\PDO::ATTR_SERVER_VERSION);
}
/**
* Get a query to run and verify the database is operational.
*
* @return string The query to check the health of the DB.
*
* @since 1.0
*/
public function getConnectedQuery()
{
return 'SELECT 1';
}
/**
* Sets an attribute on the PDO database handle.
* https://www.php.net/manual/en/pdo.setattribute.php
*
* Usage: $db->setOption(PDO::ATTR_CASE, PDO::CASE_UPPER);
*
* @param integer $key One of the PDO::ATTR_* Constants
* @param mixed $value One of the associated PDO Constants
* related to the particular attribute
* key.
*
* @return boolean
*
* @since 1.0
*/
public function setOption($key, $value)
{
$this->connect();
return $this->connection->setAttribute($key, $value);
}
/**
* Test to see if the PDO extension is available.
* Override as needed to check for specific PDO Drivers.
*
* @return boolean True on success, false otherwise.
*
* @since 1.0
*/
public static function isSupported()
{
return \defined('\\PDO::ATTR_DRIVER_NAME');
}
/**
* Determines if the connection to the server is active.
*
* @return boolean True if connected to the database engine.
*
* @since 1.0
* @throws \LogicException
*/
public function connected()
{
// Flag to prevent recursion into this function.
static $checkingConnected = false;
if ($checkingConnected) {
// Reset this flag and throw an exception.
$checkingConnected = false;
throw new \LogicException('Recursion trying to check if connected.');
}
// Backup the query state.
$sql = $this->sql;
$limit = $this->limit;
$offset = $this->offset;
$statement = $this->statement;
try {
// Set the checking connection flag.
$checkingConnected = true;
// Run a simple query to check the connection.
$this->setQuery($this->getConnectedQuery());
$status = (bool) $this->loadResult();
} catch (\Exception $e) {
// If we catch an exception here, we must not be connected.
$status = false;
}
// Restore the query state.
$this->sql = $sql;
$this->limit = $limit;
$this->offset = $offset;
$this->statement = $statement;
$checkingConnected = false;
return $status;
}
/**
* Method to get the auto-incremented value from the last INSERT statement.
*
* @return string The value of the auto-increment field from the last inserted row.
*
* @since 1.0
*/
public function insertid()
{
$this->connect();
// Error suppress this to prevent PDO warning us that the driver doesn't support this operation.
return @$this->connection->lastInsertId();
}
/**
* Select a database for use.
*
* @param string $database The name of the database to select for use.
*
* @return boolean True if the database was successfully selected.
*
* @since 1.0
* @throws \RuntimeException
*/
public function select($database)
{
$this->connect();
return true;
}
/**
* Set the connection to use UTF-8 character encoding.
*
* @return boolean True on success.
*
* @since 1.0
*/
public function setUtf()
{
return false;
}
/**
* Method to commit a transaction.
*
* @param boolean $toSavepoint If true, commit to the last savepoint.
*
* @return void
*
* @since 1.0
* @throws \RuntimeException
*/
public function transactionCommit($toSavepoint = false)
{
$this->connect();
if (!$toSavepoint || $this->transactionDepth === 1) {
$this->connection->commit();
}
$this->transactionDepth--;
}
/**
* Method to roll back a transaction.
*
* @param boolean $toSavepoint If true, rollback to the last savepoint.
*
* @return void
*
* @since 1.0
* @throws \RuntimeException
*/
public function transactionRollback($toSavepoint = false)
{
$this->connect();
if (!$toSavepoint || $this->transactionDepth === 1) {
$this->connection->rollBack();
}
$this->transactionDepth--;
}
/**
* Method to initialize a transaction.
*
* @param boolean $asSavepoint If true and a transaction is already active, a savepoint will be created.
*
* @return void
*
* @since 1.0
* @throws \RuntimeException
*/
public function transactionStart($asSavepoint = false)
{
$this->connect();
if (!$asSavepoint || !$this->transactionDepth) {
$this->connection->beginTransaction();
}
$this->transactionDepth++;
}
/**
* Prepares a SQL statement for execution
*
* @param string $query The SQL query to be prepared.
*
* @return StatementInterface
*
* @since 2.0.0
* @throws PrepareStatementFailureException
*/
protected function prepareStatement(string $query): StatementInterface
{
try {
return new PdoStatement($this->connection->prepare($query, $this->options['driverOptions']));
} catch (\PDOException $exception) {
throw new PrepareStatementFailureException($exception->getMessage(), $exception->getCode(), $exception);
}
}
/**
* PDO does not support serialize
*
* @return array
*
* @since 1.0
*/
public function __sleep()
{
$serializedProperties = [];
$reflect = new \ReflectionClass($this);
// Get properties of the current class
$properties = $reflect->getProperties();
foreach ($properties as $property) {
// Do not serialize properties that are PDO
if ($property->isStatic() === false && !($this->{$property->name} instanceof \PDO)) {
$serializedProperties[] = $property->name;
}
}
return $serializedProperties;
}
/**
* Wake up after serialization
*
* @return void
*
* @since 1.0
*/
public function __wakeup()
{
// Get connection back
$this->__construct($this->options);
}
}

View File

@ -0,0 +1,53 @@
<?php
/**
* Part of the Joomla Framework Database Package
*
* @copyright Copyright (C) 2005 - 2021 Open Source Matters, Inc. All rights reserved.
* @license GNU General Public License version 2 or later; see LICENSE
*/
namespace Joomla\Database\Pdo;
use Joomla\Database\DatabaseQuery;
/**
* PDO Query Building Class.
*
* @since 1.0
*/
abstract class PdoQuery extends DatabaseQuery
{
/**
* The list of zero or null representation of a datetime.
*
* @var array
* @since 2.0.0
*/
protected $nullDatetimeList = ['0000-00-00 00:00:00'];
/**
* Casts a value to a char.
*
* Ensure that the value is properly quoted before passing to the method.
*
* Usage:
* $query->select($query->castAsChar('a'));
* $query->select($query->castAsChar('a', 40));
*
* @param string $value The value to cast as a char.
* @param string $len The length of the char.
*
* @return string Returns the cast value.
*
* @since 1.8.0
*/
public function castAsChar($value, $len = null)
{
if (!$len) {
return $value;
} else {
return 'CAST(' . $value . ' AS CHAR(' . $len . '))';
}
}
}

View File

@ -0,0 +1,243 @@
<?php
/**
* Part of the Joomla Framework Database Package
*
* @copyright Copyright (C) 2005 - 2021 Open Source Matters, Inc. All rights reserved.
* @license GNU General Public License version 2 or later; see LICENSE
*/
namespace Joomla\Database\Pdo;
use Joomla\Database\FetchMode;
use Joomla\Database\FetchOrientation;
use Joomla\Database\ParameterType;
use Joomla\Database\StatementInterface;
/**
* PDO Database Statement.
*
* @since 2.0.0
*/
class PdoStatement implements StatementInterface
{
/**
* Mapping array for fetch modes.
*
* @var array
* @since 2.0.0
*/
private const FETCH_MODE_MAP = [
FetchMode::ASSOCIATIVE => \PDO::FETCH_ASSOC,
FetchMode::NUMERIC => \PDO::FETCH_NUM,
FetchMode::MIXED => \PDO::FETCH_BOTH,
FetchMode::STANDARD_OBJECT => \PDO::FETCH_OBJ,
FetchMode::COLUMN => \PDO::FETCH_COLUMN,
FetchMode::CUSTOM_OBJECT => \PDO::FETCH_CLASS,
];
/**
* Mapping array for parameter types.
*
* @var array
* @since 2.0.0
*/
private const PARAMETER_TYPE_MAP = [
ParameterType::BOOLEAN => \PDO::PARAM_BOOL,
ParameterType::INTEGER => \PDO::PARAM_INT,
ParameterType::LARGE_OBJECT => \PDO::PARAM_LOB,
ParameterType::NULL => \PDO::PARAM_NULL,
ParameterType::STRING => \PDO::PARAM_STR,
];
/**
* The decorated PDOStatement object.
*
* @var \PDOStatement
* @since 2.0.0
*/
protected $pdoStatement;
/**
* Statement constructor
*
* @param \PDOStatement $pdoStatement The decorated PDOStatement object.
*
* @since 2.0.0
*/
public function __construct(\PDOStatement $pdoStatement)
{
$this->pdoStatement = $pdoStatement;
}
/**
* Binds a parameter to the specified variable name.
*
* @param string|integer $parameter Parameter identifier. For a prepared statement using named placeholders, this will be a parameter
* name of the form `:name`. For a prepared statement using question mark placeholders, this will be
* the 1-indexed position of the parameter.
* @param mixed $variable Name of the PHP variable to bind to the SQL statement parameter.
* @param string $dataType Constant corresponding to a SQL datatype, this should be the processed type from the QueryInterface.
* @param integer $length The length of the variable. Usually required for OUTPUT parameters.
* @param array $driverOptions Optional driver options to be used.
*
* @return boolean
*
* @since 2.0.0
*/
public function bindParam($parameter, &$variable, string $dataType = ParameterType::STRING, ?int $length = null, ?array $driverOptions = null)
{
$type = $this->convertParameterType($dataType);
$extraParameters = array_slice(func_get_args(), 3);
if (count($extraParameters) !== 0) {
$extraParameters[0] = $extraParameters[0] ?? 0;
}
$this->pdoStatement->bindParam($parameter, $variable, $type, ...$extraParameters);
return true;
}
/**
* Closes the cursor, enabling the statement to be executed again.
*
* @return void
*
* @since 2.0.0
*/
public function closeCursor(): void
{
$this->pdoStatement->closeCursor();
}
/**
* Fetches the SQLSTATE associated with the last operation on the statement handle.
*
* @return string
*
* @since 2.0.0
*/
public function errorCode()
{
return $this->pdoStatement->errorCode();
}
/**
* Fetches extended error information associated with the last operation on the statement handle.
*
* @return array
*
* @since 2.0.0
*/
public function errorInfo()
{
return $this->pdoStatement->errorInfo();
}
/**
* Executes a prepared statement
*
* @param array|null $parameters An array of values with as many elements as there are bound parameters in the SQL statement being executed.
*
* @return boolean
*
* @since 2.0.0
*/
public function execute(?array $parameters = null)
{
return $this->pdoStatement->execute($parameters);
}
/**
* Fetches the next row from a result set
*
* @param integer|null $fetchStyle Controls how the next row will be returned to the caller. This value must be one of the
* FetchMode constants, defaulting to value of FetchMode::MIXED.
* @param integer $cursorOrientation For a StatementInterface object representing a scrollable cursor, this value determines which row
* will be returned to the caller. This value must be one of the FetchOrientation constants,
* defaulting to FetchOrientation::NEXT.
* @param integer $cursorOffset For a StatementInterface object representing a scrollable cursor for which the cursorOrientation
* parameter is set to FetchOrientation::ABS, this value specifies the absolute number of the row in
* the result set that shall be fetched. For a StatementInterface object representing a scrollable
* cursor for which the cursorOrientation parameter is set to FetchOrientation::REL, this value
* specifies the row to fetch relative to the cursor position before `fetch()` was called.
*
* @return mixed The return value of this function on success depends on the fetch type. In all cases, boolean false is returned on failure.
*
* @since 2.0.0
*/
public function fetch(?int $fetchStyle = null, int $cursorOrientation = FetchOrientation::NEXT, int $cursorOffset = 0)
{
if ($fetchStyle === null) {
return $this->pdoStatement->fetch();
}
return $this->pdoStatement->fetch($this->convertFetchMode($fetchStyle), $cursorOrientation, $cursorOffset);
}
/**
* Returns the number of rows affected by the last SQL statement.
*
* @return integer
*
* @since 2.0.0
*/
public function rowCount(): int
{
return $this->pdoStatement->rowCount();
}
/**
* Sets the fetch mode to use while iterating this statement.
*
* @param integer $fetchMode The fetch mode, must be one of the FetchMode constants.
* @param mixed ...$args Optional mode-specific arguments.
*
* @return void
*
* @since 2.0.0
*/
public function setFetchMode(int $fetchMode, ...$args): void
{
$this->pdoStatement->setFetchMode($this->convertFetchMode($fetchMode), ...$args);
}
/**
* Converts the database API's fetch mode to a PDO fetch mode
*
* @param integer $mode Fetch mode to convert
*
* @return integer
*
* @since 2.0.0
* @throws \InvalidArgumentException if the fetch mode is unsupported
*/
private function convertFetchMode(int $mode): int
{
if (!isset(self::FETCH_MODE_MAP[$mode])) {
throw new \InvalidArgumentException(sprintf('Unsupported fetch mode `%s`', $mode));
}
return self::FETCH_MODE_MAP[$mode];
}
/**
* Converts the database API's parameter type to a PDO parameter type
*
* @param string $type Parameter type to convert
*
* @return integer
*
* @since 2.0.0
* @throws \InvalidArgumentException if the parameter type is unsupported
*/
private function convertParameterType(string $type): int
{
if (!isset(self::PARAMETER_TYPE_MAP[$type])) {
throw new \InvalidArgumentException(sprintf('Unsupported parameter type `%s`', $type));
}
return self::PARAMETER_TYPE_MAP[$type];
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,184 @@
<?php
/**
* Part of the Joomla Framework Database Package
*
* @copyright Copyright (C) 2005 - 2021 Open Source Matters, Inc. All rights reserved.
* @license GNU General Public License version 2 or later; see LICENSE
*/
namespace Joomla\Database\Pgsql;
use Joomla\Database\DatabaseExporter;
/**
* PDO PostgreSQL Database Exporter.
*
* @since 1.5.0
*/
class PgsqlExporter extends DatabaseExporter
{
/**
* Builds the XML data for the tables to export.
*
* @return string An XML string
*
* @since 1.0
* @throws \Exception if an error occurs.
*/
protected function buildXml()
{
$buffer = [];
$buffer[] = '<?xml version="1.0"?>';
$buffer[] = '<postgresqldump xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">';
$buffer[] = ' <database name="">';
if ($this->options->withStructure) {
$buffer = array_merge($buffer, $this->buildXmlStructure());
}
if ($this->options->withData) {
$buffer = array_merge($buffer, $this->buildXmlData());
}
$buffer[] = ' </database>';
$buffer[] = '</postgresqldump>';
return implode("\n", $buffer);
}
/**
* Builds the XML structure to export.
*
* @return array An array of XML lines (strings).
*
* @since 1.0
* @throws \Exception if an error occurs.
*/
protected function buildXmlStructure()
{
$buffer = [];
foreach ($this->from as $table) {
// Replace the magic prefix if found.
$table = $this->getGenericTableName($table);
// Get the details columns information.
$fields = $this->db->getTableColumns($table, false);
$keys = $this->db->getTableKeys($table);
$sequences = $this->db->getTableSequences($table);
$buffer[] = ' <table_structure name="' . $table . '">';
foreach ($sequences as $sequence) {
$buffer[] = ' <sequence Name="' . $this->getGenericTableName($sequence->sequence) . '" Schema="' . $sequence->schema . '"' .
' Table="' . $table . '" Column="' . $sequence->column . '" Type="' . $sequence->data_type . '"' .
' Start_Value="' . $sequence->start_value . '" Min_Value="' . $sequence->minimum_value . '"' .
' Max_Value="' . $sequence->maximum_value . '" Last_Value="' . $this->db->getSequenceLastValue($sequence->sequence) . '"' .
' Increment="' . $sequence->increment . '" Cycle_option="' . $sequence->cycle_option . '"' .
' Is_called="' . $this->db->getSequenceIsCalled($sequence->sequence) . '"' .
' />';
}
foreach ($fields as $field) {
$buffer[] = ' <field Field="' . $field->column_name . '" Type="' . $field->type . '" Null="' . $field->null . '"' .
' Default="' . $field->Default . '" Comments="' . $field->comments . '" />';
}
foreach ($keys as $key) {
$buffer[] = ' <key Index="' . $this->getGenericTableName($key->idxName) . '" is_primary="' . $key->isPrimary . '"' .
' is_unique="' . $key->isUnique . '" Key_name="' . $this->db->getNamesKey($table, $key->indKey) . '"' .
' Query=\'' . $key->Query . '\' />';
}
$buffer[] = ' </table_structure>';
}
return $buffer;
}
/**
* Builds the XML data to export.
*
* @return array An array of XML lines (strings).
*
* @since 2.0.0
* @throws \Exception if an error occurs.
*/
protected function buildXmlData()
{
$buffer = [];
foreach ($this->from as $table) {
// Replace the magic prefix if found.
$table = $this->getGenericTableName($table);
// Get the details columns information.
$fields = $this->db->getTableColumns($table, false);
$colblob = [];
foreach ($fields as $field) {
// Catch blob for xml conversion
// PostgreSQL binary large object type
if ($field->Type == 'bytea') {
$colblob[] = $field->Field;
}
}
$query = $this->db->getQuery(true);
$query->select($query->quoteName(array_keys($fields)))
->from($query->quoteName($table));
$this->db->setQuery($query);
$rows = $this->db->loadObjectList();
if (!count($rows)) {
continue;
}
$buffer[] = ' <table_data name="' . $table . '">';
foreach ($rows as $row) {
$buffer[] = ' <row>';
foreach ($row as $key => $value) {
if (!in_array($key, $colblob)) {
$buffer[] = ' <field name="' . $key . '">' . htmlspecialchars($value, ENT_COMPAT, 'UTF-8') . '</field>';
} else {
$buffer[] = ' <field name="' . $key . '">' . stream_get_contents($value) . '</field>';
}
}
$buffer[] = ' </row>';
}
$buffer[] = ' </table_data>';
}
return $buffer;
}
/**
* Checks if all data and options are in order prior to exporting.
*
* @return $this
*
* @since 1.5.0
* @throws \RuntimeException
*/
public function check()
{
// Check if the db connector has been set.
if (!($this->db instanceof PgsqlDriver)) {
throw new \RuntimeException('Database connection wrong type.');
}
// Check if the tables have been specified.
if (empty($this->from)) {
throw new \RuntimeException('ERROR: No Tables Specified');
}
return $this;
}
}

View File

@ -0,0 +1,555 @@
<?php
/**
* Part of the Joomla Framework Database Package
*
* @copyright Copyright (C) 2005 - 2021 Open Source Matters, Inc. All rights reserved.
* @license GNU General Public License version 2 or later; see LICENSE
*/
namespace Joomla\Database\Pgsql;
use Joomla\Database\DatabaseImporter;
/**
* PDO PostgreSQL Database Importer.
*
* @since 1.5.0
*/
class PgsqlImporter extends DatabaseImporter
{
/**
* Checks if all data and options are in order prior to exporting.
*
* @return $this
*
* @since 1.5.0
* @throws \RuntimeException if an error is encountered.
*/
public function check()
{
// Check if the db connector has been set.
if (!($this->db instanceof PgsqlDriver)) {
throw new \RuntimeException('Database connection wrong type.');
}
// Check if the tables have been specified.
if (empty($this->from)) {
throw new \RuntimeException('ERROR: No Tables Specified');
}
return $this;
}
/**
* Get the SQL syntax to add an index.
*
* @param \SimpleXMLElement $field The XML index definition.
*
* @return string
*
* @since 1.0
*/
protected function getAddIndexSql(\SimpleXMLElement $field)
{
return (string) $field['Query'];
}
/**
* Get alters for table if there is a difference.
*
* @param \SimpleXMLElement $structure The XML structure of the table.
*
* @return array
*
* @since 1.0
*/
protected function getAlterTableSql(\SimpleXMLElement $structure)
{
$table = $this->getRealTableName($structure['name']);
$oldFields = $this->db->getTableColumns($table);
$oldKeys = $this->db->getTableKeys($table);
$oldSequence = $this->db->getTableSequences($table);
$alters = [];
// Get the fields and keys from the XML that we are aiming for.
$newFields = $structure->xpath('field');
$newKeys = $structure->xpath('key');
$newSequence = $structure->xpath('sequence');
/*
* Sequence section
*/
$oldSeq = $this->getSeqLookup($oldSequence);
$newSequenceLook = $this->getSeqLookup($newSequence);
foreach ($newSequenceLook as $kSeqName => $vSeq) {
if (isset($oldSeq[$kSeqName])) {
// The field exists, check it's the same.
$column = $oldSeq[$kSeqName][0];
// Test whether there is a change.
$change = ((string) $vSeq[0]['Type'] !== $column->Type)
|| ((string) $vSeq[0]['Start_Value'] !== $column->Start_Value)
|| ((string) $vSeq[0]['Min_Value'] !== $column->Min_Value)
|| ((string) $vSeq[0]['Max_Value'] !== $column->Max_Value)
|| ((string) $vSeq[0]['Increment'] !== $column->Increment)
|| ((string) $vSeq[0]['Cycle_option'] !== $column->Cycle_option)
|| ((string) $vSeq[0]['Table'] !== $column->Table)
|| ((string) $vSeq[0]['Column'] !== $column->Column)
|| ((string) $vSeq[0]['Schema'] !== $column->Schema)
|| ((string) $vSeq[0]['Name'] !== $column->Name);
if ($change) {
$alters[] = $this->getChangeSequenceSql($kSeqName, $vSeq);
$alters[] = $this->getSetvalSequenceSql($kSeqName, $vSeq);
}
// Unset this field so that what we have left are fields that need to be removed.
unset($oldSeq[$kSeqName]);
} else {
// The sequence is new
$alters[] = $this->getAddSequenceSql($newSequenceLook[$kSeqName][0]);
$alters[] = $this->getSetvalSequenceSql($newSequenceLook[$kSeqName][0]);
}
}
// Any sequences left are orphans
foreach ($oldSeq as $name => $column) {
// Delete the sequence.
$alters[] = $this->getDropSequenceSql($name);
}
/*
* Field section
*/
// Loop through each field in the new structure.
foreach ($newFields as $field) {
$fName = (string) $field['Field'];
if (isset($oldFields[$fName])) {
// The field exists, check it's the same.
$column = $oldFields[$fName];
// Test whether there is a change.
$change = ((string) $field['Type'] !== $column->Type) || ((string) $field['Null'] !== $column->Null)
|| ((string) $field['Default'] !== $column->Default);
if ($change) {
$alters[] = $this->getChangeColumnSql($table, $field);
}
// Unset this field so that what we have left are fields that need to be removed.
unset($oldFields[$fName]);
} else {
// The field is new.
$alters[] = $this->getAddColumnSql($table, $field);
}
}
// Any columns left are orphans
foreach ($oldFields as $name => $column) {
// Delete the column.
$alters[] = $this->getDropColumnSql($table, $name);
}
/*
* Index section
*/
// Get the lookups for the old and new keys
$oldLookup = $this->getKeyLookup($oldKeys);
$newLookup = $this->getKeyLookup($newKeys);
// Loop through each key in the new structure.
foreach ($newLookup as $name => $keys) {
// Check if there are keys on this field in the existing table.
if (isset($oldLookup[$name])) {
$same = true;
$newCount = \count($newLookup[$name]);
$oldCount = \count($oldLookup[$name]);
// There is a key on this field in the old and new tables. Are they the same?
if ($newCount === $oldCount) {
for ($i = 0; $i < $newCount; $i++) {
// Check only query field -> different query means different index
$same = ((string) $newLookup[$name][$i]['Query'] === $oldLookup[$name][$i]->Query);
if (!$same) {
// Break out of the loop. No need to check further.
break;
}
}
} else {
// Count is different, just drop and add.
$same = false;
}
if (!$same) {
$alters[] = $this->getDropIndexSql($name);
$alters[] = (string) $newLookup[$name][0]['Query'];
}
// Unset this field so that what we have left are fields that need to be removed.
unset($oldLookup[$name]);
} else {
// This is a new key.
$alters[] = (string) $newLookup[$name][0]['Query'];
}
}
// Any keys left are orphans.
foreach ($oldLookup as $name => $keys) {
if ($oldLookup[$name][0]->is_primary === 'TRUE') {
$alters[] = $this->getDropPrimaryKeySql($table, $oldLookup[$name][0]->Index);
} else {
$alters[] = $this->getDropIndexSql($name);
}
}
return $alters;
}
/**
* Get the SQL syntax to drop a sequence.
*
* @param string $name The name of the sequence to drop.
*
* @return string
*
* @since 1.0
*/
protected function getDropSequenceSql($name)
{
return 'DROP SEQUENCE ' . $this->db->quoteName($name);
}
/**
* Get the syntax to add a sequence.
*
* @param \SimpleXMLElement $field The XML definition for the sequence.
*
* @return string
*
* @since 1.0
*/
protected function getAddSequenceSql(\SimpleXMLElement $field)
{
$sql = 'CREATE SEQUENCE IF NOT EXISTS ' . (string) $field['Name']
. ' INCREMENT BY ' . (string) $field['Increment'] . ' MINVALUE ' . $field['Min_Value']
. ' MAXVALUE ' . (string) $field['Max_Value'] . ' START ' . (string) $field['Start_Value']
. (((string) $field['Cycle_option'] === 'NO') ? ' NO' : '') . ' CYCLE'
. ' OWNED BY ' . $this->db->quoteName((string) $field['Schema'] . '.' . (string) $field['Table'] . '.' . (string) $field['Column']);
return $sql;
}
/**
* Get the syntax to alter a sequence.
*
* @param \SimpleXMLElement $field The XML definition for the sequence.
*
* @return string
*
* @since 1.0
*/
protected function getChangeSequenceSql(\SimpleXMLElement $field)
{
$sql = 'ALTER SEQUENCE ' . (string) $field['Name']
. ' INCREMENT BY ' . (string) $field['Increment'] . ' MINVALUE ' . (string) $field['Min_Value']
. ' MAXVALUE ' . (string) $field['Max_Value'] . ' START ' . (string) $field['Start_Value']
. ' OWNED BY ' . $this->db->quoteName((string) $field['Schema'] . '.' . (string) $field['Table'] . '.' . (string) $field['Column']);
return $sql;
}
/**
* Get the syntax to setval a sequence.
*
* @param \SimpleXMLElement $field The XML definition for the sequence.
*
* @return string
*
* @since 2.0.0
*/
protected function getSetvalSequenceSql($field)
{
$is_called = $field['Is_called'] == 't' || $field['Is_called'] == '1' ? 'TRUE' : 'FALSE';
return 'SELECT setval(\'' . (string) $field['Name'] . '\', ' . (string) $field['Last_Value'] . ', ' . $is_called . ')';
}
/**
* Get the syntax to alter a column.
*
* @param string $table The name of the database table to alter.
* @param \SimpleXMLElement $field The XML definition for the field.
*
* @return string
*
* @since 1.0
*/
protected function getChangeColumnSql($table, \SimpleXMLElement $field)
{
return 'ALTER TABLE ' . $this->db->quoteName($table) . ' ALTER COLUMN ' . $this->db->quoteName((string) $field['Field']) . ' '
. $this->getAlterColumnSql($table, $field);
}
/**
* Get the SQL syntax for a single column that would be included in a table create statement.
*
* @param string $table The name of the database table to alter.
* @param \SimpleXMLElement $field The XML field definition.
*
* @return string
*
* @since 1.0
*/
protected function getAlterColumnSql($table, \SimpleXMLElement $field)
{
// TODO Incorporate into parent class and use $this.
$blobs = ['text', 'smalltext', 'mediumtext', 'largetext'];
$fName = (string) $field['Field'];
$fType = (string) $field['Type'];
$fNull = (string) $field['Null'];
$fDefault = (isset($field['Default']) && $field['Default'] != 'NULL') ?
preg_match('/^[0-9]$/', $field['Default']) ? $field['Default'] : $this->db->quote((string) $field['Default'])
: null;
$sql = ' TYPE ' . $fType;
if ($fNull === 'NO') {
if ($fDefault === null || \in_array($fType, $blobs, true)) {
$sql .= ",\nALTER COLUMN " . $this->db->quoteName($fName) . ' SET NOT NULL'
. ",\nALTER COLUMN " . $this->db->quoteName($fName) . ' DROP DEFAULT';
} else {
$sql .= ",\nALTER COLUMN " . $this->db->quoteName($fName) . ' SET NOT NULL'
. ",\nALTER COLUMN " . $this->db->quoteName($fName) . ' SET DEFAULT ' . $fDefault;
}
} else {
if ($fDefault !== null) {
$sql .= ",\nALTER COLUMN " . $this->db->quoteName($fName) . ' DROP NOT NULL'
. ",\nALTER COLUMN " . $this->db->quoteName($fName) . ' SET DEFAULT ' . $fDefault;
}
}
// Sequence was created in other function, here is associated a default value but not yet owner
if (strpos($fDefault, 'nextval') !== false) {
$sequence = $table . '_' . $fName . '_seq';
$owner = $table . '.' . $fName;
$sql .= ";\nALTER SEQUENCE " . $this->db->quoteName($sequence) . ' OWNED BY ' . $this->db->quoteName($owner);
}
return $sql;
}
/**
* Get the SQL syntax for a single column that would be included in a table create statement.
*
* @param \SimpleXMLElement $field The XML field definition.
*
* @return string
*
* @since 1.0
*/
protected function getColumnSql(\SimpleXMLElement $field)
{
$fName = (string) $field['Field'];
$fType = (string) $field['Type'];
$fNull = (string) $field['Null'];
if (strpos($field['Default'], '::') != false) {
$fDefault = strstr($field['Default'], '::', true);
} else {
$fDefault = isset($field['Default']) && strlen($field['Default']) > 0
? preg_match('/^[0-9]$/', $field['Default']) ? $field['Default'] : $this->db->quote((string) $field['Default'])
: null;
}
// Note, nextval() as default value means that type field is serial.
if (strpos($fDefault, 'nextval') !== false) {
$sql = $this->db->quoteName($fName) . ' SERIAL';
} else {
$sql = $this->db->quoteName($fName) . ' ' . $fType;
if ($fNull == 'NO') {
if ($fDefault === null) {
$sql .= ' NOT NULL';
} else {
$sql .= ' NOT NULL DEFAULT ' . $fDefault;
}
} else {
if ($fDefault !== null) {
$sql .= ' DEFAULT ' . $fDefault;
}
}
}
return $sql;
}
/**
* Get the SQL syntax to drop an index.
*
* @param string $name The name of the key to drop.
*
* @return string
*
* @since 1.0
*/
protected function getDropIndexSql($name)
{
return 'DROP INDEX ' . $this->db->quoteName($name);
}
/**
* Get the SQL syntax to drop a key.
*
* @param string $table The table name.
* @param string $name The constraint name.
*
* @return string
*
* @since 1.0
*/
protected function getDropPrimaryKeySql($table, $name)
{
return 'ALTER TABLE ONLY ' . $this->db->quoteName($table) . ' DROP CONSTRAINT ' . $this->db->quoteName($name);
}
/**
* Get the details list of keys for a table.
*
* @param array $keys An array of objects that comprise the keys for the table.
*
* @return array The lookup array. array({key name} => array(object, ...))
*
* @since 1.2.0
*/
protected function getKeyLookup($keys)
{
// First pass, create a lookup of the keys.
$lookup = [];
foreach ($keys as $key) {
if ($key instanceof \SimpleXMLElement) {
$kName = (string) $key['Index'];
} else {
$kName = $key->Index;
}
if (empty($lookup[$kName])) {
$lookup[$kName] = [];
}
$lookup[$kName][] = $key;
}
return $lookup;
}
/**
* Get the SQL syntax to add a unique constraint for a table key.
*
* @param string $table The table name.
* @param array $key The key.
*
* @return string
*
* @since 2.0.0
*/
protected function getAddUniqueSql($table, $key)
{
if ($key instanceof \SimpleXMLElement) {
$kName = (string) $key['Key_name'];
$kIndex = (string) $key['Index'];
} else {
$kName = $key->Key_name;
$kIndex = $key->Index;
}
$unique = $kIndex . ' UNIQUE (' . $kName . ')';
return 'ALTER TABLE ' . $this->db->quoteName($table) . ' ADD CONSTRAINT ' . $unique;
}
/**
* Get the details list of sequences for a table.
*
* @param array $sequences An array of objects that comprise the sequences for the table.
*
* @return array The lookup array. array({key name} => array(object, ...))
*
* @since 1.0
*/
protected function getSeqLookup($sequences)
{
// First pass, create a lookup of the keys.
$lookup = [];
foreach ($sequences as $seq) {
if ($seq instanceof \SimpleXMLElement) {
$sName = (string) $seq['Name'];
} else {
$sName = $seq->Name;
}
if (empty($lookup[$sName])) {
$lookup[$sName] = [];
}
$lookup[$sName][] = $seq;
}
return $lookup;
}
/**
* Get the SQL syntax to add a table.
*
* @param \SimpleXMLElement $table The table information.
*
* @return string
*
* @since 2.0.0
* @throws \RuntimeException
*/
protected function xmlToCreate(\SimpleXMLElement $table)
{
$existingTables = $this->db->getTableList();
$tableName = (string) $table['name'];
if (in_array($tableName, $existingTables)) {
throw new \RuntimeException('The table you are trying to create already exists');
}
$createTableStatement = 'CREATE TABLE ' . $this->db->quoteName($tableName) . ' (';
foreach ($table->xpath('field') as $field) {
$createTableStatement .= $this->getColumnSql($field) . ', ';
}
$createTableStatement = rtrim($createTableStatement, ', ');
$createTableStatement .= ');';
foreach ($table->xpath('sequence') as $seq) {
$createTableStatement .= $this->getAddSequenceSql($seq) . ';';
$createTableStatement .= $this->getSetvalSequenceSql($seq) . ';';
}
foreach ($table->xpath('key') as $key) {
if ((($key['is_primary'] == 'f') || ($key['is_primary'] == '')) && (($key['is_unique'] == 't') || ($key['is_unique'] == '1'))) {
$createTableStatement .= $this->getAddUniqueSql($tableName, $key) . ';';
} else {
$createTableStatement .= $this->getAddIndexSql($key) . ';';
}
}
return $createTableStatement;
}
}

View File

@ -0,0 +1,62 @@
<?php
/**
* Part of the Joomla Framework Database Package
*
* @copyright Copyright (C) 2005 - 2021 Open Source Matters, Inc. All rights reserved.
* @license GNU General Public License version 2 or later; see LICENSE
*/
namespace Joomla\Database\Pgsql;
use Joomla\Database\Pdo\PdoQuery;
use Joomla\Database\Query\PostgresqlQueryBuilder;
use Joomla\Database\Query\QueryElement;
/**
* PDO PostgreSQL Query Building Class.
*
* @since 1.0
*
* @property-read QueryElement $forUpdate The FOR UPDATE element used in "FOR UPDATE" lock
* @property-read QueryElement $forShare The FOR SHARE element used in "FOR SHARE" lock
* @property-read QueryElement $noWait The NOWAIT element used in "FOR SHARE" and "FOR UPDATE" lock
* @property-read QueryElement $returning The RETURNING element of INSERT INTO
*/
class PgsqlQuery extends PdoQuery
{
use PostgresqlQueryBuilder;
/**
* The list of zero or null representation of a datetime.
*
* @var array
* @since 2.0.0
*/
protected $nullDatetimeList = ['1970-01-01 00:00:00'];
/**
* Casts a value to a char.
*
* Ensure that the value is properly quoted before passing to the method.
*
* Usage:
* $query->select($query->castAsChar('a'));
* $query->select($query->castAsChar('a', 40));
*
* @param string $value The value to cast as a char.
* @param string $length The length of the char.
*
* @return string Returns the cast value.
*
* @since 1.8.0
*/
public function castAsChar($value, $length = null)
{
if ((int) $length < 1) {
return $value . '::text';
}
return 'CAST(' . $value . ' AS CHAR(' . $length . '))';
}
}

View File

@ -0,0 +1,61 @@
<?php
/**
* Part of the Joomla Framework Database Package
*
* @copyright Copyright (C) 2005 - 2021 Open Source Matters, Inc. All rights reserved.
* @license GNU General Public License version 2 or later; see LICENSE
*/
namespace Joomla\Database\Query;
use Joomla\Database\QueryInterface;
// phpcs:disable PSR1.Files.SideEffects
trigger_deprecation(
'joomla/database',
'2.0.0',
'%s() is deprecated and will be removed in 3.0, all query objects should implement %s instead.',
LimitableInterface::class,
QueryInterface::class
);
// phpcs:enable PSR1.Files.SideEffects
/**
* Joomla Database Query LimitableInterface.
*
* @since 1.0
* @deprecated 3.0 Capabilities will be required in Joomla\Database\QueryInterface
*/
interface LimitableInterface
{
/**
* Method to modify a query already in string format with the needed additions to make the query limited to a particular number of
* results, or start at a particular offset.
*
* @param string $query The query in string format
* @param integer $limit The limit for the result set
* @param integer $offset The offset for the result set
*
* @return string
*
* @since 1.0
*/
public function processLimit($query, $limit, $offset = 0);
/**
* Sets the offset and limit for the result set, if the database driver supports it.
*
* Usage:
* $query->setLimit(100, 0); (retrieve 100 rows, starting at first record)
* $query->setLimit(50, 50); (retrieve 50 rows, starting at 50th record)
*
* @param integer $limit The limit for the result set
* @param integer $offset The offset for the result set
*
* @return $this
*
* @since 1.0
*/
public function setLimit($limit = 0, $offset = 0);
}

View File

@ -0,0 +1,254 @@
<?php
/**
* Part of the Joomla Framework Database Package
*
* @copyright Copyright (C) 2005 - 2021 Open Source Matters, Inc. All rights reserved.
* @license GNU General Public License version 2 or later; see LICENSE
*/
namespace Joomla\Database\Query;
/**
* Trait for MySQL Query Building.
*
* @since 2.0.0
*/
trait MysqlQueryBuilder
{
/**
* Magic function to convert the query to a string.
*
* @return string The completed query.
*
* @since 2.0.0
*/
public function __toString()
{
switch ($this->type) {
case 'select':
if ($this->selectRowNumber) {
$orderBy = $this->selectRowNumber['orderBy'];
$tmpOffset = $this->offset;
$tmpLimit = $this->limit;
$this->offset = 0;
$this->limit = 0;
$tmpOrder = $this->order;
$this->order = null;
$query = parent::__toString();
$this->order = $tmpOrder;
$this->offset = $tmpOffset;
$this->limit = $tmpLimit;
// Add support for second order by, offset and limit
$query = PHP_EOL . 'SELECT * FROM (' . $query . PHP_EOL . "ORDER BY $orderBy" . PHP_EOL . ') w';
if ($this->order) {
$query .= (string) $this->order;
}
return $this->processLimit($query, $this->limit, $this->offset);
}
}
return parent::__toString();
}
/**
* Method to modify a query already in string format with the needed additions to make the query limited to a particular number of
* results, or start at a particular offset.
*
* @param string $query The query in string format
* @param integer $limit The limit for the result set
* @param integer $offset The offset for the result set
*
* @return string
*
* @since 2.0.0
*/
public function processLimit($query, $limit, $offset = 0)
{
if ($limit > 0 && $offset > 0) {
$query .= ' LIMIT ' . $offset . ', ' . $limit;
} elseif ($limit > 0) {
$query .= ' LIMIT ' . $limit;
}
return $query;
}
/**
* Concatenates an array of column names or values.
*
* @param string[] $values An array of values to concatenate.
* @param string|null $separator As separator to place between each value.
*
* @return string The concatenated values.
*
* @since 2.0.0
*/
public function concatenate($values, $separator = null)
{
if ($separator !== null) {
$statement = 'CONCAT_WS(' . $this->quote($separator);
foreach ($values as $value) {
$statement .= ', ' . $value;
}
return $statement . ')';
}
return 'CONCAT(' . implode(',', $values) . ')';
}
/**
* Aggregate function to get input values concatenated into a string, separated by delimiter
*
* Usage:
* $query->groupConcat('id', ',');
*
* @param string $expression The expression to apply concatenation to, this may be a column name or complex SQL statement.
* @param string $separator The delimiter of each concatenated value
*
* @return string Input values concatenated into a string, separated by delimiter
*
* @since 2.0.0
*/
public function groupConcat($expression, $separator = ',')
{
return 'GROUP_CONCAT(' . $expression . ' SEPARATOR ' . $this->quote($separator) . ')';
}
/**
* Method to quote and optionally escape a string to database requirements for insertion into the database.
*
* This method is provided for use where the query object is passed to a function for modification.
* If you have direct access to the database object, it is recommended you use the quote method directly.
*
* Note that 'q' is an alias for this method as it is in DatabaseDriver.
*
* Usage:
* $query->quote('fulltext');
* $query->q('fulltext');
* $query->q(array('option', 'fulltext'));
*
* @param array|string $text A string or an array of strings to quote.
* @param boolean $escape True (default) to escape the string, false to leave it unchanged.
*
* @return string The quoted input string.
*
* @since 2.0.0
* @throws \RuntimeException if the internal db property is not a valid object.
*/
abstract public function quote($text, $escape = true);
/**
* Get the regular expression operator
*
* Usage:
* $query->where('field ' . $query->regexp($search));
*
* @param string $value The regex pattern.
*
* @return string
*
* @since 2.0.0
*/
public function regexp($value)
{
return ' REGEXP ' . $value;
}
/**
* Get the function to return a random floating-point value
*
* Usage:
* $query->rand();
*
* @return string
*
* @since 2.0.0
*/
public function rand()
{
return ' RAND() ';
}
/**
* Find a value in a varchar used like a set.
*
* Ensure that the value is an integer before passing to the method.
*
* Usage:
* $query->findInSet((int) $parent->id, 'a.assigned_cat_ids')
*
* @param string $value The value to search for.
* @param string $set The set of values.
*
* @return string A representation of the MySQL find_in_set() function for the driver.
*
* @since 2.0.0
*/
public function findInSet($value, $set)
{
return ' find_in_set(' . $value . ', ' . $set . ')';
}
/**
* Return the number of the current row.
*
* Usage:
* $query->select('id');
* $query->selectRowNumber('ordering,publish_up DESC', 'new_ordering');
* $query->from('#__content');
*
* @param string $orderBy An expression of ordering for window function.
* @param string $orderColumnAlias An alias for new ordering column.
*
* @return $this
*
* @since 2.0.0
* @throws \RuntimeException
*/
public function selectRowNumber($orderBy, $orderColumnAlias)
{
$this->validateRowNumber($orderBy, $orderColumnAlias);
return $this->select("(SELECT @rownum := @rownum + 1 FROM (SELECT @rownum := 0) AS r) AS $orderColumnAlias");
}
/**
* Casts a value to a char.
*
* Ensure that the value is properly quoted before passing to the method.
*
* Usage:
* $query->select($query->castAs('CHAR', 'a'));
*
* @param string $type The type of string to cast as.
* @param string $value The value to cast as a char.
* @param string $length The value to cast as a char.
*
* @return string SQL statement to cast the value as a char type.
*
* @since 1.0
*/
public function castAs(string $type, string $value, ?string $length = null)
{
switch (strtoupper($type)) {
case 'CHAR':
if (!$length) {
return $value;
} else {
return 'CAST(' . $value . ' AS CHAR(' . $length . '))';
}
// No break
case 'INT':
return '(' . $value . ' + 0)';
}
return parent::castAs($type, $value, $length);
}
}

View File

@ -0,0 +1,696 @@
<?php
/**
* Part of the Joomla Framework Database Package
*
* @copyright Copyright (C) 2005 - 2021 Open Source Matters, Inc. All rights reserved.
* @license GNU General Public License version 2 or later; see LICENSE
*/
namespace Joomla\Database\Query;
/**
* Trait for PostgreSQL Query Building.
*
* @since 2.0.0
*/
trait PostgresqlQueryBuilder
{
/**
* The FOR UPDATE element used in "FOR UPDATE" lock
*
* @var QueryElement
* @since 2.0.0
*/
protected $forUpdate;
/**
* The FOR SHARE element used in "FOR SHARE" lock
*
* @var QueryElement
* @since 2.0.0
*/
protected $forShare;
/**
* The NOWAIT element used in "FOR SHARE" and "FOR UPDATE" lock
*
* @var QueryElement
* @since 2.0.0
*/
protected $noWait;
/**
* The LIMIT element
*
* @var QueryElement
* @since 2.0.0
*/
protected $limit;
/**
* The OFFSET element
*
* @var QueryElement
* @since 2.0.0
*/
protected $offset;
/**
* The RETURNING element of INSERT INTO
*
* @var QueryElement
* @since 2.0.0
*/
protected $returning;
/**
* Magic function to convert the query to a string, only for PostgreSQL specific queries
*
* @return string The completed query.
*
* @since 2.0.0
*/
public function __toString()
{
$query = '';
switch ($this->type) {
case 'select':
$query .= (string) $this->select;
$query .= (string) $this->from;
if ($this->join) {
// Special case for joins
foreach ($this->join as $join) {
$query .= (string) $join;
}
}
if ($this->where) {
$query .= (string) $this->where;
}
if ($this->selectRowNumber) {
if ($this->order) {
$query .= (string) $this->order;
}
break;
}
if ($this->group) {
$query .= (string) $this->group;
}
if ($this->having) {
$query .= (string) $this->having;
}
if ($this->merge) {
// Special case for merge
foreach ($this->merge as $element) {
$query .= (string) $element;
}
}
if ($this->order) {
$query .= (string) $this->order;
}
if ($this->forUpdate) {
$query .= (string) $this->forUpdate;
} else {
if ($this->forShare) {
$query .= (string) $this->forShare;
}
}
if ($this->noWait) {
$query .= (string) $this->noWait;
}
$query = $this->processLimit($query, $this->limit, $this->offset);
break;
case 'update':
$query .= (string) $this->update;
$query .= (string) $this->set;
if ($this->join) {
$tmpFrom = $this->from;
$tmpWhere = $this->where ? clone $this->where : null;
$this->from = null;
// Workaround for special case of JOIN with UPDATE
foreach ($this->join as $join) {
$joinElem = $join->getElements();
$this->from($joinElem[0]);
if (isset($joinElem[1])) {
$this->where($joinElem[1]);
}
}
$query .= (string) $this->from;
if ($this->where) {
$query .= (string) $this->where;
}
$this->from = $tmpFrom;
$this->where = $tmpWhere;
} elseif ($this->where) {
$query .= (string) $this->where;
}
$query = $this->processLimit($query, $this->limit, $this->offset);
break;
case 'insert':
$query .= (string) $this->insert;
if ($this->values) {
if ($this->columns) {
$query .= (string) $this->columns;
}
$elements = $this->values->getElements();
if (!($elements[0] instanceof $this)) {
$query .= ' VALUES ';
}
$query .= (string) $this->values;
if ($this->returning) {
$query .= (string) $this->returning;
}
}
$query = $this->processLimit($query, $this->limit, $this->offset);
break;
default:
$query = parent::__toString();
break;
}
if ($this->type === 'select' && $this->alias !== null) {
$query = '(' . $query . ') AS ' . $this->alias;
}
return $query;
}
/**
* Clear data from the query or a specific clause of the query.
*
* @param string $clause Optionally, the name of the clause to clear, or nothing to clear the whole query.
*
* @return $this
*
* @since 2.0.0
*/
public function clear($clause = null)
{
switch ($clause) {
case 'limit':
$this->limit = null;
break;
case 'offset':
$this->offset = null;
break;
case 'forUpdate':
$this->forUpdate = null;
break;
case 'forShare':
$this->forShare = null;
break;
case 'noWait':
$this->noWait = null;
break;
case 'returning':
$this->returning = null;
break;
case 'select':
case 'update':
case 'delete':
case 'insert':
case 'querySet':
case 'from':
case 'join':
case 'set':
case 'where':
case 'group':
case 'having':
case 'merge':
case 'order':
case 'columns':
case 'values':
parent::clear($clause);
break;
default:
$this->forUpdate = null;
$this->forShare = null;
$this->noWait = null;
$this->returning = null;
parent::clear($clause);
break;
}
return $this;
}
/**
* Casts a value to a char.
*
* Ensure that the value is properly quoted before passing to the method.
*
* Usage:
* $query->select($query->castAs('CHAR', 'a'));
*
* @param string $type The type of string to cast as.
* @param string $value The value to cast as a char.
* @param ?string $length The value to cast as a char.
*
* @return string SQL statement to cast the value as a char type.
*
* @since 1.0
*/
public function castAs(string $type, string $value, ?string $length = null)
{
switch (strtoupper($type)) {
case 'CHAR':
if (!$length) {
return $value . '::text';
} else {
return 'CAST(' . $value . ' AS CHAR(' . $length . '))';
}
// No break
case 'INT':
return 'CAST(' . $value . ' AS INTEGER)';
}
return parent::castAs($type, $value, $length);
}
/**
* Concatenates an array of column names or values.
*
* Usage:
* $query->select($query->concatenate(array('a', 'b')));
*
* @param string[] $values An array of values to concatenate.
* @param string|null $separator As separator to place between each value.
*
* @return string The concatenated values.
*
* @since 2.0.0
*/
public function concatenate($values, $separator = null)
{
if ($separator !== null) {
return implode(' || ' . $this->quote($separator) . ' || ', $values);
}
return implode(' || ', $values);
}
/**
* Gets the current date and time.
*
* @return string Return string used in query to obtain
*
* @since 2.0.0
*/
public function currentTimestamp()
{
return 'NOW()';
}
/**
* Sets the FOR UPDATE lock on select's output row
*
* @param string $tableName The table to lock
* @param string $glue The glue by which to join the conditions. Defaults to ',' .
*
* @return $this
*
* @since 2.0.0
*/
public function forUpdate($tableName, $glue = ',')
{
$this->type = 'forUpdate';
if ($this->forUpdate === null) {
$glue = strtoupper($glue);
$this->forUpdate = new QueryElement('FOR UPDATE', 'OF ' . $tableName, "$glue ");
} else {
$this->forUpdate->append($tableName);
}
return $this;
}
/**
* Sets the FOR SHARE lock on select's output row
*
* @param string $tableName The table to lock
* @param string $glue The glue by which to join the conditions. Defaults to ',' .
*
* @return $this
*
* @since 2.0.0
*/
public function forShare($tableName, $glue = ',')
{
$this->type = 'forShare';
if ($this->forShare === null) {
$glue = strtoupper($glue);
$this->forShare = new QueryElement('FOR SHARE', 'OF ' . $tableName, "$glue ");
} else {
$this->forShare->append($tableName);
}
return $this;
}
/**
* Aggregate function to get input values concatenated into a string, separated by delimiter
*
* Usage:
* $query->groupConcat('id', ',');
*
* @param string $expression The expression to apply concatenation to, this may be a column name or complex SQL statement.
* @param string $separator The delimiter of each concatenated value
*
* @return string Input values concatenated into a string, separated by delimiter
*
* @since 2.0.0
*/
public function groupConcat($expression, $separator = ',')
{
return 'string_agg(' . $expression . ', ' . $this->quote($separator) . ')';
}
/**
* Used to get a string to extract year from date column.
*
* Usage:
* $query->select($query->year($query->quoteName('dateColumn')));
*
* @param string $date Date column containing year to be extracted.
*
* @return string Returns string to extract year from a date.
*
* @since 2.0.0
*/
public function year($date)
{
return 'EXTRACT (YEAR FROM ' . $date . ')';
}
/**
* Used to get a string to extract month from date column.
*
* Usage:
* $query->select($query->month($query->quoteName('dateColumn')));
*
* @param string $date Date column containing month to be extracted.
*
* @return string Returns string to extract month from a date.
*
* @since 2.0.0
*/
public function month($date)
{
return 'EXTRACT (MONTH FROM ' . $date . ')';
}
/**
* Used to get a string to extract day from date column.
*
* Usage:
* $query->select($query->day($query->quoteName('dateColumn')));
*
* @param string $date Date column containing day to be extracted.
*
* @return string Returns string to extract day from a date.
*
* @since 2.0.0
*/
public function day($date)
{
return 'EXTRACT (DAY FROM ' . $date . ')';
}
/**
* Used to get a string to extract hour from date column.
*
* Usage:
* $query->select($query->hour($query->quoteName('dateColumn')));
*
* @param string $date Date column containing hour to be extracted.
*
* @return string Returns string to extract hour from a date.
*
* @since 2.0.0
*/
public function hour($date)
{
return 'EXTRACT (HOUR FROM ' . $date . ')';
}
/**
* Used to get a string to extract minute from date column.
*
* Usage:
* $query->select($query->minute($query->quoteName('dateColumn')));
*
* @param string $date Date column containing minute to be extracted.
*
* @return string Returns string to extract minute from a date.
*
* @since 2.0.0
*/
public function minute($date)
{
return 'EXTRACT (MINUTE FROM ' . $date . ')';
}
/**
* Used to get a string to extract seconds from date column.
*
* Usage:
* $query->select($query->second($query->quoteName('dateColumn')));
*
* @param string $date Date column containing second to be extracted.
*
* @return string Returns string to extract second from a date.
*
* @since 2.0.0
*/
public function second($date)
{
return 'EXTRACT (SECOND FROM ' . $date . ')';
}
/**
* Sets the NOWAIT lock on select's output row
*
* @return $this
*
* @since 2.0.0
*/
public function noWait()
{
$this->type = 'noWait';
if ($this->noWait === null) {
$this->noWait = new QueryElement('NOWAIT', null);
}
return $this;
}
/**
* Set the LIMIT clause to the query
*
* @param integer $limit Number of rows to return
*
* @return $this
*
* @since 2.0.0
*/
public function limit($limit = 0)
{
if ($this->limit === null) {
$this->limit = new QueryElement('LIMIT', (int) $limit);
}
return $this;
}
/**
* Set the OFFSET clause to the query
*
* @param integer $offset An integer for skipping rows
*
* @return $this
*
* @since 2.0.0
*/
public function offset($offset = 0)
{
if ($this->offset === null) {
$this->offset = new QueryElement('OFFSET', (int) $offset);
}
return $this;
}
/**
* Add the RETURNING element to INSERT INTO statement.
*
* @param mixed $pkCol The name of the primary key column.
*
* @return $this
*
* @since 2.0.0
*/
public function returning($pkCol)
{
if ($this->returning === null) {
$this->returning = new QueryElement('RETURNING', $pkCol);
}
return $this;
}
/**
* Method to modify a query already in string format with the needed additions to make the query limited to a particular number of
* results, or start at a particular offset.
*
* @param string $query The query in string format
* @param integer $limit The limit for the result set
* @param integer $offset The offset for the result set
*
* @return string
*
* @since 2.0.0
*/
public function processLimit($query, $limit, $offset = 0)
{
if ($limit > 0) {
$query .= ' LIMIT ' . $limit;
}
if ($offset > 0) {
$query .= ' OFFSET ' . $offset;
}
return $query;
}
/**
* Add to the current date and time.
*
* Usage:
* $query->select($query->dateAdd());
*
* Prefixing the interval with a - (negative sign) will cause subtraction to be used.
*
* @param string $date The db quoted string representation of the date to add to
* @param string $interval The string representation of the appropriate number of units
* @param string $datePart The part of the date to perform the addition on
*
* @return string The string with the appropriate sql for addition of dates
*
* @since 2.0.0
* @link http://www.postgresql.org/docs/9.0/static/functions-datetime.html.
*/
public function dateAdd($date, $interval, $datePart)
{
if (substr($interval, 0, 1) !== '-') {
return 'timestamp ' . $date . " + interval '" . $interval . ' ' . $datePart . "'";
}
return 'timestamp ' . $date . " - interval '" . ltrim($interval, '-') . ' ' . $datePart . "'";
}
/**
* Get the regular expression operator
*
* Usage:
* $query->where('field ' . $query->regexp($search));
*
* @param string $value The regex pattern.
*
* @return string
*
* @since 2.0.0
*/
public function regexp($value)
{
return ' ~* ' . $value;
}
/**
* Get the function to return a random floating-point value
*
* Usage:
* $query->rand();
*
* @return string
*
* @since 2.0.0
*/
public function rand()
{
return ' RANDOM() ';
}
/**
* Find a value in a varchar used like a set.
*
* Ensure that the value is an integer before passing to the method.
*
* Usage:
* $query->findInSet((int) $parent->id, 'a.assigned_cat_ids')
*
* @param string $value The value to search for.
* @param string $set The set of values.
*
* @return string A representation of the MySQL find_in_set() function for the driver.
*
* @since 2.0.0
*/
public function findInSet($value, $set)
{
return " $value = ANY (string_to_array($set, ',')::integer[]) ";
}
}

View File

@ -0,0 +1,75 @@
<?php
/**
* Part of the Joomla Framework Database Package
*
* @copyright Copyright (C) 2005 - 2021 Open Source Matters, Inc. All rights reserved.
* @license GNU General Public License version 2 or later; see LICENSE
*/
namespace Joomla\Database\Query;
use Joomla\Database\ParameterType;
use Joomla\Database\QueryInterface;
// phpcs:disable PSR1.Files.SideEffects
trigger_deprecation(
'joomla/database',
'2.0.0',
'%s() is deprecated and will be removed in 3.0, all query objects should implement %s instead.',
PreparableInterface::class,
QueryInterface::class
);
// phpcs:enable PSR1.Files.SideEffects
/**
* Joomla Database Query Preparable Interface.
*
* Adds bind/unbind methods as well as a getBounded() method to retrieve the stored bounded variables on demand prior to query execution.
*
* @since 1.0
* @deprecated 3.0 Capabilities will be required in Joomla\Database\QueryInterface
*/
interface PreparableInterface
{
/**
* Method to add a variable to an internal array that will be bound to a prepared SQL statement before query execution.
*
* @param array|string|integer $key The key that will be used in your SQL query to reference the value. Usually of
* the form ':key', but can also be an integer.
* @param mixed $value The value that will be bound. It can be an array, in this case it has to be
* same length of $key; The value is passed by reference to support output
* parameters such as those possible with stored procedures.
* @param array|string $dataType Constant corresponding to a SQL datatype. It can be an array, in this case it
* has to be same length of $key
* @param integer $length The length of the variable. Usually required for OUTPUT parameters.
* @param array $driverOptions Optional driver options to be used.
*
* @return $this
*
* @since 1.0
*/
public function bind($key, &$value, $dataType = ParameterType::STRING, $length = 0, $driverOptions = []);
/**
* Method to unbind a bound variable.
*
* @param array|string|integer $key The key or array of keys to unbind.
*
* @return $this
*
* @since 2.0.0
*/
public function unbind($key);
/**
* Retrieves the bound parameters array when key is null and returns it by reference. If a key is provided then that item is returned.
*
* @param mixed $key The bounded variable key to retrieve.
*
* @return mixed
*
* @since 1.0
*/
public function &getBounded($key = null);
}

View File

@ -0,0 +1,170 @@
<?php
/**
* Part of the Joomla Framework Database Package
*
* @copyright Copyright (C) 2005 - 2021 Open Source Matters, Inc. All rights reserved.
* @license GNU General Public License version 2 or later; see LICENSE
*/
namespace Joomla\Database\Query;
/**
* Query Element Class.
*
* @since 1.0
*/
class QueryElement
{
/**
* The name of the element.
*
* @var string
* @since 1.0
*/
protected $name;
/**
* An array of elements.
*
* @var string[]
* @since 1.0
*/
protected $elements = [];
/**
* Glue piece.
*
* @var string
* @since 1.0
*/
protected $glue;
/**
* Constructor.
*
* @param string $name The name of the element.
* @param string[]|string $elements String or array.
* @param string $glue The glue for elements.
*
* @since 1.0
*/
public function __construct($name, $elements, $glue = ',')
{
$this->name = $name;
$this->glue = $glue;
$this->append($elements);
}
/**
* Magic function to convert the query element to a string.
*
* @return string
*
* @since 1.0
*/
public function __toString()
{
if (substr($this->name, -2) === '()') {
return \PHP_EOL . substr($this->name, 0, -2) . '(' . implode($this->glue, $this->elements) . ')';
}
return \PHP_EOL . $this->name . ' ' . implode($this->glue, $this->elements);
}
/**
* Appends element parts to the internal list.
*
* @param string[]|string $elements String or array.
*
* @return void
*
* @since 1.0
*/
public function append($elements)
{
if (\is_array($elements)) {
$this->elements = array_merge($this->elements, $elements);
} else {
$this->elements = array_merge($this->elements, [$elements]);
}
}
/**
* Gets the elements of this element.
*
* @return string[]
*
* @since 1.0
*/
public function getElements()
{
return $this->elements;
}
/**
* Gets the glue of this element.
*
* @return string Glue of the element.
*
* @since 2.0.0
*/
public function getGlue()
{
return $this->glue;
}
/**
* Gets the name of this element.
*
* @return string Name of the element.
*
* @since 1.7.0
*/
public function getName()
{
return $this->name;
}
/**
* Sets the name of this element.
*
* @param string $name Name of the element.
*
* @return $this
*
* @since 1.3.0
*/
public function setName($name)
{
$this->name = $name;
return $this;
}
/**
* Method to provide basic copy support.
*
* Any object pushed into the data of this class should have its own __clone() implementation.
* This method does not support copying objects in a multidimensional array.
*
* @return void
*
* @since 1.0
*/
public function __clone()
{
foreach ($this as $k => $v) {
if (\is_object($v)) {
$this->{$k} = clone $v;
} elseif (\is_array($v)) {
foreach ($v as $i => $element) {
if (\is_object($element)) {
$this->{$k}[$i] = clone $element;
}
}
}
}
}
}

View File

@ -0,0 +1,725 @@
<?php
/**
* Part of the Joomla Framework Database Package
*
* @copyright Copyright (C) 2005 - 2021 Open Source Matters, Inc. All rights reserved.
* @license GNU General Public License version 2 or later; see LICENSE
*/
namespace Joomla\Database;
use Joomla\Database\Exception\QueryTypeAlreadyDefinedException;
use Joomla\Database\Query\LimitableInterface;
use Joomla\Database\Query\PreparableInterface;
use Joomla\Database\Exception\UnknownTypeException;
/**
* Joomla Framework Query Building Interface.
*
* @since 2.0.0
*/
interface QueryInterface extends PreparableInterface, LimitableInterface
{
/**
* Convert the query object to a string.
*
* @return string
*
* @since 2.0.0
*/
public function __toString();
/**
* Add a single column, or array of columns to the CALL clause of the query.
*
* Usage:
* $query->call('a.*')->call('b.id');
* $query->call(array('a.*', 'b.id'));
*
* @param array|string $columns A string or an array of field names.
*
* @return $this
*
* @since 2.0.0
* @throws QueryTypeAlreadyDefinedException if the query type has already been defined
*/
public function call($columns);
/**
* Casts a value to a specified type.
*
* Ensure that the value is properly quoted before passing to the method.
*
* Usage:
* $query->select($query->castAs('CHAR', 'a'));
*
* @param string $type The type of string to cast as.
* @param string $value The value to cast as a char.
* @param string $length Optionally specify the length of the field (if the type supports it otherwise
* ignored).
*
* @return string SQL statement to cast the value as a char type.
*
* @since 2.0.0
* @throws UnknownTypeException When unsupported cast for a database driver
*/
public function castAs(string $type, string $value, ?string $length = null);
/**
* Gets the number of characters in a string.
*
* Note, use 'length' to find the number of bytes in a string.
*
* Usage:
* $query->select($query->charLength('a'));
*
* @param string $field A value.
* @param string|null $operator Comparison operator between charLength integer value and $condition
* @param string|null $condition Integer value to compare charLength with.
*
* @return string SQL statement to get the length of a character.
*
* @since 2.0.0
*/
public function charLength($field, $operator = null, $condition = null);
/**
* Clear data from the query or a specific clause of the query.
*
* @param string $clause Optionally, the name of the clause to clear, or nothing to clear the whole query.
*
* @return $this
*
* @since 2.0.0
*/
public function clear($clause = null);
/**
* Adds a column, or array of column names that would be used for an INSERT INTO statement.
*
* @param array|string $columns A column name, or array of column names.
*
* @return $this
*
* @since 2.0.0
*/
public function columns($columns);
/**
* Concatenates an array of column names or values.
*
* Usage:
* $query->select($query->concatenate(array('a', 'b')));
*
* @param string[] $values An array of values to concatenate.
* @param string|null $separator As separator to place between each value.
*
* @return string SQL statement representing the concatenated values.
*
* @since 2.0.0
*/
public function concatenate($values, $separator = null);
/**
* Gets the current date and time.
*
* Usage:
* $query->where('published_up < '.$query->currentTimestamp());
*
* @return string SQL statement to get the current timestamp.
*
* @since 2.0.0
*/
public function currentTimestamp();
/**
* Add a table name to the DELETE clause of the query.
*
* Usage:
* $query->delete('#__a')->where('id = 1');
*
* @param string $table The name of the table to delete from.
*
* @return $this
*
* @since 2.0.0
* @throws QueryTypeAlreadyDefinedException if the query type has already been defined
*/
public function delete($table = null);
/**
* Add a single column, or array of columns to the EXEC clause of the query.
*
* Usage:
* $query->exec('a.*')->exec('b.id');
* $query->exec(array('a.*', 'b.id'));
*
* @param array|string $columns A string or an array of field names.
*
* @return $this
*
* @since 2.0.0
* @throws QueryTypeAlreadyDefinedException if the query type has already been defined
*/
public function exec($columns);
/**
* Find a value in a varchar used like a set.
*
* Ensure that the value is an integer before passing to the method.
*
* Usage:
* $query->findInSet((int) $parent->id, 'a.assigned_cat_ids')
*
* @param string $value The value to search for.
* @param string $set The set of values.
*
* @return string A representation of the MySQL find_in_set() function for the driver.
*
* @since 2.0.0
*/
public function findInSet($value, $set);
/**
* Add a table to the FROM clause of the query.
*
* Usage:
* $query->select('*')->from('#__a');
* $query->select('*')->from($subquery->alias('a'));
*
* @param string|QueryInterface $table The name of the table or a QueryInterface object (or a child of it) with alias set.
*
* @return $this
*
* @since 2.0.0
*/
public function from($table);
/**
* Add alias for current query.
*
* Usage:
* $query->select('*')->from('#__a')->alias('subquery');
*
* @param string $alias Alias used for a JDatabaseQuery.
*
* @return $this
*
* @since 2.0.0
*/
public function alias($alias);
/**
* Used to get a string to extract year from date column.
*
* Usage:
* $query->select($query->year($query->quoteName('dateColumn')));
*
* @param string $date Date column containing year to be extracted.
*
* @return string SQL statement to get the year from a date value.
*
* @since 2.0.0
*/
public function year($date);
/**
* Used to get a string to extract month from date column.
*
* Usage:
* $query->select($query->month($query->quoteName('dateColumn')));
*
* @param string $date Date column containing month to be extracted.
*
* @return string SQL statement to get the month from a date value.
*
* @since 2.0.0
*/
public function month($date);
/**
* Used to get a string to extract day from date column.
*
* Usage:
* $query->select($query->day($query->quoteName('dateColumn')));
*
* @param string $date Date column containing day to be extracted.
*
* @return string SQL statement to get the day from a date value.
*
* @since 2.0.0
*/
public function day($date);
/**
* Used to get a string to extract hour from date column.
*
* Usage:
* $query->select($query->hour($query->quoteName('dateColumn')));
*
* @param string $date Date column containing hour to be extracted.
*
* @return string SQL statement to get the hour from a date/time value.
*
* @since 2.0.0
*/
public function hour($date);
/**
* Used to get a string to extract minute from date column.
*
* Usage:
* $query->select($query->minute($query->quoteName('dateColumn')));
*
* @param string $date Date column containing minute to be extracted.
*
* @return string SQL statement to get the minute from a date/time value.
*
* @since 2.0.0
*/
public function minute($date);
/**
* Used to get a string to extract seconds from date column.
*
* Usage:
* $query->select($query->second($query->quoteName('dateColumn')));
*
* @param string $date Date column containing second to be extracted.
*
* @return string SQL statement to get the second from a date/time value.
*
* @since 2.0.0
*/
public function second($date);
/**
* Add a grouping column to the GROUP clause of the query.
*
* Usage:
* $query->group('id');
*
* @param array|string $columns A string or array of ordering columns.
*
* @return $this
*
* @since 2.0.0
*/
public function group($columns);
/**
* Aggregate function to get input values concatenated into a string, separated by delimiter
*
* Usage:
* $query->groupConcat('id', ',');
*
* @param string $expression The expression to apply concatenation to, this may be a column name or complex SQL statement.
* @param string $separator The delimiter of each concatenated value
*
* @return string Input values concatenated into a string, separated by delimiter
*
* @since 2.0.0
*/
public function groupConcat($expression, $separator = ',');
/**
* A conditions to the HAVING clause of the query.
*
* Usage:
* $query->group('id')->having('COUNT(id) > 5');
*
* @param array|string $conditions A string or array of columns.
* @param string $glue The glue by which to join the conditions. Defaults to AND.
*
* @return $this
*
* @since 2.0.0
*/
public function having($conditions, $glue = 'AND');
/**
* Add a table name to the INSERT clause of the query.
*
* Usage:
* $query->insert('#__a')->set('id = 1');
* $query->insert('#__a')->columns('id, title')->values('1,2')->values('3,4');
* $query->insert('#__a')->columns('id, title')->values(array('1,2', '3,4'));
*
* @param string $table The name of the table to insert data into.
* @param boolean $incrementField The name of the field to auto increment.
*
* @return $this
*
* @since 2.0.0
* @throws QueryTypeAlreadyDefinedException if the query type has already been defined
*/
public function insert($table, $incrementField = false);
/**
* Add a JOIN clause to the query.
*
* Usage:
* $query->join('INNER', 'b', 'b.id = a.id);
*
* @param string $type The type of join. This string is prepended to the JOIN keyword.
* @param string $table The name of table.
* @param string $condition The join condition.
*
* @return $this
*
* @since 2.0.0
*/
public function join($type, $table, $condition = null);
/**
* Get the length of a string in bytes.
*
* Note, use 'charLength' to find the number of characters in a string.
*
* Usage:
* query->where($query->length('a').' > 3');
*
* @param string $value The string to measure.
*
* @return integer
*
* @since 2.0.0
*/
public function length($value);
/**
* Get the null or zero representation of a timestamp for the database driver.
*
* This method is provided for use where the query object is passed to a function for modification.
* If you have direct access to the database object, it is recommended you use the nullDate method directly.
*
* Usage:
* $query->where('modified_date <> '.$query->nullDate());
*
* @param boolean $quoted Optionally wraps the null date in database quotes (true by default).
*
* @return string Null or zero representation of a timestamp.
*
* @since 2.0.0
* @throws \RuntimeException
*/
public function nullDate($quoted = true);
/**
* Generate a SQL statement to check if column represents a zero or null datetime.
*
* Usage:
* $query->where($query->isNullDatetime('modified_date'));
*
* @param string $column A column name.
*
* @return string
*
* @since 2.0.0
*/
public function isNullDatetime($column);
/**
* Add an ordering column to the ORDER clause of the query.
*
* Usage:
* $query->order('foo')->order('bar');
* $query->order(array('foo','bar'));
*
* @param array|string $columns A string or array of ordering columns.
*
* @return $this
*
* @since 2.0.0
*/
public function order($columns);
/**
* Wrap an SQL statement identifier name such as column, table or database names in quotes to prevent injection
* risks and reserved word conflicts.
*
* This method is provided for use where the query object is passed to a function for modification.
* If you have direct access to the database object, it is recommended you use the quoteName method directly.
*
* Note that 'qn' is an alias for this method as it is in DatabaseDriver.
*
* Usage:
* $query->quoteName('#__a');
* $query->qn('#__a');
*
* @param array|string $name The identifier name to wrap in quotes, or an array of identifier names to wrap in quotes.
* Each type supports dot-notation name.
* @param array|string $as The AS query part associated to $name. It can be string or array, in latter case it has to be
* same length of $name; if is null there will not be any AS part for string or array element.
*
* @return array|string The quote wrapped name, same type of $name.
*
* @since 1.0
* @throws \RuntimeException if the internal db property is not a valid object.
*/
public function quoteName($name, $as = null);
/**
* Get the function to return a random floating-point value
*
* Usage:
* $query->rand();
*
* @return string
*
* @since 2.0.0
*/
public function rand();
/**
* Get the regular expression operator
*
* Usage:
* $query->where('field ' . $query->regexp($search));
*
* @param string $value The regex pattern.
*
* @return string
*
* @since 2.0.0
*/
public function regexp($value);
/**
* Add a single column, or array of columns to the SELECT clause of the query.
*
* Usage:
* $query->select('a.*')->select('b.id');
* $query->select(array('a.*', 'b.id'));
*
* @param array|string $columns A string or an array of field names.
*
* @return $this
*
* @since 2.0.0
* @throws QueryTypeAlreadyDefinedException if the query type has already been defined
*/
public function select($columns);
/**
* Return the number of the current row.
*
* Usage:
* $query->select('id');
* $query->selectRowNumber('ordering,publish_up DESC', 'new_ordering');
* $query->from('#__content');
*
* @param string $orderBy An expression of ordering for window function.
* @param string $orderColumnAlias An alias for new ordering column.
*
* @return $this
*
* @since 2.0.0
* @throws \RuntimeException
*/
public function selectRowNumber($orderBy, $orderColumnAlias);
/**
* Add a single condition string, or an array of strings to the SET clause of the query.
*
* Usage:
* $query->set('a = 1')->set('b = 2');
* $query->set(array('a = 1', 'b = 2');
*
* @param array|string $conditions A string or array of string conditions.
* @param string $glue The glue by which to join the condition strings. Defaults to `,`.
* Note that the glue is set on first use and cannot be changed.
*
* @return $this
*
* @since 2.0.0
*/
public function set($conditions, $glue = ',');
/**
* Add a table name to the UPDATE clause of the query.
*
* Usage:
* $query->update('#__foo')->set(...);
*
* @param string $table A table to update.
*
* @return $this
*
* @since 2.0.0
* @throws QueryTypeAlreadyDefinedException if the query type has already been defined
*/
public function update($table);
/**
* Adds a tuple, or array of tuples that would be used as values for an INSERT INTO statement.
*
* Usage:
* $query->values('1,2,3')->values('4,5,6');
* $query->values(array('1,2,3', '4,5,6'));
*
* @param array|string $values A single tuple, or array of tuples.
*
* @return $this
*
* @since 2.0.0
*/
public function values($values);
/**
* Add a single condition, or an array of conditions to the WHERE clause of the query.
*
* Usage:
* $query->where('a = 1')->where('b = 2');
* $query->where(array('a = 1', 'b = 2'));
*
* @param array|string $conditions A string or array of where conditions.
* @param string $glue The glue by which to join the conditions. Defaults to AND.
* Note that the glue is set on first use and cannot be changed.
*
* @return $this
*
* @since 2.0.0
*/
public function where($conditions, $glue = 'AND');
/**
* Add a WHERE IN statement to the query.
*
* Note that all values must be the same data type.
*
* Usage
* $query->whereIn('id', [1, 2, 3]);
*
* @param string $keyName Key name for the where clause
* @param array $keyValues Array of values to be matched
* @param array|string $dataType Constant corresponding to a SQL datatype. It can be an array, in this case it
* has to be same length of $keyValues
*
* @return $this
*
* @since 2.0.0
*/
public function whereIn(string $keyName, array $keyValues, $dataType = ParameterType::INTEGER);
/**
* Add a WHERE NOT IN statement to the query.
*
* Note that all values must be the same data type.
*
* Usage
* $query->whereNotIn('id', [1, 2, 3]);
*
* @param string $keyName Key name for the where clause
* @param array $keyValues Array of values to be matched
* @param array|string $dataType Constant corresponding to a SQL datatype. It can be an array, in this case it
* has to be same length of $keyValues
*
* @return $this
*
* @since 2.0.0
*/
public function whereNotIn(string $keyName, array $keyValues, $dataType = ParameterType::INTEGER);
/**
* Extend the WHERE clause with a single condition or an array of conditions, with a potentially different logical operator from the one in the
* current WHERE clause.
*
* Usage:
* $query->where(array('a = 1', 'b = 2'))->extendWhere('XOR', array('c = 3', 'd = 4'));
* will produce: WHERE ((a = 1 AND b = 2) XOR (c = 3 AND d = 4)
*
* @param string $outerGlue The glue by which to join the conditions to the current WHERE conditions.
* @param mixed $conditions A string or array of WHERE conditions.
* @param string $innerGlue The glue by which to join the conditions. Defaults to AND.
*
* @return $this
*
* @since 2.0.0
*/
public function extendWhere($outerGlue, $conditions, $innerGlue = 'AND');
/**
* Binds an array of values and returns an array of prepared parameter names.
*
* Note that all values must be the same data type.
*
* Usage:
* $query->whereIn('column in (' . implode(',', $query->bindArray($keyValues, $dataType)) . ')');
*
* @param array $values Values to bind
* @param array|string $dataType Constant corresponding to a SQL datatype. It can be an array, in this case it
* has to be same length of $key
*
* @return array An array with parameter names
*
* @since 2.0.0
*/
public function bindArray(array $values, $dataType = ParameterType::INTEGER);
/**
* Add a query to UNION with the current query.
*
* Usage:
* $query->union('SELECT name FROM #__foo')
* $query->union('SELECT name FROM #__foo', true)
*
* @param DatabaseQuery|string $query The DatabaseQuery object or string to union.
* @param boolean $distinct True to only return distinct rows from the union.
*
* @return $this
*
* @since 1.0
*/
public function union($query, $distinct = true);
/**
* Add a query to UNION ALL with the current query.
*
* Usage:
* $query->unionAll('SELECT name FROM #__foo')
*
* @param DatabaseQuery|string $query The DatabaseQuery object or string to union.
*
* @return $this
*
* @see union
* @since 1.5.0
*/
public function unionAll($query);
/**
* Set a single query to the query set.
* On this type of DatabaseQuery you can use union(), unionAll(), order() and setLimit()
*
* Usage:
* $query->querySet($query2->select('name')->from('#__foo')->order('id DESC')->setLimit(1))
* ->unionAll($query3->select('name')->from('#__foo')->order('id')->setLimit(1))
* ->order('name')
* ->setLimit(1)
*
* @param DatabaseQuery|string $query The DatabaseQuery object or string.
*
* @return $this
*
* @since 2.0.0
*/
public function querySet($query);
/**
* Create a DatabaseQuery object of type querySet from current query.
*
* Usage:
* $query->select('name')->from('#__foo')->order('id DESC')->setLimit(1)
* ->toQuerySet()
* ->unionAll($query2->select('name')->from('#__foo')->order('id')->setLimit(1))
* ->order('name')
* ->setLimit(1)
*
* @return DatabaseQuery A new object of the DatabaseQuery.
*
* @since 2.0.0
*/
public function toQuerySet();
}

View File

@ -0,0 +1,40 @@
<?php
/**
* Part of the Joomla Framework Database Package
*
* @copyright Copyright (C) 2005 - 2021 Open Source Matters, Inc. All rights reserved.
* @license GNU General Public License version 2 or later; see LICENSE
*/
namespace Joomla\Database;
/**
* Interface defining a query monitor.
*
* @since 2.0.0
*/
interface QueryMonitorInterface
{
/**
* Act on a query being started.
*
* @param string $sql The SQL to be executed.
* @param object[]|null $boundParams List of bound params, used with the query.
* Each item is an object that holds: value, dataType
*
* @return void
*
* @since 2.0.0
*/
public function startQuery(string $sql, ?array $boundParams = null): void;
/**
* Act on a query being stopped.
*
* @return void
*
* @since 2.0.0
*/
public function stopQuery(): void;
}

View File

@ -0,0 +1,55 @@
<?php
/**
* Part of the Joomla Framework Database Package
*
* @copyright Copyright (C) 2005 - 2021 Open Source Matters, Inc. All rights reserved.
* @license GNU General Public License version 2 or later; see LICENSE
*/
namespace Joomla\Database\Service;
use Joomla\Database\DatabaseDriver;
use Joomla\Database\DatabaseFactory;
use Joomla\Database\DatabaseInterface;
use Joomla\DI\Container;
use Joomla\DI\ServiceProviderInterface;
/**
* Database service provider
*
* @since 2.0.0
*/
class DatabaseProvider implements ServiceProviderInterface
{
/**
* Registers the service provider with a DI container.
*
* @param Container $container The DI container.
*
* @return void
*
* @since 2.0.0
*/
public function register(Container $container)
{
$container->alias(DatabaseInterface::class, DatabaseDriver::class)
->share(
DatabaseDriver::class,
function (Container $container) {
/** @var \Joomla\Registry\Registry $config */
$config = $container->get('config');
$options = (array) $config->get('database');
return $container->get(DatabaseFactory::class)->getDriver($options['driver'], $options);
}
);
$container->share(
DatabaseFactory::class,
function (Container $container) {
return new DatabaseFactory();
}
);
}
}

View File

@ -0,0 +1,29 @@
<?php
/**
* Part of the Joomla Framework Database Package
*
* @copyright Copyright (C) 2005 - 2021 Open Source Matters, Inc. All rights reserved.
* @license GNU General Public License version 2 or later; see LICENSE
*/
namespace Joomla\Database\Sqlazure;
use Joomla\Database\Sqlsrv\SqlsrvDriver;
/**
* SQL Azure Database Driver
*
* @link https://msdn.microsoft.com/en-us/library/ee336279.aspx
* @since 1.0
*/
class SqlazureDriver extends SqlsrvDriver
{
/**
* The name of the database driver.
*
* @var string
* @since 1.0
*/
public $name = 'sqlazure';
}

View File

@ -0,0 +1,21 @@
<?php
/**
* Part of the Joomla Framework Database Package
*
* @copyright Copyright (C) 2005 - 2021 Open Source Matters, Inc. All rights reserved.
* @license GNU General Public License version 2 or later; see LICENSE
*/
namespace Joomla\Database\Sqlazure;
use Joomla\Database\Sqlsrv\SqlsrvQuery;
/**
* SQL Azure Query Building Class.
*
* @since 1.0
*/
class SqlazureQuery extends SqlsrvQuery
{
}

View File

@ -0,0 +1,528 @@
<?php
/**
* Part of the Joomla Framework Database Package
*
* @copyright Copyright (C) 2005 - 2021 Open Source Matters, Inc. All rights reserved.
* @license GNU General Public License version 2 or later; see LICENSE
*/
namespace Joomla\Database\Sqlite;
use Joomla\Database\Pdo\PdoDriver;
/**
* SQLite database driver supporting PDO based connections
*
* @link https://www.php.net/manual/en/ref.pdo-sqlite.php
* @since 1.0
*/
class SqliteDriver extends PdoDriver
{
/**
* The name of the database driver.
*
* @var string
* @since 1.0
*/
public $name = 'sqlite';
/**
* The character(s) used to quote SQL statement names such as table names or field names, etc.
*
* If a single character string the same character is used for both sides of the quoted name, else the first character will be used for the
* opening quote and the second for the closing quote.
*
* @var string
* @since 1.0
*/
protected $nameQuote = '`';
/**
* Destructor.
*
* @since 1.0
*/
public function __destruct()
{
$this->disconnect();
}
/**
* Alter database's character set.
*
* @param string $dbName The database name that will be altered
*
* @return boolean|resource
*
* @since 2.0.0
* @throws \RuntimeException
*/
public function alterDbCharacterSet($dbName)
{
return false;
}
/**
* Connects to the database if needed.
*
* @return void Returns void if the database connected successfully.
*
* @since 2.0.0
* @throws \RuntimeException
*/
public function connect()
{
if ($this->connection) {
return;
}
parent::connect();
$this->connection->sqliteCreateFunction(
'ROW_NUMBER',
function ($init = null) {
static $rownum, $partition;
if ($init !== null) {
$rownum = $init;
$partition = null;
return $rownum;
}
$args = \func_get_args();
array_shift($args);
$partitionBy = $args ? implode(',', $args) : null;
if ($partitionBy === null || $partitionBy === $partition) {
$rownum++;
} else {
$rownum = 1;
$partition = $partitionBy;
}
return $rownum;
}
);
}
/**
* Create a new database using information from $options object.
*
* @param \stdClass $options Object used to pass user and database name to database driver. This object must have "db_name" and "db_user" set.
* @param boolean $utf True if the database supports the UTF-8 character set.
*
* @return boolean|resource
*
* @since 2.0.0
* @throws \RuntimeException
*/
public function createDatabase($options, $utf = true)
{
// SQLite doesn't have a query for this
return true;
}
/**
* Method to escape a string for usage in an SQLite statement.
*
* Note: Using query objects with bound variables is preferable to the below.
*
* @param string $text The string to be escaped.
* @param boolean $extra Unused optional parameter to provide extra escaping.
*
* @return string The escaped string.
*
* @since 1.0
*/
public function escape($text, $extra = false)
{
if (\is_int($text)) {
return $text;
}
if (\is_float($text)) {
// Force the dot as a decimal point.
return str_replace(',', '.', $text);
}
return \SQLite3::escapeString($text);
}
/**
* Method to get the database collation in use by sampling a text field of a table in the database.
*
* @return string|boolean The collation in use by the database or boolean false if not supported.
*
* @since 1.0
*/
public function getCollation()
{
return false;
}
/**
* Method to get the database connection collation in use by sampling a text field of a table in the database.
*
* @return string|boolean The collation in use by the database connection (string) or boolean false if not supported.
*
* @since 1.6.0
* @throws \RuntimeException
*/
public function getConnectionCollation()
{
return false;
}
/**
* Method to get the database encryption details (cipher and protocol) in use.
*
* @return string The database encryption details.
*
* @since 2.0.0
* @throws \RuntimeException
*/
public function getConnectionEncryption(): string
{
// TODO: Not fake this
return '';
}
/**
* Method to test if the database TLS connections encryption are supported.
*
* @return boolean Whether the database supports TLS connections encryption.
*
* @since 2.0.0
*/
public function isConnectionEncryptionSupported(): bool
{
// TODO: Not fake this
return false;
}
/**
* Shows the table CREATE statement that creates the given tables.
*
* Note: Doesn't appear to have support in SQLite
*
* @param mixed $tables A table name or a list of table names.
*
* @return array A list of the create SQL for the tables.
*
* @since 1.0
* @throws \RuntimeException
*/
public function getTableCreate($tables)
{
$this->connect();
// Sanitize input to an array and iterate over the list.
$tables = (array) $tables;
return $tables;
}
/**
* Retrieves field information about a given table.
*
* @param string $table The name of the database table.
* @param boolean $typeOnly True to only return field types.
*
* @return array An array of fields for the database table.
*
* @since 1.0
* @throws \RuntimeException
*/
public function getTableColumns($table, $typeOnly = true)
{
$this->connect();
$columns = [];
$fieldCasing = $this->getOption(\PDO::ATTR_CASE);
$this->setOption(\PDO::ATTR_CASE, \PDO::CASE_UPPER);
$table = strtoupper($table);
$fields = $this->setQuery('pragma table_info(' . $table . ')')->loadObjectList();
if ($typeOnly) {
foreach ($fields as $field) {
$columns[$field->NAME] = $field->TYPE;
}
} else {
foreach ($fields as $field) {
// Do some dirty translation to MySQL output.
// TODO: Come up with and implement a standard across databases.
$columns[$field->NAME] = (object) [
'Field' => $field->NAME,
'Type' => $field->TYPE,
'Null' => $field->NOTNULL == '1' ? 'NO' : 'YES',
'Default' => $field->DFLT_VALUE,
'Key' => $field->PK != '0' ? 'PRI' : '',
];
}
}
$this->setOption(\PDO::ATTR_CASE, $fieldCasing);
return $columns;
}
/**
* Get the details list of keys for a table.
*
* @param string $table The name of the table.
*
* @return array An array of the column specification for the table.
*
* @since 1.0
* @throws \RuntimeException
*/
public function getTableKeys($table)
{
$this->connect();
$keys = [];
$fieldCasing = $this->getOption(\PDO::ATTR_CASE);
$this->setOption(\PDO::ATTR_CASE, \PDO::CASE_UPPER);
$table = strtoupper($table);
$rows = $this->setQuery('pragma table_info( ' . $table . ')')->loadObjectList();
foreach ($rows as $column) {
if ($column->PK == 1) {
$keys[$column->NAME] = $column;
}
}
$this->setOption(\PDO::ATTR_CASE, $fieldCasing);
return $keys;
}
/**
* Method to get an array of all tables in the database (schema).
*
* @return array An array of all the tables in the database.
*
* @since 1.0
* @throws \RuntimeException
*/
public function getTableList()
{
$this->connect();
$type = 'table';
$query = $this->createQuery()
->select('name')
->from('sqlite_master')
->where('type = :type')
->bind(':type', $type)
->order('name');
return $this->setQuery($query)->loadColumn();
}
/**
* Get the version of the database connector.
*
* @return string The database connector version.
*
* @since 1.0
*/
public function getVersion()
{
$this->connect();
return $this->setQuery('SELECT sqlite_version()')->loadResult();
}
/**
* Select a database for use.
*
* @param string $database The name of the database to select for use.
*
* @return boolean True if the database was successfully selected.
*
* @since 1.0
* @throws \RuntimeException
*/
public function select($database)
{
$this->connect();
return true;
}
/**
* Set the connection to use UTF-8 character encoding.
*
* Returns false automatically for the Oracle driver since
* you can only set the character set when the connection
* is created.
*
* @return boolean True on success.
*
* @since 1.0
*/
public function setUtf()
{
$this->connect();
return false;
}
/**
* Locks a table in the database.
*
* @param string $table The name of the table to unlock.
*
* @return $this
*
* @since 1.0
* @throws \RuntimeException
*/
public function lockTable($table)
{
return $this;
}
/**
* Renames a table in the database.
*
* @param string $oldTable The name of the table to be renamed
* @param string $newTable The new name for the table.
* @param string $backup Not used by Sqlite.
* @param string $prefix Not used by Sqlite.
*
* @return $this
*
* @since 1.0
* @throws \RuntimeException
*/
public function renameTable($oldTable, $newTable, $backup = null, $prefix = null)
{
$this->setQuery('ALTER TABLE ' . $oldTable . ' RENAME TO ' . $newTable)->execute();
return $this;
}
/**
* Method to truncate a table.
*
* @param string $table The table to truncate
*
* @return void
*
* @since 1.2.1
* @throws \RuntimeException
*/
public function truncateTable($table)
{
$this->setQuery('DELETE FROM ' . $this->quoteName($table))
->execute();
}
/**
* Unlocks tables in the database.
*
* @return $this
*
* @since 1.0
* @throws \RuntimeException
*/
public function unlockTables()
{
return $this;
}
/**
* Test to see if the PDO ODBC connector is available.
*
* @return boolean True on success, false otherwise.
*
* @since 1.0
*/
public static function isSupported()
{
return class_exists('\\PDO') && class_exists('\\SQLite3') && \in_array('sqlite', \PDO::getAvailableDrivers(), true);
}
/**
* Method to commit a transaction.
*
* @param boolean $toSavepoint If true, commit to the last savepoint.
*
* @return void
*
* @since 1.0
* @throws \RuntimeException
*/
public function transactionCommit($toSavepoint = false)
{
$this->connect();
if (!$toSavepoint || $this->transactionDepth <= 1) {
parent::transactionCommit($toSavepoint);
} else {
$this->transactionDepth--;
}
}
/**
* Method to roll back a transaction.
*
* @param boolean $toSavepoint If true, rollback to the last savepoint.
*
* @return void
*
* @since 1.0
* @throws \RuntimeException
*/
public function transactionRollback($toSavepoint = false)
{
$this->connect();
if (!$toSavepoint || $this->transactionDepth <= 1) {
parent::transactionRollback($toSavepoint);
} else {
$savepoint = 'SP_' . ($this->transactionDepth - 1);
$this->setQuery('ROLLBACK TO ' . $this->quoteName($savepoint))->execute();
$this->transactionDepth--;
}
}
/**
* Method to initialize a transaction.
*
* @param boolean $asSavepoint If true and a transaction is already active, a savepoint will be created.
*
* @return void
*
* @since 1.0
* @throws \RuntimeException
*/
public function transactionStart($asSavepoint = false)
{
$this->connect();
if (!$asSavepoint || !$this->transactionDepth) {
parent::transactionStart($asSavepoint);
} else {
$savepoint = 'SP_' . $this->transactionDepth;
$this->setQuery('SAVEPOINT ' . $this->quoteName($savepoint))->execute();
$this->transactionDepth++;
}
}
}

View File

@ -0,0 +1,269 @@
<?php
/**
* Part of the Joomla Framework Database Package
*
* @copyright Copyright (C) 2005 - 2021 Open Source Matters, Inc. All rights reserved.
* @license GNU General Public License version 2 or later; see LICENSE
*/
namespace Joomla\Database\Sqlite;
use Joomla\Database\DatabaseQuery;
use Joomla\Database\Pdo\PdoQuery;
use Joomla\Database\Query\QueryElement;
/**
* SQLite Query Building Class.
*
* @since 1.0
*/
class SqliteQuery extends PdoQuery
{
/**
* Magic function to convert the query to a string.
*
* @return string The completed query.
*
* @since 2.0.0
*/
public function __toString()
{
switch ($this->type) {
case 'select':
if ($this->selectRowNumber) {
$orderBy = $this->selectRowNumber['orderBy'];
$orderColumnAlias = $this->selectRowNumber['orderColumnAlias'];
$column = "ROW_NUMBER() AS $orderColumnAlias";
if ($this->select === null) {
$query = PHP_EOL . 'SELECT 1'
. (string) $this->from
. (string) $this->where;
} else {
$tmpOffset = $this->offset;
$tmpLimit = $this->limit;
$this->offset = 0;
$this->limit = 0;
$tmpOrder = $this->order;
$this->order = null;
$query = parent::__toString();
$column = "w.*, $column";
$this->order = $tmpOrder;
$this->offset = $tmpOffset;
$this->limit = $tmpLimit;
}
// Special sqlite query to count ROW_NUMBER
$query = PHP_EOL . "SELECT $column"
. PHP_EOL . "FROM ($query" . PHP_EOL . "ORDER BY $orderBy"
. PHP_EOL . ') AS w,(SELECT ROW_NUMBER(0)) AS r'
// Forbid to flatten subqueries.
. ((string) $this->order ?: PHP_EOL . 'ORDER BY NULL');
return $this->processLimit($query, $this->limit, $this->offset);
}
break;
case 'querySet':
$query = $this->querySet;
if ($query->order || $query->limit || $query->offset) {
// If ORDER BY or LIMIT statement exist then parentheses is required for the first query
$query = PHP_EOL . "SELECT * FROM ($query)";
}
if ($this->merge) {
// Special case for merge
foreach ($this->merge as $element) {
$query .= (string) $element;
}
}
if ($this->order) {
$query .= (string) $this->order;
}
return $query;
case 'update':
if ($this->join) {
$table = $this->update->getElements();
$table = $table[0];
$tableName = explode(' ', $table);
$tableName = $tableName[0];
if ($this->columns === null) {
$fields = $this->db->getTableColumns($tableName);
foreach ($fields as $key => $value) {
$fields[$key] = $key;
}
$this->columns = new QueryElement('()', $fields);
}
$fields = $this->columns->getElements();
$elements = $this->set->getElements();
foreach ($elements as $nameValue) {
$setArray = explode(' = ', $nameValue, 2);
if ($setArray[0][0] === '`') {
// Unquote column name
$setArray[0] = substr($setArray[0], 1, -1);
}
$fields[$setArray[0]] = $setArray[1];
}
$select = new static($this->db);
$select->select(array_values($fields))
->from($table);
$select->join = $this->join;
$select->where = $this->where;
return 'INSERT OR REPLACE INTO ' . $tableName
. ' (' . implode(',', array_keys($fields)) . ')'
. (string) $select;
}
}
return parent::__toString();
}
/**
* Gets the number of characters in a string.
*
* Note, use 'length' to find the number of bytes in a string.
*
* Usage:
* $query->select($query->charLength('a'));
*
* @param string $field A value.
* @param string|null $operator Comparison operator between charLength integer value and $condition
* @param string|null $condition Integer value to compare charLength with.
*
* @return string The required char length call.
*
* @since 1.1.0
*/
public function charLength($field, $operator = null, $condition = null)
{
$statement = 'length(' . $field . ')';
if ($operator !== null && $condition !== null) {
$statement .= ' ' . $operator . ' ' . $condition;
}
return $statement;
}
/**
* Concatenates an array of column names or values.
*
* Usage:
* $query->select($query->concatenate(array('a', 'b')));
*
* @param string[] $values An array of values to concatenate.
* @param string|null $separator As separator to place between each value.
*
* @return string The concatenated values.
*
* @since 1.1.0
*/
public function concatenate($values, $separator = null)
{
if ($separator !== null) {
return implode(' || ' . $this->quote($separator) . ' || ', $values);
}
return implode(' || ', $values);
}
/**
* Method to modify a query already in string format with the needed additions to make the query limited to a particular number of
* results, or start at a particular offset.
*
* @param string $query The query in string format
* @param integer $limit The limit for the result set
* @param integer $offset The offset for the result set
*
* @return string
*
* @since 1.0
*/
public function processLimit($query, $limit, $offset = 0)
{
if ($limit > 0 || $offset > 0) {
$query .= ' LIMIT ' . $offset . ', ' . $limit;
}
return $query;
}
/**
* Return the number of the current row.
*
* Usage:
* $query->select('id');
* $query->selectRowNumber('ordering,publish_up DESC', 'new_ordering');
* $query->from('#__content');
*
* @param string $orderBy An expression of ordering for window function.
* @param string $orderColumnAlias An alias for new ordering column.
*
* @return $this
*
* @since 2.0.0
* @throws \RuntimeException
*/
public function selectRowNumber($orderBy, $orderColumnAlias)
{
$this->validateRowNumber($orderBy, $orderColumnAlias);
return $this;
}
/**
* Add a query to UNION with the current query.
*
* Usage:
* $query->union('SELECT name FROM #__foo')
* $query->union('SELECT name FROM #__foo', true)
*
* @param DatabaseQuery|string $query The DatabaseQuery object or string to union.
* @param boolean $distinct True to only return distinct rows from the union.
*
* @return $this
*
* @since 1.0
*/
public function union($query, $distinct = true)
{
// Set up the name with parentheses, the DISTINCT flag is redundant
return $this->merge($distinct ? 'UNION SELECT * FROM ()' : 'UNION ALL SELECT * FROM ()', $query);
}
/**
* Aggregate function to get input values concatenated into a string, separated by delimiter
*
* Usage:
* $query->groupConcat('id', ',');
*
* @param string $expression The expression to apply concatenation to, this may be a column name or complex SQL statement.
* @param string $separator The delimiter of each concatenated value
*
* @return string Input values concatenated into a string, separated by delimiter
*
* @since 2.0.0
*/
public function groupConcat($expression, $separator = ',')
{
return 'group_concat(' . $expression . ', ' . $this->quote($separator) . ')';
}
}

View File

@ -0,0 +1,931 @@
<?php
/**
* Part of the Joomla Framework Database Package
*
* @copyright Copyright (C) 2005 - 2021 Open Source Matters, Inc. All rights reserved.
* @license GNU General Public License version 2 or later; see LICENSE
*/
namespace Joomla\Database\Sqlsrv;
use Joomla\Database\DatabaseDriver;
use Joomla\Database\DatabaseEvents;
use Joomla\Database\Event\ConnectionEvent;
use Joomla\Database\Exception\ConnectionFailureException;
use Joomla\Database\Exception\ExecutionFailureException;
use Joomla\Database\Exception\PrepareStatementFailureException;
use Joomla\Database\Exception\UnsupportedAdapterException;
use Joomla\Database\StatementInterface;
/**
* SQL Server Database Driver
*
* @link https://www.php.net/manual/en/book.sqlsrv.php
* @since 1.0
*/
class SqlsrvDriver extends DatabaseDriver
{
/**
* The name of the database driver.
*
* @var string
* @since 1.0
*/
public $name = 'sqlsrv';
/**
* The character(s) used to quote SQL statement names such as table names or field names, etc.
*
* If a single character string the same character is used for both sides of the quoted name, else the first character will be used for the
* opening quote and the second for the closing quote.
*
* @var string
* @since 1.0
*/
protected $nameQuote = '[]';
/**
* The null or zero representation of a timestamp for the database driver.
*
* @var string
* @since 1.0
*/
protected $nullDate = '1900-01-01 00:00:00';
/**
* The minimum supported database version.
*
* @var string
* @since 1.0
*/
protected static $dbMinimum = '11.0.2100.60';
/**
* Test to see if the SQLSRV connector is available.
*
* @return boolean True on success, false otherwise.
*
* @since 1.0
*/
public static function isSupported()
{
return \function_exists('sqlsrv_connect');
}
/**
* Constructor.
*
* @param array $options List of options used to configure the connection
*
* @since 1.0
*/
public function __construct(array $options)
{
// Get some basic values from the options.
$options['host'] = $options['host'] ?? 'localhost';
$options['user'] = $options['user'] ?? '';
$options['password'] = $options['password'] ?? '';
$options['database'] = $options['database'] ?? '';
$options['select'] = isset($options['select']) ? (bool) $options['select'] : true;
// Finalize initialisation
parent::__construct($options);
}
/**
* Connects to the database if needed.
*
* @return void Returns void if the database connected successfully.
*
* @since 1.0
* @throws \RuntimeException
*/
public function connect()
{
if ($this->connection) {
return;
}
// Make sure the SQLSRV extension for PHP is installed and enabled.
if (!static::isSupported()) {
throw new UnsupportedAdapterException('PHP extension sqlsrv_connect is not available.');
}
// Build the connection configuration array.
$config = [
'Database' => $this->options['database'],
'uid' => $this->options['user'],
'pwd' => $this->options['password'],
'CharacterSet' => 'UTF-8',
'ReturnDatesAsStrings' => true,
];
// Attempt to connect to the server.
if (!($this->connection = @ sqlsrv_connect($this->options['host'], $config))) {
throw new ConnectionFailureException('Could not connect to SQL Server');
}
// Make sure that DB warnings are not returned as errors.
sqlsrv_configure('WarningsReturnAsErrors', 0);
// If auto-select is enabled select the given database.
if ($this->options['select'] && !empty($this->options['database'])) {
$this->select($this->options['database']);
}
$this->dispatchEvent(new ConnectionEvent(DatabaseEvents::POST_CONNECT, $this));
}
/**
* Disconnects the database.
*
* @return void
*
* @since 1.0
*/
public function disconnect()
{
// Close the connection.
if (\is_resource($this->connection)) {
sqlsrv_close($this->connection);
}
parent::disconnect();
}
/**
* Get table constraints
*
* @param string $tableName The name of the database table.
*
* @return array Any constraints available for the table.
*
* @since 1.0
*/
protected function getTableConstraints($tableName)
{
$this->connect();
return $this->setQuery('SELECT CONSTRAINT_NAME FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS WHERE TABLE_NAME = ' . $this->quote($tableName))
->loadColumn();
}
/**
* Rename constraints.
*
* @param array $constraints Array(strings) of table constraints
* @param string $prefix A string
* @param string $backup A string
*
* @return void
*
* @since 1.0
*/
protected function renameConstraints($constraints = [], $prefix = null, $backup = null)
{
$this->connect();
foreach ($constraints as $constraint) {
$this->setQuery('sp_rename ' . $constraint . ',' . str_replace($prefix, $backup, $constraint))
->execute();
}
}
/**
* Method to escape a string for usage in an SQL statement.
*
* The escaping for MSSQL isn't handled in the driver though that would be nice. Because of this we need to handle the escaping ourselves.
*
* @param string $text The string to be escaped.
* @param boolean $extra Optional parameter to provide extra escaping.
*
* @return string The escaped string.
*
* @since 1.0
*/
public function escape($text, $extra = false)
{
if (\is_int($text)) {
return $text;
}
if (\is_float($text)) {
// Force the dot as a decimal point.
return str_replace(',', '.', $text);
}
$result = str_replace("'", "''", $text);
// SQL Server does not accept NULL byte in query string
$result = str_replace("\0", "' + CHAR(0) + N'", $result);
// Fix for SQL Sever escape sequence, see https://support.microsoft.com/en-us/kb/164291
$result = str_replace(
["\\\n", "\\\r", "\\\\\r\r\n"],
["\\\\\n\n", "\\\\\r\r", "\\\\\r\n\r\n"],
$result
);
if ($extra) {
// Escape special chars
$result = str_replace(
['[', '_', '%'],
['[[]', '[_]', '[%]'],
$result
);
}
return $result;
}
/**
* Quotes and optionally escapes a string to database requirements for use in database queries.
*
* @param mixed $text A string or an array of strings to quote.
* @param boolean $escape True (default) to escape the string, false to leave it unchanged.
*
* @return string The quoted input string.
*
* @since 1.6.0
*/
public function quote($text, $escape = true)
{
if (\is_array($text)) {
return parent::quote($text, $escape);
}
// To support unicode on MSSQL we have to add prefix N
return 'N\'' . ($escape ? $this->escape($text) : $text) . '\'';
}
/**
* Quotes a binary string to database requirements for use in database queries.
*
* @param string $data A binary string to quote.
*
* @return string The binary quoted input string.
*
* @since 1.7.0
*/
public function quoteBinary($data)
{
// ODBC syntax for hexadecimal literals
return '0x' . bin2hex($data);
}
/**
* Determines if the connection to the server is active.
*
* @return boolean True if connected to the database engine.
*
* @since 1.0
*/
public function connected()
{
// TODO: Run a blank query here
return true;
}
/**
* Drops a table from the database.
*
* @param string $table The name of the database table to drop.
* @param boolean $ifExists Optionally specify that the table must exist before it is dropped.
*
* @return $this
*
* @since 1.0
*/
public function dropTable($table, $ifExists = true)
{
$this->connect();
if ($ifExists) {
$this->setQuery(
'IF EXISTS(SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = '
. $this->quote($table) . ') DROP TABLE ' . $table
);
} else {
$this->setQuery('DROP TABLE ' . $table);
}
$this->execute();
return $this;
}
/**
* Method to get the database collation in use by sampling a text field of a table in the database.
*
* @return string|boolean The collation in use by the database or boolean false if not supported.
*
* @since 1.0
*/
public function getCollation()
{
// TODO: Not fake this
return 'MSSQL UTF-8 (UCS2)';
}
/**
* Method to get the database connection collation in use by sampling a text field of a table in the database.
*
* @return string|boolean The collation in use by the database connection (string) or boolean false if not supported.
*
* @since 1.6.0
* @throws \RuntimeException
*/
public function getConnectionCollation()
{
// TODO: Not fake this
return 'MSSQL UTF-8 (UCS2)';
}
/**
* Method to get the database encryption details (cipher and protocol) in use.
*
* @return string The database encryption details.
*
* @since 2.0.0
* @throws \RuntimeException
*/
public function getConnectionEncryption(): string
{
// TODO: Not fake this
return '';
}
/**
* Method to test if the database TLS connections encryption are supported.
*
* @return boolean Whether the database supports TLS connections encryption.
*
* @since 2.0.0
*/
public function isConnectionEncryptionSupported(): bool
{
// TODO: Not fake this
return false;
}
/**
* Retrieves field information about the given tables.
*
* @param mixed $table A table name
* @param boolean $typeOnly True to only return field types.
*
* @return array An array of fields.
*
* @since 1.0
* @throws \RuntimeException
*/
public function getTableColumns($table, $typeOnly = true)
{
$result = [];
$table_temp = $this->replacePrefix((string) $table);
// Set the query to get the table fields statement.
$this->setQuery(
'SELECT column_name as Field, data_type as Type, is_nullable as \'Null\', column_default as \'Default\'' .
' FROM information_schema.columns WHERE table_name = ' . $this->quote($table_temp)
);
$fields = $this->loadObjectList();
// If we only want the type as the value add just that to the list.
if ($typeOnly) {
foreach ($fields as $field) {
$result[$field->Field] = preg_replace('/[(0-9)]/', '', $field->Type);
}
} else {
// If we want the whole field data object add that to the list.
foreach ($fields as $field) {
$field->Default = preg_replace("/(^(\(\(|\('|\(N'|\()|(('\)|(?<!\()\)\)|\))$))/i", '', $field->Default);
$result[$field->Field] = $field;
}
}
return $result;
}
/**
* Shows the table CREATE statement that creates the given tables.
*
* This is unsupported by MSSQL.
*
* @param mixed $tables A table name or a list of table names.
*
* @return array A list of the create SQL for the tables.
*
* @since 1.0
* @throws \RuntimeException
*/
public function getTableCreate($tables)
{
$this->connect();
return [];
}
/**
* Get the details list of keys for a table.
*
* @param string $table The name of the table.
*
* @return array An array of the column specification for the table.
*
* @since 1.0
* @throws \RuntimeException
*/
public function getTableKeys($table)
{
$this->connect();
// TODO To implement.
return [];
}
/**
* Method to get an array of all tables in the database.
*
* @return array An array of all the tables in the database.
*
* @since 1.0
* @throws \RuntimeException
*/
public function getTableList()
{
$this->connect();
// Set the query to get the tables statement.
return $this->setQuery('SELECT name FROM ' . $this->getDatabase() . '.sys.Tables WHERE type = \'U\';')->loadColumn();
}
/**
* Get the version of the database connector.
*
* @return string The database connector version.
*
* @since 1.0
*/
public function getVersion()
{
$this->connect();
$version = sqlsrv_server_info($this->connection);
return $version['SQLServerVersion'];
}
/**
* Inserts a row into a table based on an object's properties.
*
* @param string $table The name of the database table to insert into.
* @param object $object A reference to an object whose public properties match the table fields.
* @param string $key The name of the primary key. If provided the object property is updated.
*
* @return boolean True on success.
*
* @since 1.0
* @throws \RuntimeException
*/
public function insertObject($table, &$object, $key = null)
{
$fields = [];
$values = [];
$tableColumns = $this->getTableColumns($table);
$statement = 'INSERT INTO ' . $this->quoteName($table) . ' (%s) VALUES (%s)';
foreach (get_object_vars($object) as $k => $v) {
// Skip columns that don't exist in the table.
if (!\array_key_exists($k, $tableColumns)) {
continue;
}
// Only process non-null scalars.
if (\is_array($v) || \is_object($v) || $v === null) {
continue;
}
if (!$this->checkFieldExists($table, $k)) {
continue;
}
if ($k[0] === '_') {
// Internal field
continue;
}
if ($k === $key && $key == 0) {
continue;
}
$fields[] = $this->quoteName($k);
$values[] = $this->quote($v);
}
// Set the query and execute the insert.
$this->setQuery(sprintf($statement, implode(',', $fields), implode(',', $values)))->execute();
$id = $this->insertid();
if ($key && $id) {
$object->$key = $id;
}
return true;
}
/**
* Method to get the auto-incremented value from the last INSERT statement.
*
* @return integer The value of the auto-increment field from the last inserted row.
*
* @since 1.0
*/
public function insertid()
{
$this->connect();
// TODO: SELECT IDENTITY
$this->setQuery('SELECT @@IDENTITY');
return (int) $this->loadResult();
}
/**
* Execute the SQL statement.
*
* @return boolean
*
* @since 1.0
* @throws \RuntimeException
*/
public function execute()
{
$this->connect();
// Take a local copy so that we don't modify the original query and cause issues later
$sql = $this->replacePrefix((string) $this->sql);
// Increment the query counter.
$this->count++;
// Get list of bounded parameters
$bounded =& $this->sql->getBounded();
// If there is a monitor registered, let it know we are starting this query
if ($this->monitor) {
$this->monitor->startQuery($sql, $bounded);
}
// Execute the query.
$this->executed = false;
// Bind the variables
foreach ($bounded as $key => $obj) {
$this->statement->bindParam($key, $obj->value, $obj->dataType);
}
try {
$this->executed = $this->statement->execute();
// If there is a monitor registered, let it know we have finished this query
if ($this->monitor) {
$this->monitor->stopQuery();
}
return true;
} catch (ExecutionFailureException $exception) {
// If there is a monitor registered, let it know we have finished this query
if ($this->monitor) {
$this->monitor->stopQuery();
}
// Check if the server was disconnected.
if (!$this->connected()) {
try {
// Attempt to reconnect.
$this->connection = null;
$this->connect();
} catch (ConnectionFailureException $e) {
// If connect fails, ignore that exception and throw the normal exception.
throw $exception;
}
// Since we were able to reconnect, run the query again.
return $this->execute();
}
// Throw the normal query exception.
throw $exception;
}
}
/**
* This function replaces a string identifier with the configured table prefix.
*
* @param string $sql The SQL statement to prepare.
* @param string $prefix The table prefix.
*
* @return string The processed SQL statement.
*
* @since 1.0
*/
public function replacePrefix($sql, $prefix = '#__')
{
$escaped = false;
$startPos = 0;
$quoteChar = '';
$literal = '';
$sql = trim($sql);
$n = \strlen($sql);
while ($startPos < $n) {
$ip = strpos($sql, $prefix, $startPos);
if ($ip === false) {
break;
}
$j = strpos($sql, "N'", $startPos);
$k = strpos($sql, '"', $startPos);
if (($k !== false) && (($k < $j) || ($j === false))) {
$quoteChar = '"';
$j = $k;
} else {
$quoteChar = "'";
}
if ($j === false) {
$j = $n;
}
$literal .= str_replace($prefix, $this->tablePrefix, substr($sql, $startPos, $j - $startPos));
$startPos = $j;
$j = $startPos + 1;
if ($j >= $n) {
break;
}
// Quote comes first, find end of quote
while (true) {
$k = strpos($sql, $quoteChar, $j);
$escaped = false;
if ($k === false) {
break;
}
$l = $k - 1;
while ($l >= 0 && $sql[$l] === '\\') {
$l--;
$escaped = !$escaped;
}
if ($escaped) {
$j = $k + 1;
continue;
}
break;
}
if ($k === false) {
// Error in the query - no end quote; ignore it
break;
}
$literal .= substr($sql, $startPos, $k - $startPos + 1);
$startPos = $k + 1;
}
if ($startPos < $n) {
$literal .= substr($sql, $startPos, $n - $startPos);
}
return $literal;
}
/**
* Select a database for use.
*
* @param string $database The name of the database to select for use.
*
* @return boolean True if the database was successfully selected.
*
* @since 1.0
* @throws ConnectionFailureException
*/
public function select($database)
{
$this->connect();
if (!$database) {
return false;
}
if (!sqlsrv_query($this->connection, 'USE [' . $database . ']', null, ['scrollable' => \SQLSRV_CURSOR_STATIC])) {
throw new ConnectionFailureException('Could not connect to database');
}
return true;
}
/**
* Set the connection to use UTF-8 character encoding.
*
* @return boolean True on success.
*
* @since 1.0
*/
public function setUtf()
{
return true;
}
/**
* Method to commit a transaction.
*
* @param boolean $toSavepoint If true, commit to the last savepoint.
*
* @return void
*
* @since 1.0
* @throws \RuntimeException
*/
public function transactionCommit($toSavepoint = false)
{
$this->connect();
if (!$toSavepoint || $this->transactionDepth <= 1) {
$this->setQuery('COMMIT TRANSACTION')->execute();
$this->transactionDepth = 0;
return;
}
$this->transactionDepth--;
}
/**
* Method to roll back a transaction.
*
* @param boolean $toSavepoint If true, rollback to the last savepoint.
*
* @return void
*
* @since 1.0
* @throws \RuntimeException
*/
public function transactionRollback($toSavepoint = false)
{
$this->connect();
if (!$toSavepoint || $this->transactionDepth <= 1) {
$this->setQuery('ROLLBACK TRANSACTION')->execute();
$this->transactionDepth = 0;
return;
}
$savepoint = 'SP_' . ($this->transactionDepth - 1);
$this->setQuery('ROLLBACK TRANSACTION ' . $this->quoteName($savepoint))->execute();
$this->transactionDepth--;
}
/**
* Method to initialize a transaction.
*
* @param boolean $asSavepoint If true and a transaction is already active, a savepoint will be created.
*
* @return void
*
* @since 1.0
* @throws \RuntimeException
*/
public function transactionStart($asSavepoint = false)
{
$this->connect();
if (!$asSavepoint || !$this->transactionDepth) {
$this->setQuery('BEGIN TRANSACTION')->execute();
$this->transactionDepth = 1;
return;
}
$savepoint = 'SP_' . $this->transactionDepth;
$this->setQuery('BEGIN TRANSACTION ' . $this->quoteName($savepoint))->execute();
$this->transactionDepth++;
}
/**
* Method to check and see if a field exists in a table.
*
* @param string $table The table in which to verify the field.
* @param string $field The field to verify.
*
* @return boolean True if the field exists in the table.
*
* @since 1.0
*/
protected function checkFieldExists($table, $field)
{
$this->connect();
$table = $this->replacePrefix((string) $table);
$this->setQuery(
"SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = '$table' AND COLUMN_NAME = '$field' ORDER BY ORDINAL_POSITION"
);
return (bool) $this->loadResult();
}
/**
* Prepares a SQL statement for execution
*
* @param string $query The SQL query to be prepared.
*
* @return StatementInterface
*
* @since 2.0.0
* @throws PrepareStatementFailureException
*/
protected function prepareStatement(string $query): StatementInterface
{
return new SqlsrvStatement($this->connection, $query);
}
/**
* Renames a table in the database.
*
* @param string $oldTable The name of the table to be renamed
* @param string $newTable The new name for the table.
* @param string $backup Table prefix
* @param string $prefix For the table - used to rename constraints in non-mysql databases
*
* @return $this
*
* @since 1.0
* @throws \RuntimeException
*/
public function renameTable($oldTable, $newTable, $backup = null, $prefix = null)
{
$constraints = [];
if ($prefix !== null && $backup !== null) {
$constraints = $this->getTableConstraints($oldTable);
}
if (!empty($constraints)) {
$this->renameConstraints($constraints, $prefix, $backup);
}
$this->setQuery("sp_rename '" . $oldTable . "', '" . $newTable . "'");
$this->execute();
return $this;
}
/**
* Locks a table in the database.
*
* @param string $tableName The name of the table to lock.
*
* @return $this
*
* @since 1.0
* @throws \RuntimeException
*/
public function lockTable($tableName)
{
return $this;
}
/**
* Unlocks tables in the database.
*
* @return $this
*
* @since 1.0
* @throws \RuntimeException
*/
public function unlockTables()
{
return $this;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,546 @@
<?php
/**
* Part of the Joomla Framework Database Package
*
* @copyright Copyright (C) 2005 - 2021 Open Source Matters, Inc. All rights reserved.
* @license GNU General Public License version 2 or later; see LICENSE
*/
namespace Joomla\Database\Sqlsrv;
use Joomla\Database\Exception\ExecutionFailureException;
use Joomla\Database\Exception\PrepareStatementFailureException;
use Joomla\Database\FetchMode;
use Joomla\Database\FetchOrientation;
use Joomla\Database\ParameterType;
use Joomla\Database\StatementInterface;
/**
* SQL Server Database Statement.
*
* This class is modeled on \Doctrine\DBAL\Driver\SQLSrv\SQLSrvStatement
*
* @since 2.0.0
*/
class SqlsrvStatement implements StatementInterface
{
/**
* The database connection resource.
*
* @var resource
* @since 2.0.0
*/
protected $connection;
/**
* The default fetch mode for the statement.
*
* @var integer
* @since 2.0.0
*/
protected $defaultFetchStyle = FetchMode::MIXED;
/**
* The default class to use for building object result sets.
*
* @var integer
* @since 2.0.0
*/
protected $defaultObjectClass = \stdClass::class;
/**
* Mapping array converting fetch modes to the native engine type.
*
* @var array
* @since 2.0.0
*/
private $fetchMap = [
FetchMode::MIXED => SQLSRV_FETCH_BOTH,
FetchMode::ASSOCIATIVE => SQLSRV_FETCH_ASSOC,
FetchMode::NUMERIC => SQLSRV_FETCH_NUMERIC,
];
/**
* The query string being prepared.
*
* @var string
* @since 2.0.0
*/
protected $query;
/**
* Internal tracking flag to set whether there is a result set available for processing
*
* @var boolean
* @since 2.0.0
*/
private $result = false;
/**
* The prepared statement.
*
* @var resource
* @since 2.0.0
*/
protected $statement;
/**
* Bound parameter types.
*
* @var array
* @since 2.0.0
*/
protected $typesKeyMapping;
/**
* References to the variables bound as statement parameters.
*
* @var array
* @since 2.0.0
*/
private $bindedValues = [];
/**
* Mapping between named parameters and position in query.
*
* @var array
* @since 2.0.0
*/
protected $parameterKeyMapping;
/**
* Mapping array for parameter types.
*
* @var array
* @since 2.0.0
*/
protected $parameterTypeMapping = [
ParameterType::BOOLEAN => ParameterType::BOOLEAN,
ParameterType::INTEGER => ParameterType::INTEGER,
ParameterType::LARGE_OBJECT => ParameterType::LARGE_OBJECT,
ParameterType::NULL => ParameterType::NULL,
ParameterType::STRING => ParameterType::STRING,
];
/**
* Constructor.
*
* @param resource $connection The database connection resource
* @param string $query The query this statement will process
*
* @since 2.0.0
* @throws PrepareStatementFailureException
*/
public function __construct($connection, string $query)
{
// Initial parameter types for prepared statements
$this->parameterTypeMapping = [
ParameterType::BOOLEAN => SQLSRV_PHPTYPE_INT,
ParameterType::INTEGER => SQLSRV_PHPTYPE_INT,
ParameterType::LARGE_OBJECT => SQLSRV_PHPTYPE_STREAM(SQLSRV_ENC_BINARY),
ParameterType::NULL => SQLSRV_PHPTYPE_NULL,
ParameterType::STRING => SQLSRV_PHPTYPE_STRING(SQLSRV_ENC_CHAR),
];
$this->connection = $connection;
$this->query = $this->prepareParameterKeyMapping($query);
}
/**
* Replace named parameters with numbered parameters
*
* @param string $sql The SQL statement to prepare.
*
* @return string The processed SQL statement.
*
* @since 2.0.0
*/
public function prepareParameterKeyMapping($sql)
{
$escaped = false;
$startPos = 0;
$quoteChar = '';
$literal = '';
$mapping = [];
$replace = [];
$matches = [];
$pattern = '/([:][a-zA-Z0-9_]+)/';
if (!preg_match($pattern, $sql, $matches)) {
return $sql;
}
$sql = trim($sql);
$n = \strlen($sql);
while ($startPos < $n) {
if (!preg_match($pattern, $sql, $matches, 0, $startPos)) {
break;
}
$j = strpos($sql, "'", $startPos);
$k = strpos($sql, '"', $startPos);
if (($k !== false) && (($k < $j) || ($j === false))) {
$quoteChar = '"';
$j = $k;
} else {
$quoteChar = "'";
}
if ($j === false) {
$j = $n;
}
// Search for named prepared parameters and replace it with ? and save its position
$substring = substr($sql, $startPos, $j - $startPos);
if (preg_match_all($pattern, $substring, $matches, PREG_PATTERN_ORDER + PREG_OFFSET_CAPTURE)) {
foreach ($matches[0] as $i => $match) {
if ($i === 0) {
$literal .= substr($substring, 0, $match[1]);
}
$mapping[$match[0]] = \count($mapping);
$endOfPlaceholder = $match[1] + strlen($match[0]);
$beginOfNextPlaceholder = $matches[0][$i + 1][1] ?? strlen($substring);
$beginOfNextPlaceholder -= $endOfPlaceholder;
$literal .= '?' . substr($substring, $endOfPlaceholder, $beginOfNextPlaceholder);
}
} else {
$literal .= $substring;
}
$startPos = $j;
$j++;
if ($j >= $n) {
break;
}
// Quote comes first, find end of quote
while (true) {
$k = strpos($sql, $quoteChar, $j);
$escaped = false;
if ($k === false) {
break;
}
$l = $k - 1;
while ($l >= 0 && $sql[$l] === '\\') {
$l--;
$escaped = !$escaped;
}
if ($escaped) {
$j = $k + 1;
continue;
}
break;
}
if ($k === false) {
// Error in the query - no end quote; ignore it
break;
}
$literal .= substr($sql, $startPos, $k - $startPos + 1);
$startPos = $k + 1;
}
if ($startPos < $n) {
$literal .= substr($sql, $startPos, $n - $startPos);
}
$this->parameterKeyMapping = $mapping;
return $literal;
}
/**
* Binds a parameter to the specified variable name.
*
* @param string|integer $parameter Parameter identifier. For a prepared statement using named placeholders, this will be a parameter
* name of the form `:name`. For a prepared statement using question mark placeholders, this will be
* the 1-indexed position of the parameter.
* @param mixed $variable Name of the PHP variable to bind to the SQL statement parameter.
* @param string $dataType Constant corresponding to a SQL datatype, this should be the processed type from the QueryInterface.
* @param ?integer $length The length of the variable. Usually required for OUTPUT parameters.
* @param ?array $driverOptions Optional driver options to be used.
*
* @return boolean
*
* @since 2.0.0
*/
public function bindParam($parameter, &$variable, string $dataType = ParameterType::STRING, ?int $length = null, ?array $driverOptions = null)
{
$this->bindedValues[$parameter] =& $variable;
// Validate parameter type
if (!isset($this->parameterTypeMapping[$dataType])) {
throw new \InvalidArgumentException(sprintf('Unsupported parameter type `%s`', $dataType));
}
$this->typesKeyMapping[$parameter] = $this->parameterTypeMapping[$dataType];
$this->statement = null;
return true;
}
/**
* Binds a value to the specified variable.
*
* @param string|integer $parameter Parameter identifier. For a prepared statement using named placeholders, this will be a parameter
* name of the form `:name`. For a prepared statement using question mark placeholders, this will be
* the 1-indexed position of the parameter.
* @param mixed $variable Name of the PHP variable to bind to the SQL statement parameter.
* @param string $dataType Constant corresponding to a SQL datatype, this should be the processed type from the QueryInterface.
*
* @return void
*
* @since 2.0.0
*/
private function bindValue($parameter, $variable, $dataType = ParameterType::STRING)
{
$this->bindedValues[$parameter] = $variable;
$this->typesKeyMapping[$parameter] = $dataType;
}
/**
* Closes the cursor, enabling the statement to be executed again.
*
* @return void
*
* @since 2.0.0
*/
public function closeCursor(): void
{
if (!$this->result || !\is_resource($this->statement)) {
return;
}
// Emulate freeing the result fetching and discarding rows, similarly to what PDO does in this case
while (sqlsrv_fetch($this->statement)) {
// Do nothing (see above)
}
$this->result = false;
}
/**
* Fetches the SQLSTATE associated with the last operation on the statement handle.
*
* @return string
*
* @since 2.0.0
*/
public function errorCode()
{
$errors = sqlsrv_errors(SQLSRV_ERR_ERRORS);
if ($errors) {
return $errors[0]['code'];
}
return false;
}
/**
* Fetches extended error information associated with the last operation on the statement handle.
*
* @return array
*
* @since 2.0.0
*/
public function errorInfo()
{
return sqlsrv_errors(SQLSRV_ERR_ERRORS);
}
/**
* Executes a prepared statement
*
* @param array|null $parameters An array of values with as many elements as there are bound parameters in the SQL statement being executed.
*
* @return boolean
*
* @since 2.0.0
*/
public function execute(?array $parameters = null)
{
if (empty($this->bindedValues) && $parameters !== null) {
$hasZeroIndex = array_key_exists(0, $parameters);
foreach ($parameters as $key => $val) {
$key = ($hasZeroIndex && is_numeric($key)) ? $key + 1 : $key;
$this->bindValue($key, $val);
}
}
if (!$this->statement) {
$this->statement = $this->prepare();
}
if (!sqlsrv_execute($this->statement)) {
$errors = $this->errorInfo();
throw new ExecutionFailureException($this->query, $errors[0]['message'], $errors[0]['code']);
}
$this->result = true;
return true;
}
/**
* Fetches the next row from a result set
*
* @param integer|null $fetchStyle Controls how the next row will be returned to the caller. This value must be one of the
* FetchMode constants, defaulting to value of FetchMode::MIXED.
* @param integer $cursorOrientation For a StatementInterface object representing a scrollable cursor, this value determines which row
* will be returned to the caller. This value must be one of the FetchOrientation constants,
* defaulting to FetchOrientation::NEXT.
* @param integer $cursorOffset For a StatementInterface object representing a scrollable cursor for which the cursorOrientation
* parameter is set to FetchOrientation::ABS, this value specifies the absolute number of the row in
* the result set that shall be fetched. For a StatementInterface object representing a scrollable
* cursor for which the cursorOrientation parameter is set to FetchOrientation::REL, this value
* specifies the row to fetch relative to the cursor position before `fetch()` was called.
*
* @return mixed The return value of this function on success depends on the fetch type. In all cases, boolean false is returned on failure.
*
* @since 2.0.0
*/
public function fetch(?int $fetchStyle = null, int $cursorOrientation = FetchOrientation::NEXT, int $cursorOffset = 0)
{
if (!$this->result) {
return false;
}
$fetchStyle = $fetchStyle ?: $this->defaultFetchStyle;
if ($fetchStyle === FetchMode::COLUMN) {
return $this->fetchColumn();
}
if (isset($this->fetchMap[$fetchStyle])) {
return sqlsrv_fetch_array($this->statement, $this->fetchMap[$fetchStyle]) ?: false;
}
if (\in_array($fetchStyle, [FetchMode::STANDARD_OBJECT, FetchMode::CUSTOM_OBJECT], true)) {
return sqlsrv_fetch_object($this->statement, $this->defaultObjectClass) ?: false;
}
throw new \InvalidArgumentException("Unknown fetch type '{$fetchStyle}'");
}
/**
* Returns a single column from the next row of a result set
*
* @param integer $columnIndex 0-indexed number of the column you wish to retrieve from the row.
* If no value is supplied, the first column is retrieved.
*
* @return mixed Returns a single column from the next row of a result set or boolean false if there are no more rows.
*
* @since 2.0.0
*/
public function fetchColumn($columnIndex = 0)
{
$row = $this->fetch(FetchMode::NUMERIC);
if ($row === false) {
return false;
}
return $row[$columnIndex] ?? null;
}
/**
* Prepares the SQL Server statement resource for execution
*
* @return resource
*
* @since 2.0.0
*/
private function prepare()
{
$params = [];
$options = [];
foreach ($this->bindedValues as $key => &$value) {
$variable = [
&$value,
SQLSRV_PARAM_IN,
];
if ($this->typesKeyMapping[$key] === $this->parameterTypeMapping[ParameterType::LARGE_OBJECT]) {
$variable[] = $this->typesKeyMapping[$key];
$variable[] = SQLSRV_SQLTYPE_VARBINARY('max');
}
if (isset($this->parameterKeyMapping[$key])) {
$params[$this->parameterKeyMapping[$key]] = $variable;
} else {
$params[] = $variable;
}
}
// Cleanup referenced variable
unset($value);
// SQLSRV Function sqlsrv_num_rows requires a static or keyset cursor.
if (strncmp(strtoupper(ltrim($this->query)), 'SELECT', \strlen('SELECT')) === 0) {
$options = ['Scrollable' => SQLSRV_CURSOR_KEYSET];
}
$statement = sqlsrv_prepare($this->connection, $this->query, $params, $options);
if (!$statement) {
$errors = $this->errorInfo();
throw new PrepareStatementFailureException($errors[0]['message'], $errors[0]['code']);
}
return $statement;
}
/**
* Returns the number of rows affected by the last SQL statement.
*
* @return integer
*
* @since 2.0.0
*/
public function rowCount(): int
{
if (strncmp(strtoupper(ltrim($this->query)), 'SELECT', \strlen('SELECT')) === 0) {
return sqlsrv_num_rows($this->statement);
}
return sqlsrv_rows_affected($this->statement);
}
/**
* Sets the fetch mode to use while iterating this statement.
*
* @param integer $fetchMode The fetch mode, must be one of the FetchMode constants.
* @param mixed ...$args Optional mode-specific arguments.
*
* @return void
*
* @since 2.0.0
*/
public function setFetchMode(int $fetchMode, ...$args): void
{
$this->defaultFetchStyle = $fetchMode;
if (isset($args[0])) {
$this->defaultObjectClass = $args[0];
}
}
}

View File

@ -0,0 +1,116 @@
<?php
/**
* Part of the Joomla Framework Database Package
*
* @copyright Copyright (C) 2005 - 2021 Open Source Matters, Inc. All rights reserved.
* @license GNU General Public License version 2 or later; see LICENSE
*/
namespace Joomla\Database;
/**
* Interface defining a query statement.
*
* This interface is a partial standalone implementation of PDOStatement.
*
* @since 2.0.0
*/
interface StatementInterface
{
/**
* Binds a parameter to the specified variable name.
*
* @param string|integer $parameter Parameter identifier. For a prepared statement using named placeholders, this will be a parameter
* name of the form `:name`. For a prepared statement using question mark placeholders, this will be
* the 1-indexed position of the parameter.
* @param mixed $variable Name of the PHP variable to bind to the SQL statement parameter.
* @param string $dataType Constant corresponding to a SQL datatype, this should be the processed type from the QueryInterface.
* @param ?integer $length The length of the variable. Usually required for OUTPUT parameters.
* @param ?array $driverOptions Optional driver options to be used.
*
* @return boolean
*
* @since 2.0.0
*/
public function bindParam($parameter, &$variable, string $dataType = ParameterType::STRING, ?int $length = null, ?array $driverOptions = null);
/**
* Closes the cursor, enabling the statement to be executed again.
*
* @return void
*
* @since 2.0.0
*/
public function closeCursor(): void;
/**
* Fetches the SQLSTATE associated with the last operation on the statement handle.
*
* @return string
*
* @since 2.0.0
*/
public function errorCode();
/**
* Fetches extended error information associated with the last operation on the statement handle.
*
* @return array
*
* @since 2.0.0
*/
public function errorInfo();
/**
* Executes a prepared statement
*
* @param array|null $parameters An array of values with as many elements as there are bound parameters in the SQL statement being executed.
*
* @return boolean
*
* @since 2.0.0
*/
public function execute(?array $parameters = null);
/**
* Fetches the next row from a result set
*
* @param integer|null $fetchStyle Controls how the next row will be returned to the caller. This value must be one of the
* FetchMode constants, defaulting to value of FetchMode::MIXED.
* @param integer $cursorOrientation For a StatementInterface object representing a scrollable cursor, this value determines which row
* will be returned to the caller. This value must be one of the FetchOrientation constants,
* defaulting to FetchOrientation::NEXT.
* @param integer $cursorOffset For a StatementInterface object representing a scrollable cursor for which the cursorOrientation
* parameter is set to FetchOrientation::ABS, this value specifies the absolute number of the row in
* the result set that shall be fetched. For a StatementInterface object representing a scrollable
* cursor for which the cursorOrientation parameter is set to FetchOrientation::REL, this value
* specifies the row to fetch relative to the cursor position before `fetch()` was called.
*
* @return mixed The return value of this function on success depends on the fetch type. In all cases, boolean false is returned on failure.
*
* @since 2.0.0
*/
public function fetch(?int $fetchStyle = null, int $cursorOrientation = FetchOrientation::NEXT, int $cursorOffset = 0);
/**
* Returns the number of rows affected by the last SQL statement.
*
* @return integer
*
* @since 2.0.0
*/
public function rowCount(): int;
/**
* Sets the fetch mode to use while iterating this statement.
*
* @param integer $fetchMode The fetch mode, must be one of the FetchMode constants.
* @param mixed ...$args Optional mode-specific arguments.
*
* @return void
*
* @since 2.0.0
*/
public function setFetchMode(int $fetchMode, ...$args): void;
}

View File

@ -0,0 +1,40 @@
<?php
/**
* Part of the Joomla Framework Database Package
*
* @copyright Copyright (C) 2005 - 2021 Open Source Matters, Inc. All rights reserved.
* @license GNU General Public License version 2 or later; see LICENSE
*/
namespace Joomla\Database;
/**
* Interface defining a driver which has support for the MySQL `utf8mb4` character set
*
* @since 2.0.0
*/
interface UTF8MB4SupportInterface
{
/**
* Automatically downgrade a CREATE TABLE or ALTER TABLE query from utf8mb4 (UTF-8 Multibyte) to plain utf8.
*
* Used when the server doesn't support UTF-8 Multibyte.
*
* @param string $query The query to convert
*
* @return string The converted query
*
* @since 2.0.0
*/
public function convertUtf8mb4QueryToUtf8($query);
/**
* Check whether the database engine supports the UTF-8 Multibyte (utf8mb4) character encoding.
*
* @return boolean True if the database engine supports UTF-8 Multibyte.
*
* @since 2.0.0
*/
public function hasUtf8mb4Support();
}