add trashes
This commit is contained in:
@@ -0,0 +1,631 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Database\Postgre;
|
||||
|
||||
use CodeIgniter\Database\BaseBuilder;
|
||||
use CodeIgniter\Database\Exceptions\DatabaseException;
|
||||
use CodeIgniter\Database\RawSql;
|
||||
use CodeIgniter\Exceptions\InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Builder for Postgre
|
||||
*/
|
||||
class Builder extends BaseBuilder
|
||||
{
|
||||
/**
|
||||
* ORDER BY random keyword
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $randomKeyword = [
|
||||
'RANDOM()',
|
||||
];
|
||||
|
||||
/**
|
||||
* Specifies which sql statements
|
||||
* support the ignore option.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $supportedIgnoreStatements = [
|
||||
'insert' => 'ON CONFLICT DO NOTHING',
|
||||
];
|
||||
|
||||
/**
|
||||
* Checks if the ignore option is supported by
|
||||
* the Database Driver for the specific statement.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function compileIgnore(string $statement)
|
||||
{
|
||||
$sql = parent::compileIgnore($statement);
|
||||
|
||||
if (! empty($sql)) {
|
||||
$sql = ' ' . trim($sql);
|
||||
}
|
||||
|
||||
return $sql;
|
||||
}
|
||||
|
||||
/**
|
||||
* ORDER BY
|
||||
*
|
||||
* @param string $direction ASC, DESC or RANDOM
|
||||
*
|
||||
* @return BaseBuilder
|
||||
*/
|
||||
public function orderBy(string $orderBy, string $direction = '', ?bool $escape = null)
|
||||
{
|
||||
$direction = strtoupper(trim($direction));
|
||||
if ($direction === 'RANDOM') {
|
||||
if (ctype_digit($orderBy)) {
|
||||
$orderBy = (float) ($orderBy > 1 ? "0.{$orderBy}" : $orderBy);
|
||||
}
|
||||
|
||||
if (is_float($orderBy)) {
|
||||
$this->db->simpleQuery("SET SEED {$orderBy}");
|
||||
}
|
||||
|
||||
$orderBy = $this->randomKeyword[0];
|
||||
$direction = '';
|
||||
$escape = false;
|
||||
}
|
||||
|
||||
return parent::orderBy($orderBy, $direction, $escape);
|
||||
}
|
||||
|
||||
/**
|
||||
* Increments a numeric column by the specified value.
|
||||
*
|
||||
* @return mixed
|
||||
*
|
||||
* @throws DatabaseException
|
||||
*/
|
||||
public function increment(string $column, int $value = 1)
|
||||
{
|
||||
$column = $this->db->protectIdentifiers($column);
|
||||
|
||||
$sql = $this->_update($this->QBFrom[0], [$column => "to_number({$column}, '9999999') + {$value}"]);
|
||||
|
||||
if (! $this->testMode) {
|
||||
$this->resetWrite();
|
||||
|
||||
return $this->db->query($sql, $this->binds, false);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrements a numeric column by the specified value.
|
||||
*
|
||||
* @return mixed
|
||||
*
|
||||
* @throws DatabaseException
|
||||
*/
|
||||
public function decrement(string $column, int $value = 1)
|
||||
{
|
||||
$column = $this->db->protectIdentifiers($column);
|
||||
|
||||
$sql = $this->_update($this->QBFrom[0], [$column => "to_number({$column}, '9999999') - {$value}"]);
|
||||
|
||||
if (! $this->testMode) {
|
||||
$this->resetWrite();
|
||||
|
||||
return $this->db->query($sql, $this->binds, false);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compiles an replace into string and runs the query.
|
||||
* Because PostgreSQL doesn't support the replace into command,
|
||||
* we simply do a DELETE and an INSERT on the first key/value
|
||||
* combo, assuming that it's either the primary key or a unique key.
|
||||
*
|
||||
* @param array|null $set An associative array of insert values
|
||||
*
|
||||
* @return mixed
|
||||
*
|
||||
* @throws DatabaseException
|
||||
*/
|
||||
public function replace(?array $set = null)
|
||||
{
|
||||
if ($set !== null) {
|
||||
$this->set($set);
|
||||
}
|
||||
|
||||
if ($this->QBSet === []) {
|
||||
if ($this->db->DBDebug) {
|
||||
throw new DatabaseException('You must use the "set" method to update an entry.');
|
||||
}
|
||||
|
||||
return false; // @codeCoverageIgnore
|
||||
}
|
||||
|
||||
$table = $this->QBFrom[0];
|
||||
$set = $this->binds;
|
||||
|
||||
array_walk($set, static function (array &$item): void {
|
||||
$item = $item[0];
|
||||
});
|
||||
|
||||
$key = array_key_first($set);
|
||||
$value = $set[$key];
|
||||
|
||||
$builder = $this->db->table($table);
|
||||
$exists = $builder->where($key, $value, true)->get()->getFirstRow();
|
||||
|
||||
if (empty($exists) && $this->testMode) {
|
||||
$result = $this->getCompiledInsert();
|
||||
} elseif (empty($exists)) {
|
||||
$result = $builder->insert($set);
|
||||
} elseif ($this->testMode) {
|
||||
$result = $this->where($key, $value, true)->getCompiledUpdate();
|
||||
} else {
|
||||
array_shift($set);
|
||||
$result = $builder->where($key, $value, true)->update($set);
|
||||
}
|
||||
|
||||
unset($builder);
|
||||
$this->resetWrite();
|
||||
$this->binds = [];
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a platform-specific insert string from the supplied data
|
||||
*/
|
||||
protected function _insert(string $table, array $keys, array $unescapedKeys): string
|
||||
{
|
||||
return trim(sprintf('INSERT INTO %s (%s) VALUES (%s) %s', $table, implode(', ', $keys), implode(', ', $unescapedKeys), $this->compileIgnore('insert')));
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a platform-specific insert string from the supplied data.
|
||||
*/
|
||||
protected function _insertBatch(string $table, array $keys, array $values): string
|
||||
{
|
||||
$sql = $this->QBOptions['sql'] ?? '';
|
||||
|
||||
// if this is the first iteration of batch then we need to build skeleton sql
|
||||
if ($sql === '') {
|
||||
$sql = 'INSERT INTO ' . $table . '(' . implode(', ', $keys) . ")\n{:_table_:}\n";
|
||||
|
||||
$sql .= $this->compileIgnore('insert');
|
||||
|
||||
$this->QBOptions['sql'] = $sql;
|
||||
}
|
||||
|
||||
if (isset($this->QBOptions['setQueryAsData'])) {
|
||||
$data = $this->QBOptions['setQueryAsData'];
|
||||
} else {
|
||||
$data = 'VALUES ' . implode(', ', $this->formatValues($values));
|
||||
}
|
||||
|
||||
return str_replace('{:_table_:}', $data, $sql);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compiles a delete string and runs the query
|
||||
*
|
||||
* @param mixed $where
|
||||
*
|
||||
* @return mixed
|
||||
*
|
||||
* @throws DatabaseException
|
||||
*/
|
||||
public function delete($where = '', ?int $limit = null, bool $resetData = true)
|
||||
{
|
||||
if ($limit !== null && $limit !== 0 || ! empty($this->QBLimit)) {
|
||||
throw new DatabaseException('PostgreSQL does not allow LIMITs on DELETE queries.');
|
||||
}
|
||||
|
||||
return parent::delete($where, $limit, $resetData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a platform-specific LIMIT clause.
|
||||
*/
|
||||
protected function _limit(string $sql, bool $offsetIgnore = false): string
|
||||
{
|
||||
return $sql . ' LIMIT ' . $this->QBLimit . ($this->QBOffset ? " OFFSET {$this->QBOffset}" : '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a platform-specific update string from the supplied data
|
||||
*
|
||||
* @throws DatabaseException
|
||||
*/
|
||||
protected function _update(string $table, array $values): string
|
||||
{
|
||||
if (! empty($this->QBLimit)) {
|
||||
throw new DatabaseException('Postgres does not support LIMITs with UPDATE queries.');
|
||||
}
|
||||
|
||||
$this->QBOrderBy = [];
|
||||
|
||||
return parent::_update($table, $values);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a platform-specific delete string from the supplied data
|
||||
*/
|
||||
protected function _delete(string $table): string
|
||||
{
|
||||
$this->QBLimit = false;
|
||||
|
||||
return parent::_delete($table);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a platform-specific truncate string from the supplied data
|
||||
*
|
||||
* If the database does not support the truncate() command,
|
||||
* then this method maps to 'DELETE FROM table'
|
||||
*/
|
||||
protected function _truncate(string $table): string
|
||||
{
|
||||
return 'TRUNCATE ' . $table . ' RESTART IDENTITY';
|
||||
}
|
||||
|
||||
/**
|
||||
* Platform independent LIKE statement builder.
|
||||
*
|
||||
* In PostgreSQL, the ILIKE operator will perform case insensitive
|
||||
* searches according to the current locale.
|
||||
*
|
||||
* @see https://www.postgresql.org/docs/9.2/static/functions-matching.html
|
||||
*/
|
||||
protected function _like_statement(?string $prefix, string $column, ?string $not, string $bind, bool $insensitiveSearch = false): string
|
||||
{
|
||||
$op = $insensitiveSearch ? 'ILIKE' : 'LIKE';
|
||||
|
||||
return "{$prefix} {$column} {$not} {$op} :{$bind}:";
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the JOIN portion of the query
|
||||
*
|
||||
* @param RawSql|string $cond
|
||||
*
|
||||
* @return BaseBuilder
|
||||
*/
|
||||
public function join(string $table, $cond, string $type = '', ?bool $escape = null)
|
||||
{
|
||||
if (! in_array('FULL OUTER', $this->joinTypes, true)) {
|
||||
$this->joinTypes = array_merge($this->joinTypes, ['FULL OUTER']);
|
||||
}
|
||||
|
||||
return parent::join($table, $cond, $type, $escape);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a platform-specific batch update string from the supplied data
|
||||
*
|
||||
* @used-by batchExecute()
|
||||
*
|
||||
* @param string $table Protected table name
|
||||
* @param list<string> $keys QBKeys
|
||||
* @param list<list<int|string>> $values QBSet
|
||||
*/
|
||||
protected function _updateBatch(string $table, array $keys, array $values): string
|
||||
{
|
||||
$sql = $this->QBOptions['sql'] ?? '';
|
||||
|
||||
// if this is the first iteration of batch then we need to build skeleton sql
|
||||
if ($sql === '') {
|
||||
$constraints = $this->QBOptions['constraints'] ?? [];
|
||||
|
||||
if ($constraints === []) {
|
||||
if ($this->db->DBDebug) {
|
||||
throw new DatabaseException('You must specify a constraint to match on for batch updates.'); // @codeCoverageIgnore
|
||||
}
|
||||
|
||||
return ''; // @codeCoverageIgnore
|
||||
}
|
||||
|
||||
$updateFields = $this->QBOptions['updateFields'] ??
|
||||
$this->updateFields($keys, false, $constraints)->QBOptions['updateFields'] ??
|
||||
[];
|
||||
|
||||
$alias = $this->QBOptions['alias'] ?? '_u';
|
||||
|
||||
$sql = 'UPDATE ' . $this->compileIgnore('update') . $table . "\n";
|
||||
|
||||
$sql .= "SET\n";
|
||||
|
||||
$that = $this;
|
||||
$sql .= implode(
|
||||
",\n",
|
||||
array_map(
|
||||
static fn ($key, $value): string => $key . ($value instanceof RawSql ?
|
||||
' = ' . $value :
|
||||
' = ' . $that->cast($alias . '.' . $value, $that->getFieldType($table, $key))),
|
||||
array_keys($updateFields),
|
||||
$updateFields,
|
||||
),
|
||||
) . "\n";
|
||||
|
||||
$sql .= "FROM (\n{:_table_:}";
|
||||
|
||||
$sql .= ') ' . $alias . "\n";
|
||||
|
||||
$sql .= 'WHERE ' . implode(
|
||||
' AND ',
|
||||
array_map(
|
||||
static function ($key, $value) use ($table, $alias, $that): string|RawSql {
|
||||
if ($value instanceof RawSql && is_string($key)) {
|
||||
return $table . '.' . $key . ' = ' . $value;
|
||||
}
|
||||
|
||||
if ($value instanceof RawSql) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
return $table . '.' . $value . ' = '
|
||||
. $that->cast($alias . '.' . $value, $that->getFieldType($table, $value));
|
||||
},
|
||||
array_keys($constraints),
|
||||
$constraints,
|
||||
),
|
||||
);
|
||||
|
||||
$this->QBOptions['sql'] = $sql;
|
||||
}
|
||||
|
||||
if (isset($this->QBOptions['setQueryAsData'])) {
|
||||
$data = $this->QBOptions['setQueryAsData'];
|
||||
} else {
|
||||
$data = implode(
|
||||
" UNION ALL\n",
|
||||
array_map(
|
||||
static fn ($value): string => 'SELECT ' . implode(', ', array_map(
|
||||
static fn ($key, $index): string => $index . ' ' . $key,
|
||||
$keys,
|
||||
$value,
|
||||
)),
|
||||
$values,
|
||||
),
|
||||
) . "\n";
|
||||
}
|
||||
|
||||
return str_replace('{:_table_:}', $data, $sql);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns cast expression.
|
||||
*
|
||||
* @TODO move this to BaseBuilder in 4.5.0
|
||||
*/
|
||||
private function cast(string $expression, ?string $type): string
|
||||
{
|
||||
return ($type === null) ? $expression : 'CAST(' . $expression . ' AS ' . strtoupper($type) . ')';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the filed type from database meta data.
|
||||
*
|
||||
* @param string $table Protected table name.
|
||||
* @param string $fieldName Field name. May be protected.
|
||||
*/
|
||||
private function getFieldType(string $table, string $fieldName): ?string
|
||||
{
|
||||
$fieldName = trim($fieldName, $this->db->escapeChar);
|
||||
|
||||
if (! isset($this->QBOptions['fieldTypes'][$table])) {
|
||||
$this->QBOptions['fieldTypes'][$table] = [];
|
||||
|
||||
foreach ($this->db->getFieldData($table) as $field) {
|
||||
$type = $field->type;
|
||||
|
||||
// If `character` (or `char`) lacks a specifier, it is equivalent
|
||||
// to `character(1)`.
|
||||
// See https://www.postgresql.org/docs/current/datatype-character.html
|
||||
if ($field->type === 'character') {
|
||||
$type = $field->type . '(' . $field->max_length . ')';
|
||||
}
|
||||
|
||||
$this->QBOptions['fieldTypes'][$table][$field->name] = $type;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->QBOptions['fieldTypes'][$table][$fieldName] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a platform-specific upsertBatch string from the supplied data
|
||||
*
|
||||
* @throws DatabaseException
|
||||
*/
|
||||
protected function _upsertBatch(string $table, array $keys, array $values): string
|
||||
{
|
||||
$sql = $this->QBOptions['sql'] ?? '';
|
||||
|
||||
// if this is the first iteration of batch then we need to build skeleton sql
|
||||
if ($sql === '') {
|
||||
$fieldNames = array_map(static fn ($columnName): string => trim($columnName, '"'), $keys);
|
||||
|
||||
$constraints = $this->QBOptions['constraints'] ?? [];
|
||||
|
||||
if (empty($constraints)) {
|
||||
$allIndexes = array_filter($this->db->getIndexData($table), static function ($index) use ($fieldNames): bool {
|
||||
$hasAllFields = count(array_intersect($index->fields, $fieldNames)) === count($index->fields);
|
||||
|
||||
return ($index->type === 'UNIQUE' || $index->type === 'PRIMARY') && $hasAllFields;
|
||||
});
|
||||
|
||||
foreach ($allIndexes as $index) {
|
||||
$constraints = $index->fields;
|
||||
break;
|
||||
}
|
||||
|
||||
$constraints = $this->onConstraint($constraints)->QBOptions['constraints'] ?? [];
|
||||
}
|
||||
|
||||
if (empty($constraints)) {
|
||||
if ($this->db->DBDebug) {
|
||||
throw new DatabaseException('No constraint found for upsert.');
|
||||
}
|
||||
|
||||
return ''; // @codeCoverageIgnore
|
||||
}
|
||||
|
||||
// in value set - replace null with DEFAULT where constraint is presumed not null
|
||||
// autoincrement identity field must use DEFAULT and not NULL
|
||||
// this could be removed in favour of leaving to developer but does make things easier and function like other DBMS
|
||||
foreach ($constraints as $constraint) {
|
||||
$key = array_search(trim((string) $constraint, '"'), $fieldNames, true);
|
||||
|
||||
if ($key !== false) {
|
||||
foreach ($values as $arrayKey => $value) {
|
||||
if (strtoupper((string) $value[$key]) === 'NULL') {
|
||||
$values[$arrayKey][$key] = 'DEFAULT';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$alias = $this->QBOptions['alias'] ?? '"excluded"';
|
||||
|
||||
if (strtolower($alias) !== '"excluded"') {
|
||||
throw new InvalidArgumentException('Postgres alias is always named "excluded". A custom alias cannot be used.');
|
||||
}
|
||||
|
||||
$updateFields = $this->QBOptions['updateFields'] ?? $this->updateFields($keys, false, $constraints)->QBOptions['updateFields'] ?? [];
|
||||
|
||||
$sql = 'INSERT INTO ' . $table . ' (';
|
||||
|
||||
$sql .= implode(', ', $keys);
|
||||
|
||||
$sql .= ")\n";
|
||||
|
||||
$sql .= '{:_table_:}';
|
||||
|
||||
$sql .= 'ON CONFLICT(' . implode(',', $constraints) . ")\n";
|
||||
|
||||
$sql .= "DO UPDATE SET\n";
|
||||
|
||||
$sql .= implode(
|
||||
",\n",
|
||||
array_map(
|
||||
static fn ($key, $value): string => $key . ($value instanceof RawSql ?
|
||||
" = {$value}" :
|
||||
" = {$alias}.{$value}"),
|
||||
array_keys($updateFields),
|
||||
$updateFields,
|
||||
),
|
||||
);
|
||||
|
||||
$this->QBOptions['sql'] = $sql;
|
||||
}
|
||||
|
||||
if (isset($this->QBOptions['setQueryAsData'])) {
|
||||
$data = $this->QBOptions['setQueryAsData'];
|
||||
} else {
|
||||
$data = 'VALUES ' . implode(', ', $this->formatValues($values)) . "\n";
|
||||
}
|
||||
|
||||
return str_replace('{:_table_:}', $data, $sql);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a platform-specific batch update string from the supplied data
|
||||
*/
|
||||
protected function _deleteBatch(string $table, array $keys, array $values): string
|
||||
{
|
||||
$sql = $this->QBOptions['sql'] ?? '';
|
||||
|
||||
// if this is the first iteration of batch then we need to build skeleton sql
|
||||
if ($sql === '') {
|
||||
$constraints = $this->QBOptions['constraints'] ?? [];
|
||||
|
||||
if ($constraints === []) {
|
||||
if ($this->db->DBDebug) {
|
||||
throw new DatabaseException('You must specify a constraint to match on for batch deletes.'); // @codeCoverageIgnore
|
||||
}
|
||||
|
||||
return ''; // @codeCoverageIgnore
|
||||
}
|
||||
|
||||
$alias = $this->QBOptions['alias'] ?? '_u';
|
||||
|
||||
$sql = 'DELETE FROM ' . $table . "\n";
|
||||
|
||||
$sql .= "USING (\n{:_table_:}";
|
||||
|
||||
$sql .= ') ' . $alias . "\n";
|
||||
|
||||
$that = $this;
|
||||
$sql .= 'WHERE ' . implode(
|
||||
' AND ',
|
||||
array_map(
|
||||
static function ($key, $value) use ($table, $alias, $that): RawSql|string {
|
||||
if ($value instanceof RawSql) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
if (is_string($key)) {
|
||||
return $table . '.' . $key . ' = '
|
||||
. $that->cast(
|
||||
$alias . '.' . $value,
|
||||
$that->getFieldType($table, $key),
|
||||
);
|
||||
}
|
||||
|
||||
return $table . '.' . $value . ' = ' . $alias . '.' . $value;
|
||||
},
|
||||
array_keys($constraints),
|
||||
$constraints,
|
||||
),
|
||||
);
|
||||
|
||||
// convert binds in where
|
||||
foreach ($this->QBWhere as $key => $where) {
|
||||
foreach ($this->binds as $field => $bind) {
|
||||
$this->QBWhere[$key]['condition'] = str_replace(':' . $field . ':', $bind[0], $where['condition']);
|
||||
}
|
||||
}
|
||||
|
||||
$sql .= ' ' . str_replace(
|
||||
'WHERE ',
|
||||
'AND ',
|
||||
$this->compileWhereHaving('QBWhere'),
|
||||
);
|
||||
|
||||
$this->QBOptions['sql'] = $sql;
|
||||
}
|
||||
|
||||
if (isset($this->QBOptions['setQueryAsData'])) {
|
||||
$data = $this->QBOptions['setQueryAsData'];
|
||||
} else {
|
||||
$data = implode(
|
||||
" UNION ALL\n",
|
||||
array_map(
|
||||
static fn ($value): string => 'SELECT ' . implode(', ', array_map(
|
||||
static fn ($key, $index): string => $index . ' ' . $key,
|
||||
$keys,
|
||||
$value,
|
||||
)),
|
||||
$values,
|
||||
),
|
||||
) . "\n";
|
||||
}
|
||||
|
||||
return str_replace('{:_table_:}', $data, $sql);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,604 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Database\Postgre;
|
||||
|
||||
use CodeIgniter\Database\BaseConnection;
|
||||
use CodeIgniter\Database\Exceptions\DatabaseException;
|
||||
use CodeIgniter\Database\RawSql;
|
||||
use CodeIgniter\Database\TableName;
|
||||
use ErrorException;
|
||||
use PgSql\Connection as PgSqlConnection;
|
||||
use PgSql\Result as PgSqlResult;
|
||||
use stdClass;
|
||||
use Stringable;
|
||||
|
||||
/**
|
||||
* Connection for Postgre
|
||||
*
|
||||
* @extends BaseConnection<PgSqlConnection, PgSqlResult>
|
||||
*/
|
||||
class Connection extends BaseConnection
|
||||
{
|
||||
/**
|
||||
* Database driver
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $DBDriver = 'Postgre';
|
||||
|
||||
/**
|
||||
* Database schema
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $schema = 'public';
|
||||
|
||||
/**
|
||||
* Identifier escape character
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $escapeChar = '"';
|
||||
|
||||
protected $connect_timeout;
|
||||
protected $options;
|
||||
protected $sslmode;
|
||||
protected $service;
|
||||
|
||||
/**
|
||||
* Connect to the database.
|
||||
*
|
||||
* @return false|PgSqlConnection
|
||||
*/
|
||||
public function connect(bool $persistent = false)
|
||||
{
|
||||
if (empty($this->DSN)) {
|
||||
$this->buildDSN();
|
||||
}
|
||||
|
||||
// Convert DSN string
|
||||
// @TODO This format is for PDO_PGSQL.
|
||||
// https://www.php.net/manual/en/ref.pdo-pgsql.connection.php
|
||||
// Should deprecate?
|
||||
if (mb_strpos($this->DSN, 'pgsql:') === 0) {
|
||||
$this->convertDSN();
|
||||
}
|
||||
|
||||
$this->connID = $persistent ? pg_pconnect($this->DSN) : pg_connect($this->DSN);
|
||||
|
||||
if ($this->connID !== false) {
|
||||
if (
|
||||
$persistent
|
||||
&& pg_connection_status($this->connID) === PGSQL_CONNECTION_BAD
|
||||
&& pg_ping($this->connID) === false
|
||||
) {
|
||||
$error = pg_last_error($this->connID);
|
||||
|
||||
throw new DatabaseException($error);
|
||||
}
|
||||
|
||||
if (! empty($this->schema)) {
|
||||
$this->simpleQuery("SET search_path TO {$this->schema},public");
|
||||
}
|
||||
|
||||
if ($this->setClientEncoding($this->charset) === false) {
|
||||
$error = pg_last_error($this->connID);
|
||||
|
||||
throw new DatabaseException($error);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->connID;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the DSN with semicolon syntax.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function convertDSN()
|
||||
{
|
||||
// Strip pgsql
|
||||
$this->DSN = mb_substr($this->DSN, 6);
|
||||
|
||||
// Convert semicolons to spaces in DSN format like:
|
||||
// pgsql:host=localhost;port=5432;dbname=database_name
|
||||
// https://www.php.net/manual/en/function.pg-connect.php
|
||||
$allowedParams = ['host', 'port', 'dbname', 'user', 'password', 'connect_timeout', 'options', 'sslmode', 'service'];
|
||||
|
||||
$parameters = explode(';', $this->DSN);
|
||||
|
||||
$output = '';
|
||||
$previousParameter = '';
|
||||
|
||||
foreach ($parameters as $parameter) {
|
||||
[$key, $value] = explode('=', $parameter, 2);
|
||||
if (in_array($key, $allowedParams, true)) {
|
||||
if ($previousParameter !== '') {
|
||||
if (array_search($key, $allowedParams, true) < array_search($previousParameter, $allowedParams, true)) {
|
||||
$output .= ';';
|
||||
} else {
|
||||
$output .= ' ';
|
||||
}
|
||||
}
|
||||
$output .= $parameter;
|
||||
$previousParameter = $key;
|
||||
} else {
|
||||
$output .= ';' . $parameter;
|
||||
}
|
||||
}
|
||||
|
||||
$this->DSN = $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Keep or establish the connection if no queries have been sent for
|
||||
* a length of time exceeding the server's idle timeout.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function reconnect()
|
||||
{
|
||||
if ($this->connID === false || pg_ping($this->connID) === false) {
|
||||
$this->close();
|
||||
$this->initialize();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the database connection.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function _close()
|
||||
{
|
||||
pg_close($this->connID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Select a specific database table to use.
|
||||
*/
|
||||
public function setDatabase(string $databaseName): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string containing the version of the database being used.
|
||||
*/
|
||||
public function getVersion(): string
|
||||
{
|
||||
if (isset($this->dataCache['version'])) {
|
||||
return $this->dataCache['version'];
|
||||
}
|
||||
|
||||
if (! $this->connID) {
|
||||
$this->initialize();
|
||||
}
|
||||
|
||||
$pgVersion = pg_version($this->connID);
|
||||
$this->dataCache['version'] = isset($pgVersion['server']) ?
|
||||
(preg_match('/^(\d+\.\d+)/', $pgVersion['server'], $matches) ? $matches[1] : '') :
|
||||
'';
|
||||
|
||||
return $this->dataCache['version'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the query against the database.
|
||||
*
|
||||
* @return false|PgSqlResult
|
||||
*/
|
||||
protected function execute(string $sql)
|
||||
{
|
||||
try {
|
||||
return pg_query($this->connID, $sql);
|
||||
} catch (ErrorException $e) {
|
||||
log_message('error', (string) $e);
|
||||
|
||||
if ($this->DBDebug) {
|
||||
throw new DatabaseException($e->getMessage(), $e->getCode(), $e);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the prefix of the function to access the DB.
|
||||
*/
|
||||
protected function getDriverFunctionPrefix(): string
|
||||
{
|
||||
return 'pg_';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the total number of rows affected by this query.
|
||||
*/
|
||||
public function affectedRows(): int
|
||||
{
|
||||
if ($this->resultID === false) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return pg_affected_rows($this->resultID);
|
||||
}
|
||||
|
||||
/**
|
||||
* "Smart" Escape String
|
||||
*
|
||||
* Escapes data based on type
|
||||
*
|
||||
* @param array|bool|float|int|object|string|null $str
|
||||
*
|
||||
* @return array|float|int|string
|
||||
* @phpstan-return ($str is array ? array : float|int|string)
|
||||
*/
|
||||
public function escape($str)
|
||||
{
|
||||
if (! $this->connID) {
|
||||
$this->initialize();
|
||||
}
|
||||
|
||||
if ($str instanceof Stringable) {
|
||||
if ($str instanceof RawSql) {
|
||||
return $str->__toString();
|
||||
}
|
||||
|
||||
$str = (string) $str;
|
||||
}
|
||||
|
||||
if (is_string($str)) {
|
||||
return pg_escape_literal($this->connID, $str);
|
||||
}
|
||||
|
||||
if (is_bool($str)) {
|
||||
return $str ? 'TRUE' : 'FALSE';
|
||||
}
|
||||
|
||||
return parent::escape($str);
|
||||
}
|
||||
|
||||
/**
|
||||
* Platform-dependant string escape
|
||||
*/
|
||||
protected function _escapeString(string $str): string
|
||||
{
|
||||
if (! $this->connID) {
|
||||
$this->initialize();
|
||||
}
|
||||
|
||||
return pg_escape_string($this->connID, $str);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the SQL for listing tables in a platform-dependent manner.
|
||||
*
|
||||
* @param string|null $tableName If $tableName is provided will return only this table if exists.
|
||||
*/
|
||||
protected function _listTables(bool $prefixLimit = false, ?string $tableName = null): string
|
||||
{
|
||||
$sql = 'SELECT "table_name" FROM "information_schema"."tables" WHERE "table_schema" = \'' . $this->schema . "'";
|
||||
|
||||
if ($tableName !== null) {
|
||||
return $sql . ' AND "table_name" LIKE ' . $this->escape($tableName);
|
||||
}
|
||||
|
||||
if ($prefixLimit && $this->DBPrefix !== '') {
|
||||
return $sql . ' AND "table_name" LIKE \''
|
||||
. $this->escapeLikeString($this->DBPrefix) . "%' "
|
||||
. sprintf($this->likeEscapeStr, $this->likeEscapeChar);
|
||||
}
|
||||
|
||||
return $sql;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a platform-specific query string so that the column names can be fetched.
|
||||
*
|
||||
* @param string|TableName $table
|
||||
*/
|
||||
protected function _listColumns($table = ''): string
|
||||
{
|
||||
if ($table instanceof TableName) {
|
||||
$tableName = $this->escape($table->getActualTableName());
|
||||
} else {
|
||||
$tableName = $this->escape($this->DBPrefix . strtolower($table));
|
||||
}
|
||||
|
||||
return 'SELECT "column_name"
|
||||
FROM "information_schema"."columns"
|
||||
WHERE LOWER("table_name") = ' . $tableName
|
||||
. ' ORDER BY "ordinal_position"';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of objects with field data
|
||||
*
|
||||
* @return list<stdClass>
|
||||
*
|
||||
* @throws DatabaseException
|
||||
*/
|
||||
protected function _fieldData(string $table): array
|
||||
{
|
||||
$sql = 'SELECT "column_name", "data_type", "character_maximum_length", "numeric_precision", "column_default", "is_nullable"
|
||||
FROM "information_schema"."columns"
|
||||
WHERE LOWER("table_name") = '
|
||||
. $this->escape(strtolower($table))
|
||||
. ' ORDER BY "ordinal_position"';
|
||||
|
||||
if (($query = $this->query($sql)) === false) {
|
||||
throw new DatabaseException(lang('Database.failGetFieldData'));
|
||||
}
|
||||
$query = $query->getResultObject();
|
||||
|
||||
$retVal = [];
|
||||
|
||||
for ($i = 0, $c = count($query); $i < $c; $i++) {
|
||||
$retVal[$i] = new stdClass();
|
||||
|
||||
$retVal[$i]->name = $query[$i]->column_name;
|
||||
$retVal[$i]->type = $query[$i]->data_type;
|
||||
$retVal[$i]->max_length = $query[$i]->character_maximum_length > 0 ? $query[$i]->character_maximum_length : $query[$i]->numeric_precision;
|
||||
$retVal[$i]->nullable = $query[$i]->is_nullable === 'YES';
|
||||
$retVal[$i]->default = $query[$i]->column_default;
|
||||
}
|
||||
|
||||
return $retVal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of objects with index data
|
||||
*
|
||||
* @return array<string, stdClass>
|
||||
*
|
||||
* @throws DatabaseException
|
||||
*/
|
||||
protected function _indexData(string $table): array
|
||||
{
|
||||
$sql = 'SELECT "indexname", "indexdef"
|
||||
FROM "pg_indexes"
|
||||
WHERE LOWER("tablename") = ' . $this->escape(strtolower($table)) . '
|
||||
AND "schemaname" = ' . $this->escape('public');
|
||||
|
||||
if (($query = $this->query($sql)) === false) {
|
||||
throw new DatabaseException(lang('Database.failGetIndexData'));
|
||||
}
|
||||
$query = $query->getResultObject();
|
||||
|
||||
$retVal = [];
|
||||
|
||||
foreach ($query as $row) {
|
||||
$obj = new stdClass();
|
||||
$obj->name = $row->indexname;
|
||||
$_fields = explode(',', preg_replace('/^.*\((.+?)\)$/', '$1', trim($row->indexdef)));
|
||||
$obj->fields = array_map(static fn ($v): string => trim($v), $_fields);
|
||||
|
||||
if (str_starts_with($row->indexdef, 'CREATE UNIQUE INDEX pk')) {
|
||||
$obj->type = 'PRIMARY';
|
||||
} else {
|
||||
$obj->type = (str_starts_with($row->indexdef, 'CREATE UNIQUE')) ? 'UNIQUE' : 'INDEX';
|
||||
}
|
||||
|
||||
$retVal[$obj->name] = $obj;
|
||||
}
|
||||
|
||||
return $retVal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of objects with Foreign key data
|
||||
*
|
||||
* @return array<string, stdClass>
|
||||
*
|
||||
* @throws DatabaseException
|
||||
*/
|
||||
protected function _foreignKeyData(string $table): array
|
||||
{
|
||||
$sql = 'SELECT c.constraint_name,
|
||||
x.table_name,
|
||||
x.column_name,
|
||||
y.table_name as foreign_table_name,
|
||||
y.column_name as foreign_column_name,
|
||||
c.delete_rule,
|
||||
c.update_rule,
|
||||
c.match_option
|
||||
FROM information_schema.referential_constraints c
|
||||
JOIN information_schema.key_column_usage x
|
||||
on x.constraint_name = c.constraint_name
|
||||
JOIN information_schema.key_column_usage y
|
||||
on y.ordinal_position = x.position_in_unique_constraint
|
||||
and y.constraint_name = c.unique_constraint_name
|
||||
WHERE x.table_name = ' . $this->escape($table) .
|
||||
'order by c.constraint_name, x.ordinal_position';
|
||||
|
||||
if (($query = $this->query($sql)) === false) {
|
||||
throw new DatabaseException(lang('Database.failGetForeignKeyData'));
|
||||
}
|
||||
|
||||
$query = $query->getResultObject();
|
||||
$indexes = [];
|
||||
|
||||
foreach ($query as $row) {
|
||||
$indexes[$row->constraint_name]['constraint_name'] = $row->constraint_name;
|
||||
$indexes[$row->constraint_name]['table_name'] = $table;
|
||||
$indexes[$row->constraint_name]['column_name'][] = $row->column_name;
|
||||
$indexes[$row->constraint_name]['foreign_table_name'] = $row->foreign_table_name;
|
||||
$indexes[$row->constraint_name]['foreign_column_name'][] = $row->foreign_column_name;
|
||||
$indexes[$row->constraint_name]['on_delete'] = $row->delete_rule;
|
||||
$indexes[$row->constraint_name]['on_update'] = $row->update_rule;
|
||||
$indexes[$row->constraint_name]['match'] = $row->match_option;
|
||||
}
|
||||
|
||||
return $this->foreignKeyDataToObjects($indexes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns platform-specific SQL to disable foreign key checks.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function _disableForeignKeyChecks()
|
||||
{
|
||||
return 'SET CONSTRAINTS ALL DEFERRED';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns platform-specific SQL to enable foreign key checks.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function _enableForeignKeyChecks()
|
||||
{
|
||||
return 'SET CONSTRAINTS ALL IMMEDIATE;';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the last error code and message.
|
||||
* Must return this format: ['code' => string|int, 'message' => string]
|
||||
* intval(code) === 0 means "no error".
|
||||
*
|
||||
* @return array<string, int|string>
|
||||
*/
|
||||
public function error(): array
|
||||
{
|
||||
return [
|
||||
'code' => '',
|
||||
'message' => pg_last_error($this->connID),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int|string
|
||||
*/
|
||||
public function insertID()
|
||||
{
|
||||
$v = pg_version($this->connID);
|
||||
// 'server' key is only available since PostgreSQL 7.4
|
||||
$v = explode(' ', $v['server'])[0] ?? 0;
|
||||
|
||||
$table = func_num_args() > 0 ? func_get_arg(0) : null;
|
||||
$column = func_num_args() > 1 ? func_get_arg(1) : null;
|
||||
|
||||
if ($table === null && $v >= '8.1') {
|
||||
$sql = 'SELECT LASTVAL() AS ins_id';
|
||||
} elseif ($table !== null) {
|
||||
if ($column !== null && $v >= '8.0') {
|
||||
$sql = "SELECT pg_get_serial_sequence('{$table}', '{$column}') AS seq";
|
||||
$query = $this->query($sql);
|
||||
$query = $query->getRow();
|
||||
$seq = $query->seq;
|
||||
} else {
|
||||
// seq_name passed in table parameter
|
||||
$seq = $table;
|
||||
}
|
||||
|
||||
$sql = "SELECT CURRVAL('{$seq}') AS ins_id";
|
||||
} else {
|
||||
return pg_last_oid($this->resultID);
|
||||
}
|
||||
|
||||
$query = $this->query($sql);
|
||||
$query = $query->getRow();
|
||||
|
||||
return (int) $query->ins_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a DSN from the provided parameters
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function buildDSN()
|
||||
{
|
||||
if ($this->DSN !== '') {
|
||||
$this->DSN = '';
|
||||
}
|
||||
|
||||
// If UNIX sockets are used, we shouldn't set a port
|
||||
if (str_contains($this->hostname, '/')) {
|
||||
$this->port = '';
|
||||
}
|
||||
|
||||
if ($this->hostname !== '') {
|
||||
$this->DSN = "host={$this->hostname} ";
|
||||
}
|
||||
|
||||
// ctype_digit only accepts strings
|
||||
$port = (string) $this->port;
|
||||
|
||||
if ($port !== '' && ctype_digit($port)) {
|
||||
$this->DSN .= "port={$port} ";
|
||||
}
|
||||
|
||||
if ($this->username !== '') {
|
||||
$this->DSN .= "user={$this->username} ";
|
||||
|
||||
// An empty password is valid!
|
||||
// password must be set to null to ignore it.
|
||||
if ($this->password !== null) {
|
||||
$this->DSN .= "password='{$this->password}' ";
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->database !== '') {
|
||||
$this->DSN .= "dbname={$this->database} ";
|
||||
}
|
||||
|
||||
// We don't have these options as elements in our standard configuration
|
||||
// array, but they might be set by parse_url() if the configuration was
|
||||
// provided via string> Example:
|
||||
//
|
||||
// Postgre://username:password@localhost:5432/database?connect_timeout=5&sslmode=1
|
||||
foreach (['connect_timeout', 'options', 'sslmode', 'service'] as $key) {
|
||||
if (isset($this->{$key}) && is_string($this->{$key}) && $this->{$key} !== '') {
|
||||
$this->DSN .= "{$key}='{$this->{$key}}' ";
|
||||
}
|
||||
}
|
||||
|
||||
$this->DSN = rtrim($this->DSN);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set client encoding
|
||||
*/
|
||||
protected function setClientEncoding(string $charset): bool
|
||||
{
|
||||
return pg_set_client_encoding($this->connID, $charset) === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Begin Transaction
|
||||
*/
|
||||
protected function _transBegin(): bool
|
||||
{
|
||||
return (bool) pg_query($this->connID, 'BEGIN');
|
||||
}
|
||||
|
||||
/**
|
||||
* Commit Transaction
|
||||
*/
|
||||
protected function _transCommit(): bool
|
||||
{
|
||||
return (bool) pg_query($this->connID, 'COMMIT');
|
||||
}
|
||||
|
||||
/**
|
||||
* Rollback Transaction
|
||||
*/
|
||||
protected function _transRollback(): bool
|
||||
{
|
||||
return (bool) pg_query($this->connID, 'ROLLBACK');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Database\Postgre;
|
||||
|
||||
use CodeIgniter\Database\Forge as BaseForge;
|
||||
|
||||
/**
|
||||
* Forge for Postgre
|
||||
*/
|
||||
class Forge extends BaseForge
|
||||
{
|
||||
/**
|
||||
* CHECK DATABASE EXIST statement
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $checkDatabaseExistStr = 'SELECT 1 FROM pg_database WHERE datname = ?';
|
||||
|
||||
/**
|
||||
* DROP CONSTRAINT statement
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $dropConstraintStr = 'ALTER TABLE %s DROP CONSTRAINT %s';
|
||||
|
||||
/**
|
||||
* DROP INDEX statement
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $dropIndexStr = 'DROP INDEX %s';
|
||||
|
||||
/**
|
||||
* UNSIGNED support
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $_unsigned = [
|
||||
'INT2' => 'INTEGER',
|
||||
'SMALLINT' => 'INTEGER',
|
||||
'INT' => 'BIGINT',
|
||||
'INT4' => 'BIGINT',
|
||||
'INTEGER' => 'BIGINT',
|
||||
'INT8' => 'NUMERIC',
|
||||
'BIGINT' => 'NUMERIC',
|
||||
'REAL' => 'DOUBLE PRECISION',
|
||||
'FLOAT' => 'DOUBLE PRECISION',
|
||||
];
|
||||
|
||||
/**
|
||||
* NULL value representation in CREATE/ALTER TABLE statements
|
||||
*
|
||||
* @var string
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
protected $null = 'NULL';
|
||||
|
||||
/**
|
||||
* @var Connection
|
||||
*/
|
||||
protected $db;
|
||||
|
||||
/**
|
||||
* CREATE TABLE attributes
|
||||
*
|
||||
* @param array $attributes Associative array of table attributes
|
||||
*/
|
||||
protected function _createTableAttributes(array $attributes): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array|string $processedFields Processed column definitions
|
||||
* or column names to DROP
|
||||
*
|
||||
* @return false|list<string>|string SQL string or false
|
||||
* @phpstan-return ($alterType is 'DROP' ? string : list<string>|false)
|
||||
*/
|
||||
protected function _alterTable(string $alterType, string $table, $processedFields)
|
||||
{
|
||||
if (in_array($alterType, ['DROP', 'ADD'], true)) {
|
||||
return parent::_alterTable($alterType, $table, $processedFields);
|
||||
}
|
||||
|
||||
$sql = 'ALTER TABLE ' . $this->db->escapeIdentifiers($table);
|
||||
$sqls = [];
|
||||
|
||||
foreach ($processedFields as $field) {
|
||||
if ($field['_literal'] !== false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (version_compare($this->db->getVersion(), '8', '>=') && isset($field['type'])) {
|
||||
$sqls[] = $sql . ' ALTER COLUMN ' . $this->db->escapeIdentifiers($field['name'])
|
||||
. " TYPE {$field['type']}{$field['length']}";
|
||||
}
|
||||
|
||||
if (! empty($field['default'])) {
|
||||
$sqls[] = $sql . ' ALTER COLUMN ' . $this->db->escapeIdentifiers($field['name'])
|
||||
. " SET DEFAULT {$field['default']}";
|
||||
}
|
||||
|
||||
$nullable = true; // Nullable by default.
|
||||
if (isset($field['null']) && ($field['null'] === false || $field['null'] === ' NOT ' . $this->null)) {
|
||||
$nullable = false;
|
||||
}
|
||||
$sqls[] = $sql . ' ALTER COLUMN ' . $this->db->escapeIdentifiers($field['name'])
|
||||
. ($nullable ? ' DROP' : ' SET') . ' NOT NULL';
|
||||
|
||||
if (! empty($field['new_name'])) {
|
||||
$sqls[] = $sql . ' RENAME COLUMN ' . $this->db->escapeIdentifiers($field['name'])
|
||||
. ' TO ' . $this->db->escapeIdentifiers($field['new_name']);
|
||||
}
|
||||
|
||||
if (! empty($field['comment'])) {
|
||||
$sqls[] = 'COMMENT ON COLUMN' . $this->db->escapeIdentifiers($table)
|
||||
. '.' . $this->db->escapeIdentifiers($field['name'])
|
||||
. " IS {$field['comment']}";
|
||||
}
|
||||
}
|
||||
|
||||
return $sqls;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process column
|
||||
*/
|
||||
protected function _processColumn(array $processedField): string
|
||||
{
|
||||
return $this->db->escapeIdentifiers($processedField['name'])
|
||||
. ' ' . $processedField['type'] . ($processedField['type'] === 'text' ? '' : $processedField['length'])
|
||||
. $processedField['default']
|
||||
. $processedField['null']
|
||||
. $processedField['auto_increment']
|
||||
. $processedField['unique'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a data type mapping between different databases.
|
||||
*/
|
||||
protected function _attributeType(array &$attributes)
|
||||
{
|
||||
// Reset field lengths for data types that don't support it
|
||||
if (isset($attributes['CONSTRAINT']) && str_contains(strtolower($attributes['TYPE']), 'int')) {
|
||||
$attributes['CONSTRAINT'] = null;
|
||||
}
|
||||
|
||||
switch (strtoupper($attributes['TYPE'])) {
|
||||
case 'TINYINT':
|
||||
$attributes['TYPE'] = 'SMALLINT';
|
||||
$attributes['UNSIGNED'] = false;
|
||||
break;
|
||||
|
||||
case 'MEDIUMINT':
|
||||
$attributes['TYPE'] = 'INTEGER';
|
||||
$attributes['UNSIGNED'] = false;
|
||||
break;
|
||||
|
||||
case 'DATETIME':
|
||||
$attributes['TYPE'] = 'TIMESTAMP';
|
||||
break;
|
||||
|
||||
case 'BLOB':
|
||||
$attributes['TYPE'] = 'BYTEA';
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Field attribute AUTO_INCREMENT
|
||||
*/
|
||||
protected function _attributeAutoIncrement(array &$attributes, array &$field)
|
||||
{
|
||||
if (! empty($attributes['AUTO_INCREMENT']) && $attributes['AUTO_INCREMENT'] === true) {
|
||||
$field['type'] = $field['type'] === 'NUMERIC' || $field['type'] === 'BIGINT' ? 'BIGSERIAL' : 'SERIAL';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a platform-specific DROP TABLE string
|
||||
*/
|
||||
protected function _dropTable(string $table, bool $ifExists, bool $cascade): string
|
||||
{
|
||||
$sql = parent::_dropTable($table, $ifExists, $cascade);
|
||||
|
||||
if ($cascade) {
|
||||
$sql .= ' CASCADE';
|
||||
}
|
||||
|
||||
return $sql;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs sql to check if key is a constraint.
|
||||
*/
|
||||
protected function _dropKeyAsConstraint(string $table, string $constraintName): string
|
||||
{
|
||||
return "SELECT con.conname
|
||||
FROM pg_catalog.pg_constraint con
|
||||
INNER JOIN pg_catalog.pg_class rel
|
||||
ON rel.oid = con.conrelid
|
||||
INNER JOIN pg_catalog.pg_namespace nsp
|
||||
ON nsp.oid = connamespace
|
||||
WHERE nsp.nspname = '{$this->db->schema}'
|
||||
AND rel.relname = '" . trim($table, '"') . "'
|
||||
AND con.conname = '" . trim($constraintName, '"') . "'";
|
||||
}
|
||||
}
|
||||
+134
@@ -0,0 +1,134 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Database\Postgre;
|
||||
|
||||
use CodeIgniter\Database\BasePreparedQuery;
|
||||
use CodeIgniter\Database\Exceptions\DatabaseException;
|
||||
use CodeIgniter\Exceptions\BadMethodCallException;
|
||||
use Exception;
|
||||
use PgSql\Connection as PgSqlConnection;
|
||||
use PgSql\Result as PgSqlResult;
|
||||
|
||||
/**
|
||||
* Prepared query for Postgre
|
||||
*
|
||||
* @extends BasePreparedQuery<PgSqlConnection, PgSqlResult, PgSqlResult>
|
||||
*/
|
||||
class PreparedQuery extends BasePreparedQuery
|
||||
{
|
||||
/**
|
||||
* Stores the name this query can be
|
||||
* used under by postgres. Only used internally.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $name;
|
||||
|
||||
/**
|
||||
* The result resource from a successful
|
||||
* pg_exec. Or false.
|
||||
*
|
||||
* @var false|PgSqlResult
|
||||
*/
|
||||
protected $result;
|
||||
|
||||
/**
|
||||
* Prepares the query against the database, and saves the connection
|
||||
* info necessary to execute the query later.
|
||||
*
|
||||
* NOTE: This version is based on SQL code. Child classes should
|
||||
* override this method.
|
||||
*
|
||||
* @param array $options Passed to the connection's prepare statement.
|
||||
* Unused in the MySQLi driver.
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function _prepare(string $sql, array $options = []): PreparedQuery
|
||||
{
|
||||
$this->name = (string) random_int(1, 10_000_000_000_000_000);
|
||||
|
||||
$sql = $this->parameterize($sql);
|
||||
|
||||
// Update the query object since the parameters are slightly different
|
||||
// than what was put in.
|
||||
$this->query->setQuery($sql);
|
||||
|
||||
if (! $this->statement = pg_prepare($this->db->connID, $this->name, $sql)) {
|
||||
$this->errorCode = 0;
|
||||
$this->errorString = pg_last_error($this->db->connID);
|
||||
|
||||
if ($this->db->DBDebug) {
|
||||
throw new DatabaseException($this->errorString . ' code: ' . $this->errorCode);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a new set of data and runs it against the currently
|
||||
* prepared query. Upon success, will return a Results object.
|
||||
*/
|
||||
public function _execute(array $data): bool
|
||||
{
|
||||
if (! isset($this->statement)) {
|
||||
throw new BadMethodCallException('You must call prepare before trying to execute a prepared statement.');
|
||||
}
|
||||
|
||||
foreach ($data as &$item) {
|
||||
if (is_string($item) && $this->isBinary($item)) {
|
||||
$item = pg_escape_bytea($this->db->connID, $item);
|
||||
}
|
||||
}
|
||||
|
||||
$this->result = pg_execute($this->db->connID, $this->name, $data);
|
||||
|
||||
return (bool) $this->result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the result object for the prepared query or false on failure.
|
||||
*
|
||||
* @return PgSqlResult|null
|
||||
*/
|
||||
public function _getResult()
|
||||
{
|
||||
return $this->result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deallocate prepared statements.
|
||||
*/
|
||||
protected function _close(): bool
|
||||
{
|
||||
return pg_query($this->db->connID, 'DEALLOCATE "' . $this->db->escapeIdentifiers($this->name) . '"') !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces the ? placeholders with $1, $2, etc parameters for use
|
||||
* within the prepared query.
|
||||
*/
|
||||
public function parameterize(string $sql): string
|
||||
{
|
||||
// Track our current value
|
||||
$count = 0;
|
||||
|
||||
return preg_replace_callback('/\?/', static function () use (&$count): string {
|
||||
$count++;
|
||||
|
||||
return "\${$count}";
|
||||
}, $sql);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Database\Postgre;
|
||||
|
||||
use CodeIgniter\Database\BaseResult;
|
||||
use CodeIgniter\Entity\Entity;
|
||||
use PgSql\Connection as PgSqlConnection;
|
||||
use PgSql\Result as PgSqlResult;
|
||||
use stdClass;
|
||||
|
||||
/**
|
||||
* Result for Postgre
|
||||
*
|
||||
* @extends BaseResult<PgSqlConnection, PgSqlResult>
|
||||
*/
|
||||
class Result extends BaseResult
|
||||
{
|
||||
/**
|
||||
* Gets the number of fields in the result set.
|
||||
*/
|
||||
public function getFieldCount(): int
|
||||
{
|
||||
return pg_num_fields($this->resultID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an array of column names in the result set.
|
||||
*/
|
||||
public function getFieldNames(): array
|
||||
{
|
||||
$fieldNames = [];
|
||||
|
||||
for ($i = 0, $c = $this->getFieldCount(); $i < $c; $i++) {
|
||||
$fieldNames[] = pg_field_name($this->resultID, $i);
|
||||
}
|
||||
|
||||
return $fieldNames;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an array of objects representing field meta-data.
|
||||
*/
|
||||
public function getFieldData(): array
|
||||
{
|
||||
$retVal = [];
|
||||
|
||||
for ($i = 0, $c = $this->getFieldCount(); $i < $c; $i++) {
|
||||
$retVal[$i] = new stdClass();
|
||||
$retVal[$i]->name = pg_field_name($this->resultID, $i);
|
||||
$retVal[$i]->type = pg_field_type_oid($this->resultID, $i);
|
||||
$retVal[$i]->type_name = pg_field_type($this->resultID, $i);
|
||||
$retVal[$i]->max_length = pg_field_size($this->resultID, $i);
|
||||
$retVal[$i]->length = $retVal[$i]->max_length;
|
||||
// $retVal[$i]->primary_key = (int)($fieldData[$i]->flags & 2);
|
||||
// $retVal[$i]->default = $fieldData[$i]->def;
|
||||
}
|
||||
|
||||
return $retVal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Frees the current result.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function freeResult()
|
||||
{
|
||||
if ($this->resultID !== false) {
|
||||
pg_free_result($this->resultID);
|
||||
$this->resultID = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves the internal pointer to the desired offset. This is called
|
||||
* internally before fetching results to make sure the result set
|
||||
* starts at zero.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function dataSeek(int $n = 0)
|
||||
{
|
||||
return pg_result_seek($this->resultID, $n);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the result set as an array.
|
||||
*
|
||||
* Overridden by driver classes.
|
||||
*
|
||||
* @return array|false
|
||||
*/
|
||||
protected function fetchAssoc()
|
||||
{
|
||||
return pg_fetch_assoc($this->resultID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the result set as an object.
|
||||
*
|
||||
* Overridden by child classes.
|
||||
*
|
||||
* @return Entity|false|object|stdClass
|
||||
*/
|
||||
protected function fetchObject(string $className = 'stdClass')
|
||||
{
|
||||
if (is_subclass_of($className, Entity::class)) {
|
||||
return empty($data = $this->fetchAssoc()) ? false : (new $className())->injectRawData($data);
|
||||
}
|
||||
|
||||
return pg_fetch_object($this->resultID, null, $className);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of rows in the resultID (i.e., PostgreSQL query result resource)
|
||||
*/
|
||||
public function getNumRows(): int
|
||||
{
|
||||
if (! is_int($this->numRows)) {
|
||||
$this->numRows = pg_num_rows($this->resultID);
|
||||
}
|
||||
|
||||
return $this->numRows;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Database\Postgre;
|
||||
|
||||
use CodeIgniter\Database\BaseUtils;
|
||||
use CodeIgniter\Database\Exceptions\DatabaseException;
|
||||
|
||||
/**
|
||||
* Utils for Postgre
|
||||
*/
|
||||
class Utils extends BaseUtils
|
||||
{
|
||||
/**
|
||||
* List databases statement
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $listDatabases = 'SELECT datname FROM pg_database';
|
||||
|
||||
/**
|
||||
* OPTIMIZE TABLE statement
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $optimizeTable = 'REINDEX TABLE %s';
|
||||
|
||||
/**
|
||||
* Platform dependent version of the backup function.
|
||||
*
|
||||
* @return never
|
||||
*/
|
||||
public function _backup(?array $prefs = null)
|
||||
{
|
||||
throw new DatabaseException('Unsupported feature of the database platform you are using.');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user