501 lines
14 KiB
PHP
501 lines
14 KiB
PHP
<?php
|
|
|
|
namespace Nest\Database\PostgreSql;
|
|
|
|
use Nest\Database\DatabaseAdapter;
|
|
use Nest\Database\Migrations\Diff\TableColumn;
|
|
use Nest\Database\Migrations\Diff\TableForeignKey;
|
|
use Nest\Database\Migrations\Diff\TableIndex;
|
|
use Nest\Database\Query\Join\JoinBuilder;
|
|
use Nest\Database\Query\Raw;
|
|
use Nest\Database\Query\Where\ConditionBuilder;
|
|
use Nest\Database\Exceptions\Query\MissingConditionValueException;
|
|
use Override;
|
|
use function Nest\Database\Utils\format_object_name;
|
|
|
|
/**
|
|
* PostgreSQL database queries adapter.
|
|
*/
|
|
class PostgreSqlAdapter extends DatabaseAdapter
|
|
{
|
|
/**
|
|
* @inheritDoc
|
|
*/
|
|
#[\Override] public function newTransaction(): void
|
|
{
|
|
$this->database->execute("BEGIN;");
|
|
}
|
|
|
|
/**
|
|
* @inheritDoc
|
|
*/
|
|
#[\Override] public function rollbackTransaction(): void
|
|
{
|
|
$this->database->execute("ROLLBACK;");
|
|
}
|
|
|
|
/**
|
|
* @inheritDoc
|
|
*/
|
|
#[\Override] public function commitTransaction(): void
|
|
{
|
|
$this->database->execute("COMMIT;");
|
|
}
|
|
|
|
/**
|
|
* @inheritDoc
|
|
*/
|
|
#[Override] public function createTable(string $tableName, bool $ifNotExists = false): void
|
|
{
|
|
$this->database->execute("CREATE TABLE ".($ifNotExists ? "IF NOT EXISTS " : "").format_object_name($tableName)."();");
|
|
}
|
|
|
|
/**
|
|
* @inheritDoc
|
|
*/
|
|
#[\Override] public function dropTable(string $tableName, bool $ifExists = false): void
|
|
{
|
|
$this->database->execute("DROP TABLE ".($ifExists ? "IF EXISTS " : "").format_object_name($tableName).";");
|
|
}
|
|
|
|
/**
|
|
* @inheritDoc
|
|
*/
|
|
#[\Override] public function renameTable(string $tableName, string $newTableName): void
|
|
{
|
|
$this->database->execute("ALTER TABLE ".format_object_name($tableName)." RENAME TO ".format_object_name($newTableName).";");
|
|
}
|
|
|
|
/**
|
|
* @inheritDoc
|
|
*/
|
|
#[\Override] public function addTableColumn(TableColumn $tableColumn, bool $ifNotExists = false): void
|
|
{
|
|
// SQL base query.
|
|
$sql = "ADD COLUMN".($ifNotExists ? " IF NOT EXISTS" : "")." ".format_object_name($tableColumn->name)." $tableColumn->type";
|
|
|
|
// Set nullable or not.
|
|
if (!empty($tableColumn->nullable))
|
|
$sql .= " NULL";
|
|
else
|
|
$sql .= " NOT NULL";
|
|
|
|
// Set default value if there is one.
|
|
if (isset($tableColumn->default))
|
|
$sql .= " DEFAULT ".((string) $tableColumn->default);
|
|
|
|
if (!empty($tableColumn->primary))
|
|
$sql .= " PRIMARY KEY";
|
|
|
|
// Execute add table column.
|
|
$this->database->execute("ALTER TABLE ".format_object_name($tableColumn->table->getTableName())." {$sql};");
|
|
}
|
|
|
|
/**
|
|
* @inheritDoc
|
|
*/
|
|
#[\Override] public function setTableColumnIndex(TableColumn $tableColumn): void
|
|
{
|
|
// Create index name.
|
|
$indexName = $tableColumn->table->getTableName()."_".$tableColumn->name."_index";
|
|
|
|
// Drop existing index, if it exists.
|
|
$this->database->execute("DROP INDEX IF EXISTS {$indexName};");
|
|
|
|
// SQL base query.
|
|
$sql = "CREATE ".($tableColumn->index->unique ? "UNIQUE " : "")."INDEX $indexName ON ".format_object_name($tableColumn->table->getTableName());
|
|
|
|
// Set index method if specified.
|
|
if (!empty($tableColumn->index->method))
|
|
$sql .= " USING ({$tableColumn->index->method})";
|
|
|
|
// Set column name in new index.
|
|
$sql .= " ({$tableColumn->name})";
|
|
|
|
// Execute add column index.
|
|
$this->database->execute("$sql;");
|
|
}
|
|
|
|
/**
|
|
* @inheritDoc
|
|
*/
|
|
#[\Override] public function renameTableColumn(TableColumn $tableColumn, string $newName): void
|
|
{
|
|
$this->database->execute("ALTER TABLE ".format_object_name($tableColumn->table->getTableName()).
|
|
" RENAME COLUMN ".format_object_name($tableColumn->name).
|
|
" TO ".format_object_name($newName));
|
|
}
|
|
|
|
/**
|
|
* @inheritDoc
|
|
*/
|
|
#[\Override] public function alterTableColumn(TableColumn $tableColumn): void
|
|
{
|
|
// SQL base query.
|
|
$sql = "ALTER COLUMN ".format_object_name($tableColumn->name);
|
|
|
|
if (!empty($tableColumn->type))
|
|
$sql .= " TYPE $tableColumn->type";
|
|
|
|
// Set nullable or not.
|
|
if (isset($tableColumn->nullable))
|
|
{
|
|
if ($tableColumn->nullable)
|
|
$sql .= " DROP NOT NULL";
|
|
else
|
|
$sql .= " SET NOT NULL";
|
|
}
|
|
|
|
// Set default value if there is one.
|
|
if (isset($tableColumn->default))
|
|
$sql .= " SET DEFAULT ".((string) $tableColumn->default);
|
|
|
|
// Execute add table column.
|
|
$this->database->execute("ALTER TABLE ".format_object_name($tableColumn->table->getTableName())." {$sql};");
|
|
|
|
if (!empty($tableColumn->primary))
|
|
// Set the column as a primary key.
|
|
$this->database->execute("ALTER TABLE ".format_object_name($tableColumn->table->getTableName())." ADD PRIMARY KEY (".format_object_name($tableColumn->name).");");
|
|
}
|
|
|
|
/**
|
|
* @inheritDoc
|
|
*/
|
|
#[\Override] public function dropTableColumn(string $tableName, string $columnName, bool $ifExists = false): void
|
|
{
|
|
// SQL base query.
|
|
$sql = "DROP COLUMN ".($ifExists ? "IF EXISTS " : "").format_object_name($columnName);
|
|
|
|
// Execute drop table column.
|
|
$this->database->execute("ALTER TABLE ".format_object_name($tableName)." {$sql};");
|
|
}
|
|
|
|
/**
|
|
* @inheritDoc
|
|
*/
|
|
#[\Override] public function dropConstraint(string $tableName, string $constraintName, bool $ifExists = false): void
|
|
{
|
|
// SQL base query.
|
|
$sql = "DROP CONSTRAINT ".($ifExists ? "IF EXISTS " : "").format_object_name($constraintName);
|
|
|
|
// Execute drop table constraint.
|
|
$this->database->execute("ALTER TABLE ".format_object_name($tableName)." {$sql};");
|
|
}
|
|
|
|
/**
|
|
* @inheritDoc
|
|
*/
|
|
#[\Override] public function createForeignKey(TableForeignKey $foreignKey): void
|
|
{
|
|
// Format columns list.
|
|
$columns = implode(", ", array_map(fn ($column) => format_object_name($column), $foreignKey->columns));
|
|
|
|
// Format referenced columns list.
|
|
$referencedColumns = implode(", ", array_map(fn ($column) => format_object_name($column), $foreignKey->referencedColumns));
|
|
|
|
// Base SQL.
|
|
$sql = "ADD FOREIGN KEY ($columns) REFERENCES ".format_object_name($foreignKey->referencedTable)."($referencedColumns)";
|
|
|
|
// Set ON DELETE and ON UPDATE actions.
|
|
if (!empty($foreignKey->onDelete))
|
|
$sql .= " ON DELETE {$foreignKey->onDelete}";
|
|
if (!empty($foreignKey->onUpdate))
|
|
$sql .= " ON UPDATE {$foreignKey->onUpdate}";
|
|
|
|
// Execute add foreign key constraint.
|
|
$this->database->execute("ALTER TABLE ".format_object_name($foreignKey->table->getTableName())." {$sql};");
|
|
}
|
|
|
|
/**
|
|
* @inheritDoc
|
|
*/
|
|
#[\Override] public function createIndex(TableIndex $index, bool $ifNotExists = false): void
|
|
{
|
|
// Base SQL.
|
|
$sql = "CREATE (UNIQUE) INDEX".($ifNotExists ? " IF NOT EXISTS" : "")." $index->name ON ".format_object_name($index->table->getTableName());
|
|
|
|
if (!empty($index->method))
|
|
$sql .= " USING $index->method";
|
|
|
|
$sql .= "(";
|
|
|
|
if (!empty($index->rawExpression))
|
|
$sql .= "($index->rawExpression)";
|
|
else
|
|
{ // Format indexed columns.
|
|
// Format columns list.
|
|
$columns = implode(", ", array_map(fn ($column) => format_object_name($column), $index->columns));
|
|
$sql .= "($columns)";
|
|
}
|
|
|
|
// Set index order, if defined.
|
|
if (!empty($index->order))
|
|
$sql .= " $index->order";
|
|
if (!empty($index->nulls))
|
|
$sql .= " $index->nulls";
|
|
|
|
$sql .= ")";
|
|
|
|
// Execute index creation.
|
|
$this->database->execute("$sql;");
|
|
}
|
|
|
|
/**
|
|
* @inheritDoc
|
|
*/
|
|
#[\Override] public function renameIndex(string $indexName, string $newName, bool $ifExists = false): void
|
|
{
|
|
// Execute index rename.
|
|
$this->database->execute("ALTER INDEX".($ifExists ? "IF EXISTS " : "")." ".format_object_name($indexName).
|
|
" RENAME TO ".format_object_name($newName).";");
|
|
}
|
|
|
|
/**
|
|
* @inheritDoc
|
|
*/
|
|
#[\Override] public function dropIndex(TableIndex $index, bool $ifExists = false): void
|
|
{
|
|
// Execute index drop.
|
|
$this->database->execute("DROP INDEX".($ifExists ? "IF EXISTS " : "")." ".format_object_name($index->name).";");
|
|
}
|
|
|
|
/**
|
|
* Build JOIN clauses from join builders.
|
|
* @param JoinBuilder[] $joins Joins to build.
|
|
* @return array{string, array} The built SQL with its bindings, if there are some. Bindings can be empty or non-existent.
|
|
* @throws MissingConditionValueException
|
|
*/
|
|
protected function buildJoins(array $joins): array
|
|
{
|
|
// Return empty join clauses if there are no conditions.
|
|
if (empty($joins)) return ["", []];
|
|
|
|
$sql = "";
|
|
$bindings = [];
|
|
|
|
// Build all join clauses.
|
|
foreach ($joins as $join)
|
|
{ // For each join clause, build its conditions and append the built bindings.
|
|
[$onSql, $onBindings] = $this->buildWhere($join->getConditions(), "ON");
|
|
$sql .= "$join->type JOIN \"$join->table\" $onSql ";
|
|
array_push($bindings, ...$onBindings);
|
|
}
|
|
|
|
// Return all joins clauses SQL and bindings.
|
|
return [$sql, $bindings];
|
|
}
|
|
|
|
/**
|
|
* Build WHERE clause from conditions builders.
|
|
* @param ConditionBuilder[] $wheres Conditions to build.
|
|
* @param string $keyword The keyword to use. "WHERE" by default, but can be replaced by "ON" for JOIN conditions.
|
|
* @return array{string, array} The built SQL with its bindings, if there are some. Bindings can be empty or non-existent.
|
|
* @throws MissingConditionValueException
|
|
*/
|
|
protected function buildWhere(array $wheres, string $keyword = "WHERE"): array
|
|
{
|
|
// Return empty where clause if there are no conditions.
|
|
if (empty($wheres)) return ["", []];
|
|
|
|
// Build all conditions.
|
|
$conditions = array_map(fn (ConditionBuilder $condition) => $condition->toSql(), $wheres);
|
|
// Join all conditions with AND.
|
|
$sql = implode("AND", array_column($conditions, 0));
|
|
// Join all values bindings.
|
|
$bindings = array_merge(...array_column($conditions, 1));
|
|
|
|
// Return built WHERE clause.
|
|
return ["$keyword $sql", $bindings];
|
|
}
|
|
|
|
/**
|
|
* @inheritDoc
|
|
*/
|
|
#[\Override] public function buildSelect(string $tableName, array $selected, array $joins, array $wheres, ?int $limit = null): Raw
|
|
{
|
|
$bindings = []; // Initialize empty bindings.
|
|
|
|
// SQL base query: select the table columns.
|
|
$sql = "SELECT ".implode(", ", array_map(function (Raw|string|array $select) use (&$bindings) {
|
|
if ($select instanceof Raw)
|
|
{ // Raw selection.
|
|
// Add its bindings to the list.
|
|
array_push($bindings, ...$select->bindings);
|
|
// Return raw SQL.
|
|
return (string) $select;
|
|
}
|
|
else
|
|
// Format selection and return its SQL.
|
|
return format_object_name((string) $select);
|
|
}, $selected));
|
|
|
|
// Append FROM clause.
|
|
$sql .= " FROM ".format_object_name($tableName);
|
|
|
|
// Append JOIN clauses.
|
|
[$joinsSql, $joinsBindings] = $this->buildJoins($joins);
|
|
if (!empty($joinsSql))
|
|
{
|
|
$sql .= " $joinsSql";
|
|
array_push($bindings, ...$joinsBindings);
|
|
}
|
|
|
|
// Append WHERE clause.
|
|
[$whereSql, $whereBindings] = $this->buildWhere($wheres);
|
|
if (!empty($whereSql))
|
|
{
|
|
$sql .= " $whereSql";
|
|
array_push($bindings, ...$whereBindings);
|
|
}
|
|
|
|
// Append LIMIT clause.
|
|
if (!is_null($limit))
|
|
$sql .= " LIMIT $limit";
|
|
|
|
// Return raw SQL with its bindings.
|
|
return new Raw($sql, $bindings);
|
|
}
|
|
|
|
/**
|
|
* @inheritDoc
|
|
*/
|
|
#[\Override] public function select(string $tableName, array $selected, array $joins, array $wheres, ?int $limit = null): array
|
|
{
|
|
// Build SELECT query.
|
|
$raw = $this->buildSelect($tableName, $selected, $joins, $wheres, $limit);
|
|
|
|
// Execute built query and return result.
|
|
return $this->database->execute($raw->sql, $raw->bindings);
|
|
}
|
|
|
|
/**
|
|
* @inheritDoc
|
|
*/
|
|
#[\Override] public function insert(string $tableName, array $columns, array $rows, bool $returning = false): array|bool
|
|
{
|
|
// SQL base query: insert into the table columns.
|
|
$sql = "INSERT INTO ".format_object_name($tableName).
|
|
" (".implode(", ", array_map(fn (string $column) => format_object_name($column), $columns)).")";
|
|
|
|
// Add values (with bindings).
|
|
$sql .= " VALUES ";
|
|
$rowsValues = [];
|
|
$bindings = [];
|
|
foreach ($rows as $row)
|
|
{ // Create a new VALUES tuple for each row.
|
|
// Initialize row parts of the current row.
|
|
$rowParts = [];
|
|
$rowBindings = [];
|
|
|
|
foreach ($row as $value)
|
|
{ // For each row value, add a row part corresponding to its type.
|
|
if ($value instanceof Raw)
|
|
// Add a new raw row part.
|
|
$rowParts[] = (string) $value;
|
|
else
|
|
{ // Add a new row part, with its binding.
|
|
$rowParts[] = "?";
|
|
$rowBindings[] = $value;
|
|
}
|
|
}
|
|
|
|
// Append current row tuple to values, with its bindings.
|
|
$rowsValues[] = "(".implode(", ", $rowParts).")";
|
|
array_push($bindings, ...$rowBindings);
|
|
}
|
|
|
|
// Build values SQL and append it to the query.
|
|
$sql .= implode(", ", $rowsValues);
|
|
|
|
if ($returning)
|
|
// Inserted rows shall be returned, indicating it in the query.
|
|
$sql .= "RETURNING *";
|
|
|
|
// SQL query end.
|
|
$sql .= ";";
|
|
|
|
// Execute INSERT query.
|
|
$result = $this->database->execute($sql, $bindings);
|
|
|
|
if ($returning)
|
|
// Returning inserted rows.
|
|
return $result;
|
|
else
|
|
// INSERT executed successfully.
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @inheritDoc
|
|
*/
|
|
#[\Override] public function update(string $tableName, array $set, array $wheres, bool $returning = false): array|bool
|
|
{
|
|
// SQL base query: update the given table.
|
|
$sql = "UPDATE ".format_object_name($tableName);
|
|
$bindings = []; // Initialize empty bindings.
|
|
|
|
// Initialize SET clause.
|
|
$sql .= " SET ";
|
|
$setParts = [];
|
|
foreach ($set as $columnName => $value)
|
|
{ // For each set value, add a set part corresponding to its type.
|
|
$currentSet = format_object_name($columnName)." = ";
|
|
if ($value instanceof Raw)
|
|
// Add a new raw set part.
|
|
$setParts[] = $currentSet.((string) $value);
|
|
else
|
|
{ // Add a new set part, with its binding.
|
|
$setParts[] = $currentSet."?";
|
|
$bindings[] = $value;
|
|
}
|
|
}
|
|
|
|
// Append SET columns.
|
|
$sql .= implode(", ", $setParts);
|
|
|
|
// Append WHERE clause.
|
|
[$whereSql, $whereBindings] = $this->buildWhere($wheres);
|
|
if (!empty($whereSql))
|
|
{
|
|
$sql .= " $whereSql";
|
|
array_push($bindings, ...$whereBindings);
|
|
}
|
|
|
|
if ($returning)
|
|
// Inserted rows shall be returned, indicating it in the query.
|
|
$sql .= "RETURNING *";
|
|
|
|
// SQL query end.
|
|
$sql .= ";";
|
|
|
|
// Execute UPDATE query.
|
|
$result = $this->database->execute($sql, $bindings);
|
|
|
|
if ($returning)
|
|
// Returning updated rows.
|
|
return $result;
|
|
else
|
|
// UPDATE executed successfully.
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @inheritDoc
|
|
*/
|
|
#[\Override] public function delete(string $tableName, array $wheres): void
|
|
{
|
|
// SQL base query: delete from the given table.
|
|
$sql = "DELETE FROM ".format_object_name($tableName);
|
|
|
|
// Append WHERE clause.
|
|
[$whereSql, $bindings] = $this->buildWhere($wheres);
|
|
if (!empty($whereSql))
|
|
$sql .= " $whereSql";
|
|
|
|
// SQL query end.
|
|
$sql .= ";";
|
|
|
|
// Execute DELETE query.
|
|
$this->database->execute($sql, $bindings);
|
|
}
|
|
}
|