primo commit

This commit is contained in:
2024-12-17 17:34:10 +01:00
commit e650f8df99
16435 changed files with 2451012 additions and 0 deletions

View File

@ -0,0 +1,177 @@
<?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\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
/**
* 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');
$filenames = [];
$zipFilesArray = [];
}
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];
$filenames[] = $filename;
}
$symfonyStyle->text(sprintf('Exported data for %s in %d seconds', $table, round(microtime(true) - $taskTime, 3)));
}
}
if ($zip) {
$zipArchive->create($zipFile, $zipFilesArray);
foreach ($filenames as $fname) {
File::delete($fname);
}
}
$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 and not the views.
return $this->setQuery('SHOW FULL TABLES WHERE table_type="BASE TABLE"')->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,588 @@
<?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 = [];
$position = 0;
$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]);
}
if (!isset($mapping[$match[0]])) {
$mapping[$match[0]] = [];
}
$mapping[$match[0]][] = $position++;
$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) {
$paramKey = $this->parameterKeyMapping[$key];
foreach ($paramKey as $currentKey) {
$params[$currentKey] =& $value;
$types[$currentKey] = $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,263 @@
<?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
*
* @todo Remove this method when the database version requirements have been raised
* to >= 8.0.0 for MySQL and >= 10.2.0 for MariaDB so the ROW_NUMBER() window
* function can be used in any case.
*/
public function selectRowNumber($orderBy, $orderColumnAlias)
{
// Use parent method with ROW_NUMBER() window function on MariaDB 11.0.0 and newer.
if ($this->db->isMariaDb() && version_compare($this->db->getVersion(), '11.0.0', '>=')) {
return parent::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\Exception\UnknownTypeException;
use Joomla\Database\Query\LimitableInterface;
use Joomla\Database\Query\PreparableInterface;
/**
* 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,933 @@
<?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;
$options['encrypt'] = isset($options['encrypt']) ? (bool) $options['encrypt'] : 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,
'Encrypt' => $this->options['encrypt'],
];
// 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,554 @@
<?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 = [];
$position = 0;
$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]);
}
if (!isset($mapping[$match[0]])) {
$mapping[$match[0]] = [];
}
$mapping[$match[0]][] = $position++;
$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])) {
$paramKey = $this->parameterKeyMapping[$key];
foreach ($paramKey as $currentKey) {
$params[$currentKey] = $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();
}