Triggers + EntityLog

This commit is contained in:
Roman 2021-04-08 18:29:47 +02:00
parent 140f428491
commit 43d9a65def
20 changed files with 741 additions and 334 deletions

@ -0,0 +1,63 @@
<?php
namespace Configuration\Patch;
use Configuration\DatabaseScript;
use Driver\SQL\Column\IntColumn;
use Driver\SQL\Condition\Compare;
use Driver\SQL\SQL;
use Driver\SQL\Type\CurrentColumn;
use Driver\SQL\Type\CurrentTable;
use Driver\SQL\Type\Trigger;
class log extends DatabaseScript {
public static function createQueries(SQL $sql): array {
$queries = array();
$queries[] = $sql->createTable("EntityLog")
->addInt("entityId")
->addString("tableName")
->addDateTime("modified", false, $sql->now())
->addInt("lifetime", false, 90);
$insertProcedure = $sql->createProcedure("InsertEntityLog")
->param(new CurrentTable())
->param(new IntColumn("uid"))
->returns(new Trigger())
->exec(array(
$sql->insert("EntityLog", ["entityId", "tableName"])
->addRow(new CurrentColumn("uid"), new CurrentTable())
));
$updateProcedure = $sql->createProcedure("UpdateEntityLog")
->param(new CurrentTable())
->param(new IntColumn("uid"))
->returns(new Trigger())
->exec(array(
$sql->update("EntityLog")
->set("modified", $sql->now())
->where(new Compare("entityId",new CurrentColumn("uid")))
->where(new Compare("tableName",new CurrentTable()))
));
$queries[] = $insertProcedure;
$queries[] = $updateProcedure;
$tables = ["ContactRequest"];
foreach ($tables as $table) {
$queries[] = $sql->createTrigger("${table}_trg_insert")
->after()->insert($table)
->exec($insertProcedure);
$queries[] = $sql->createTrigger("${table}_trg_update")
->after()->update($table)
->exec($updateProcedure);
}
return $queries;
}
}

@ -588,7 +588,7 @@ namespace Documents\Install {
private function createProgessMainview(): string {
$isDocker = $this->isDocker();
$defaultHost = ($isDocker ? "db" : "");
$defaultHost = ($isDocker ? "db" : "localhost");
$defaultUsername = ($isDocker ? "root" : "");
$defaultPassword = ($isDocker ? "webbasedb" : "");
$defaultDatabase = ($isDocker ? "webbase" : "");

@ -16,8 +16,14 @@ use Driver\SQL\Column\JsonColumn;
use Driver\SQL\Condition\CondRegex;
use Driver\SQL\Expression\Add;
use Driver\SQL\Query\CreateProcedure;
use Driver\SQL\Query\CreateTrigger;
use Driver\SQL\Query\Query;
use Driver\SQL\Strategy\Strategy;
use \Driver\SQL\Strategy\UpdateStrategy;
use Driver\SQL\Type\CurrentColumn;
use Driver\SQL\Type\CurrentTable;
use Driver\SQL\Type\Trigger;
class MySQL extends SQL {
@ -67,7 +73,7 @@ class MySQL extends SQL {
return true;
}
public function getLastError() {
public function getLastError(): string {
$lastError = parent::getLastError();
if (empty($lastError)) {
$lastError = mysqli_error($this->connection);
@ -175,7 +181,7 @@ class MySQL extends SQL {
return ($success && $returnValues) ? $resultRows : $success;
}
protected function getOnDuplicateStrategy(?Strategy $strategy, &$params) {
public function getOnDuplicateStrategy(?Strategy $strategy, &$params): ?string {
if (is_null($strategy)) {
return "";
} else if ($strategy instanceof UpdateStrategy) {
@ -199,7 +205,7 @@ class MySQL extends SQL {
} else {
$strategyClass = get_class($strategy);
$this->lastError = "ON DUPLICATE Strategy $strategyClass is not supported yet.";
return false;
return null;
}
}
@ -207,40 +213,51 @@ class MySQL extends SQL {
$this->lastInsertId = mysqli_insert_id($this->connection);
}
public function getColumnDefinition(Column $column) {
$columnName = $this->columnName($column->getName());
$defaultValue = $column->getDefaultValue();
public function getColumnType(Column $column): ?string {
if ($column instanceof StringColumn) {
$maxSize = $column->getMaxSize();
if ($maxSize) {
$type = "VARCHAR($maxSize)";
return "VARCHAR($maxSize)";
} else {
$type = "TEXT";
return "TEXT";
}
} else if($column instanceof SerialColumn) {
$type = "INTEGER AUTO_INCREMENT";
return "INTEGER AUTO_INCREMENT";
} else if($column instanceof IntColumn) {
$type = "INTEGER";
return "INTEGER";
} else if($column instanceof DateTimeColumn) {
$type = "DATETIME";
} else if($column instanceof EnumColumn) {
$values = array();
foreach($column->getValues() as $value) {
$values[] = $this->getValueDefinition($value);
}
$values = implode(",", $values);
$type = "ENUM($values)";
return "DATETIME";
} else if($column instanceof BoolColumn) {
$type = "BOOLEAN";
return "BOOLEAN";
} else if($column instanceof JsonColumn) {
$type = "LONGTEXT"; # some maria db setups don't allow JSON here…
$defaultValue = NULL; # must be null :(
return "LONGTEXT"; # some maria db setups don't allow JSON here…
} else {
$this->lastError = "Unsupported Column Type: " . get_class($column);
return NULL;
}
}
public function getColumnDefinition(Column $column): ?string {
$columnName = $this->columnName($column->getName());
$defaultValue = $column->getDefaultValue();
$type = $this->getColumnType($column);
if (!$type) {
if ($column instanceof EnumColumn) {
$values = array();
foreach($column->getValues() as $value) {
$values[] = $this->getValueDefinition($value);
}
$values = implode(",", $values);
$type = "ENUM($values)";
} else {
return null;
}
}
if ($type === "LONGTEXT") {
$defaultValue = NULL; # must be null :(
}
$notNull = $column->notNull() ? " NOT NULL" : "";
if (!is_null($defaultValue) || !$column->notNull()) {
@ -267,16 +284,20 @@ class MySQL extends SQL {
}
}
protected function addValue($val, &$params) {
public function addValue($val, &$params = NULL) {
if ($val instanceof Keyword) {
return $val->getValue();
} else if ($val instanceof CurrentColumn) {
return $val->getName();
} else if ($val instanceof Column) {
return $this->columnName($val->getName());
} else {
$params[] = $val;
return "?";
}
}
protected function tableName($table) {
public function tableName($table): string {
if (is_array($table)) {
$tables = array();
foreach($table as $t) $tables[] = $this->tableName($t);
@ -286,7 +307,7 @@ class MySQL extends SQL {
}
}
protected function columnName($col) {
public function columnName($col): string {
if ($col instanceof Keyword) {
return $col->getValue();
} elseif(is_array($col)) {
@ -308,11 +329,72 @@ class MySQL extends SQL {
}
}
public function currentTimestamp() {
public function currentTimestamp(): Keyword {
return new Keyword("NOW()");
}
public function getStatus() {
return mysqli_stat($this->connection);
}
public function createTriggerBody(CreateTrigger $trigger): ?string {
$values = array();
foreach ($trigger->getProcedure()->getParameters() as $param) {
if ($param instanceof CurrentTable) {
$values[] = $this->getUnsafeValue($trigger->getTable());
} else {
$values[] = $this->columnName("NEW." . $param->getName());
}
}
$procName = $trigger->getProcedure()->getName();
$procParameters = implode(",", $values);
return "CALL $procName($procParameters)";
}
private function getParameterDefinition(Column $parameter, bool $out = false): string {
$out = ($out ? "OUT" : "IN");
$name = $parameter->getName();
$type = $this->getColumnType($parameter);
return "$out $name $type";
}
public function getProcedureHead(CreateProcedure $procedure): ?string {
$name = $procedure->getName();
$returns = $procedure->getReturns();
$paramDefs = [];
foreach ($procedure->getParameters() as $param) {
if ($param instanceof Column) {
$paramDefs[] = $this->getParameterDefinition($param);
} else {
$this->setLastError("PROCEDURE parameter type " . gettype($returns) . " is not implemented yet");
return null;
}
}
if ($returns) {
if ($returns instanceof Column) {
$paramDefs[] = $this->getParameterDefinition($returns, true);
} else if (!($returns instanceof Trigger)) { // mysql does not need to return triggers here
$this->setLastError("PROCEDURE RETURN type " . gettype($returns) . " is not implemented yet");
return null;
}
}
$paramDefs = implode(",", $paramDefs);
return "CREATE PROCEDURE $name($paramDefs)";
}
protected function buildUnsafe(Query $statement): string {
$params = [];
$query = $statement->build($params);
foreach ($params as $value) {
$query = preg_replace("?", $this->getUnsafeValue($value), $query, 1);
}
return $query;
}
}

@ -4,6 +4,7 @@ namespace Driver\SQL;
use \Api\Parameter\Parameter;
use Api\User\Create;
use Driver\SQL\Column\Column;
use \Driver\SQL\Column\IntColumn;
use \Driver\SQL\Column\SerialColumn;
@ -15,8 +16,14 @@ use Driver\SQL\Column\JsonColumn;
use Driver\SQL\Condition\CondRegex;
use Driver\SQL\Expression\Add;
use Driver\SQL\Query\CreateProcedure;
use Driver\SQL\Query\CreateTrigger;
use Driver\SQL\Query\Query;
use Driver\SQL\Strategy\Strategy;
use Driver\SQL\Strategy\UpdateStrategy;
use Driver\SQL\Type\CurrentColumn;
use Driver\SQL\Type\CurrentTable;
use Driver\SQL\Type\Trigger;
class PostgreSQL extends SQL {
@ -71,7 +78,7 @@ class PostgreSQL extends SQL {
@pg_close($this->connection);
}
public function getLastError() {
public function getLastError(): string {
$lastError = parent::getLastError();
if (empty($lastError)) {
$lastError = trim(pg_last_error($this->connection) . " " . pg_last_error($this->connection));
@ -134,39 +141,39 @@ class PostgreSQL extends SQL {
}
}
protected function getOnDuplicateStrategy(?Strategy $strategy, &$params) {
public function getOnDuplicateStrategy(?Strategy $strategy, &$params): ?string {
if (!is_null($strategy)) {
if ($strategy instanceof UpdateStrategy) {
$updateValues = array();
foreach($strategy->getValues() as $key => $value) {
$leftColumn = $this->columnName($key);
if ($value instanceof Column) {
$columnName = $this->columnName($value->getName());
$updateValues[] = "$leftColumn=EXCLUDED.$columnName";
} else if ($value instanceof Add) {
$columnName = $this->columnName($value->getColumn());
$operator = $value->getOperator();
$value = $value->getValue();
$updateValues[] = "$leftColumn=$columnName$operator" . $this->addValue($value, $params);
} else {
$updateValues[] = "$leftColumn=" . $this->addValue($value, $parameters);
}
}
$conflictingColumns = $this->columnName($strategy->getConflictingColumns());
$updateValues = implode(",", $updateValues);
return " ON CONFLICT ($conflictingColumns) DO UPDATE SET $updateValues";
} else {
$strategyClass = get_class($strategy);
$this->lastError = "ON DUPLICATE Strategy $strategyClass is not supported yet.";
return false;
$updateValues = array();
foreach($strategy->getValues() as $key => $value) {
$leftColumn = $this->columnName($key);
if ($value instanceof Column) {
$columnName = $this->columnName($value->getName());
$updateValues[] = "$leftColumn=EXCLUDED.$columnName";
} else if ($value instanceof Add) {
$columnName = $this->columnName($value->getColumn());
$operator = $value->getOperator();
$value = $value->getValue();
$updateValues[] = "$leftColumn=$columnName$operator" . $this->addValue($value, $params);
} else {
$updateValues[] = "$leftColumn=" . $this->addValue($value, $parameters);
}
}
$conflictingColumns = $this->columnName($strategy->getConflictingColumns());
$updateValues = implode(",", $updateValues);
return " ON CONFLICT ($conflictingColumns) DO UPDATE SET $updateValues";
} else {
$strategyClass = get_class($strategy);
$this->lastError = "ON DUPLICATE Strategy $strategyClass is not supported yet.";
return null;
}
} else {
return "";
}
}
protected function getReturning(?string $columns) {
public function getReturning(?string $columns): string {
return $columns ? (" RETURNING " . $this->columnName($columns)) : "";
}
@ -175,12 +182,7 @@ class PostgreSQL extends SQL {
}
// UGLY but.. what should i do?
private function createEnum(EnumColumn $enumColumn) {
$typeName = $enumColumn->getName();
if(!endsWith($typeName, "_type")) {
$typeName = "${typeName}_type";
}
private function createEnum(EnumColumn $enumColumn, string $typeName): string {
$values = array();
foreach($enumColumn->getValues() as $value) {
$values[] = $this->getValueDefinition($value);
@ -194,36 +196,50 @@ class PostgreSQL extends SQL {
WHEN duplicate_object THEN null;
END $$;";
$this->execute($query);
return $typeName;
return $this->execute($query);
}
protected function getColumnDefinition($column) {
$columnName = $this->columnName($column->getName());
public function getColumnType(Column $column): ?string {
if ($column instanceof StringColumn) {
$maxSize = $column->getMaxSize();
if ($maxSize) {
$type = "VARCHAR($maxSize)";
return "VARCHAR($maxSize)";
} else {
$type = "TEXT";
return "TEXT";
}
} else if($column instanceof SerialColumn) {
$type = "SERIAL";
return "SERIAL";
} else if($column instanceof IntColumn) {
$type = "INTEGER";
return "INTEGER";
} else if($column instanceof DateTimeColumn) {
$type = "TIMESTAMP";
return "TIMESTAMP";
} else if($column instanceof EnumColumn) {
$type = $this->createEnum($column);
$typeName = $column->getName();
if(!endsWith($typeName, "_type")) {
$typeName = "${typeName}_type";
}
return $typeName;
} else if($column instanceof BoolColumn) {
$type = "BOOLEAN";
return "BOOLEAN";
} else if($column instanceof JsonColumn) {
$type = "JSON";
return "JSON";
} else {
$this->lastError = "Unsupported Column Type: " . get_class($column);
return NULL;
}
}
public function getColumnDefinition($column): ?string {
$columnName = $this->columnName($column->getName());
$type = $this->getColumnType($column);
if (!$type) {
return null;
} else if ($column instanceof EnumColumn) {
if (!$this->createEnum($column, $type)) {
return null;
}
}
$notNull = $column->notNull() ? " NOT NULL" : "";
$defaultValue = "";
@ -249,16 +265,22 @@ class PostgreSQL extends SQL {
}
}
protected function addValue($val, &$params) {
public function addValue($val, &$params = NULL) {
if ($val instanceof Keyword) {
return $val->getValue();
} else if ($val instanceof CurrentTable) {
return "TG_TABLE_NAME";
} else if ($val instanceof CurrentColumn) {
return "NEW." . $this->columnName($val->getName());
} else if ($val instanceof Column) {
return $this->columnName($val->getName());
} else {
$params[] = is_bool($val) ? ($val ? "TRUE" : "FALSE") : $val;
return '$' . count($params);
}
}
protected function tableName($table) {
public function tableName($table): string {
if (is_array($table)) {
$tables = array();
foreach($table as $t) $tables[] = $this->tableName($t);
@ -268,7 +290,7 @@ class PostgreSQL extends SQL {
}
}
protected function columnName($col) {
public function columnName($col): string {
if ($col instanceof KeyWord) {
return $col->getValue();
} elseif(is_array($col)) {
@ -291,7 +313,7 @@ class PostgreSQL extends SQL {
}
// Special Keywords and functions
public function currentTimestamp() {
public function currentTimestamp(): Keyword {
return new Keyword("CURRENT_TIMESTAMP");
}
@ -317,4 +339,75 @@ class PostgreSQL extends SQL {
return parent::buildCondition($condition, $params);
}
}
private function createTriggerProcedure(string $name, array $statements) {
$params = [];
$query = "CREATE OR REPLACE FUNCTION $name() RETURNS TRIGGER AS \$table\$ BEGIN ";
foreach($statements as $stmt) {
if ($stmt instanceof Keyword) {
$query .= $stmt->getValue() . ";";
} else {
$query .= $stmt->build($this, $params) . ";";
}
}
$query .= "END;";
$query .= "\$table\$ LANGUAGE plpgsql;";
var_dump($query);
var_dump($params);
return $this->execute($query, $params);
}
public function createTriggerBody(CreateTrigger $trigger): ?string {
$procName = $this->tableName($trigger->getProcedure()->getName());
return "EXECUTE PROCEDURE $procName()";
}
public function getProcedureHead(CreateProcedure $procedure): ?string {
$name = $this->tableName($procedure->getName());
$returns = $procedure->getReturns() ?? "";
$paramDefs = [];
if (!($procedure->getReturns() instanceof Trigger)) {
foreach ($procedure->getParameters() as $parameter) {
$paramDefs[] = $parameter->getName() . " " . $this->getColumnType($parameter);
}
}
$paramDefs = implode(",", $paramDefs);
if ($returns) {
if ($returns instanceof Column) {
$returns = " RETURNS " . $this->getColumnType($returns);
} else if ($returns instanceof Keyword) {
$returns = " RETURNS " . $returns->getValue();
}
}
return "CREATE OR REPLACE FUNCTION $name($paramDefs)$returns AS $$";
}
public function getProcedureTail(): string {
return "$$ LANGUAGE plpgsql;";
}
public function getProcedureBody(CreateProcedure $procedure): string {
$statements = parent::getProcedureBody($procedure);
if ($procedure->getReturns() instanceof Trigger) {
$statements .= "RETURN NEW;";
}
return $statements;
}
protected function buildUnsafe(Query $statement): string {
$params = [];
$query = $statement->build($params);
foreach ($params as $index => $value) {
$value = $this->getUnsafeValue($value);
$query = preg_replace("\$$index", $value, $query, 1);
}
return $query;
}
}

@ -4,6 +4,8 @@ namespace Driver\SQL\Query;
use Driver\SQL\Column\Column;
use Driver\SQL\Constraint\Constraint;
use Driver\SQL\Constraint\ForeignKey;
use Driver\SQL\Constraint\PrimaryKey;
use Driver\SQL\SQL;
class AlterTable extends Query {
@ -52,13 +54,48 @@ class AlterTable extends Query {
return $this;
}
public function execute(): bool {
return $this->sql->executeAlter($this);
}
public function getAction(): string { return $this->action; }
public function getColumn(): ?Column { return $this->column; }
public function getConstraint(): ?Constraint { return $this->constraint; }
public function getTable(): string { return $this->table; }
public function build(array &$params, Query $context = NULL): ?string {
$tableName = $this->sql->tableName($this->getTable());
$action = $this->getAction();
$column = $this->getColumn();
$constraint = $this->getConstraint();
$query = "ALTER TABLE $tableName $action ";
if ($column) {
$query .= "COLUMN ";
if ($action === "DROP") {
$query .= $this->sql->columnName($column->getName());
} else {
// ADD or modify
$query .= $this->sql->getColumnDefinition($column);
}
} else if ($constraint) {
if ($action === "DROP") {
if ($constraint instanceof PrimaryKey) {
$query .= "PRIMARY KEY";
} else if ($constraint instanceof ForeignKey) {
// TODO: how can we pass the constraint name here?
$this->sql->setLastError("DROP CONSTRAINT foreign key is not supported yet.");
return null;
}
} else if ($action === "ADD") {
$query .= "CONSTRAINT ";
$query .= $this->sql->getConstraintDefinition($constraint);
} else if ($action === "MODIFY") {
$this->sql->setLastError("MODIFY CONSTRAINT foreign key is not supported.");
return null;
}
} else {
$this->sql->setLastError("ALTER TABLE requires at least a column or a constraint.");
return null;
}
return $query;
}
}

@ -0,0 +1,50 @@
<?php
namespace Driver\SQL\Query;
use Driver\SQL\Column\Column;
use Driver\SQL\SQL;
class CreateProcedure extends Query {
private string $name;
private array $parameters;
private array $statements;
private $returns;
public function __construct(SQL $sql, string $procName) {
parent::__construct($sql);
$this->name = $procName;
$this->parameters = [];
$this->statements = [];
$this->returns = NULL;
}
public function param(Column $parameter): CreateProcedure {
$this->parameters[] = $parameter;
return $this;
}
public function returns($column): CreateProcedure {
$this->returns = $column;
return $this;
}
public function exec(array $statements): CreateProcedure {
$this->statements = $statements;
return $this;
}
public function build(array &$params, Query $context = NULL): ?string {
$head = $this->sql->getProcedureHead($this);
$body = $this->sql->getProcedureBody($this);
$tail = $this->sql->getProcedureTail();
return "$head BEGIN $body END; $tail";
}
public function getName(): string { return $this->name; }
public function getParameters(): array { return $this->parameters; }
public function getReturns() { return $this->returns; }
public function getStatements(): array { return $this->statements; }
}

@ -86,12 +86,31 @@ class CreateTable extends Query {
return $this;
}
public function execute(): bool {
return $this->sql->executeCreateTable($this);
}
public function ifNotExists(): bool { return $this->ifNotExists; }
public function getTableName(): string { return $this->tableName; }
public function getColumns(): array { return $this->columns; }
public function getConstraints(): array { return $this->constraints; }
public function build(array &$params): ?string {
$tableName = $this->sql->tableName($this->getTableName());
$ifNotExists = $this->ifNotExists() ? " IF NOT EXISTS" : "";
$entries = array();
foreach ($this->getColumns() as $column) {
$entries[] = ($tmp = $this->sql->getColumnDefinition($column));
if (is_null($tmp)) {
return false;
}
}
foreach ($this->getConstraints() as $constraint) {
$entries[] = ($tmp = $this->sql->getConstraintDefinition($constraint));
if (is_null($tmp)) {
return false;
}
}
$entries = implode(",", $entries);
return "CREATE TABLE$ifNotExists $tableName ($entries)";
}
}

@ -0,0 +1,80 @@
<?php
namespace Driver\SQL\Query;
use Api\User\Create;
use Driver\SQL\SQL;
class CreateTrigger extends Query {
private string $name;
private string $time;
private string $event;
private string $tableName;
private ?CreateProcedure $procedure;
public function __construct(SQL $sql, string $triggerName) {
parent::__construct($sql);
$this->name = $triggerName;
$this->time = "AFTER";
$this->tableName = "";
$this->event = "";
$this->procedure = null;
}
public function before(): CreateTrigger {
$this->time = "BEFORE";
return $this;
}
public function after(): CreateTrigger {
$this->time = "AFTER";
return $this;
}
public function update(string $table): CreateTrigger {
$this->tableName = $table;
$this->event = "UPDATE";
return $this;
}
public function insert(string $table): CreateTrigger {
$this->tableName = $table;
$this->event = "INSERT";
return $this;
}
public function delete(string $table): CreateTrigger {
$this->tableName = $table;
$this->event = "DELETE";
return $this;
}
public function exec(CreateProcedure $procedure): CreateTrigger {
$this->procedure = $procedure;
return $this;
}
public function getName(): string { return $this->name; }
public function getTime(): string { return $this->time; }
public function getEvent(): string { return $this->event; }
public function getTable(): string { return $this->tableName; }
public function getProcedure(): CreateProcedure { return $this->procedure; }
public function build(array &$params, Query $context = NULL): ?string {
$name = $this->sql->tableName($this->getName());
$time = $this->getTime();
$event = $this->getEvent();
$tableName = $this->sql->tableName($this->getTable());
$params = array();
$query = "CREATE TRIGGER $name $time $event ON $tableName FOR EACH ROW ";
$triggerBody = $this->sql->createTriggerBody($this);
if ($triggerBody === null) {
return null;
}
$query .= $triggerBody;
return $query;
}
}

@ -21,10 +21,12 @@ class Delete extends Query {
return $this;
}
public function execute(): bool {
return $this->sql->executeDelete($this);
}
public function getTable(): string { return $this->table; }
public function getConditions(): array { return $this->conditions; }
public function build(array &$params, Query $context = NULL): ?string {
$table = $this->sql->tableName($this->getTable());
$where = $this->sql->getWhereClause($this->getConditions(), $params);
return "DELETE FROM $table$where";
}
}

@ -19,11 +19,11 @@ class Drop extends Query {
$this->table = $table;
}
public function execute(): bool {
return $this->sql->executeDrop($this);
}
public function getTable(): string {
return $this->table;
}
public function build(array &$params, Query $context = NULL): ?string {
return "DROP TABLE " . $this->sql->tableName($this->getTable());
}
}

@ -37,8 +37,9 @@ class Insert extends Query {
return $this;
}
public function execute(): bool {
return $this->sql->executeInsert($this);
public function execute() {
$fetchResult = !empty($this->sql->getReturning($this->returning));
return $this->sql->executeQuery($this, $fetchResult);
}
public function getTableName(): string { return $this->tableName; }
@ -46,4 +47,42 @@ class Insert extends Query {
public function getRows(): array { return $this->rows; }
public function onDuplicateKey(): ?Strategy { return $this->onDuplicateKey; }
public function getReturning(): ?string { return $this->returning; }
public function build(array &$params, Query $context = NULL): ?string {
$tableName = $this->sql->tableName($this->getTableName());
$columns = $this->getColumns();
$rows = $this->getRows();
if (empty($rows)) {
$this->sql->setLastError("No rows to insert given.");
return null;
}
if (is_null($columns) || empty($columns)) {
$columnStr = "";
} else {
$columnStr = " (" . $this->sql->columnName($columns) . ")";
}
$values = array();
foreach ($rows as $row) {
$rowPlaceHolder = array();
foreach ($row as $val) {
$rowPlaceHolder[] = $this->sql->addValue($val, $params);
}
$values[] = "(" . implode(",", $rowPlaceHolder) . ")";
}
$values = implode(",", $values);
$onDuplicateKey = $this->sql->getOnDuplicateStrategy($this->onDuplicateKey(), $params);
if ($onDuplicateKey === FALSE) {
return null;
}
$returningCol = $this->getReturning();
$returning = $this->sql->getReturning($returningCol);
return "INSERT INTO $tableName$columnStr VALUES $values$onDuplicateKey$returning";
}
}

@ -20,6 +20,9 @@ abstract class Query {
}
// can actually return bool|array (depending on success and query type)
public abstract function execute();
public function execute() {
return $this->sql->executeQuery($this);
}
public abstract function build(array &$params): ?string;
}

@ -4,6 +4,7 @@ namespace Driver\SQL\Query;
use Driver\SQL\Condition\CondOr;
use Driver\SQL\Join;
use Driver\SQL\SQL;
class Select extends Query {
@ -81,7 +82,7 @@ class Select extends Query {
}
public function execute() {
return $this->sql->executeSelect($this);
return $this->sql->executeQuery($this, true);
}
public function getColumns(): array { return $this->columns; }
@ -94,4 +95,46 @@ class Select extends Query {
public function getOffset(): int { return $this->offset; }
public function getGroupBy(): array { return $this->groupColumns; }
public function build(array &$params, Query $context = NULL): ?string {
$columns = $this->sql->columnName($this->getColumns());
$tables = $this->getTables();
if (!$tables) {
return "SELECT $columns";
}
$tables = $this->sql->tableName($tables);
$where = $this->sql->getWhereClause($this->getConditions(), $params);
$joinStr = "";
$joins = $this->getJoins();
if (!empty($joins)) {
foreach ($joins as $join) {
$type = $join->getType();
$joinTable = $this->sql->tableName($join->getTable());
$columnA = $this->sql->columnName($join->getColumnA());
$columnB = $this->sql->columnName($join->getColumnB());
$tableAlias = ($join->getTableAlias() ? " " . $join->getTableAlias() : "");
$joinStr .= " $type JOIN $joinTable$tableAlias ON $columnA=$columnB";
}
}
$groupBy = "";
$groupColumns = $this->getGroupBy();
if (!empty($groupColumns)) {
$groupBy = " GROUP BY " . $this->sql->columnName($groupColumns);
}
$orderBy = "";
$orderColumns = $this->getOrderBy();
if (!empty($orderColumns)) {
$orderBy = " ORDER BY " . $this->sql->columnName($orderColumns);
$orderBy .= ($this->isOrderedAscending() ? " ASC" : " DESC");
}
$limit = ($this->getLimit() > 0 ? (" LIMIT " . $this->getLimit()) : "");
$offset = ($this->getOffset() > 0 ? (" OFFSET " . $this->getOffset()) : "");
return "SELECT $columns FROM $tables$joinStr$where$groupBy$orderBy$limit$offset";
}
}

@ -13,9 +13,9 @@ class Truncate extends Query {
$this->tableName = $name;
}
public function execute(): bool {
return $this->sql->executeTruncate($this);
}
public function getTable(): string { return $this->tableName; }
public function build(array &$params, Query $context = NULL): ?string {
return "TRUNCATE " . $this->sql->tableName($this->getTable());
}
}

@ -28,11 +28,20 @@ class Update extends Query {
return $this;
}
public function execute() {
return $this->sql->executeUpdate($this);
}
public function getTable(): string { return $this->table; }
public function getConditions(): array { return $this->conditions; }
public function getValues(): array { return $this->values; }
public function build(array &$params, Query $context = NULL): ?string {
$table = $this->sql->tableName($this->getTable());
$valueStr = array();
foreach($this->getValues() as $key => $val) {
$valueStr[] = $this->sql->columnName($key) . "=" . $this->sql->addValue($val, $params);
}
$valueStr = implode(",", $valueStr);
$where = $this->sql->getWhereClause($this->getConditions(), $params);
return "UPDATE $table SET $valueStr$where";
}
}

@ -16,7 +16,9 @@ use \Driver\SQL\Constraint\Unique;
use \Driver\SQL\Constraint\PrimaryKey;
use \Driver\SQL\Constraint\ForeignKey;
use Driver\SQL\Query\AlterTable;
use Driver\SQL\Query\CreateProcedure;
use Driver\SQL\Query\CreateTable;
use Driver\SQL\Query\CreateTrigger;
use Driver\SQL\Query\Delete;
use Driver\SQL\Query\Drop;
use Driver\SQL\Query\Insert;
@ -44,46 +46,55 @@ abstract class SQL {
$this->lastInsertId = 0;
}
public function isConnected() {
public function isConnected(): bool {
return !is_null($this->connection);
}
public function getLastError() {
public function getLastError(): string {
return trim($this->lastError);
}
public function createTable($tableName) {
public function createTable($tableName): CreateTable {
return new CreateTable($this, $tableName);
}
public function insert($tableName, $columns=array()) {
public function insert($tableName, $columns=array()): Insert {
return new Insert($this, $tableName, $columns);
}
public function select(...$columNames) {
public function select(...$columNames): Select {
return new Select($this, $columNames);
}
public function truncate($table) {
public function truncate($table): Truncate {
return new Truncate($this, $table);
}
public function delete($table) {
public function delete($table): Delete {
return new Delete($this, $table);
}
public function update($table) {
public function update($table): Update {
return new Update($this, $table);
}
public function drop(string $table) {
public function drop(string $table): Drop {
return new Drop($this, $table);
}
public function alterTable($tableName) {
public function alterTable($tableName): AlterTable {
return new AlterTable($this, $tableName);
}
public function createTrigger($triggerName): CreateTrigger {
return new CreateTrigger($this, $triggerName);
}
public function createProcedure(string $procName): CreateProcedure {
return new CreateProcedure($this, $procName);
}
// ####################
// ### ABSTRACT METHODS
// ####################
@ -92,223 +103,37 @@ abstract class SQL {
public abstract function checkRequirements();
public abstract function getDriverName();
// Connection Managment
// Connection Management
public abstract function connect();
public abstract function disconnect();
// Querybuilder
protected function buildQuery(Query $query, array &$params) {
if ($query instanceof Select) {
$select = $query;
$columns = $this->columnName($select->getColumns());
$tables = $select->getTables();
public function executeQuery(Query $query, bool $fetchResult = false) {
if (!$tables) {
return $this->execute("SELECT $columns", $params, true);
}
$parameters = [];
$queryStr = $query->build($parameters);
$tables = $this->tableName($tables);
$where = $this->getWhereClause($select->getConditions(), $params);
$joinStr = "";
$joins = $select->getJoins();
if (!empty($joins)) {
foreach($joins as $join) {
$type = $join->getType();
$joinTable = $this->tableName($join->getTable());
$columnA = $this->columnName($join->getColumnA());
$columnB = $this->columnName($join->getColumnB());
$tableAlias = ($join->getTableAlias() ? " " . $join->getTableAlias() : "");
$joinStr .= " $type JOIN $joinTable$tableAlias ON $columnA=$columnB";
}
}
$groupBy = "";
$groupColumns = $select->getGroupBy();
if (!empty($groupColumns)) {
$groupBy = " GROUP BY " . $this->columnName($groupColumns);
}
$orderBy = "";
$orderColumns = $select->getOrderBy();
if (!empty($orderColumns)) {
$orderBy = " ORDER BY " . $this->columnName($orderColumns);
$orderBy .= ($select->isOrderedAscending() ? " ASC" : " DESC");
}
$limit = ($select->getLimit() > 0 ? (" LIMIT " . $select->getLimit()) : "");
$offset = ($select->getOffset() > 0 ? (" OFFSET " . $select->getOffset()) : "");
return "SELECT $columns FROM $tables$joinStr$where$groupBy$orderBy$limit$offset";
} else {
$this->lastError = "buildQuery() not implemented for type: " . get_class($query);
return FALSE;
}
}
public function executeCreateTable(CreateTable $createTable) {
$tableName = $this->tableName($createTable->getTableName());
$ifNotExists = $createTable->ifNotExists() ? " IF NOT EXISTS": "";
$entries = array();
foreach($createTable->getColumns() as $column) {
$entries[] = ($tmp = $this->getColumnDefinition($column));
if (is_null($tmp)) {
return false;
}
if($query->dump) {
var_dump($queryStr);
var_dump($parameters);
}
foreach($createTable->getConstraints() as $constraint) {
$entries[] = ($tmp = $this->getConstraintDefinition($constraint));
if (is_null($tmp)) {
return false;
}
}
$entries = implode(",", $entries);
$query = "CREATE TABLE$ifNotExists $tableName ($entries)";
return $this->execute($query);
}
public function executeInsert(Insert $insert) {
$tableName = $this->tableName($insert->getTableName());
$columns = $insert->getColumns();
$rows = $insert->getRows();
if (empty($rows)) {
$this->lastError = "No rows to insert given.";
if ($queryStr === null) {
return false;
}
if (is_null($columns) || empty($columns)) {
$columnStr = "";
} else {
$columnStr = " (" . $this->columnName($columns) . ")";
}
$parameters = array();
$values = array();
foreach($rows as $row) {
$rowPlaceHolder = array();
foreach($row as $val) {
$rowPlaceHolder[] = $this->addValue($val, $parameters);
}
$values[] = "(" . implode(",", $rowPlaceHolder) . ")";
}
$values = implode(",", $values);
$onDuplicateKey = $this->getOnDuplicateStrategy($insert->onDuplicateKey(), $parameters);
if ($onDuplicateKey === FALSE) {
return false;
}
$returningCol = $insert->getReturning();
$returning = $this->getReturning($returningCol);
$query = "INSERT INTO $tableName$columnStr VALUES $values$onDuplicateKey$returning";
if($insert->dump) { var_dump($query); var_dump($parameters); }
$res = $this->execute($query, $parameters, !empty($returning));
$res = $this->execute($queryStr, $parameters, $fetchResult);
$success = ($res !== FALSE);
if($success && $returningCol) {
$this->fetchReturning($res, $returningCol);
// fetch generated serial ids for Insert statements
$generatedColumn = ($query instanceof Insert ? $query->getReturning() : null);
if($success && $fetchResult && $generatedColumn) {
$this->fetchReturning($res, $generatedColumn);
}
return $success;
return $fetchResult ? $res : $success;
}
public function executeSelect(Select $select) {
$params = array();
$query = $this->buildQuery($select, $params);
if($select->dump) { var_dump($query); var_dump($params); }
return $this->execute($query, $params, true);
}
public function executeDelete(Delete $delete) {
$params = array();
$table = $this->tableName($delete->getTable());
$where = $this->getWhereClause($delete->getConditions(), $params);
$query = "DELETE FROM $table$where";
if($delete->dump) { var_dump($query); }
return $this->execute($query, $params);
}
public function executeTruncate(Truncate $truncate) {
$query = "TRUNCATE " . $this->tableName($truncate->getTable());
if ($truncate->dump) { var_dump($query); }
return $this->execute($query);
}
public function executeUpdate(Update $update) {
$params = array();
$table = $this->tableName($update->getTable());
$valueStr = array();
foreach($update->getValues() as $key => $val) {
$valueStr[] = $this->columnName($key) . "=" . $this->addValue($val, $params);
}
$valueStr = implode(",", $valueStr);
$where = $this->getWhereClause($update->getConditions(), $params);
$query = "UPDATE $table SET $valueStr$where";
if($update->dump) { var_dump($query); var_dump($params); }
return $this->execute($query, $params);
}
public function executeDrop(Drop $drop) {
$query = "DROP TABLE " . $this->tableName($drop->getTable());
if ($drop->dump) { var_dump($query); }
return $this->execute($query);
}
public function executeAlter(AlterTable $alter): bool {
$tableName = $this->tableName($alter->getTable());
$action = $alter->getAction();
$column = $alter->getColumn();
$constraint = $alter->getConstraint();
$query = "ALTER TABLE $tableName $action ";
if ($column) {
$query .= "COLUMN ";
if ($action === "DROP") {
$query .= $this->columnName($column->getName());
} else {
// ADD or modify
$query .= $this->getColumnDefinition($column);
}
} else if ($constraint) {
if ($action === "DROP") {
if ($constraint instanceof PrimaryKey) {
$query .= "PRIMARY KEY";
} else if ($constraint instanceof ForeignKey) {
// TODO: how can we pass the constraint name here?
$this->lastError = "DROP CONSTRAINT foreign key is not supported yet.";
return false;
}
} else if ($action === "ADD") {
$query .= "CONSTRAINT ";
$query .= $this->getConstraintDefinition($constraint);
} else if ($action === "MODIFY") {
$this->lastError = "MODIFY CONSTRAINT foreign key is not supported.";
return false;
}
} else {
$this->lastError = "ALTER TABLE requires at least a column or a constraint.";
return false;
}
if ($alter->dump) { var_dump($query); }
return $this->execute($query);
}
protected function getWhereClause($conditions, &$params) {
public function getWhereClause($conditions, &$params): string {
if (!$conditions) {
return "";
} else {
@ -316,7 +141,7 @@ abstract class SQL {
}
}
public function getConstraintDefinition(Constraint $constraint) {
public function getConstraintDefinition(Constraint $constraint): ?string {
$columnName = $this->columnName($constraint->getColumnNames());
if ($constraint instanceof PrimaryKey) {
return "PRIMARY KEY ($columnName)";
@ -338,29 +163,52 @@ abstract class SQL {
return $code;
} else {
$this->lastError = "Unsupported constraint type: " . get_class($constraint);
return false;
return null;
}
}
protected function getReturning(?string $columns) {
return "";
protected abstract function fetchReturning($res, string $returningCol);
public abstract function getColumnDefinition(Column $column): ?string;
public abstract function getOnDuplicateStrategy(?Strategy $strategy, &$params): ?string;
public abstract function createTriggerBody(CreateTrigger $trigger): ?string;
public abstract function getProcedureHead(CreateProcedure $procedure): ?string;
public abstract function getColumnType(Column $column): ?string;
public function getProcedureTail(): string { return ""; }
public function getReturning(?string $columns): string { return ""; }
public function getProcedureBody(CreateProcedure $procedure): string {
$statements = "";
foreach ($procedure->getStatements() as $statement) {
$statements .= $this->buildUnsafe($statement) . ";";
}
return $statements;
}
protected abstract function getColumnDefinition(Column $column);
protected abstract function fetchReturning($res, string $returningCol);
protected abstract function getOnDuplicateStrategy(?Strategy $strategy, &$params);
protected function getUnsafeValue($value): ?string {
if (is_string($value) || is_numeric($value) || is_bool($value)) {
return "'" . addslashes("$value") . "'"; // unsafe operation here...
} else if ($value instanceof Column) {
return $this->columnName($value);
} else if ($value === null) {
return "NULL";
} else {
$this->lastError = "Cannot create unsafe value of type: " . gettype($value);
return null;
}
}
protected abstract function getValueDefinition($val);
protected abstract function addValue($val, &$params);
public abstract function addValue($val, &$params = NULL);
protected abstract function buildUnsafe(Query $statement): string;
protected abstract function tableName($table);
protected abstract function columnName($col);
public abstract function tableName($table): string;
public abstract function columnName($col): string;
// Special Keywords and functions
public function now() { return $this->currentTimestamp(); }
public abstract function currentTimestamp();
public function now(): Keyword { return $this->currentTimestamp(); }
public abstract function currentTimestamp(): Keyword;
public function count($col = NULL) {
public function count($col = NULL): Keyword {
if (is_null($col)) {
return new Keyword("COUNT(*) AS count");
} else if($col instanceof Keyword) {
@ -372,13 +220,13 @@ abstract class SQL {
}
}
public function sum($col) {
public function sum($col): Keyword {
$sumCol = strtolower(str_replace(".","_", $col)) . "_sum";
$col = $this->columnName($col);
return new Keyword("SUM($col) AS $sumCol");
}
public function distinct($col) {
public function distinct($col): Keyword {
$col = $this->columnName($col);
return new Keyword("DISTINCT($col)");
}
@ -431,7 +279,7 @@ abstract class SQL {
$values = implode(",", $values);
} else if($expression instanceof Select) {
$values = $this->buildQuery($expression, $params);
$values = $expression->build($params);
} else {
$this->lastError = "Unsupported in-expression value: " . get_class($condition);
return false;
@ -466,7 +314,7 @@ abstract class SQL {
$this->lastError = $str;
}
public function getLastInsertId() {
public function getLastInsertId(): int {
return $this->lastInsertId;
}

@ -0,0 +1,14 @@
<?php
namespace Driver\SQL\Type;
use Driver\SQL\Column\Column;
class CurrentColumn extends Column {
public function __construct(string $string) {
parent::__construct($string);
}
}

@ -0,0 +1,11 @@
<?php
namespace Driver\SQL\Type;
use Driver\SQL\Column\StringColumn;
class CurrentTable extends StringColumn {
public function __construct() {
parent::__construct("CURRENT_TABLE");
}
}

@ -0,0 +1,11 @@
<?php
namespace Driver\SQL\Type;
use Driver\SQL\Keyword;
class Trigger extends Keyword {
public function __construct() {
parent::__construct("TRIGGER");
}
}

@ -1,6 +1,7 @@
version: "3.9"
services:
web:
container_name: web
image: nginx:latest
ports:
- "80:80"
@ -11,6 +12,7 @@ services:
- db
- php
db:
container_name: db
image: mariadb:latest
ports:
- '3306:3306'
@ -18,6 +20,7 @@ services:
- "MYSQL_ROOT_PASSWORD=webbasedb"
- "MYSQL_DATABASE=webbase"
php:
container_name: php
volumes:
- .:/application:rw
- ./docker/php/php.ini:/usr/local/etc/php/php.ini:ro