<?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\PdoDriver;

/**
 * PostgreSQL PDO Database Driver
 *
 * @since  1.5.0
 */
class PgsqlDriver extends PdoDriver
{
	/**
	 * The database driver name
	 *
	 * @var    string
	 * @since  1.5.0
	 */
	public $name = 'pgsql';

	/**
	 * The character(s) used to quote SQL statement names such as table names or field names,
	 * etc. The child classes should define this as necessary.  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.5.0
	 */
	protected $nameQuote = '"';

	/**
	 * The null or zero representation of a timestamp for the database driver.  This should be
	 * defined in child classes to hold the appropriate value for the engine.
	 *
	 * @var    string
	 * @since  1.5.0
	 */
	protected $nullDate = '1970-01-01 00:00:00';

	/**
	 * The minimum supported database version.
	 *
	 * @var    string
	 * @since  1.5.0
	 */
	protected static $dbMinimum = '9.4.0';

	/**
	 * Operator used for concatenation
	 *
	 * @var    string
	 * @since  1.5.0
	 */
	protected $concat_operator = '||';

	/**
	 * Database object constructor
	 *
	 * @param   array  $options  List of options used to configure the connection
	 *
	 * @since	1.5.0
	 */
	public function __construct($options)
	{
		$options['driver']   = 'pgsql';
		$options['host']     = $options['host'] ?? 'localhost';
		$options['user']     = $options['user'] ?? '';
		$options['password'] = $options['password'] ?? '';
		$options['database'] = $options['database'] ?? '';
		$options['port']     = $options['port'] ?? null;

		// Finalize initialization
		parent::__construct($options);
	}

	/**
	 * Connects to the database if needed.
	 *
	 * @return  void
	 *
	 * @since   1.5.0
	 * @throws  \RuntimeException
	 */
	public function connect()
	{
		if ($this->getConnection())
		{
			return;
		}

		parent::connect();

		$this->setQuery('SET standard_conforming_strings = off')->execute();
	}

	/**
	 * 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.5.0
	 * @throws  \RuntimeException
	 */
	public function getCollation()
	{
		$this->setQuery('SHOW LC_COLLATE');
		$array = $this->loadAssocList();

		return $array[0]['lc_collate'];
	}

	/**
	 * 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->setQuery('SHOW LC_COLLATE');
		$array = $this->loadAssocList();

		return $array[0]['lc_collate'];
	}

	/**
	 * 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
	{
		// Requires PostgreSQL 9.5 or newer
		if (version_compare($this->getVersion(), '9.5', '<'))
		{
			return '';
		}

		$query = $this->getQuery(true)
			->select($this->quoteName(['version', 'cipher']))
			->from($this->quoteName('pg_stat_ssl'))
			->where($this->quoteName('pid') . ' = pg_backend_pid()');

		$variables = $this->setQuery($query)->loadAssoc();

		if (!empty($variables['cipher']))
		{
			return $variables['version'] . ' (' . $variables['cipher'] . ')';
		}

		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
	{
		$variables = $this->setQuery('SHOW "ssl"')->loadAssoc();

		return !empty($variables['ssl']) && $variables['ssl'] === 'on';
	}

	/**
	 * Internal function to get the name of the default schema for the current PostgreSQL connection.
	 * That is the schema where tables are created by Joomla.
	 *
	 * @return  string
	 *
	 * @since   1.8.0
	 */
	private function getDefaultSchema()
	{
		// Supported since PostgreSQL 7.3
		$this->setQuery('SELECT (current_schemas(false))[1]');

		return $this->loadResult();
	}

	/**
	 * Shows the table CREATE statement that creates the given tables.
	 *
	 * This is unsupported by PostgreSQL.
	 *
	 * @param   mixed  $tables  A table name or a list of table names.
	 *
	 * @return  string  An empty string because this function is not supported by PostgreSQL.
	 *
	 * @since   1.5.0
	 * @throws  \RuntimeException
	 */
	public function getTableCreate($tables)
	{
		return '';
	}

	/**
	 * 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.5.0
	 * @throws  \RuntimeException
	 */
	public function getTableColumns($table, $typeOnly = true)
	{
		$this->connect();

		$result        = [];
		$tableSub      = $this->replacePrefix($table);
		$defaultSchema = $this->getDefaultSchema();

		$this->setQuery('
			SELECT a.attname AS "column_name",
				pg_catalog.format_type(a.atttypid, a.atttypmod) as "type",
				CASE WHEN a.attnotnull IS TRUE
					THEN \'NO\'
					ELSE \'YES\'
				END AS "null",
				CASE WHEN pg_catalog.pg_get_expr(adef.adbin, adef.adrelid, true) IS NOT NULL
					THEN pg_catalog.pg_get_expr(adef.adbin, adef.adrelid, true)
				END as "Default",
				CASE WHEN pg_catalog.col_description(a.attrelid, a.attnum) IS NULL
				THEN \'\'
				ELSE pg_catalog.col_description(a.attrelid, a.attnum)
				END  AS "comments"
			FROM pg_catalog.pg_attribute a
			LEFT JOIN pg_catalog.pg_attrdef adef ON a.attrelid=adef.adrelid AND a.attnum=adef.adnum
			LEFT JOIN pg_catalog.pg_type t ON a.atttypid=t.oid
			WHERE a.attrelid =
				(SELECT oid FROM pg_catalog.pg_class WHERE relname=' . $this->quote($tableSub) . '
					AND relnamespace = (SELECT oid FROM pg_catalog.pg_namespace WHERE
					nspname = ' . $this->quote($defaultSchema) . ')
				)
			AND a.attnum > 0 AND NOT a.attisdropped
			ORDER BY a.attnum'
		);

		$fields = $this->loadObjectList();

		if ($typeOnly)
		{
			foreach ($fields as $field)
			{
				$result[$field->column_name] = preg_replace('/[(0-9)]/', '', $field->type);
			}
		}
		else
		{
			foreach ($fields as $field)
			{
				if ($field->Default !== null)
				{
					// Normalise default values like datetime
					if (preg_match('/^\'(.*)\'::.*/', $field->Default, $matches))
					{
						$field->Default = $matches[1];
					}

					// Change Postgresql's NULL::* type with PHP's null one. Do this last to avoid PHP type issues in PHP 8.1 and up
					if (preg_match('/^NULL::*/', $field->Default))
					{
						$field->Default = null;
					}
				}

				// Do some dirty translation to MySQL output.
				// @todo: Come up with and implement a standard across databases.
				$result[$field->column_name] = (object) [
					'column_name' => $field->column_name,
					'type'        => $field->type,
					'null'        => $field->null,
					'Default'     => $field->Default,
					'comments'    => '',
					'Field'       => $field->column_name,
					'Type'        => $field->type,
					'Null'        => $field->null,
					// @todo: Improve query above to return primary key info as well
					// 'Key' => ($field->PK == '1' ? 'PRI' : '')
				];
			}
		}

		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.5.0
	 * @throws  \RuntimeException
	 */
	public function getTableKeys($table)
	{
		$this->connect();

		// To check if table exists and prevent SQL injection
		$tableList = $this->getTableList();
		$tableSub  = $this->replacePrefix($table);

		if (\in_array($tableSub, $tableList, true))
		{
			// Get the details columns information.
			$this->setQuery('
				SELECT indexname AS "idxName", indisprimary AS "isPrimary", indisunique  AS "isUnique", indkey AS "indKey",
					CASE WHEN indisprimary = true THEN
						( SELECT \'ALTER TABLE \' || tablename || \' ADD \' || pg_catalog.pg_get_constraintdef(const.oid, true)
							FROM pg_constraint AS const WHERE const.conname= pgClassFirst.relname )
					ELSE pg_catalog.pg_get_indexdef(indexrelid, 0, true)
					END AS "Query"
				FROM pg_indexes
				LEFT JOIN pg_class AS pgClassFirst ON indexname=pgClassFirst.relname
				LEFT JOIN pg_index AS pgIndex ON pgClassFirst.oid=pgIndex.indexrelid
				WHERE tablename=' . $this->quote($tableSub) . ' ORDER BY indkey'
			);

			return $this->loadObjectList();
		}

		return [];
	}

	/**
	 * Get the list of column names this index indexes.
	 *
	 * @param   string  $table   The name of the table.
	 * @param   string  $indKey  The list of column numbers for the table
	 *
	 * @return  string  A list of the column names for the table.
	 *
	 * @since   2.0.0
	 * @throws  \RuntimeException
	 */
	public function getNamesKey($table, $indKey)
	{
		$this->connect();

		$tableSub = $this->replacePrefix($table);

		$tabInd   = explode(' ', $indKey);
		$colNames = [];

		foreach ($tabInd as $numCol)
		{
			$query = $this->getQuery(true)
				->select('attname')
				->from('pg_attribute')
				->join('LEFT', 'pg_class ON pg_class.relname=' . $this->quote($tableSub))
				->where('attnum=' . $numCol . ' AND attrelid=pg_class.oid');
			$this->setQuery($query);
			$colNames[] = $this->loadResult();
		}

		return implode(', ', $colNames);
	}

	/**
	 * Method to get an array of all tables in the database.
	 *
	 * @return  array  An array of all the tables in the database.
	 *
	 * @since   1.5.0
	 * @throws  \RuntimeException
	 */
	public function getTableList()
	{
		$query = $this->getQuery(true)
			->select('table_name')
			->from('information_schema.tables')
			->where('table_type = ' . $this->quote('BASE TABLE'))
			->where('table_schema NOT IN (' . $this->quote('pg_catalog') . ', ' . $this->quote('information_schema') . ')')
			->order('table_name ASC');

		$this->setQuery($query);

		return $this->loadColumn();
	}

	/**
	 * Get the details list of sequences for a table.
	 *
	 * @param   string  $table  The name of the table.
	 *
	 * @return  array  An array of sequences specification for the table.
	 *
	 * @since   1.5.0
	 * @throws  \RuntimeException
	 */
	public function getTableSequences($table)
	{
		// To check if table exists and prevent SQL injection
		$tableList = $this->getTableList();
		$tableSub  = $this->replacePrefix($table);

		if (\in_array($tableSub, $tableList, true))
		{
			$name = [
				's.relname', 'n.nspname', 't.relname', 'a.attname', 'info.data_type',
				'info.minimum_value', 'info.maximum_value', 'info.increment', 'info.cycle_option', 'info.start_value',
			];

			$as = [
				'sequence', 'schema', 'table', 'column', 'data_type', 'minimum_value', 'maximum_value', 'increment', 'cycle_option', 'start_value',
			];

			// Get the details columns information.
			$query = $this->getQuery(true)
				->select($this->quoteName($name, $as))
				->from('pg_class AS s')
				->leftJoin("pg_depend d ON d.objid = s.oid AND d.classid = 'pg_class'::regclass AND d.refclassid = 'pg_class'::regclass")
				->leftJoin('pg_class t ON t.oid = d.refobjid')
				->leftJoin('pg_namespace n ON n.oid = t.relnamespace')
				->leftJoin('pg_attribute a ON a.attrelid = t.oid AND a.attnum = d.refobjsubid')
				->leftJoin('information_schema.sequences AS info ON info.sequence_name = s.relname')
				->where('s.relkind = ' . $this->quote('S') . ' AND d.deptype = ' . $this->quote('a') . ' AND t.relname = ' . $this->quote($tableSub));
			$this->setQuery($query);

			return $this->loadObjectList();
		}

		return [];
	}

	/**
	 * Method to get the last value of a sequence in the database.
	 *
	 * @param   string  $sequence  The name of the sequence.
	 *
	 * @return  integer  The last value of the sequence.
	 *
	 * @since   2.0.0
	 * @throws  \RuntimeException
	 */
	public function getSequenceLastValue($sequence)
	{
		$this->connect();

		$query = $this->getQuery(true)
			->select($this->quoteName('last_value'))
			->from($sequence);

		$this->setQuery($query);

		return $this->loadResult();
	}

	/**
	 * Method to get the is_called attribute of a sequence.
	 *
	 * @param   string  $sequence  The name of the sequence.
	 *
	 * @return  boolean  The is_called attribute of the sequence.
	 *
	 * @since   2.0.0
	 * @throws  \RuntimeException
	 */
	public function getSequenceIsCalled($sequence)
	{
		$this->connect();

		$query = $this->getQuery(true)
			->select($this->quoteName('is_called'))
			->from($sequence);

		$this->setQuery($query);

		return $this->loadResult();
	}

	/**
	 * Locks a table in the database.
	 *
	 * @param   string  $tableName  The name of the table to unlock.
	 *
	 * @return  $this
	 *
	 * @since   1.5.0
	 * @throws  \RuntimeException
	 */
	public function lockTable($tableName)
	{
		$this->transactionStart();
		$this->setQuery('LOCK TABLE ' . $this->quoteName($tableName) . ' IN ACCESS EXCLUSIVE MODE')->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 PostgreSQL.
	 * @param   string  $prefix    Not used by PostgreSQL.
	 *
	 * @return  $this
	 *
	 * @since   1.5.0
	 * @throws  \RuntimeException
	 */
	public function renameTable($oldTable, $newTable, $backup = null, $prefix = null)
	{
		$this->connect();

		$oldTable = $this->replacePrefix($oldTable);
		$newTable = $this->replacePrefix($newTable);

		// To check if table exists and prevent SQL injection
		$tableList = $this->getTableList();

		// Origin Table does not exist
		if (!\in_array($oldTable, $tableList, true))
		{
			// Origin Table not found
			throw new \RuntimeException('Table not found in Postgresql database.');
		}

		// Rename indexes
		$subQuery = $this->getQuery(true)
			->select('indexrelid')
			->from('pg_index, pg_class')
			->where('pg_class.relname = ' . $this->quote($oldTable))
			->where('pg_class.oid = pg_index.indrelid');

		$this->setQuery(
			$this->getQuery(true)
				->select('relname')
				->from('pg_class')
				->where('oid IN (' . (string) $subQuery . ')')
		);

		$oldIndexes = $this->loadColumn();

		foreach ($oldIndexes as $oldIndex)
		{
			$changedIdxName = str_replace($oldTable, $newTable, $oldIndex);
			$this->setQuery('ALTER INDEX ' . $this->escape($oldIndex) . ' RENAME TO ' . $this->escape($changedIdxName))->execute();
		}

		// Rename sequences
		$subQuery = $this->getQuery(true)
			->select('oid')
			->from('pg_namespace')
			->where('nspname NOT LIKE ' . $this->quote('pg_%'))
			->where('nspname != ' . $this->quote('information_schema'));

		$this->setQuery(
			$this->getQuery(true)
				->select('relname')
				->from('pg_class')
				->where('relkind = ' . $this->quote('S'))
				->where('relnamespace IN (' . (string) $subQuery . ')')
				->where('relname LIKE ' . $this->quote("%$oldTable%"))
		);

		$oldSequences = $this->loadColumn();

		foreach ($oldSequences as $oldSequence)
		{
			$changedSequenceName = str_replace($oldTable, $newTable, $oldSequence);
			$this->setQuery('ALTER SEQUENCE ' . $this->escape($oldSequence) . ' RENAME TO ' . $this->escape($changedSequenceName))->execute();
		}

		// Rename table
		$this->setQuery('ALTER TABLE ' . $this->escape($oldTable) . ' RENAME TO ' . $this->escape($newTable))->execute();

		return $this;
	}

	/**
	 * This function return a field value as a prepared string to be used in a SQL statement.
	 *
	 * @param   array   $columns     Array of table's column returned by ::getTableColumns.
	 * @param   string  $fieldName   The table field's name.
	 * @param   string  $fieldValue  The variable value to quote and return.
	 *
	 * @return  string  The quoted string.
	 *
	 * @since   1.5.0
	 */
	public function sqlValue($columns, $fieldName, $fieldValue)
	{
		switch ($columns[$fieldName])
		{
			case 'boolean':
				$val = 'NULL';

				if ($fieldValue === 't' || $fieldValue === true || $fieldValue === 1 || $fieldValue === '1')
				{
					$val = 'TRUE';
				}
				elseif ($fieldValue === 'f' || $fieldValue === false || $fieldValue === 0 || $fieldValue === '0')
				{
					$val = 'FALSE';
				}

				break;

			case 'bigint':
			case 'bigserial':
			case 'integer':
			case 'money':
			case 'numeric':
			case 'real':
			case 'smallint':
			case 'serial':
			case 'numeric,':
				$val = $fieldValue === '' ? 'NULL' : $fieldValue;

				break;

			case 'timestamp without time zone':
			case 'date':
				if (empty($fieldValue))
				{
					$fieldValue = $this->getNullDate();
				}

				$val = $this->quote($fieldValue);

				break;

			default:
				$val = $this->quote($fieldValue);

				break;
		}

		return $val;
	}

	/**
	 * 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--;
				$this->setQuery('RELEASE SAVEPOINT ' . $this->quoteName($savepoint))->execute();
			}
		}
	}

	/**
	 * 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++;
			}
		}
	}

	/**
	 * 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.5.0
	 * @throws  \RuntimeException
	 */
	public function insertObject($table, &$object, $key = null)
	{
		$columns = $this->getTableColumns($table);

		$fields = [];
		$values = [];

		// 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, $columns))
			{
				continue;
			}

			// Only process non-null scalars.
			if (\is_array($v) || \is_object($v) || $v === null)
			{
				continue;
			}

			// Ignore any internal fields or primary keys with value 0.
			if (($k[0] === '_') || ($k == $key && (($v === 0) || ($v === '0'))))
			{
				continue;
			}

			// Ignore null timestamp fields.
			if ($columns[$k] === 'timestamp without time zone' && empty($v))
			{
				continue;
			}

			// Prepare and sanitize the fields and values for the database query.
			$fields[] = $this->quoteName($k);
			$values[] = $this->sqlValue($columns, $k, $v);
		}

		// Create the base insert statement.
		$query = $this->getQuery(true);

		$query->insert($this->quoteName($table))
			->columns($fields)
			->values(implode(',', $values));

		if ($key)
		{
			$query->returning($key);

			// Set the query and execute the insert.
			$object->$key = $this->setQuery($query)->loadResult();
		}
		else
		{
			// Set the query and execute the insert.
			$this->setQuery($query)->execute();
		}

		return true;
	}

	/**
	 * Test to see if the PostgreSQL connector is available.
	 *
	 * @return  boolean  True on success, false otherwise.
	 *
	 * @since   1.5.0
	 */
	public static function isSupported()
	{
		return class_exists('\\PDO') && \in_array('pgsql', \PDO::getAvailableDrivers(), true);
	}

	/**
	 * Returns an array containing database's table list.
	 *
	 * @return  array  The database's table list.
	 *
	 * @since   1.5.0
	 */
	public function showTables()
	{
		$query = $this->getQuery(true)
			->select('table_name')
			->from('information_schema.tables')
			->where('table_type=' . $this->quote('BASE TABLE'))
			->where('table_schema NOT IN (' . $this->quote('pg_catalog') . ', ' . $this->quote('information_schema') . ' )');

		$this->setQuery($query);

		return $this->loadColumn();
	}

	/**
	 * Get the substring position inside a string
	 *
	 * @param   string  $substring  The string being sought
	 * @param   string  $string     The string/column being searched
	 *
	 * @return  integer  The position of $substring in $string
	 *
	 * @since   1.5.0
	 */
	public function getStringPositionSql($substring, $string)
	{
		$this->setQuery("SELECT POSITION($substring IN $string)");
		$position = $this->loadRow();

		return $position['position'];
	}

	/**
	 * Generate a random value
	 *
	 * @return  float  The random generated number
	 *
	 * @since   1.5.0
	 */
	public function getRandom()
	{
		$this->setQuery('SELECT RANDOM()');
		$random = $this->loadAssoc();

		return $random['random'];
	}

	/**
	 * Get 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   1.5.0
	 */
	public function getAlterDbCharacterSet($dbName)
	{
		return 'ALTER DATABASE ' . $this->quoteName($dbName) . ' SET CLIENT_ENCODING TO ' . $this->quote('UTF8');
	}

	/**
	 * Get the query string to create new Database in correct PostgreSQL syntax.
	 *
	 * @param   object   $options  object coming from "initialise" function to pass user and database name to database driver.
	 * @param   boolean  $utf      True if the database supports the UTF-8 character set, not used in PostgreSQL "CREATE DATABASE" query.
	 *
	 * @return  string	The query that creates database, owned by $options['user']
	 *
	 * @since   1.5.0
	 */
	public function getCreateDbQuery($options, $utf)
	{
		$query = 'CREATE DATABASE ' . $this->quoteName($options->db_name) . ' OWNER ' . $this->quoteName($options->db_user);

		if ($utf)
		{
			$query .= ' ENCODING ' . $this->quote('UTF-8');
		}

		return $query;
	}

	/**
	 * 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.5.0
	 */
	public function replacePrefix($sql, $prefix = '#__')
	{
		$sql = trim($sql);

		if (strpos($sql, '\''))
		{
			// Sequence name quoted with ' ' but need to be replaced
			if (strpos($sql, 'currval'))
			{
				$sql = explode('currval', $sql);

				for ($nIndex = 1, $nIndexMax = \count($sql); $nIndex < $nIndexMax; $nIndex += 2)
				{
					$sql[$nIndex] = str_replace($prefix, $this->tablePrefix, $sql[$nIndex]);
				}

				$sql = implode('currval', $sql);
			}

			// Sequence name quoted with ' ' but need to be replaced
			if (strpos($sql, 'nextval'))
			{
				$sql = explode('nextval', $sql);

				for ($nIndex = 1, $nIndexMax = \count($sql); $nIndex < $nIndexMax; $nIndex += 2)
				{
					$sql[$nIndex] = str_replace($prefix, $this->tablePrefix, $sql[$nIndex]);
				}

				$sql = implode('nextval', $sql);
			}

			// Sequence name quoted with ' ' but need to be replaced
			if (strpos($sql, 'setval'))
			{
				$sql = explode('setval', $sql);

				for ($nIndex = 1, $nIndexMax = \count($sql); $nIndex < $nIndexMax; $nIndex += 2)
				{
					$sql[$nIndex] = str_replace($prefix, $this->tablePrefix, $sql[$nIndex]);
				}

				$sql = implode('setval', $sql);
			}

			$explodedQuery = explode('\'', $sql);

			for ($nIndex = 0, $nIndexMax = \count($explodedQuery); $nIndex < $nIndexMax; $nIndex += 2)
			{
				if (strpos($explodedQuery[$nIndex], $prefix))
				{
					$explodedQuery[$nIndex] = str_replace($prefix, $this->tablePrefix, $explodedQuery[$nIndex]);
				}
			}

			$replacedQuery = implode('\'', $explodedQuery);
		}
		else
		{
			$replacedQuery = str_replace($prefix, $this->tablePrefix, $sql);
		}

		return $replacedQuery;
	}

	/**
	 * Unlocks tables in the database, this command does not exist in PostgreSQL, it is automatically done on commit or rollback.
	 *
	 * @return  $this
	 *
	 * @since   1.5.0
	 * @throws  \RuntimeException
	 */
	public function unlockTables()
	{
		$this->transactionCommit();

		return $this;
	}

	/**
	 * 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   1.5.0
	 * @throws  \RuntimeException
	 */
	public function updateObject($table, &$object, $key, $nulls = false)
	{
		$columns = $this->getTableColumns($table);
		$fields  = [];
		$where   = [];

		if (\is_string($key))
		{
			$key = [$key];
		}

		if (\is_object($key))
		{
			$key = (array) $key;
		}

		// Create the base update statement.
		$statement = 'UPDATE ' . $this->quoteName($table) . ' SET %s WHERE %s';

		// Iterate over the object variables to build the query fields/value pairs.
		foreach (get_object_vars($object) as $k => $v)
		{
			// Skip columns that don't exist in the table.
			if (!\array_key_exists($k, $columns))
			{
				continue;
			}

			// Only process scalars that are not internal fields.
			if (\is_array($v) || \is_object($v) || $k[0] === '_')
			{
				continue;
			}

			// Set the primary key to the WHERE clause instead of a field to update.
			if (\in_array($k, $key, true))
			{
				$key_val = $this->sqlValue($columns, $k, $v);
				$where[] = $this->quoteName($k) . '=' . $key_val;

				continue;
			}

			// Prepare and sanitize the fields and values for the database query.
			if ($v === null)
			{
				// If the value is null and we do not want to update nulls then ignore this field.
				if (!$nulls)
				{
					continue;
				}

				// If the value is null and we want to update nulls then set it.
				$val = 'NULL';
			}
			else
			{
				// The field is not null so we prep it for update.
				$val = $this->sqlValue($columns, $k, $v);
			}

			// Add the field to be updated.
			$fields[] = $this->quoteName($k) . '=' . $val;
		}

		// We don't have any fields to update.
		if (empty($fields))
		{
			return true;
		}

		// Set the query and execute the update.
		$this->setQuery(sprintf($statement, implode(',', $fields), implode(' AND ', $where)));

		return $this->execute();
	}

	/**
	 * 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)
	{
		return "decode('" . bin2hex($data) . "', 'hex')";
	}

	/**
	 * 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)
	{
		if (\is_resource($data))
		{
			return stream_get_contents($data);
		}

		return $data;
	}
}
