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); } }