Database/src/PostgreSql/PostgreSqlAdapter.php

502 lines
14 KiB
PHP
Raw Normal View History

2024-11-08 16:33:44 +01:00
<?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);
}
}