diff --git a/core/Configuration/Patch/log.class.php b/core/Configuration/Patch/log.class.php new file mode 100644 index 0000000..12616c9 --- /dev/null +++ b/core/Configuration/Patch/log.class.php @@ -0,0 +1,63 @@ +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; + } + +} diff --git a/core/Documents/Install.class.php b/core/Documents/Install.class.php index a119068..cc6fdf6 100644 --- a/core/Documents/Install.class.php +++ b/core/Documents/Install.class.php @@ -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" : ""); diff --git a/core/Driver/SQL/MySQL.class.php b/core/Driver/SQL/MySQL.class.php index 3733bbf..d8a2764 100644 --- a/core/Driver/SQL/MySQL.class.php +++ b/core/Driver/SQL/MySQL.class.php @@ -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; + } } diff --git a/core/Driver/SQL/PostgreSQL.class.php b/core/Driver/SQL/PostgreSQL.class.php index d5cc4cf..2983833 100644 --- a/core/Driver/SQL/PostgreSQL.class.php +++ b/core/Driver/SQL/PostgreSQL.class.php @@ -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; + } } \ No newline at end of file diff --git a/core/Driver/SQL/Query/AlterTable.class.php b/core/Driver/SQL/Query/AlterTable.class.php index 6058e24..221d539 100644 --- a/core/Driver/SQL/Query/AlterTable.class.php +++ b/core/Driver/SQL/Query/AlterTable.class.php @@ -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; + } } \ No newline at end of file diff --git a/core/Driver/SQL/Query/CreateProcedure.class.php b/core/Driver/SQL/Query/CreateProcedure.class.php new file mode 100644 index 0000000..9175e56 --- /dev/null +++ b/core/Driver/SQL/Query/CreateProcedure.class.php @@ -0,0 +1,50 @@ +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; } +} \ No newline at end of file diff --git a/core/Driver/SQL/Query/CreateTable.class.php b/core/Driver/SQL/Query/CreateTable.class.php index 27c2a47..e58d39a 100644 --- a/core/Driver/SQL/Query/CreateTable.class.php +++ b/core/Driver/SQL/Query/CreateTable.class.php @@ -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)"; + } } diff --git a/core/Driver/SQL/Query/CreateTrigger.class.php b/core/Driver/SQL/Query/CreateTrigger.class.php new file mode 100644 index 0000000..1a651b6 --- /dev/null +++ b/core/Driver/SQL/Query/CreateTrigger.class.php @@ -0,0 +1,80 @@ +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; + } +} \ No newline at end of file diff --git a/core/Driver/SQL/Query/Delete.class.php b/core/Driver/SQL/Query/Delete.class.php index b98f41a..cdbc5bc 100644 --- a/core/Driver/SQL/Query/Delete.class.php +++ b/core/Driver/SQL/Query/Delete.class.php @@ -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"; + } } diff --git a/core/Driver/SQL/Query/Drop.php b/core/Driver/SQL/Query/Drop.php index ebcc193..4fb96b4 100644 --- a/core/Driver/SQL/Query/Drop.php +++ b/core/Driver/SQL/Query/Drop.php @@ -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()); + } } \ No newline at end of file diff --git a/core/Driver/SQL/Query/Insert.class.php b/core/Driver/SQL/Query/Insert.class.php index aa0e2ea..89c5504 100644 --- a/core/Driver/SQL/Query/Insert.class.php +++ b/core/Driver/SQL/Query/Insert.class.php @@ -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"; + } } \ No newline at end of file diff --git a/core/Driver/SQL/Query/Query.class.php b/core/Driver/SQL/Query/Query.class.php index 31b4f4c..9c69ca4 100644 --- a/core/Driver/SQL/Query/Query.class.php +++ b/core/Driver/SQL/Query/Query.class.php @@ -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; } \ No newline at end of file diff --git a/core/Driver/SQL/Query/Select.class.php b/core/Driver/SQL/Query/Select.class.php index 915eda9..7e997ab 100644 --- a/core/Driver/SQL/Query/Select.class.php +++ b/core/Driver/SQL/Query/Select.class.php @@ -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"; + } } \ No newline at end of file diff --git a/core/Driver/SQL/Query/Truncate.class.php b/core/Driver/SQL/Query/Truncate.class.php index 7cb57c4..888ab9c 100644 --- a/core/Driver/SQL/Query/Truncate.class.php +++ b/core/Driver/SQL/Query/Truncate.class.php @@ -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()); + } } \ No newline at end of file diff --git a/core/Driver/SQL/Query/Update.class.php b/core/Driver/SQL/Query/Update.class.php index 35c0a0c..d67e90a 100644 --- a/core/Driver/SQL/Query/Update.class.php +++ b/core/Driver/SQL/Query/Update.class.php @@ -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"; + } } \ No newline at end of file diff --git a/core/Driver/SQL/SQL.class.php b/core/Driver/SQL/SQL.class.php index e81f62e..da4d816 100644 --- a/core/Driver/SQL/SQL.class.php +++ b/core/Driver/SQL/SQL.class.php @@ -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; } diff --git a/core/Driver/SQL/Type/CurrentColumn.php b/core/Driver/SQL/Type/CurrentColumn.php new file mode 100644 index 0000000..b95d86f --- /dev/null +++ b/core/Driver/SQL/Type/CurrentColumn.php @@ -0,0 +1,14 @@ +